diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/app-detail-preview.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/app-detail-preview.tsx new file mode 100644 index 0000000000..db6abb1d7a --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/app-detail-preview.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' +import AppContent from './client/app-content' +import { Aside as AppAside } from './client/aside' +import AppHeader from './common/ui-app-header' +import { useParams } from 'react-router' +import { AppDetailData } from '@/reducers/developer' +import { Grid, Loader, GridItem, Section } from '@reapit/elements' +import { BackToAppsSection } from '../app-detail/app-sections' +import useReactResponsive from '@/components/hooks/use-react-responsive' + +export type AppDetailPreviewProps = {} + +export const loadAppDetailPreviewDataFromLocalStorage = ( + appId: string, + setAppDetailPreviewData: React.Dispatch>, +) => () => { + try { + const appDataString = localStorage.getItem('developer-preview-app') + if (!appDataString) { + throw 'No app preview' + } + + const appData = JSON.parse(appDataString) as AppDetailData + if (appData?.id !== appId) { + throw 'No app preview' + } + setAppDetailPreviewData(appData) + } catch (err) {} +} + +const AppDetailPreview: React.FC = () => { + const { isMobile } = useReactResponsive() + const [appDetailPreviewData, setAppDetailPreviewData] = React.useState(null) + const { appId } = useParams() + + React.useEffect(loadAppDetailPreviewDataFromLocalStorage(appId, setAppDetailPreviewData), [appId]) + + return ( + + {!appDetailPreviewData ? ( + + ) : ( + <> + + + + +
+ + + {!isMobile && {}} />} +
+
+ + )} +
+ ) +} +export default AppDetailPreview diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-button-group.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-button-group.test.tsx.snap new file mode 100644 index 0000000000..48dc51fdc2 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-button-group.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClientAppDetailButtonGroup should match snapshot when both buttons are hidden 1`] = ` + + + +`; + +exports[`ClientAppDetailButtonGroup should match snapshot when both buttons are showing 1`] = ` + + + Uninstall App + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-content.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-content.test.tsx.snap new file mode 100644 index 0000000000..288783aeb1 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-content.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClientAppContent ClientAppContent - should match snapshot 1`] = ` + + + + + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-detail.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-detail.test.tsx.snap new file mode 100644 index 0000000000..147f5b6cff --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-detail.test.tsx.snap @@ -0,0 +1,488 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppDetail renderAppHeaderButtonGroup should match snapshot 1`] = ` +
+ +
+`; + +exports[`AppDetail renderAppHeaderButtonGroup should match snapshot 2`] = ` +
+ +
+`; + +exports[`AppDetail should match a snapshot 1`] = ` + + + + + +
+ +
+
+
+
+
+
+ + + + + Confirm + + + Cancel + + + } + title="Confirm undefined uninstallation" + visible={false} + /> + + + + + + Confirm + + + Cancel + + + } + title="Confirm undefined installation" + visible={false} + /> + +
+ + + + + +`; + +exports[`AppDetail should render loader when client.appDetail.data is an empty object 1`] = ` + + + + + +
+ +
+
+
+
+
+
+ + + + + Confirm + + + Cancel + + + } + title="Confirm undefined uninstallation" + visible={false} + /> + + + + + + Confirm + + + Cancel + + + } + title="Confirm undefined installation" + visible={false} + /> + +
+ + + + + +`; + +exports[`AppDetail should render loader when isLoadingAppDetail = true 1`] = ` + + + + + +
+ +
+
+
+
+
+
+ + + + + Confirm + + + Cancel + + + } + title="Confirm undefined uninstallation" + visible={false} + /> + + + + + + Confirm + + + Cancel + + + } + title="Confirm undefined installation" + visible={false} + /> + +
+ + + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-install-confirmation.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-install-confirmation.test.tsx.snap new file mode 100644 index 0000000000..81e038b37a --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-install-confirmation.test.tsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClientAppInstallConfirmation should match a snapshot 1`] = ` + + + + + + + Confirm + + + Cancel + + + } + title="Confirm Peter's Properties installation" + visible={true} + /> + + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-uninstall-confirmation.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-uninstall-confirmation.test.tsx.snap new file mode 100644 index 0000000000..f70d53f0b6 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/app-uninstall-confirmation.test.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClientAppUninstallConfirmation renderUninstallConfirmationModalFooter should match snapshot 1`] = ` +
+ + Confirm + + + Cancel + +
+`; + +exports[`ClientAppUninstallConfirmation should match a snapshot 1`] = ` + + + + + + + Confirm + + + Cancel + + + } + title="Confirm Peter's Properties uninstallation" + visible={true} + /> + + + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/aside.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/aside.test.tsx.snap new file mode 100644 index 0000000000..d57896ae40 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/aside.test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClientAside ClientAside - should match snapshot 1`] = ` + + + + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/contact-developer-modal.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/contact-developer-modal.test.tsx.snap new file mode 100644 index 0000000000..35d000d8cd --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/__snapshots__/contact-developer-modal.test.tsx.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContactDeveloperSection should match snapshot with gutter 1`] = ` + + + NEED HELP? + + + Close + + } + title="Contact Details" + visible={false} + > + + + + + Company name + + + +

+ Reapit Ltd +

+
+
+ + + + Telephone Number + + + +

+ 0777 777 777 +

+
+
+ + + + Support Email + + + +

+ + reapit@reapit.com + +

+
+
+ + + + Home Page + + + +

+ + https://reapit.com + +

+
+
+
+
+
+`; + +exports[`ContactDeveloperSection should match snapshot without gutter 1`] = ` + + + NEED HELP? + + + Close + + } + title="Contact Details" + visible={false} + > + + + + + Company name + + + +

+ Reapit Ltd +

+
+
+ + + + Telephone Number + + + +

+ 0777 777 777 +

+
+
+ + + + Support Email + + + +

+ + reapit@reapit.com + +

+
+
+ + + + Home Page + + + +

+ + https://reapit.com + +

