diff --git a/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/app-content.test.tsx.snap b/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/app-content.test.tsx.snap
new file mode 100644
index 0000000000..8ccf4af89d
--- /dev/null
+++ b/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/app-content.test.tsx.snap
@@ -0,0 +1,211 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AppContent AppContent - should match snapshot 1`] = `
+
+
+
+
+ Description
+
+
+
+
+
+ Permission Required
+
+
+
+
+
+ Destop Integration
+
+
+ Identity Check
+
+
+ Property Marketing Information
+
+
+ Vendor Marketing Report
+
+
+ Property Details Generation
+
+
+ Applicant Export
+
+
+ Property
+
+
+ Applicant
+
+
+
+ TBC
+
+
+
+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam aliquid culpa saepe asperiores debitis illo dolorum dolore incidunt officiis praesentium, nemo similique veritatis exercitationem perferendis non mollitia animi laboriosam perspiciatis.
+
+
+
+
+ NEED HELP?
+
+
+
+`;
+
+exports[`AppContent RenderWithHeader - should match snapshot 1`] = `
+
+
+ test
+
+ some text
+
+`;
+
+exports[`AppContent Tag - should match snapshot 1`] = `
+
+ Test
+
+`;
+
+exports[`AppContent renderCategory - should match snapshot 1`] = `
`;
+
+exports[`AppContent renderDescripion - should match snapshot 1`] = `
+
+
+
+ Description
+
+
+
+
+`;
+
+exports[`AppContent renderDesktopIntegrationTypes - should match snapshot 1`] = `
+
+
+
+ Destop Integration
+
+
+ Identity Check
+
+
+ Property Marketing Information
+
+
+ Vendor Marketing Report
+
+
+ Property Details Generation
+
+
+ Applicant Export
+
+
+ Property
+
+
+ Applicant
+
+
+
+`;
+
+exports[`AppContent renderExtraMedia - should match snapshot 1`] = `
+
+`;
+
+exports[`AppContent renderPermissions - should match snapshot 1`] = `
+
+
+
+ Permission Required
+
+
+
+ Read data about developers
+
+
+ Write data about developers
+
+
+
+
+`;
diff --git a/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/client-app-detail.test.tsx.snap b/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/client-app-detail.test.tsx.snap
index 5cb1726154..17ab739451 100644
--- a/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/client-app-detail.test.tsx.snap
+++ b/packages/marketplace/src/components/pages/client-app-detail/__test__/__snapshots__/client-app-detail.test.tsx.snap
@@ -2,15 +2,34 @@
exports[`ClientAppDetail renderAppHeaderButtonGroup should match snapshot 1`] = `
-
-
+
+ Uninstall App
+
+
+
+`;
+
+exports[`ClientAppDetail renderAppHeaderButtonGroup should match snapshot 2`] = `
+
+
+
-
-
- Installed
-
-
+ Uninstall App
+
`;
@@ -71,97 +90,264 @@ exports[`ClientAppDetail should match a snapshot 1`] = `
}
>
-
+
+
+
+
+
+
+
+`;
+
+exports[`ClientAppDetail should not render loader when client.appDetail.data is not empty object and isLoadingAppDetail = true 1`] = `
+
+
+
+
+
}
>
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ Verified by Reapit
+
-
-
-
-
-
-
-
-
-
+
+
-
+
+
-
-
-
-
+
+
+
+
+
+ Description
+
-
+
+
+
+
+
+
+
+ Permission Required
+
+
-
+
+
+
+
+ Developer
+
+
+ TBC
+
+
+
+
+
+
+ About Developer
+
+
+
+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam aliquid culpa saepe asperiores debitis illo dolorum dolore incidunt officiis praesentium, nemo similique veritatis exercitationem perferendis non mollitia animi laboriosam perspiciatis.
+
+
+
+
+
+
+
+ Contact Developer
+
+
+
+
+ NEED HELP?
+
+
+
+
+
@@ -169,3 +355,153 @@ exports[`ClientAppDetail should match a snapshot 1`] = `
`;
+
+exports[`ClientAppDetail should render loader when client.appDetail.data is an empty object 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`ClientAppDetail should render loader when isLoadingAppDetail = true 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/marketplace/src/components/pages/client-app-detail/__test__/app-content.test.tsx b/packages/marketplace/src/components/pages/client-app-detail/__test__/app-content.test.tsx
new file mode 100644
index 0000000000..714d76e7c6
--- /dev/null
+++ b/packages/marketplace/src/components/pages/client-app-detail/__test__/app-content.test.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+import AppContent, {
+ renderCategory,
+ renderDescripion,
+ renderDesktopIntegrationTypes,
+ renderExtraMedia,
+ renderPermissions,
+ RenderWithHeader,
+ Tag,
+} from '../app-content'
+
+import { appDetailDataStub } from '@/sagas/__stubs__/app-detail'
+import { integrationTypesStub } from '@/sagas/__stubs__/integration-types'
+
+import { shallow } from 'enzyme'
+import { DesktopIntegrationTypeModel } from '@reapit/foundations-ts-definitions'
+import { AppDetailDataNotNull } from '@/reducers/client/app-detail'
+
+describe('AppContent', () => {
+ test('renderCategory - should match snapshot', () => {
+ const result = renderCategory(appDetailDataStub.data.category)
+ const wrapper = shallow({result}
)
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('renderDescripion - should match snapshot', () => {
+ const result = renderDescripion(appDetailDataStub.data.description as string)
+ const wrapper = shallow({result}
)
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('renderDesktopIntegrationTypes - should match snapshot', () => {
+ const result = renderDesktopIntegrationTypes(integrationTypesStub.data as DesktopIntegrationTypeModel[])
+ const wrapper = shallow({result}
)
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('renderExtraMedia - should match snapshot', () => {
+ const result = renderExtraMedia(appDetailDataStub.data.media)
+ const wrapper = shallow({result}
)
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('renderPermissions - should match snapshot', () => {
+ const result = renderPermissions(appDetailDataStub.data.scopes)
+ const wrapper = shallow({result}
)
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('RenderWithHeader - should match snapshot', () => {
+ const wrapper = shallow(some text )
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('Tag - should match snapshot', () => {
+ const wrapper = shallow(Test )
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('AppContent - should match snapshot', () => {
+ const wrapper = shallow(
+ ,
+ )
+ expect(wrapper).toMatchSnapshot()
+ })
+})
diff --git a/packages/marketplace/src/components/pages/client-app-detail/__test__/app-header.text.tsx b/packages/marketplace/src/components/pages/client-app-detail/__test__/app-header.text.tsx
new file mode 100644
index 0000000000..5f5e5b64e2
--- /dev/null
+++ b/packages/marketplace/src/components/pages/client-app-detail/__test__/app-header.text.tsx
@@ -0,0 +1,17 @@
+import React from 'react'
+import AppHeader, { VerifiedByReapit } from '../app-header'
+
+import { appDetailDataStub } from '@/sagas/__stubs__/app-detail'
+import { shallow } from 'enzyme'
+import { AppDetailModel } from '@reapit/foundations-ts-definitions'
+
+describe('AppHeader', () => {
+ test('VerifiedByReapit - should match snapshot', () => {
+ const wrapper = shallow( )
+ expect(wrapper).toMatchSnapshot()
+ })
+ test('AppHeader - should match snapshot', () => {
+ const wrapper = shallow( )
+ expect(wrapper).toMatchSnapshot()
+ })
+})
diff --git a/packages/marketplace/src/components/pages/client-app-detail/__test__/client-app-detail.test.tsx b/packages/marketplace/src/components/pages/client-app-detail/__test__/client-app-detail.test.tsx
index d62b7f7ed6..5aa9cbeb12 100644
--- a/packages/marketplace/src/components/pages/client-app-detail/__test__/client-app-detail.test.tsx
+++ b/packages/marketplace/src/components/pages/client-app-detail/__test__/client-app-detail.test.tsx
@@ -18,7 +18,87 @@ describe('ClientAppDetail', () => {
beforeEach(() => {
/* mocking store */
const mockStore = configureStore()
- store = mockStore(appState)
+ store = mockStore({
+ ...appState,
+ client: {
+ appDetail: {
+ data: {},
+ loading: false,
+ },
+ },
+ })
+ })
+ it('should render loader when isLoadingAppDetail = true', () => {
+ const mockStore = configureStore()
+ const customStore = mockStore({
+ ...appState,
+ client: {
+ appDetail: {
+ isAppDetailLoading: true,
+ data: {},
+ },
+ },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper).toMatchSnapshot()
+ const loader = wrapper.find('[data-test="client-app-detail-loader"]')
+ expect(loader.length).toBe(1)
+ })
+ it('should render loader when client.appDetail.data is an empty object', () => {
+ const mockStore = configureStore()
+ const customStore = mockStore({
+ ...appState,
+ client: {
+ appDetail: {
+ data: {},
+ loading: false,
+ },
+ },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper).toMatchSnapshot()
+ const loader = wrapper.find('[data-test="client-app-detail-loader"]')
+ expect(loader.length).toBe(1)
+ })
+ it('should not render loader when client.appDetail.data is not empty object and isLoadingAppDetail = true', () => {
+ const mockStore = configureStore()
+ const customStore = mockStore({
+ ...appState,
+ client: {
+ appDetail: {
+ data: { key: 'value' },
+ loading: false,
+ },
+ },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper).toMatchSnapshot()
+ const container = wrapper.find('[data-test="client-app-detail-container"]')
+ expect(container.length).toBe(1)
})
it('should match a snapshot', () => {
expect(
@@ -36,30 +116,38 @@ describe('ClientAppDetail', () => {
const mockAppId = 'test'
const mockInstalledOn = '2020-2-20'
it('should match snapshot', () => {
- const wrapper = shallow({renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), false)}
)
- expect(wrapper).toMatchSnapshot()
+ const wrapperWithIsInstallBtnHiddenTrue = shallow(
+ {renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), true)}
,
+ )
+ expect(wrapperWithIsInstallBtnHiddenTrue).toMatchSnapshot()
+ const wrapperWithIsInstallBtnHiddenFalse = shallow(
+ {renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), false)}
,
+ )
+ expect(wrapperWithIsInstallBtnHiddenFalse).toMatchSnapshot()
})
it('should render header button group when appId is existed', () => {
- const testRenderer = TestRenderer.create(renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), false))
+ const testRenderer = TestRenderer.create(
+ renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), false),
+ )
const testInstance = testRenderer.root
expect(testInstance.children.length).toBe(1)
})
it('should render install app button if installedOn is empty', () => {
- const testRenderer = TestRenderer.create(renderAppHeaderButtonGroup(mockAppId, '', jest.fn(), false))
+ const testRenderer = TestRenderer.create(renderAppHeaderButtonGroup(mockAppId, '', jest.fn(), jest.fn(), false))
const testInstance = testRenderer.root
expect(testInstance.findByType(Button).props.children).toBe('Install App')
})
- it('should render installed label if installedOn is existed', () => {
- const testRenderer = TestRenderer.create(renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), false))
+ it('should render uninstall app button if installedOn is existed', () => {
+ const testRenderer = TestRenderer.create(
+ renderAppHeaderButtonGroup(mockAppId, 'exist', jest.fn(), jest.fn(), false),
+ )
const testInstance = testRenderer.root
- expect(
- testInstance.findByProps({
- id: 'installed-label-container',
- }).children.length,
- ).toBeGreaterThan(0)
+ expect(testInstance.findByType(Button).props.children).toBe('Uninstall App')
})
it('should not render header button group when appId is empty', () => {
- const testRenderer = TestRenderer.create(renderAppHeaderButtonGroup('', mockInstalledOn, jest.fn(), false))
+ const testRenderer = TestRenderer.create(
+ renderAppHeaderButtonGroup('', mockInstalledOn, jest.fn(), jest.fn(), false),
+ )
const testInstance = testRenderer.getInstance
expect(testInstance).toHaveLength(0)
})
diff --git a/packages/marketplace/src/components/pages/client-app-detail/app-content.tsx b/packages/marketplace/src/components/pages/client-app-detail/app-content.tsx
new file mode 100644
index 0000000000..d96e552c95
--- /dev/null
+++ b/packages/marketplace/src/components/pages/client-app-detail/app-content.tsx
@@ -0,0 +1,114 @@
+import * as React from 'react'
+import { H5, HTMLRender, Button } from '@reapit/elements'
+import { CategoryModel, DesktopIntegrationTypeModel } from '@reapit/foundations-ts-definitions'
+import styles from '@/styles/blocks/app-detail.scss?mod'
+import { ScopeModel, MediaModel } from '@reapit/foundations-ts-definitions'
+import '@/styles/vendor/slick.scss'
+import clientAppDetailStyles from '@/styles/pages/client-app-detail.scss?mod'
+
+import { AppDetailDataNotNull } from '@/reducers/client/app-detail'
+
+export type AppContentProps = {
+ appDetailData: AppDetailDataNotNull
+ desktopIntegrationTypes: DesktopIntegrationTypeModel[]
+}
+
+export const Tag = ({ children }) => {children}
+
+export const renderDescripion = (description: string) => (
+
+
Description
+
+
+)
+
+export const renderPermissions = (scopes: ScopeModel[] = []) => (
+
+
Permission Required
+
+ {scopes.map(scope => (
+ {scope?.description}
+ ))}
+
+
+)
+
+export const renderCategory = (category: CategoryModel | undefined) => {
+ if (!category) {
+ return null
+ }
+
+ return (
+
+
Category
+ {category.name}
+
+ )
+}
+
+export const renderDesktopIntegrationTypes = (desktopIntegrationTypes: DesktopIntegrationTypeModel[]) => {
+ if (desktopIntegrationTypes.length === 0) {
+ return null
+ }
+
+ return (
+
+
Destop Integration
+ {desktopIntegrationTypes.map(({ name }) => (
+ {name}
+ ))}
+
+ )
+}
+
+export const RenderWithHeader: React.FC<{ header: string }> = ({ header, children }) => (
+
+
{header}
+ {children}
+
+)
+
+export const renderExtraMedia = (media: MediaModel[] = []) =>
+ media.map(({ uri }) => (
+
+
+
+ ))
+
+const AppContent: React.FC = ({ appDetailData, desktopIntegrationTypes = [] }) => {
+ const { summary = '', description = '', category, scopes = [], media = [] } = appDetailData
+ /**
+ * 0 = icon
+ * 1 = featured media
+ * 2 -> nth: extra media
+ */
+ const extraMedia = media.filter((_, index) => index > 1)
+
+ return (
+
+
{summary}
+ {renderDescripion(description)}
+ {summary &&
{summary}
}
+ {media[0] &&
}
+ {renderPermissions(scopes)}
+ {renderExtraMedia(extraMedia)}
+ {renderCategory(category)}
+ {renderDesktopIntegrationTypes(desktopIntegrationTypes)}
+
TBC
+
+
+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam aliquid culpa saepe asperiores debitis illo
+ dolorum dolore incidunt officiis praesentium, nemo similique veritatis exercitationem perferendis non mollitia
+ animi laboriosam perspiciatis.
+
+
+
+
+ NEED HELP?
+
+
+
+ )
+}
+
+export default AppContent
diff --git a/packages/marketplace/src/components/pages/client-app-detail/app-header.tsx b/packages/marketplace/src/components/pages/client-app-detail/app-header.tsx
new file mode 100644
index 0000000000..db76acf5aa
--- /dev/null
+++ b/packages/marketplace/src/components/pages/client-app-detail/app-header.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react'
+import { H2 } from '@reapit/elements'
+import { FaCheck } from 'react-icons/fa'
+import developerAppDetailstyles from '@/styles/pages/developer-app-detail.scss?mod'
+import { AppDetailModel } from '@reapit/foundations-ts-definitions'
+import clientAppDetailStyles from '@/styles/pages/client-app-detail.scss?mod'
+
+export type AppHeaderProps = {
+ appDetailData: AppDetailModel & {
+ apiKey?: string | undefined
+ }
+ buttonGroup?: React.ReactNode
+}
+
+const appIconContainerClassName = [
+ developerAppDetailstyles.appIconContainer,
+ clientAppDetailStyles.appIconContainer,
+].join(' ')
+
+const appIconClassName = ['images', clientAppDetailStyles.appIcon].join(' ')
+
+const placeHolderUrl = 'https://bulma.io/images/placeholders/48x48.png'
+
+export const VerifiedByReapit: React.FC<{ appName: string }> = ({ appName }) => (
+
+
{appName}
+
+ Verified by Reapit
+
+
+)
+
+const AppHeader: React.FC = ({ appDetailData, buttonGroup }) => {
+ const { media, name = '' } = appDetailData
+ const appIcon = media?.filter(({ type }) => type === 'icon')[0]
+
+ return (
+
+
+
+
+
+
+
+ {buttonGroup}
+
+
+
+
+
+ )
+}
+
+export default AppHeader
diff --git a/packages/marketplace/src/components/pages/client-app-detail/client-app-detail.tsx b/packages/marketplace/src/components/pages/client-app-detail/client-app-detail.tsx
index 00e8db6e54..b4efa77335 100644
--- a/packages/marketplace/src/components/pages/client-app-detail/client-app-detail.tsx
+++ b/packages/marketplace/src/components/pages/client-app-detail/client-app-detail.tsx
@@ -1,17 +1,29 @@
import * as React from 'react'
-import { FaCheck } from 'react-icons/fa'
+import {
+ handleUninstallAppButtonClick,
+ handleCloseUninstallConfirmationModal,
+} from '../client-app-detail-manage/client-app-detail-manage'
+import ClientAppUninstallConfirmation from '@/components/ui/client-app-detail/client-app-uninstall-confirmation'
+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 { selectLoginType, selectIsAdmin } from '@/selector/auth'
-import AppHeader from '@/components/ui/app-detail/app-header'
-import AppContent from '@/components/ui/app-detail/app-content'
-
+import AppHeader from './app-header'
+import AppContent from './app-content'
import { Loader, Button } from '@reapit/elements'
-import styles from '@/styles/pages/developer-app-detail.scss?mod'
+import developerAppDetailStyles from '@/styles/pages/developer-app-detail.scss?mod'
+import clientAppDetailStyles from '@/styles/pages/client-app-detail.scss?mod'
import ClientAppInstallConfirmation from '@/components/ui/client-app-detail/client-app-install-confirmation'
export type ClientAppDetailProps = {}
+const appDetailContainerClassNames = [
+ clientAppDetailStyles.appDetailContainer,
+ developerAppDetailStyles.appDetailContainer,
+].join(' ')
+
export const handleCloseInstallConfirmationModal = (setIsVisibleInstallConfirmation: (isVisible: boolean) => void) => {
return () => {
setIsVisibleInstallConfirmation(false)
@@ -28,17 +40,22 @@ export const renderAppHeaderButtonGroup = (
id: string,
installedOn: string,
onInstallConfirmationModal: () => void,
+ onUninstallConfirmationModal: () => void,
isInstallBtnHidden: boolean,
) => {
return (
<>
{id && (
-
+
{installedOn ? (
-
-
- Installed
-
+
+ Uninstall App
+
) : (
isInstallBtnHidden || (
= () => {
const [isVisibleInstallConfirmation, setIsVisibleInstallConfirmation] = React.useState(false)
+ const [isVisibleUninstallConfirmation, setIsVisibleUninstallConfirmation] = React.useState(false)
+ const closeUninstallConfirmationModal = React.useCallback(
+ handleCloseUninstallConfirmationModal(setIsVisibleUninstallConfirmation),
+ [],
+ )
const closeInstallConfirmationModal = React.useCallback(
handleCloseInstallConfirmationModal(setIsVisibleInstallConfirmation),
[],
)
+
const onInstallConfirmationModal = React.useCallback(handleInstallAppButtonClick(setIsVisibleInstallConfirmation), [])
+ const onUninstsallConfirmationModal = React.useCallback(
+ handleUninstallAppButtonClick(setIsVisibleUninstallConfirmation),
+ [],
+ )
- const appDetailData = useSelector(selectAppDetailData)
+ const desktopIntegrationTypes = useSelector(selectIntegrationTypes) as DesktopIntegrationTypeModel[]
+ const appDetailData = useSelector(selectAppDetailData) as AppDetailDataNotNull
const isLoadingAppDetail = useSelector(selectAppDetailLoading)
const loginType = useSelector(selectLoginType)
const isAdmin = useSelector(selectIsAdmin)
-
const isInstallBtnHidden = loginType === 'CLIENT' && !isAdmin
+ // selector selectAppDetailData return {} if not data
+ const unfetched = Object.keys(appDetailData).length === 0
const { id = '', installedOn = '' } = appDetailData
+ if (isLoadingAppDetail || unfetched) {
+ return
+ }
+
return (
-
+
-
+
+ {isVisibleUninstallConfirmation && (
+
+ )}
{isVisibleInstallConfirmation && (
= () => {
closeInstallConfirmationModal={closeInstallConfirmationModal}
/>
)}
- {isLoadingAppDetail && }
)
}
diff --git a/packages/marketplace/src/components/ui/client-app-detail/__test__/__snapshots__/client-app-uninstall-confirmation.test.tsx.snap b/packages/marketplace/src/components/ui/client-app-detail/__test__/__snapshots__/client-app-uninstall-confirmation.test.tsx.snap
index 037c83f1cc..3d26f55c22 100644
--- a/packages/marketplace/src/components/ui/client-app-detail/__test__/__snapshots__/client-app-uninstall-confirmation.test.tsx.snap
+++ b/packages/marketplace/src/components/ui/client-app-detail/__test__/__snapshots__/client-app-uninstall-confirmation.test.tsx.snap
@@ -150,7 +150,7 @@ exports[`ClientAppUninstallConfirmation should match a snapshot 1`] = `
}
- title="Confirm Peter's Properties installation"
+ title="Confirm Peter's Properties uninstallation"
visible={true}
/>
diff --git a/packages/marketplace/src/components/ui/developer-analytics/cost-explorer/cost-explorer-component/cost-explorer.tsx b/packages/marketplace/src/components/ui/developer-analytics/cost-explorer/cost-explorer-component/cost-explorer.tsx
index 1b2ec95706..c611984590 100644
--- a/packages/marketplace/src/components/ui/developer-analytics/cost-explorer/cost-explorer-component/cost-explorer.tsx
+++ b/packages/marketplace/src/components/ui/developer-analytics/cost-explorer/cost-explorer-component/cost-explorer.tsx
@@ -6,7 +6,7 @@ import CostExplorerTable from './cost-explorer-table'
import { useDispatch, useSelector } from 'react-redux'
import { Dispatch } from 'redux'
import { fetchMonthlyBilling } from '@/actions/developer'
-import { selectDeveloperApps } from '@/selector'
+import { selectDeveloperApps } from '@/selector/developer'
import { AppSummaryModel } from '@reapit/foundations-ts-definitions'
export type CostExplorerProps = {}
diff --git a/packages/marketplace/src/reducers/client/app-detail.ts b/packages/marketplace/src/reducers/client/app-detail.ts
index c3a8488bf8..03c373e2a8 100644
--- a/packages/marketplace/src/reducers/client/app-detail.ts
+++ b/packages/marketplace/src/reducers/client/app-detail.ts
@@ -3,7 +3,11 @@ import { Action } from '@/types/core'
import { clientFetchAppDetail, clientFetchAppDetailSuccess, clientFetchAppDetailFailed } from '@/actions/client'
import { isType } from '@/utils/actions'
-export type AppDetailData = (AppDetailModel & { apiKey?: string }) | null
+export type AppDetailDataNotNull = AppDetailModel & {
+ apiKey?: string
+}
+
+export type AppDetailData = AppDetailDataNotNull | null
export interface ClientAppDetailState {
data: AppDetailData
diff --git a/packages/marketplace/src/sagas/apps/__test__/apps.test.ts b/packages/marketplace/src/sagas/apps/__test__/apps.test.ts
index 98749e56a2..f160aecae7 100644
--- a/packages/marketplace/src/sagas/apps/__test__/apps.test.ts
+++ b/packages/marketplace/src/sagas/apps/__test__/apps.test.ts
@@ -1,9 +1,11 @@
+import { integrationTypesReceiveData } from '@/actions/app-integration-types'
import appDetailSagas, {
fetchClientAppDetailSaga,
clientAppDetailDataListen,
fetchDeveloperAppDetailSaga,
developerAppDetailDataListen,
} from '../apps'
+import { fetchDesktopIntegrationTypes } from '@/services/apps'
import ActionTypes from '@/constants/action-types'
import { put, takeLatest, all, fork, call } from '@redux-saga/core/effects'
import { Action } from '@/types/core'
@@ -14,6 +16,7 @@ import { FetchAppByIdParams, fetchAppById } from '@/services/apps'
import { fetchApiKeyInstallationById } from '@/services/installations'
import { clientFetchAppDetailSuccess } from '@/actions/client'
import { appDetailDataStub } from '@/sagas/__stubs__/app-detail'
+import { integrationTypesStub } from '@/sagas/__stubs__/integration-types'
import { developerFetchAppDetailSuccess } from '@/actions/developer'
jest.mock('@reapit/elements')
@@ -36,7 +39,9 @@ describe('fetch client app detail with clientId', () => {
test('api call success', () => {
const clone = gen.clone()
- expect(clone.next(appDetailDataStub.data).value).toEqual(put(clientFetchAppDetailSuccess(appDetailDataStub.data)))
+ expect(clone.next(appDetailDataStub.data).value).toEqual(call(fetchDesktopIntegrationTypes))
+ expect(clone.next(integrationTypesStub).value).toEqual(put(integrationTypesReceiveData(integrationTypesStub)))
+ expect(clone.next().value).toEqual(put(clientFetchAppDetailSuccess(appDetailDataStub.data)))
})
test('api call error', () => {
@@ -59,7 +64,9 @@ describe('fetch client app detail without clientId', () => {
test('api call success', () => {
const clone = gen.clone()
- expect(clone.next(appDetailDataStub.data).value).toEqual(put(clientFetchAppDetailSuccess(appDetailDataStub.data)))
+ expect(clone.next(appDetailDataStub.data).value).toEqual(call(fetchDesktopIntegrationTypes))
+ expect(clone.next(integrationTypesStub).value).toEqual(put(integrationTypesReceiveData(integrationTypesStub)))
+ expect(clone.next().value).toEqual(put(clientFetchAppDetailSuccess(appDetailDataStub.data)))
})
test('api call error', () => {
@@ -91,9 +98,18 @@ describe('client app detail fetch data and fetch apiKey', () => {
isWebComponent,
installationId,
}).value,
- ).toEqual(call(fetchApiKeyInstallationById, { installationId }))
- expect(clone.next({ apiKey }).value).toEqual(
- put(clientFetchAppDetailSuccess({ ...appDetailDataStub.data, isWebComponent, installationId, apiKey })),
+ ).toMatchObject(call(fetchApiKeyInstallationById, { installationId }))
+ expect(clone.next({ apiKey }).value).toEqual(call(fetchDesktopIntegrationTypes))
+ expect(clone.next(integrationTypesStub).value).toMatchObject(put(integrationTypesReceiveData(integrationTypesStub)))
+ expect(clone.next().value).toEqual(
+ put(
+ clientFetchAppDetailSuccess({
+ ...appDetailDataStub.data,
+ apiKey,
+ isWebComponent,
+ installationId,
+ }),
+ ),
)
})
@@ -172,13 +188,16 @@ describe('client app detail fetch data and fetch apiKey', () => {
const installationId = '09682122-0811-4f36-9bfa-05e337de3065'
const isWebComponent = true
const apiKey = 'mockApiKey'
+ // keep getting "serialize the same string here - jest bug"
expect(
- clone.next({
- ...appDetailDataStub.data,
- isWebComponent,
- installationId,
- }).value,
- ).toEqual(call(fetchApiKeyInstallationById, { installationId }))
+ JSON.stringify(
+ clone.next({
+ ...appDetailDataStub.data,
+ isWebComponent,
+ installationId,
+ }).value,
+ ),
+ ).toBe(JSON.stringify(call(fetchApiKeyInstallationById, { installationId })))
expect(clone.next({ apiKey }).value).toEqual(
put(developerFetchAppDetailSuccess({ ...appDetailDataStub.data, isWebComponent, installationId, apiKey })),
)
diff --git a/packages/marketplace/src/sagas/apps/apps.ts b/packages/marketplace/src/sagas/apps/apps.ts
index 26f57cf871..52def57db7 100644
--- a/packages/marketplace/src/sagas/apps/apps.ts
+++ b/packages/marketplace/src/sagas/apps/apps.ts
@@ -1,13 +1,14 @@
import { clientFetchAppDetailSuccess } from '@/actions/client'
+import { integrationTypesReceiveData } from '@/actions/app-integration-types'
import { put, call, fork, takeLatest, all } from '@redux-saga/core/effects'
import ActionTypes from '@/constants/action-types'
import { errorThrownServer } from '@/actions/error'
import errorMessages from '@/constants/error-messages'
import { Action } from '@/types/core'
import { logger } from 'logger'
-import { fetchAppById, FetchAppByIdParams } from '@/services/apps'
-import { fetchApiKeyInstallationById } from '@/services/installations'
+import { fetchAppById, fetchDesktopIntegrationTypes, FetchAppByIdParams } from '@/services/apps'
import { developerFetchAppDetailSuccess } from '@/actions/developer'
+import { fetchApiKeyInstallationById } from '@/services/installations'
export const fetchClientAppDetailSaga = function*({ data }: Action) {
try {
@@ -18,6 +19,9 @@ export const fetchClientAppDetailSaga = function*({ data }: Action {
+ it('fetcher should run correctly', () => {
+ const appId = 'testAppid'
+ const clientId = 'testClientId'
+ const headers = generateHeader(window.reapit.config.marketplaceApiKey)
+ const url = `${URLS.apps}/${appId}?clientId=${clientId}`
+
+ fetchDesktopIntegrationTypes().then(() => {
+ expect(fetcher).toBeCalledWith({
+ headers,
+ url,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ })
+ })
+ })
+})
+
+describe('fetchAppDetail', () => {
+ it('fetcher should run correctly in case clientId is existed', () => {
+ const appId = 'testAppid'
+ const clientId = 'testClientId'
+ const headers = generateHeader(window.reapit.config.marketplaceApiKey)
+ const url = `${URLS.apps}/${appId}?clientId=${clientId}`
+
+ fetchAppDetail({ clientId, id: appId }).then(() => {
+ expect(fetcher).toBeCalledWith({
+ headers,
+ url,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ })
+ })
+ })
+ it('fetcher should run correctly in case clientId is empty', () => {
+ const appId = 'testAppid'
+ const headers = generateHeader(window.reapit.config.marketplaceApiKey)
+ const url = `${URLS.apps}/${appId}`
+
+ fetchAppDetail({ clientId: null, id: appId }).then(() => {
+ expect(fetcher).toBeCalledWith({
+ headers,
+ url,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ })
+ })
+ })
+})
+
+describe('fetchAppApiKey', () => {
+ it('fetcher should run correctly', () => {
+ const installationId = 'testInstallationId'
+ const headers = generateHeader(window.reapit.config.marketplaceApiKey)
+ const url = `${URLS.installations}/${installationId}/apiKey`
+
+ fetchAppApiKey({ installationId }).then(() => {
+ expect(fetcher).toBeCalledWith({
+ headers,
+ url,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ })
+ })
+ })
+})
diff --git a/packages/marketplace/src/services/apps.ts b/packages/marketplace/src/services/apps.ts
index b820763174..a6b9d83d54 100644
--- a/packages/marketplace/src/services/apps.ts
+++ b/packages/marketplace/src/services/apps.ts
@@ -255,3 +255,33 @@ export const fetchAppSecretById = async (params: FetchAppSecretByIdParams): Prom
throw new Error(error)
}
}
+
+export const fetchDesktopIntegrationTypes = async () => {
+ const response = await fetcher({
+ url: URLS.desktopIntegrationTypes,
+ method: 'GET',
+ api: window.reapit.config.marketplaceApiUrl,
+ headers: generateHeader(window.reapit.config.marketplaceApiKey),
+ })
+ return response
+}
+
+export const fetchAppDetail = async ({ clientId, id }) => {
+ const response = await fetcher({
+ url: clientId ? `${URLS.apps}/${id}?clientId=${clientId}` : `${URLS.apps}/${id}`,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ headers: generateHeader(window.reapit.config.marketplaceApiKey),
+ })
+ return response
+}
+
+export const fetchAppApiKey = async ({ installationId }) => {
+ const response = await fetcher({
+ url: `${URLS.installations}/${installationId}/apiKey`,
+ api: window.reapit.config.marketplaceApiUrl,
+ method: 'GET',
+ headers: generateHeader(window.reapit.config.marketplaceApiKey),
+ })
+ return response
+}
diff --git a/packages/marketplace/src/styles/pages/client-app-detail.scss b/packages/marketplace/src/styles/pages/client-app-detail.scss
new file mode 100644
index 0000000000..254cccf72a
--- /dev/null
+++ b/packages/marketplace/src/styles/pages/client-app-detail.scss
@@ -0,0 +1,69 @@
+@import '../base/colors.scss';
+
+.appHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ h2.appName {
+ font-family: 'Museo Sans W03_300', 'Roboto', Helvetica, Arial, sans-serif;
+ margin-bottom: 0;
+ }
+}
+
+.verifyByReapitContainer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+
+ .check {
+ /*should be px regard their base px (rem) else looks really ugly*/
+ margin-right: 5px;
+ }
+}
+
+.appIconContainer {
+ margin-left: 0;
+ margin-right: 2rem;
+}
+
+.check {
+ color: #a0c862;
+}
+
+.appDetailContainer {
+ height:auto;
+}
+
+.gutter {
+ margin: 1rem 0;
+}
+
+.appContentContainer {
+ h5,
+ h5:not(:last-child) {
+ margin-bottom: 1rem;
+ }
+ h5.headerWithoutMargin {
+ margin-bottom: 0;
+ }
+}
+
+.tag {
+ display: inline-block;
+ padding: 1rem;
+ background: #e3ebf6;
+ margin: 1rem 1rem 0 0;
+}
+
+.permissionList {
+ list-style-type: disc;
+ list-style-position: inside;
+}
+
+.appHeaderFeaturedImage {
+ width: 50%;
+}
+