Skip to content

Commit

Permalink
feat: #1127 As a developer, I should be able to see a preview of my a…
Browse files Browse the repository at this point in the history
…pp (#1431)
  • Loading branch information
Trường An authored Jun 3, 2020
1 parent 59dbffa commit 705a747
Show file tree
Hide file tree
Showing 17 changed files with 223 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/marketplace/src/actions/__tests__/developer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
fetchMonthlyBillingFailure,
developerWebhookPing,
developerSetWebhookPingStatus,
developerApplyAppDetails,
} from '../developer'
import ActionTypes from '../../constants/action-types'
import { appsDataStub } from '../../sagas/__stubs__/apps'
Expand Down Expand Up @@ -112,4 +113,8 @@ describe('developer actions', () => {
expect(developerSetWebhookPingStatus.type).toEqual(ActionTypes.DEVELOPER_SET_PING_WEBHOOK_STATUS)
expect(developerSetWebhookPingStatus('SUCCESS').data).toEqual('SUCCESS')
})
it('should create a developerApplyAppDetails action', () => {
expect(developerApplyAppDetails.type).toEqual(ActionTypes.DEVELOPER_APPLY_APP_DETAIL)
expect(developerApplyAppDetails({}).data).toEqual({})
})
})
1 change: 1 addition & 0 deletions packages/marketplace/src/actions/developer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ export const developerWebhookPing = actionCreator<PingWebhooksByIdParams>(Action
export const developerSetWebhookPingStatus = actionCreator<WebhookPingTestStatus>(
ActionTypes.DEVELOPER_SET_PING_WEBHOOK_STATUS,
)
export const developerApplyAppDetails = actionCreator<AppDetailData>(ActionTypes.DEVELOPER_APPLY_APP_DETAIL)
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import ClientAppDetail, {
handleCloseInstallConfirmationModal,
handleInstallAppButtonClick,
renderAppHeaderButtonGroup,
handleApplyAppDetailsFromLocalStorage,
} from '../client-app-detail'
import { Button } from '@reapit/elements'
import Routes from '@/constants/routes'
import appState from '@/reducers/__stubs__/app-state'
import { developerApplyAppDetails } from '@/actions/developer'
import { AppDetailData } from '@/reducers/client/app-detail'

describe('ClientAppDetail', () => {
let store
Expand Down Expand Up @@ -169,4 +172,18 @@ describe('ClientAppDetail', () => {
expect(mockFunction).toBeCalledWith(true)
})
})

describe('handleApplyAppDetailsFromLocalStorage', () => {
it('should run correctly', () => {
const dispatch = jest.fn()
const appId = 'appId'
const value = { id: 'appId' } as AppDetailData
const stringValue = JSON.stringify(value)
const spyLocalStorageGetItem = jest.spyOn(window.localStorage, 'getItem').mockImplementation(() => stringValue)
const fn = handleApplyAppDetailsFromLocalStorage(dispatch, 'DEVELOPER', appId)
fn()
expect(spyLocalStorageGetItem).toBeCalledWith('developer-preview-app')
expect(dispatch).toBeCalledWith(developerApplyAppDetails(value))
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import ClientAppUninstallConfirmation from '@/components/ui/client-app-detail/cl
import { DesktopIntegrationTypeModel } from '@/actions/app-integration-types'
import { AppDetailDataNotNull } from '@/reducers/client/app-detail'
import { selectIntegrationTypes } from '@/selector/integration-types'
import { useSelector } from 'react-redux'
import { selectAppDetailData, selectAppDetailLoading } from '@/selector/client-app-detail'
import { useSelector, useDispatch } from 'react-redux'
import { selectAppDetailData, selectAppDetailLoading, selectAppDetailError } from '@/selector/client-app-detail'
import { selectLoginType, selectIsAdmin } from '@/selector/auth'
import AppHeader from '@/components/ui/standalone-app-detail/app-header'
import AppContent from './app-content'
import { Loader, Button, FormSection } from '@reapit/elements'
import { Loader, Button, Alert, FormSection } from '@reapit/elements'
import clientAppDetailStyles from '@/styles/pages/client-app-detail.scss?mod'
import ClientAppInstallConfirmation from '@/components/ui/client-app-detail/client-app-install-confirmation'
import { Aside } from './aside'
import { clientFetchAppDetailFailed } from '@/actions/client'
import { developerApplyAppDetails } from '@/actions/developer'
import { useParams } from 'react-router'
import { Dispatch } from 'redux'
import { getDesktopIntegrationTypes } from '@/utils/get-desktop-integration-types'
import Routes from '@/constants/routes'
import { LoginType } from '@reapit/cognito-auth'

export type ClientAppDetailProps = {}

Expand All @@ -36,7 +41,34 @@ export const handleInstallAppButtonClick = (setIsVisibleInstallConfirmation: (is
}
}

export const onBackToAppsButtonClick = (history: History) => {
export const handleApplyAppDetailsFromLocalStorage = (
dispatch: Dispatch,
loginType: LoginType,
appId?: string,
) => () => {
if (loginType !== 'DEVELOPER' || !appId) return
try {
const appDataString = localStorage.getItem('developer-preview-app')
if (!appDataString) {
throw 'No app preview'
}

const appData = JSON.parse(appDataString)
if (appData.id !== appId) {
throw 'No app preview'
}

dispatch(developerApplyAppDetails(appData))
} catch (err) {
dispatch(clientFetchAppDetailFailed(err))
}
}

export const onBackToAppsButtonClick = (history: History, loginType: LoginType) => {
if (loginType === 'DEVELOPER')
return () => {
history.push(Routes.DEVELOPER_MY_APPS)
}
return () => {
history.push(Routes.CLIENT)
}
Expand Down Expand Up @@ -81,7 +113,10 @@ export const renderAppHeaderButtonGroup = (
}

const ClientAppDetail: React.FC<ClientAppDetailProps> = () => {
const dispatch = useDispatch()
const history = useHistory()
const { appId } = useParams()

const [isVisibleInstallConfirmation, setIsVisibleInstallConfirmation] = React.useState(false)
const [isVisibleUninstallConfirmation, setIsVisibleUninstallConfirmation] = React.useState(false)
const closeUninstallConfirmationModal = React.useCallback(
Expand All @@ -108,11 +143,16 @@ const ClientAppDetail: React.FC<ClientAppDetailProps> = () => {
const isLoadingAppDetail = useSelector(selectAppDetailLoading)
const loginType = useSelector(selectLoginType)
const isAdmin = useSelector(selectIsAdmin)
const error = useSelector(selectAppDetailError)

const isInstallBtnHidden = loginType === 'CLIENT' && !isAdmin
// selector selectAppDetailData return {} if not data
const unfetched = Object.keys(appDetailData).length === 0
const { id = '', installedOn = '' } = appDetailData

React.useEffect(handleApplyAppDetailsFromLocalStorage(dispatch, loginType, appId), [dispatch])

if (error) return <Alert message={error} type="danger"></Alert>
if (isLoadingAppDetail || unfetched) {
return <Loader dataTest="client-app-detail-loader" />
}
Expand All @@ -133,7 +173,7 @@ const ClientAppDetail: React.FC<ClientAppDetailProps> = () => {
/>
<AppContent desktopIntegrationTypes={userDesktopIntegrationTypes} appDetailData={appDetailData} />
<FormSection className={classNames('is-clearfix', clientAppDetailStyles.footerContainer)}>
<Button className="is-pulled-right" onClick={onBackToAppsButtonClick(history)}>
<Button className="is-pulled-right" onClick={onBackToAppsButtonClick(history, loginType)}>
Back To Apps
</Button>
</FormSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,21 @@ exports[`DeveloperSubmitApp should match a snapshot 1`] = `
<div
className="column "
>
<Component
onClick={[Function]}
type="button"
variant="primary"
>
<button
className="button is-primary "
data-test=""
disabled={false}
onClick={[Function]}
type="button"
>
Preview
</button>
</Component>
<Component
dataTest="submit-app-button"
disabled={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import DeveloperSubmitApp, {
handleGoBackToApps,
handleOnSubmitAnotherApp,
generateInitialValues,
handleOpenAppPreview,
} from '../developer-submit-app'
import { getMockRouterProps } from '@/utils/mock-helper'
import { FIELD_ERROR_DESCRIPTION } from '@/constants/form'
Expand Down Expand Up @@ -292,4 +293,18 @@ describe('DeveloperSubmitApp', () => {
})
})
})

describe('handleOpenAppPreview', () => {
it('should run correctly', () => {
const params = { appDetails: {}, values: {}, scopes: [], appId: 'appId' }
const spyLocalStorageSetItem = jest.spyOn(window.localStorage, 'setItem')
const spyOpenUrl = jest.spyOn(window, 'open')
const expected = JSON.stringify({ scopes: [] })

const fn = handleOpenAppPreview(params)
fn()
expect(spyLocalStorageSetItem).toBeCalledWith('developer-preview-app', expected)
expect(spyOpenUrl).toBeCalledWith('developer/apps/appId/preview', '_blank')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
FormikHelpers,
H6,
FlexContainerResponsive,
FormikValues,
} from '@reapit/elements'
import { FIELD_ERROR_DESCRIPTION } from '@/constants/form'

Expand Down Expand Up @@ -44,6 +45,7 @@ import UploadImageSection from './upload-image-section'
import MarketplaceStatusSection from './marketplace-status-section'
import PermissionSection from './permission-section'
import styles from '@/styles/pages/developer-submit-app.scss?mod'
import { ScopeModel } from '@/types/marketplace-api-schema'

export type DeveloperSubmitAppProps = {}

Expand Down Expand Up @@ -274,6 +276,25 @@ export const handleOnSubmitAnotherApp = (dispatch: Dispatch) => {
}
}

export type HandleOpenAppPreview = {
scopes: ScopeModel[]
values: FormikValues
appId?: string
appDetails?: AppDetailModel & { apiKey?: string }
}

export const handleOpenAppPreview = ({ appDetails, values, scopes, appId }: HandleOpenAppPreview) => () => {
const appDetailState = {
...appDetails,
...values,
scopes: scopes.filter(scope => values.scopes.includes(scope.name)),
}

const url = `developer/apps/${appId}/preview`
localStorage.setItem('developer-preview-app', JSON.stringify(appDetailState))
window.open(url, '_blank')
}

export const DeveloperSubmitApp: React.FC<DeveloperSubmitAppProps> = () => {
let initialValues
let formState
Expand Down Expand Up @@ -377,6 +398,18 @@ export const DeveloperSubmitApp: React.FC<DeveloperSubmitAppProps> = () => {
<LevelRight>
<Grid>
<GridItem>
<Button
onClick={handleOpenAppPreview({
appDetails: appDetailState?.appDetailData?.data,
values,
scopes,
appId: appid,
})}
variant="primary"
type="button"
>
Preview
</Button>
{!isSubmitApp && (
<Button onClick={goBackToApps} variant="primary" type="button">
Back To Apps
Expand Down
1 change: 1 addition & 0 deletions packages/marketplace/src/constants/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const ActionTypes = {
DEVELOPER_FETCH_APP_DETAIL: 'DEVELOPER_FETCH_APP_DETAIL',
DEVELOPER_FETCH_APP_DETAIL_FAILED: 'DEVELOPER_FETCH_APP_DETAIL_FAILED',
DEVELOPER_FETCH_APP_DETAIL_SUCCESS: 'DEVELOPER_FETCH_APP_DETAIL_SUCCESS',
DEVELOPER_APPLY_APP_DETAIL: 'DEVELOPER_APPLY_APP_DETAIL',

// App Detail actions
APP_DETAIL_REQUEST_DATA: 'APP_DETAIL_REQUEST_DATA',
Expand Down
1 change: 1 addition & 0 deletions packages/marketplace/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Routes = {
SETTINGS: '/developer/settings',
SUBMIT_APP: '/developer/submit-app',
DEVELOPER_HELP: '/developer/help',
DEVELOPER_APP_PREVIEW: '/developer/apps/:appId/preview',
CLIENT_HELP: '/client/help',
REGISTER: '/register',
REGISTER_CONFIRM: '/register/confirm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,19 @@ exports[`Router should match a snapshot 1`] = `
fetcher={true}
path="/developer/help"
/>
<Component
allow="DEVELOPER"
component={
Object {
"$$typeof": Symbol(react.lazy),
"_ctor": [Function],
"_result": null,
"_status": -1,
}
}
exact={true}
path="/developer/apps/:appId/preview"
/>
<Component
allow="ADMIN"
component={
Expand Down
1 change: 1 addition & 0 deletions packages/marketplace/src/core/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const Router = () => {
fetcher
component={DeveloperHelpPage}
/>
<PrivateRoute allow="DEVELOPER" path={Routes.DEVELOPER_APP_PREVIEW} exact component={ClientAppDetail} />

<PrivateRoute allow="ADMIN" path={Routes.ADMIN_APPROVALS} component={AdminApprovalsPage} exact fetcher />
<PrivateRoute allow="ADMIN" path={Routes.ADMIN_APPS} component={AdminAppsPage} fetcher exact />
Expand Down
48 changes: 47 additions & 1 deletion packages/marketplace/src/sagas/apps/__test__/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import appDetailSagas, {
clientAppDetailDataListen,
fetchDeveloperAppDetailSaga,
developerAppDetailDataListen,
developerApplyAppDetailsSaga,
developerApplyAppDetailsListen,
} from '../apps'
import { fetchDesktopIntegrationTypes } from '@/services/apps'
import ActionTypes from '@/constants/action-types'
Expand All @@ -18,6 +20,7 @@ import { clientFetchAppDetailSuccess } from '@/actions/client'
import { appDetailDataStub } from '@/sagas/__stubs__/app-detail'
import { integrationTypesStub } from '@/sagas/__stubs__/integration-types'
import { developerFetchAppDetailSuccess } from '@/actions/developer'
import { AppDetailData } from '@/reducers/client/app-detail'

jest.mock('@reapit/elements')

Expand Down Expand Up @@ -219,6 +222,33 @@ describe('client app detail fetch data and fetch apiKey', () => {
})
})

describe('client app detail fetch data from local storage', () => {
const params: Action<AppDetailData> = {
data: appDetailDataStub.data as AppDetailData,
type: 'DEVELOPER_APPLY_APP_DETAIL',
}
const gen = cloneableGenerator(developerApplyAppDetailsSaga)(params)
expect(gen.next().value).toEqual(call(fetchDesktopIntegrationTypes))

test('api call success', () => {
const clone = gen.clone()
expect(clone.next(integrationTypesStub).value).toEqual(put(integrationTypesReceiveData(integrationTypesStub)))
expect(clone.next().value).toEqual(put(clientFetchAppDetailSuccess(appDetailDataStub.data)))
})
test('api call error', () => {
const clone = gen.clone()
// @ts-ignore
expect(clone.throw('error').value).toEqual(
put(
errorThrownServer({
type: 'SERVER',
message: errorMessages.DEFAULT_SERVER_ERROR,
}),
),
)
})
})

describe('client app detail thunks', () => {
describe('clientAppDetailDataListen', () => {
it('should trigger request data when called', () => {
Expand All @@ -240,11 +270,27 @@ describe('client app detail thunks', () => {
})
})

describe('developerApplyAppDetailsListen', () => {
it('should trigger request data when called', () => {
const gen = developerApplyAppDetailsListen()
expect(gen.next().value).toEqual(
takeLatest<Action<AppDetailData>>(ActionTypes.DEVELOPER_APPLY_APP_DETAIL, developerApplyAppDetailsSaga),
)
expect(gen.next().done).toBe(true)
})
})

describe('appDetailSagas', () => {
it('should listen data request', () => {
const gen = appDetailSagas()

expect(gen.next().value).toEqual(all([fork(clientAppDetailDataListen), fork(developerAppDetailDataListen)]))
expect(gen.next().value).toEqual(
all([
fork(clientAppDetailDataListen),
fork(developerAppDetailDataListen),
fork(developerApplyAppDetailsListen),
]),
)
expect(gen.next().done).toBe(true)
})
})
Expand Down
Loading

0 comments on commit 705a747

Please sign in to comment.