+
+
+
+
+
+`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-button-group.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-button-group.test.tsx new file mode 100644 index 0000000000..4d6ab6e782 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-button-group.test.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { AppDetailButtonGroup } from '../app-detail-button-group' +import { shallow } from 'enzyme' + +describe('ClientAppDetailButtonGroup', () => { + it('should match snapshot when both buttons are showing', () => { + const wrapper = shallow( + , + ) + expect(wrapper).toMatchSnapshot() + }) + + it('should match snapshot when both buttons are hidden', () => { + const wrapper = shallow( + , + ) + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-content.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-content.test.tsx new file mode 100644 index 0000000000..b1afabd887 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-content.test.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import AppContent from '../app-content' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { shallow } from 'enzyme' +import { AppDetailDataNotNull } from '@/reducers/client/app-detail' + +describe('ClientAppContent', () => { + it('ClientAppContent - should match snapshot', () => { + const wrapper = shallow() + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-detail.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-detail.test.tsx new file mode 100644 index 0000000000..e21aba589a --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-detail.test.tsx @@ -0,0 +1,190 @@ +import * as React from 'react' +import * as ReactRedux from 'react-redux' +import TestRenderer from 'react-test-renderer' +import { MemoryRouter } from 'react-router' +import { mount, shallow } from 'enzyme' +import configureStore from 'redux-mock-store' +import { getMockRouterProps } from '@/utils/mock-helper' +import AppDetail, { + handleCloseInstallConfirmationModal, + handleInstallAppButtonClick, + renderAppHeaderButtonGroup, + handleCloseUnInstallConfirmationModal, + handleUnInstallAppButtonClick, + onBackToAppsButtonClick, +} from '../app-detail' +import { Button } from '@reapit/elements' +import Routes from '@/constants/routes' +import appState from '@/reducers/__stubs__/app-state' + +describe('AppDetail', () => { + const { history } = getMockRouterProps({}) + let store + beforeEach(() => { + /* mocking store */ + const mockStore = configureStore() + 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 match a snapshot', () => { + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + describe('renderAppHeaderButtonGroup', () => { + const mockAppId = 'test' + const mockInstalledOn = '2020-2-20' + + it('should match snapshot', () => { + const wrapperWithIsInstallBtnHiddenTrue = shallow( +
{renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), true, 'CLIENT')}
, + ) + expect(wrapperWithIsInstallBtnHiddenTrue).toMatchSnapshot() + const wrapperWithIsInstallBtnHiddenFalse = shallow( +
{renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), false, 'CLIENT')}
, + ) + expect(wrapperWithIsInstallBtnHiddenFalse).toMatchSnapshot() + }) + + it('should render header button group when appId is existed', () => { + const testRenderer = TestRenderer.create( +
{renderAppHeaderButtonGroup(mockAppId, mockInstalledOn, jest.fn(), jest.fn(), false, 'CLIENT')}
, + ) + 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(), jest.fn(), false, 'CLIENT')}
, + ) + const testInstance = testRenderer.root + expect(testInstance.findByType(Button).props.children).toBe('Install App') + }) + + it('should render uninstall app button if installedOn is existed', () => { + const testRenderer = TestRenderer.create( +
{renderAppHeaderButtonGroup(mockAppId, 'exist', jest.fn(), jest.fn(), false, 'CLIENT')}
, + ) + const testInstance = testRenderer.root + 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(), jest.fn(), false, 'CLIENT')}
, + ) + const testInstance = testRenderer.getInstance + expect(testInstance).toHaveLength(0) + }) + }) + + describe('handleCloseInstallConfirmationModal', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleCloseInstallConfirmationModal(mockFunction) + fn() + expect(mockFunction).toBeCalledWith(false) + }) + }) + + describe('handleInstallAppButtonClick', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleInstallAppButtonClick(mockFunction) + fn() + expect(mockFunction).toBeCalledWith(true) + }) + }) + + describe('handleCloseUnInstallConfirmationModal', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleCloseUnInstallConfirmationModal(mockFunction) + fn() + expect(mockFunction).toBeCalledWith(false) + }) + }) + + describe('handleUnInstallAppButtonClick', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleUnInstallAppButtonClick(mockFunction) + fn() + expect(mockFunction).toBeCalledWith(true) + }) + }) + + describe('onBackToAppsButtonClick', () => { + it('should run correctly', () => { + const fn = onBackToAppsButtonClick(history) + fn() + expect(history.push).toBeCalledWith(Routes.APPS) + }) + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-install-confirmation.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-install-confirmation.test.tsx new file mode 100644 index 0000000000..776bf30ef9 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-install-confirmation.test.tsx @@ -0,0 +1,107 @@ +import * as React from 'react' +import * as ReactRedux from 'react-redux' +import { mount } from 'enzyme' +import configureStore from 'redux-mock-store' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { MemoryRouter } from 'react-router' +import AppInstallConfirmation, { + AppInstallConfirmationProps, + handleInstallButtonClick, + handleInstallAppSuccessCallback, + handleSuccessAlertButtonClick, + handleSuccessAlertMessageAfterClose, +} from '../app-install-confirmation' +import { appInstallationsRequestInstall } from '@/actions/app-installations' +import { clientFetchAppDetail } from '@/actions/client' +import routes from '@/constants/routes' +import Routes from '@/constants/routes' +import appState from '@/reducers/__stubs__/app-state' + +const mockProps: AppInstallConfirmationProps = { + appDetailData: appDetailDataStub.data, + visible: true, + closeInstallConfirmationModal: jest.fn(), +} + +describe('ClientAppInstallConfirmation', () => { + let store + let spyDispatch + const appId = mockProps.appDetailData?.id || '' + const clientId = appState.auth.loginSession?.loginIdentity.clientId || '' + + beforeEach(() => { + /* mocking store */ + const mockStore = configureStore() + store = mockStore(appState) + /* mocking useDispatch on our mock store */ + spyDispatch = jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => store.dispatch) + }) + + it('should match a snapshot', () => { + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + describe('handleInstallButtonClick', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleInstallButtonClick( + appId, + clientId, + spyDispatch, + mockFunction, + mockProps.closeInstallConfirmationModal, + false, + ) + fn() + expect(spyDispatch).toBeCalledWith( + appInstallationsRequestInstall({ + appId, + callback: expect.any(Function), + }), + ) + }) + }) + + describe('handleSuccessAlertMessageAfterClose', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleSuccessAlertMessageAfterClose(appId, clientId, mockFunction, spyDispatch) + fn() + expect(mockFunction).toBeCalledWith(false) + expect(spyDispatch).toBeCalledWith( + clientFetchAppDetail({ + id: appId, + clientId, + }), + ) + }) + }) + + describe('handleInstallAppSuccessCallback', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleInstallAppSuccessCallback(mockFunction, mockProps.closeInstallConfirmationModal, false) + fn() + + expect(mockProps.closeInstallConfirmationModal).toBeCalled() + expect(mockFunction).toBeCalledWith(true) + }) + }) + + describe('handleSuccessAlertButtonClick', () => { + const history = { + replace: jest.fn(), + } as any + const fn = handleSuccessAlertButtonClick(history) + fn() + expect(history.replace).toBeCalledWith(routes.APPS) + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-uninstall-confirmation.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-uninstall-confirmation.test.tsx new file mode 100644 index 0000000000..ca10ed9e53 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/app-uninstall-confirmation.test.tsx @@ -0,0 +1,130 @@ +import * as React from 'react' +import * as ReactRedux from 'react-redux' +import { mount, shallow } from 'enzyme' +import configureStore from 'redux-mock-store' +import { MemoryRouter } from 'react-router' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { appInstallationsRequestUninstall } from '@/actions/app-installations' +import { clientFetchAppDetail } from '@/actions/client' +import ClientAppUninstallConfirmation, { + AppUninstallConfirmationProps, + onUninstallButtonClick, + handleUninstallAppSuccessCallback, + handleSuccessAlertButtonClick, + handleSuccessAlertMessageAfterClose, + renderUninstallConfirmationModalFooter, +} from '../app-uninstall-confirmation' +import Routes from '@/constants/routes' +import appState from '@/reducers/__stubs__/app-state' + +const mockProps: AppUninstallConfirmationProps = { + appDetailData: appDetailDataStub.data, + visible: true, + closeUninstallConfirmationModal: jest.fn(), +} + +describe('ClientAppUninstallConfirmation', () => { + let store + let spyDispatch + const appId = mockProps.appDetailData?.id || '' + const installationId = mockProps.appDetailData?.installationId || '' + const clientId = appState.auth.loginSession?.loginIdentity.clientId || '' + + beforeEach(() => { + /* mocking store */ + const mockStore = configureStore() + store = mockStore(appState) + /* mocking useDispatch on our mock store */ + spyDispatch = jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => store.dispatch) + }) + + it('should match a snapshot', () => { + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + describe('onUninstallButtonClick', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = onUninstallButtonClick( + appId, + clientId, + installationId, + spyDispatch, + mockFunction, + mockProps.closeUninstallConfirmationModal, + false, + ) + fn() + expect(spyDispatch).toBeCalledWith( + appInstallationsRequestUninstall({ + appId, + installationId, + terminatedReason: 'User uninstall', + callback: expect.any(Function), + }), + ) + }) + }) + + describe('handleUninstallAppSuccessCallback', () => { + it('should run correctly', () => { + const mockFunction = jest.fn() + const fn = handleUninstallAppSuccessCallback(mockFunction, mockProps.closeUninstallConfirmationModal, false) + fn() + expect(mockProps.closeUninstallConfirmationModal).toBeCalled() + expect(mockFunction).toBeCalledWith(true) + }) + }) + + describe('handleSuccessAlertButtonClick', () => { + const history = { + replace: jest.fn(), + } as any + const fn = handleSuccessAlertButtonClick(history) + fn() + expect(history.replace).toBeCalledWith(Routes.APPS) + }) + + describe('handleSuccessAlertMessageAfterClose', () => { + it('should match snapshot', () => { + const mockFunction = jest.fn() + const fn = handleSuccessAlertMessageAfterClose(appId, clientId, mockFunction, spyDispatch) + fn() + expect(spyDispatch).toBeCalledWith( + clientFetchAppDetail({ + id: appId, + clientId, + }), + ) + expect(mockFunction).toBeCalledWith(false) + }) + }) + + describe('renderUninstallConfirmationModalFooter', () => { + it('should match snapshot', () => { + const wrapper = shallow( +
+ {renderUninstallConfirmationModalFooter( + false, + appId, + clientId, + installationId, + spyDispatch, + jest.fn(), + jest.fn(), + false, + )} +
, + ) + expect(wrapper).toMatchSnapshot() + }) + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/aside.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/aside.test.tsx new file mode 100644 index 0000000000..758bf2021d --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/__tests__/aside.test.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Aside } from '../aside' +import { shallow } from 'enzyme' +import { integrationTypesStub } from '@/sagas/__stubs__/integration-types' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { AppDetailDataNotNull } from '@/reducers/client/app-detail' +import { DesktopIntegrationTypeModel } from '@/actions/app-integration-types' + +describe('ClientAside', () => { + test('ClientAside - should match snapshot', () => { + expect( + shallow( +
+ +
+
+
+ + + + + + } + > +
+ + + + + + + + +
+
+ + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap new file mode 100644 index 0000000000..2cdda1d8f4 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config-modal should match snapshot 1`] = ` + + + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/client-web-component-config.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/client-web-component-config.test.tsx new file mode 100644 index 0000000000..dc9f9fc2fe --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/client-web-component-config.test.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { Provider } from 'react-redux' +import appState from '@/reducers/__stubs__/app-state' +import configureStore from 'redux-mock-store' +import { mount } from 'enzyme' +import { WebComponentConfig } from '../client-web-component-config' +import Routes from '@/constants/routes' +import { MemoryRouter } from 'react-router' + +describe('WebComponentConfig', () => { + const extendStore = { + ...appState, + client: { + webComponent: { isShowModal: true }, + }, + } + const mockStore = configureStore() + const store = mockStore(extendStore) + + it('Should match snapshot', () => { + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal-inner.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal-inner.test.tsx new file mode 100644 index 0000000000..53c60ec457 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal-inner.test.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import { + updateWebComponentConfig, + WebComponentConfigModalFooter, + WebComponentConfigModalInner, + genarateNegotiatorOptions, +} from '../config-modal-inner' +import { mount } from 'enzyme' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import { UpdateWebComponentConfigParams } from '@/services/web-component' +import { clientUpdateWebComponentConfig } from '@/actions/client' +import { webComponentStub } from '../__stubs__/web-component-config' +import appState from '@/reducers/__stubs__/app-state' +import { FormikProps } from '@reapit/elements' + +const params = { + appId: 'appid', + appointmentLength: 1, + appointmentTimeGap: 1, + customerId: 'string', + daysOfWeek: ['1', '2'], +} as UpdateWebComponentConfigParams + +const extendAppState = webComponent => { + return { + ...appState, + client: { ...appState.client, webComponent }, + } +} + +describe('Config-modal-inner', () => { + const mockStore = configureStore() + const store = mockStore(extendAppState(webComponentStub)) + + it('should WebComponentConfigModalFooter match a snapshot', () => { + const mockProps = { + closeModal: jest.fn(), + formikProps: {} as FormikProps, + } + expect( + mount( + + + , + ), + ).toMatchSnapshot() + }) + + it('should WebComponentConfigModalInner match a snapshot', () => { + const mockStore = configureStore() + const store = mockStore(extendAppState(webComponentStub)) + expect( + mount( + + + , + ), + ).toMatchSnapshot() + }) + + it('should updateWebComponentConfig run correctly', () => { + const dispatch = jest.fn() + const closeModal = jest.fn() + + const fn = updateWebComponentConfig(dispatch, 'appid', closeModal) + fn(params) + expect(dispatch).toBeCalledWith(clientUpdateWebComponentConfig({ ...params, callback: closeModal })) + }) +}) + +describe('should return correctly', () => { + it('should return correctly', () => { + const list = [ + { + active: true, + created: 'string', + email: 'string', + id: 'string', + jobTitle: 'string', + metadata: 'any', + mobilePhone: 'string', + modified: 'string', + name: 'string', + officeId: 'string', + workPhone: 'string', + }, + ] + + const result = genarateNegotiatorOptions(list) + const expected = [{ value: 'string', label: 'string', description: 'string' }] + expect(result).toEqual(expected) + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal.test.tsx new file mode 100644 index 0000000000..b696c59bd9 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/__test__/config-modal.test.tsx @@ -0,0 +1,15 @@ +import { WebComponentModal, WebComponentModalProps } from '../config-modal' +import React from 'react' +import { shallow } from 'enzyme' + +describe('Config-modal', () => { + it('should match snapshot', () => { + const mockProps = { + type: 'BOOK_VIEWING', + afterClose: jest.fn(), + closeModal: jest.fn(), + } as WebComponentModalProps + + expect(shallow()).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/client-web-component-config.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/client-web-component-config.tsx new file mode 100644 index 0000000000..e8316fd1b0 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/client-web-component-config.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react' +import { Button } from '@reapit/elements' +import { useDispatch, useSelector } from 'react-redux' +import { selectWebComponentData } from '@/selector/client' +import { clientFetchWebComponentConfig } from '@/actions/client' +import { Dispatch } from 'redux' +import WebComponentModal from '@/components/pages/app-detail/client/web-component-config-modal/config-modal' +import { AppDetailSection } from '../../common/ui-helpers' +import { selectClientId } from '@/selector/auth' +import { useParams } from 'react-router-dom' + +export const toggleWebComponentModal = (setIsOpenConfigModal, isOpen) => () => { + setIsOpenConfigModal(isOpen) +} + +export const handleFetchWebComponentConfig = ( + dispatch: Dispatch, + customerId?: string, + applicationId?: string, +) => () => { + customerId && applicationId && dispatch(clientFetchWebComponentConfig({ customerId, applicationId })) +} + +export const WebComponentConfig: React.FC = () => { + const dispatch = useDispatch() + const [isOpenConfigModal, setIsOpenConfigModal] = useState(false) + + const clientId = useSelector(selectClientId) || '' + const webComponentData = useSelector(selectWebComponentData) + const { appid: applicationId } = useParams() + + const handleToggleWebComponentModal = toggleWebComponentModal(setIsOpenConfigModal, true) + const handleCloseWebComponentModal = toggleWebComponentModal(setIsOpenConfigModal, false) + + useEffect(handleFetchWebComponentConfig(dispatch, clientId, applicationId), []) + + if (!webComponentData) return null + + return ( + + + {isOpenConfigModal && ( + + )} + + ) +} + +export default WebComponentConfig diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal-inner.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal-inner.tsx new file mode 100644 index 0000000000..f3df5e8680 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal-inner.tsx @@ -0,0 +1,172 @@ +import React from 'react' +import { + ModalHeader, + ModalBody, + ModalFooter, + Button, + Formik, + RadioSelect, + Checkbox, + Loader, + FormikValues, + DropdownSelect, + SelectOption, + FormikProps, +} from '@reapit/elements' +import { useDispatch, useSelector } from 'react-redux' +import { clientUpdateWebComponentConfig } from '@/actions/client' +import { + selectWebComponentData, + selectWebComponentLoading, + selectWebComponentUpdating, + selectWebComponentNegotiators, +} from '@/selector/client' +import { UpdateWebComponentConfigParams } from '@/services/web-component' +import { Dispatch } from 'redux' +import { NegotiatorItem } from '@/services/negotiators' +import { selectAppDetailData } from '@/selector/client-app-detail' + +export const updateWebComponentConfig = (dispatch: Dispatch, appId: string, callback) => (params: FormikValues) => { + dispatch(clientUpdateWebComponentConfig({ ...params, appId, callback } as UpdateWebComponentConfigParams)) +} + +export const genarateNegotiatorOptions = (negotiators: NegotiatorItem[]): SelectOption[] => { + return negotiators.map( + negotiator => + ({ + value: negotiator.id, + label: negotiator.name, + description: negotiator.name, + } as SelectOption), + ) +} +export type WebComponentConfigModalBodyProps = { + subtext: string + formikProps: FormikProps +} +export const WebComponentConfigModalBody = ({ subtext, formikProps }: WebComponentConfigModalBodyProps) => { + const { values, setFieldValue } = formikProps + const negotiators = useSelector(selectWebComponentNegotiators) + const negotiatorOptions = genarateNegotiatorOptions(negotiators) + + return ( + <> +

{subtext}

+ + +
+
+ + +
+ + + + + + + +
+
+
+ + + ) +} + +export type WebComponentConfigModalFooterProps = { + closeModal: () => void + formikProps: FormikProps +} + +export const WebComponentConfigModalFooter = ({ closeModal, formikProps }: WebComponentConfigModalFooterProps) => { + const updating = useSelector(selectWebComponentUpdating) + const { handleSubmit } = formikProps + return ( + <> + + + + ) +} + +export type WebComponentConfigModalInnerProps = { + closeModal: () => void +} + +export const WebComponentConfigModalInner = ({ closeModal }: WebComponentConfigModalInnerProps) => { + const dispatch = useDispatch() + + const webComponentData = useSelector(selectWebComponentData) + const loading = useSelector(selectWebComponentLoading) + const appDetails = useSelector(selectAppDetailData) + const { name, id = '' } = appDetails + + const handleUpdateWebComponentConfig = updateWebComponentConfig(dispatch, id, closeModal) + + const title = `${name} Configuration` + const subtext = `Please use the following form to configure your diary settings for your + ‘${name}’ widget on your website` + + const initialFormValues = (webComponentData || { + appointmentLength: 30, + appointmentTimeGap: 30, + daysOfWeek: ['1', '2', '3', '4', '5', '6'], + }) as FormikValues + + if (loading) return + return ( + + {formikProps => ( + <> + + } /> + } + /> + + )} + + ) +} diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal.tsx new file mode 100644 index 0000000000..2c498c47bc --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/config-modal.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { Modal } from '@reapit/elements' +import { WebComponentConfigModalInner } from './config-modal-inner' + +export type WebComponentModalProps = { + afterClose: () => void + closeModal: () => void +} + +export const WebComponentModal = ({ afterClose, closeModal }: WebComponentModalProps) => { + return ( + + + + ) +} +export default WebComponentModal diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/index.ts b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/index.ts new file mode 100644 index 0000000000..fb707ab989 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/client/web-component-config-modal/index.ts @@ -0,0 +1 @@ +export { default } from './client-web-component-config' diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/app-authentication-detail.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/app-authentication-detail.test.tsx.snap new file mode 100644 index 0000000000..cb2c6409a3 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/app-authentication-detail.test.tsx.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppAuthenticationDetail should match a snapshot 1`] = ` + + + +
+ +
+ Authentication: +
+
+ + Show Secret + +
+
+
+
+`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-app-header.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-app-header.test.tsx.snap new file mode 100644 index 0000000000..53470f105f --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-app-header.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppHeader should match a snapshot 1`] = ` + + +
+
+ Peter's Properties +
+
+ + Peter's Properties + + + Verified by Reapit + + +
+
+
+ + Featured Image + +
+`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-helpers.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-helpers.test.tsx.snap new file mode 100644 index 0000000000..63ca3cfe79 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-helpers.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ui Sections should match snapshot 1`] = ` + + + test + + some text + +`; + +exports[`Ui Sections should match snapshot for has image 1`] = ` + + some image + +`; + +exports[`Ui Sections should match snapshot for no images 1`] = `""`; + +exports[`Ui Sections should match snapshot 1`] = ` +
+ Test +
+`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-sections.test.tsx.snap b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-sections.test.tsx.snap new file mode 100644 index 0000000000..84daec0213 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/__snapshots__/ui-sections.test.tsx.snap @@ -0,0 +1,431 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdditionalImagesSection should match a snapshot where has images 1`] = ` + + + + + + + + +`; + +exports[`AdditionalImagesSection should match a snapshot where has no images 1`] = `""`; + +exports[`AuthenticationSection should match a snapshot where authFlow is CLIENT_SECRET and not sidebar 1`] = ` + +
+ + Client ID: + + SOME_ID +
+
+`; + +exports[`AuthenticationSection should match a snapshot where authFlow is USER_SESSION sidebar 1`] = ` + +
+ + Client ID: + + SOME_ID +
+
+`; + +exports[`BackToAppsSection should match a snapshot 1`] = ` + + + Back To Apps + + +`; + +exports[`CategorySection should match a snapshot where category is defined and is sidebar 1`] = ` + +

