Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #1127 As a developer, I should be able to see a preview of my app #1431

Merged
merged 6 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
duong-se marked this conversation as resolved.
Show resolved Hide resolved
}

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