+ None +

+
+`; + +exports[`CategorySection should match a snapshot where category undefined and not sidebar 1`] = ` + +

+ None +

+
+`; + +exports[`DescriptionSection should match a snapshot 1`] = ` + +`; + +exports[`DesktopIntegrationSection should match a snapshot where desktopIntegrationTypes empty and not sidebar 1`] = ` + +

+ None +

+
+`; + +exports[`DesktopIntegrationSection should match a snapshot where desktopIntegrationTypes hasLength and is sidebar 1`] = ` + + + Identity Check + + + Property Marketing Information + + + Vendor Marketing Report + + + Property Details Generation + + + Applicant Export + + + Property + + + Applicant + + +`; + +exports[`DeveloperAboutSection should match a snapshot 1`] = ` + +`; + +exports[`DeveloperSection should match a snapshot 1`] = ` + + Developer Name + +`; + +exports[`DirectApiSection should match a snapshot where isDirectApi false and not sidebar 1`] = ` + + No + +`; + +exports[`DirectApiSection should match a snapshot where isDirectApi true and is sidebar 1`] = ` + + Yes + +`; + +exports[`InstallationsTableSection should not render a table when has no data 1`] = ` + + + +
+ +
+ Installations +
+
+

+ Currently, there are no installations for your app +

+
+
+
+
+`; + +exports[`InstallationsTableSection should render a table and match a snapshot when has data 1`] = ` + + + +
+ +
+ Installations +
+
+ +
+ + + +
+
+
+
+
+
+
+`; + +exports[`ListingPreviewSection should match a snapshot 1`] = ` + + + See listing preview + + + + + + + } + isSidebar={true} +> +

+ The listing preview will display your app as it would appear in the Marketplace +

+
+`; + +exports[`PermissionsSection should match a snapshot where has no permissions 1`] = ` + + + +`; + +exports[`PermissionsSection should match a snapshot where has permissions 1`] = ` + + + + Read data about developers + + + Write data about developers + + + +`; + +exports[`PrivateAppSection should match a snapshot where limitToClientIds empty and not sidebar 1`] = ` + + No + +`; + +exports[`PrivateAppSection should match a snapshot where limitToClientIds hasLength and is sidebar 1`] = ` + + Yes + +`; + +exports[`StatusSection should match a snapshot where isListed false and not sidebar 1`] = ` + +
+ Not listed +
+
+`; + +exports[`StatusSection should match a snapshot where isListed true and is sidebar 1`] = ` + +
+ Listed + +
+
+`; + +exports[`SummarySection should match a snapshot 1`] = ` + + Lorem ipsum + +`; diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/app-authentication-detail.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/app-authentication-detail.test.tsx new file mode 100644 index 0000000000..98d2688509 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/app-authentication-detail.test.tsx @@ -0,0 +1,63 @@ +import * as React from 'react' +import { mount } from 'enzyme' +import * as ReactRedux from 'react-redux' +import configureStore from 'redux-mock-store' +import appState from '@/reducers/__stubs__/app-state' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { + AppAuthenticationDetail, + AppAuthenticationDetailProps, + handleCopyCode, + handleShowAuthCode, + handleMouseLeave, +} from '../app-authentication-detail' +import { requestAuthenticationCode } from '@/actions/app-detail' + +const props: AppAuthenticationDetailProps = { + appId: appDetailDataStub.data.id || '', +} + +describe('AppAuthenticationDetail', () => { + let store + let spyDispatch + const mockSetTooltipMessage = jest.fn() + + beforeEach(() => { + /* mocking store */ + const mockStore = configureStore() + store = mockStore(appState) + spyDispatch = jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => store.dispatch) + }) + it('should match a snapshot', () => { + expect( + mount( + + + , + ), + ).toMatchSnapshot() + }) + describe('handleCopyCode', () => { + it('should copy the code to clipboard', () => { + const fn = handleCopyCode(mockSetTooltipMessage) + fn() + expect(mockSetTooltipMessage).toHaveBeenCalledWith('Copied') + }) + }) + describe('handleShowAuthCode', () => { + it('should run correctly', () => { + const mockedEvent = { preventDefault: jest.fn() } + const fn = handleShowAuthCode(props.appId, spyDispatch) + fn(mockedEvent) + expect(mockedEvent.preventDefault).toBeCalled() + expect(spyDispatch).toBeCalledWith(requestAuthenticationCode(props.appId)) + }) + }) + describe('handleMouseLeave', () => { + it('should run correctly', () => { + const fn = handleMouseLeave(mockSetTooltipMessage) + fn() + expect(mockSetTooltipMessage).toBeCalledWith('Copy') + }) + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-app-header.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-app-header.test.tsx new file mode 100644 index 0000000000..2312ad09c9 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-app-header.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { shallow } from 'enzyme' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import AppHeader, { AppHeaderProps } from '../ui-app-header' + +const mockProps: AppHeaderProps = { + appDetailData: { + ...appDetailDataStub.data, + apiKey: '', + }, +} + +describe('AppHeader', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-helpers.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-helpers.test.tsx new file mode 100644 index 0000000000..4cdbb85a32 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-helpers.test.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { Tag, AppDetailSection, ImageSection } from '../ui-helpers' +import { shallow } from 'enzyme' + +describe('Ui Sections', () => { + test(' should match snapshot', () => { + const wrapper = shallow( + + some text + , + ) + expect(wrapper).toMatchSnapshot() + }) + + test(' should match snapshot', () => { + const wrapper = shallow(Test) + expect(wrapper).toMatchSnapshot() + }) + + test(' should match snapshot for no images', () => { + const wrapper = shallow(Test) + expect(wrapper).toMatchSnapshot() + }) + + test(' should match snapshot for has image', () => { + const wrapper = shallow( + + Test + , + ) + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-sections.test.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-sections.test.tsx new file mode 100644 index 0000000000..ddf84e6a48 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/__tests__/ui-sections.test.tsx @@ -0,0 +1,200 @@ +import * as React from 'react' +import { shallow, mount } from 'enzyme' +import { + CategorySection, + DesktopIntegrationSection, + PrivateAppSection, + DirectApiSection, + StatusSection, + BackToAppsSection, + ListingPreviewSection, + AuthenticationSection, + SummarySection, + DeveloperSection, + DeveloperAboutSection, + AdditionalImagesSection, +} from '../ui-sections' +import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' +import { integrationTypesStub } from '@/sagas/__stubs__/integration-types' +import { + DesktopIntegrationTypeModel, + InstallationModel, + MediaModel, + ScopeModel, +} from '@reapit/foundations-ts-definitions' +import { Button } from '@reapit/elements' +import { InstallationsTableSection, PermissionsSection, DescriptionSection } from '../ui-sections' +import { installationsStub } from '@/sagas/__stubs__/installations' + +describe('CategorySection', () => { + it('should match a snapshot where category undefined and not sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should match a snapshot where category is defined and is sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('DesktopIntegrationSection', () => { + it('should match a snapshot where desktopIntegrationTypes empty and not sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should match a snapshot where desktopIntegrationTypes hasLength and is sidebar', () => { + expect( + shallow( + , + ), + ).toMatchSnapshot() + }) +}) + +describe('PrivateAppSection', () => { + it('should match a snapshot where limitToClientIds empty and not sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should match a snapshot where limitToClientIds hasLength and is sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('DirectApiSection', () => { + it('should match a snapshot where isDirectApi false and not sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should match a snapshot where isDirectApi true and is sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('StatusSection', () => { + it('should match a snapshot where isListed false and not sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should match a snapshot where isListed true and is sidebar', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('BackToAppsSection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should respond to a button click', () => { + const onClick = jest.fn() + const button = shallow() + .find(Button) + .first() + + button.simulate('click') + expect(onClick).toHaveBeenCalledTimes(1) + }) +}) + +describe('ListingPreviewSection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) + + it('should respond to a link click', () => { + const onClick = jest.fn() + const link = mount() + .find('a') + .first() + + link.simulate('click') + expect(onClick).toHaveBeenCalledTimes(1) + }) +}) + +describe('AuthenticationSection', () => { + it('should match a snapshot where authFlow is CLIENT_SECRET and not sidebar', () => { + expect( + shallow(), + ).toMatchSnapshot() + }) + + it('should match a snapshot where authFlow is USER_SESSION sidebar', () => { + expect( + shallow(), + ).toMatchSnapshot() + }) +}) + +describe('SummarySection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('InstallationsTableSection', () => { + it('should render a table and match a snapshot when has data', () => { + const wrapper = mount( + , + ) + expect(wrapper.find('[data-test="render-installations-table"]').length).toBe(1) + expect(wrapper).toMatchSnapshot() + }) + + it('should not render a table when has no data', () => { + const wrapper = mount() + expect(wrapper.find('[data-test="render-installations-table-empty-text"]').length).toBe(1) + expect(wrapper).toMatchSnapshot() + }) +}) + +describe('DeveloperSection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('DeveloperAboutSection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('AdditionalImagesSection', () => { + it('should match a snapshot where has images', () => { + expect( + shallow( + , + ), + ).toMatchSnapshot() + }) + + it('should match a snapshot where has no images', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('PermissionsSection', () => { + it('should match a snapshot where has permissions', () => { + expect( + shallow(), + ).toMatchSnapshot() + }) + + it('should match a snapshot where has no permissions', () => { + expect(shallow()).toMatchSnapshot() + }) +}) + +describe('DescriptionSection', () => { + it('should match a snapshot', () => { + expect(shallow()).toMatchSnapshot() + }) +}) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/app-authentication-detail.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/app-authentication-detail.tsx new file mode 100644 index 0000000000..e670274857 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/app-authentication-detail.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' +import { Dispatch } from 'redux' +import { useSelector, useDispatch } from 'react-redux' +import { requestAuthenticationCode } from '@/actions/app-detail' +import styles from '@/styles/blocks/app-authentication-detail.scss?mod' +import { Loader, Content, H5 } from '@reapit/elements' +import { FaCopy } from 'react-icons/fa' +import { CopyToClipboard } from 'react-copy-to-clipboard' +import { selectAppAuthenticationCode, selectAppAuthenticationLoading } from '@/selector/app-detail' + +export type AppAuthenticationDetailProps = { + appId: string + withCustomHeader?: boolean +} + +export const handleCopyCode = (setTooltipMessage: React.Dispatch>) => { + return () => { + setTooltipMessage('Copied') + } +} + +export const handleShowAuthCode = (appId: string, dispatch: Dispatch) => { + return e => { + e.preventDefault() + dispatch(requestAuthenticationCode(appId)) + } +} + +export const handleMouseLeave = (setTooltipMessage: React.Dispatch>) => { + return () => { + setTooltipMessage('Copy') + } +} + +export const AppAuthenticationDetail: React.FunctionComponent = ({ + appId, + withCustomHeader, +}) => { + const dispatch = useDispatch() + const [tooltipMessage, setTooltipMessage] = React.useState('Copy') + const loading = useSelector(selectAppAuthenticationLoading) + const code = useSelector(selectAppAuthenticationCode) + + return ( + <> + + {!withCustomHeader &&
Authentication:
} + + Show Secret + +
+ {loading && } + {!loading && code && ( +
+

{code}

+ +
+ + {tooltipMessage} +
+
+
+ )} + + ) +} + +export default AppAuthenticationDetail diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-app-header.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-app-header.tsx new file mode 100644 index 0000000000..9a6b02e8ae --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-app-header.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { H3, Grid, GridItem, SubTitleH6 } from '@reapit/elements' +import { FaCheck } from 'react-icons/fa' +import { AppDetailModel } from '@reapit/foundations-ts-definitions' +import styles from '@/styles/blocks/standalone-app-detail.scss?mod' +import { MEDIA_INDEX } from '@/constants/media' +import ImagePlaceHolder from '@/assets/images/default-app-icon.jpg' +import featureImagePlaceHolder from '@/assets/images/default-feature-image.jpg' +import { cx } from 'linaria' + +export type AppHeaderProps = { + appDetailData: AppDetailModel & { + apiKey?: string | undefined + } + buttonGroup?: React.ReactNode +} + +const AppHeader: React.FC = ({ appDetailData, buttonGroup }) => { + const { media } = appDetailData + const appIcon = media?.filter(({ type }) => type === 'icon')[MEDIA_INDEX.ICON] + const featureImageSrc = appDetailData?.media?.[MEDIA_INDEX.FEATURE_IMAGE]?.uri + const { containerOuterHeader, headerContent, containerHeader, check, appIconContainer } = styles + + return ( + + +
+
+ {appDetailData.name} +
+
+

{appDetailData.name || ''}

+ + Verified by Reapit + + + {buttonGroup && buttonGroup} +
+
+
+ + Featured Image + +
+ ) +} + +export default AppHeader diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-helpers.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-helpers.tsx new file mode 100644 index 0000000000..ba299f4bcd --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-helpers.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import styles from '@/styles/blocks/standalone-app-detail.scss?mod' + +import { H5, Content } from '@reapit/elements' + +interface AppDetailSectionProps { + headerText: string | React.ReactNode + dataTest?: string + isSidebar?: boolean +} + +interface ImageSectionProps { + uri?: string + alt?: string +} + +export const AppDetailSection: React.FC = ({ + headerText, + children, + dataTest = '', + isSidebar = false, +}) => ( + +
{headerText}
+ {children} +
+) + +export const Tag: React.FC = ({ children }) =>
{children}
+ +export const ImageSection: React.FC = ({ uri, alt = '' }) => { + return uri ? ( + + {alt} + + ) : null +} diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-sections.tsx b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-sections.tsx new file mode 100644 index 0000000000..f44d07d923 --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/common/ui-sections.tsx @@ -0,0 +1,247 @@ +import * as React from 'react' +import { + CategoryModel, + DesktopIntegrationTypeModel, + InstallationModel, + MediaModel, + ScopeModel, +} from '@reapit/foundations-ts-definitions' +import { AppDetailSection, Tag, ImageSection } from './ui-helpers' +import { convertBooleanToYesNoString } from '@/utils/boolean-to-yes-no-string' +import { FaCheck, FaExternalLinkAlt } from 'react-icons/fa' +import styles from '@/styles/blocks/standalone-app-detail.scss?mod' +import { + LevelRight, + Button, + Table, + Grid, + GridItem, + GridFourCol, + GridFourColItem, + HTMLRender, + Content, +} from '@reapit/elements' +import AuthFlow from '@/constants/app-auth-flow' +import AppAuthenticationDetail from '../../app-detail/app-authentication-detail' + +interface IsSidebar { + isSidebar?: boolean +} + +interface CategorySectionProps extends IsSidebar { + category: CategoryModel | undefined +} + +interface DesktopIntegrationSectionProps extends IsSidebar { + desktopIntegrationTypes: DesktopIntegrationTypeModel[] +} + +interface PrivateAppSectionProps extends IsSidebar { + limitToClientIds: string[] +} + +interface DirectApiSectionProps extends IsSidebar { + isDirectApi: boolean | undefined +} + +interface StatusSectionProps extends IsSidebar { + isListed: boolean | undefined +} + +interface BackToAppsSectionProps { + onClick: () => void +} + +interface ListingPreviewSectionProps extends IsSidebar { + onClick: () => void +} + +interface AuthenticationSectionProps extends IsSidebar { + authFlow: string + id: string + externalId: string +} + +interface SummarySectionProps { + summary: string +} + +interface InstallationsTableSectionProps extends IsSidebar { + data: InstallationModel[] + columns: any[] +} + +interface DeveloperSectionProps extends IsSidebar { + developer: string +} + +interface AdditionalImagesSectionProps { + images: MediaModel[] + splitIndex: number + numberImages: number +} + +interface PermissionSectionProps { + permissions: ScopeModel[] +} + +interface DescriptionSectionProps { + description: string +} + +export const CategorySection: React.FC = ({ category, isSidebar = false }) => ( + + {category ? {category.name} :

None

} +
+) + +export const DesktopIntegrationSection: React.FC = ({ + desktopIntegrationTypes, + isSidebar = false, +}) => ( + + {desktopIntegrationTypes.length ? ( + desktopIntegrationTypes.map(({ name }) => {name}) + ) : ( +

None

+ )} +
+) + +export const PrivateAppSection: React.FC = ({ limitToClientIds, isSidebar = false }) => ( + + {convertBooleanToYesNoString(Boolean(limitToClientIds.length > 0))} + +) + +export const DirectApiSection: React.FC = ({ isDirectApi, isSidebar = false }) => ( + + {convertBooleanToYesNoString(Boolean(isDirectApi))} + +) + +export const StatusSection: React.FC = ({ isListed, isSidebar = false }) => ( + +
+ {isListed ? ( + <> + Listed + + ) : ( + 'Not listed' + )} +
+
+) + +export const BackToAppsSection: React.FC = ({ onClick }) => ( + + + +) + +export const ListingPreviewSection: React.FC = ({ onClick, isSidebar = false }) => { + const onListingPreviewClick = (e: React.MouseEvent) => { + e.preventDefault() + onClick() + } + return ( + + See listing preview{' '} + + + + + } + isSidebar={isSidebar} + > +

The listing preview will display your app as it would appear in the Marketplace

+
+ ) +} + +export const AuthenticationSection: React.FC = ({ + authFlow, + id, + externalId, + isSidebar = false, +}) => ( + +
+ Client ID: + {externalId} +
+ {authFlow === AuthFlow.CLIENT_SECRET && } +
+) + +export const SummarySection: React.FC = ({ summary }) => ( + {summary} +) + +export const InstallationsTableSection: React.FC = ({ + data, + columns, + isSidebar = false, +}) => { + const testId = !data.length ? 'render-installations-table-empty-text' : 'render-installations-table' + return ( + + {!data.length ? ( +

Currently, there are no installations for your app

+ ) : ( + + )} + + ) +} + +export const DeveloperSection: React.FC = ({ isSidebar = false, developer }) => ( + + {developer} + +) + +export const DeveloperAboutSection: React.FC = ({ isSidebar = false }) => ( + +) + +export const AdditionalImagesSection: React.FC = ({ + images, + splitIndex, + numberImages, +}) => { + const extraImages = images.filter((_, index) => index > splitIndex) + return ( + (extraImages.length && ( + + {extraImages.map(({ uri }, index) => { + if (index < numberImages) { + return ( + + + + ) + } + })} + + )) || + null + ) +} + +export const PermissionsSection: React.FC = ({ permissions }) => ( + + + {permissions.map(permission => ( + {permission?.description ?? ''} + ))} + + +) + +export const DescriptionSection: React.FC = ({ description }) => ( + +) diff --git a/packages/developer-portal/src/components/pages/app-detail-preview/index.ts b/packages/developer-portal/src/components/pages/app-detail-preview/index.ts new file mode 100644 index 0000000000..fb8e68535f --- /dev/null +++ b/packages/developer-portal/src/components/pages/app-detail-preview/index.ts @@ -0,0 +1,2 @@ +import AppDetailPreview from './app-detail-preview' +export default AppDetailPreview diff --git a/packages/developer-portal/src/core/router.tsx b/packages/developer-portal/src/core/router.tsx index b6cfcc0bc4..f90ea1017f 100644 --- a/packages/developer-portal/src/core/router.tsx +++ b/packages/developer-portal/src/core/router.tsx @@ -24,6 +24,7 @@ const AnalyticsPage = React.lazy(() => catchChunkError(() => import('@/component const RegisterConfirm = React.lazy(() => catchChunkError(() => import('../components/pages/register-confirm'))) const WebhooksPage = React.lazy(() => catchChunkError(() => import('../components/pages/webhooks'))) const SettingsPage = React.lazy(() => catchChunkError(() => import('../components/pages/settings/'))) +const AppDetailPreview = React.lazy(() => catchChunkError(() => import('../components/pages/app-detail-preview'))) const SettingsOrganisationTabPage = React.lazy(() => catchChunkError(() => import('../components/pages/settings/settings-organisation-tab')), @@ -84,6 +85,7 @@ const Router = () => { path={Routes.DEVELOPER_EDITION_DOWNLOAD} component={EditionDownloadPage} /> + } /> diff --git a/packages/developer-portal/src/styles/blocks/installed-app-card.scss b/packages/developer-portal/src/styles/blocks/installed-app-card.scss deleted file mode 100644 index 8a96ca9731..0000000000 --- a/packages/developer-portal/src/styles/blocks/installed-app-card.scss +++ /dev/null @@ -1,53 +0,0 @@ -$icon-width: 80px; -$icon-height: 80px; - -.container { - display: flex; - width: 50%; - flex-direction: column; - align-items: center; - - @media (min-width: 400px) { - width: 33%; - } - - @media (min-width: 600px) { - width: 25%; - } -} -.wrapIcon { - width: $icon-width; - height: $icon-height; - cursor: pointer; - background-color: #ffffff; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; -} - -.icon { - width: $icon-width - 30px; - max-height: $icon-height - 30px; -} - -.appTitle { - width: $icon-width * 1.5; - white-space: nowrap; - margin-top: 1rem; - text-align: center; - text-overflow: ellipsis; - overflow: hidden; - font-size: 16px; - font-weight: bold; -} - -.content { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - max-width: 700px; - overflow: hidden; - text-overflow: ellipsis; - height: 4.5rem; -} diff --git a/packages/developer-portal/src/styles/blocks/installed-app-list.scss b/packages/developer-portal/src/styles/blocks/installed-app-list.scss deleted file mode 100644 index 08e5a4d6ac..0000000000 --- a/packages/developer-portal/src/styles/blocks/installed-app-list.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import './installed-app-card.scss'; - -.contentIsLoading { - opacity: 0.75; -} -.wrapList { - display: flex; - flex-wrap: wrap; - justify-items: center; - justify-content: flex-start; - align-items: center; - - & > * { - margin-bottom: 1rem; - } -} -