diff --git a/packages/elements/src/components/Checkbox/__tests__/__snapshots__/index.tsx.snap b/packages/elements/src/components/Checkbox/__tests__/__snapshots__/index.tsx.snap index 5e9add694f..16f532c396 100644 --- a/packages/elements/src/components/Checkbox/__tests__/__snapshots__/index.tsx.snap +++ b/packages/elements/src/components/Checkbox/__tests__/__snapshots__/index.tsx.snap @@ -2,7 +2,7 @@ exports[`Checkbox should match a snapshot 1`] = `
{ +export const Checkbox = ({ name, labelText, id, dataTest = '', value = '', className = '' }: CheckboxProps) => { return ( -
+
{({ field }: FieldProps) => { diff --git a/packages/elements/src/components/RadioSelect/__tests__/index.tsx b/packages/elements/src/components/RadioSelect/__tests__/index.tsx index 6585442166..b96d748a14 100644 --- a/packages/elements/src/components/RadioSelect/__tests__/index.tsx +++ b/packages/elements/src/components/RadioSelect/__tests__/index.tsx @@ -39,7 +39,7 @@ describe('RadioSelect', () => { )} , ) - expect(wrapper.find('label')).toHaveLength(3) + expect(wrapper.find('label')).toHaveLength(4) expect(wrapper.find('input')).toHaveLength(2) }) }) diff --git a/packages/elements/src/components/RadioSelect/index.tsx b/packages/elements/src/components/RadioSelect/index.tsx index 692a9b668c..1b0d1189bc 100644 --- a/packages/elements/src/components/RadioSelect/index.tsx +++ b/packages/elements/src/components/RadioSelect/index.tsx @@ -10,38 +10,43 @@ export type RadioSelectOption = { export type RadioSelectProps = { name: string labelText?: string + subText?: string id: string dataTest?: string options: RadioSelectOption[] setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void state: any disabled?: boolean + className?: string } export const RadioSelect: React.FC = ({ name, labelText, + subText, id, dataTest, options, setFieldValue, state, disabled = false, + className, }) => { return ( {({ meta }: FieldProps) => { const hasError = checkError(meta) return ( -
+
+ {options.map(({ label, value }: RadioSelectOption, index: number) => (
= ({ disabled={disabled} onChange={() => setFieldValue(name, value)} /> - +
))}
diff --git a/packages/elements/src/styles/components/checkbox.scss b/packages/elements/src/styles/components/checkbox.scss index f082ca1759..42f4c37efb 100644 --- a/packages/elements/src/styles/components/checkbox.scss +++ b/packages/elements/src/styles/components/checkbox.scss @@ -1,7 +1,6 @@ @import '../base/colors.scss'; .field { - .checkbox { position: absolute; width: 0; @@ -79,7 +78,19 @@ margin-bottom: 4px; } - + .control { + .subtext { + font-style: italic; + display: block; + } + } + + &.checkbox-inline { + .radio-wrap { + display: inline-block; + margin-right: 1rem; + } + } } .field-checkbox { diff --git a/packages/elements/src/styles/components/layout.scss b/packages/elements/src/styles/components/layout.scss index 96144b684d..a713f8fd54 100644 --- a/packages/elements/src/styles/components/layout.scss +++ b/packages/elements/src/styles/components/layout.scss @@ -44,5 +44,8 @@ &.has-background { background-color: $white; } -} + &.flex-wrap { + flex-wrap: wrap; + } +} diff --git a/packages/elements/src/tests/badges/badge-branches.svg b/packages/elements/src/tests/badges/badge-branches.svg index b39bbc2dc8..981914f5a0 100644 --- a/packages/elements/src/tests/badges/badge-branches.svg +++ b/packages/elements/src/tests/badges/badge-branches.svg @@ -1 +1 @@ -Coverage:branchesCoverage:branches77.14%77.14% \ No newline at end of file +Coverage:branchesCoverage:branches76.68%76.68% \ No newline at end of file diff --git a/packages/elements/src/tests/badges/badge-functions.svg b/packages/elements/src/tests/badges/badge-functions.svg index dedfb0e83a..b946feb75d 100644 --- a/packages/elements/src/tests/badges/badge-functions.svg +++ b/packages/elements/src/tests/badges/badge-functions.svg @@ -1 +1 @@ -Coverage:functionsCoverage:functions95.32%95.32% \ No newline at end of file +Coverage:functionsCoverage:functions94.67%94.67% \ No newline at end of file diff --git a/packages/elements/src/tests/badges/badge-lines.svg b/packages/elements/src/tests/badges/badge-lines.svg index c17252992a..7fd5278264 100644 --- a/packages/elements/src/tests/badges/badge-lines.svg +++ b/packages/elements/src/tests/badges/badge-lines.svg @@ -1 +1 @@ -Coverage:linesCoverage:lines96.05%96.05% \ No newline at end of file +Coverage:linesCoverage:lines95.58%95.58% \ No newline at end of file diff --git a/packages/elements/src/tests/badges/badge-statements.svg b/packages/elements/src/tests/badges/badge-statements.svg index 467cca33b6..5f9a9c6d66 100644 --- a/packages/elements/src/tests/badges/badge-statements.svg +++ b/packages/elements/src/tests/badges/badge-statements.svg @@ -1 +1 @@ -Coverage:statementsCoverage:statements95.94%95.94% \ No newline at end of file +Coverage:statementsCoverage:statements95.49%95.49% \ No newline at end of file diff --git a/packages/marketplace/src/actions/__tests__/client.ts b/packages/marketplace/src/actions/__tests__/client.ts index 47493d9942..a8b040e668 100644 --- a/packages/marketplace/src/actions/__tests__/client.ts +++ b/packages/marketplace/src/actions/__tests__/client.ts @@ -3,6 +3,11 @@ import { clientFetchAppSummarySuccess, clientFetchAppSummaryFailed, clientClearAppSummary, + clientOpenWebComponentConfig, + clientCloseWebComponentConfig, + clientFetchWebComponentConfig, + clientFetchWebComponentConfigSuccess, + clientPutWebComponentConfig, } from '../client' import ActionTypes from '../../constants/action-types' import { appsDataStub, featuredAppsDataStub } from '../../sagas/__stubs__/apps' @@ -29,4 +34,20 @@ describe('client actions', () => { expect(clientClearAppSummary.type).toEqual(ActionTypes.CLIENT_CLEAR_APP_SUMMARY) expect(clientClearAppSummary(null).data).toEqual(null) }) + it('should create a clientOpenWebComponentConfig action', () => { + expect(clientOpenWebComponentConfig.type).toEqual(ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_OPEN) + }) + it('should create a clientCloseWebComponentConfig action', () => { + expect(clientCloseWebComponentConfig.type).toEqual(ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_CLOSE) + }) + it('should create a clientFetchWebComponentConfig action', () => { + expect(clientFetchWebComponentConfig.type).toEqual(ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG) + expect(clientFetchWebComponentConfig({ customerId: 'DXX' }).data).toEqual({ customerId: 'DXX' }) + }) + it('should create a clientFetchWebComponentConfigSuccess action', () => { + expect(clientFetchWebComponentConfigSuccess.type).toEqual(ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS) + }) + it('should create a clientPutWebComponentConfig action', () => { + expect(clientPutWebComponentConfig.type).toEqual(ActionTypes.CLIENT_PUT_WEB_COMPONENT_CONFIG) + }) }) diff --git a/packages/marketplace/src/actions/client.ts b/packages/marketplace/src/actions/client.ts index 5928bd3add..ed9d7abff6 100644 --- a/packages/marketplace/src/actions/client.ts +++ b/packages/marketplace/src/actions/client.ts @@ -3,6 +3,11 @@ import ActionTypes from '@/constants/action-types' import { ClientAppSummary, ClientAppSummaryParams } from '@/reducers/client/app-summary' import { AppDetailData } from '@/reducers/client/app-detail' import { FetchAppByIdParams } from '@/services/apps' +import { + PutWebComponentConfigParams, + FetchWebComponentConfigParams, + WebComponentConfigResult, +} from '@/services/web-component' export const clientFetchAppSummary = actionCreator(ActionTypes.CLIENT_FETCH_APP_SUMMARY) export const clientFetchAppSummarySuccess = actionCreator( @@ -15,3 +20,15 @@ export const clientClearAppSummary = actionCreator(ActionTypes.CLIENT_CLEA export const clientFetchAppDetail = actionCreator(ActionTypes.CLIENT_FETCH_APP_DETAIL) export const clientFetchAppDetailSuccess = actionCreator(ActionTypes.CLIENT_FETCH_APP_DETAIL_SUCCESS) export const clientFetchAppDetailFailed = actionCreator(ActionTypes.CLIENT_FETCH_APP_DETAIL_FAILED) + +export const clientOpenWebComponentConfig = actionCreator(ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_OPEN) +export const clientCloseWebComponentConfig = actionCreator(ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_CLOSE) +export const clientFetchWebComponentConfig = actionCreator( + ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG, +) +export const clientFetchWebComponentConfigSuccess = actionCreator( + ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS, +) +export const clientPutWebComponentConfig = actionCreator( + ActionTypes.CLIENT_PUT_WEB_COMPONENT_CONFIG, +) diff --git a/packages/marketplace/src/components/pages/client-app-detail-manage/__test__/__snapshots__/client-app-detail-manage.test.tsx.snap b/packages/marketplace/src/components/pages/client-app-detail-manage/__test__/__snapshots__/client-app-detail-manage.test.tsx.snap index 29a83e64a5..942a133cd1 100644 --- a/packages/marketplace/src/components/pages/client-app-detail-manage/__test__/__snapshots__/client-app-detail-manage.test.tsx.snap +++ b/packages/marketplace/src/components/pages/client-app-detail-manage/__test__/__snapshots__/client-app-detail-manage.test.tsx.snap @@ -96,6 +96,36 @@ exports[`ClientAppDetailManage should match a snapshot 1`] = ` src="https://bulma.io/images/placeholders/48x48.png" />
+ +
+ +
+ Settings +
+
+ + + +
+
+ +
+ +
+ Settings +
+
+ + + +
+
+ +
+ +
+ Settings +
+
+ + + +
+
- + -
- Peter's Properties +
+ +
+
+ Peter's Properties +
+ +
+ +
+ Settings +
+
+ + + +
+
+ + + +
+
+ +
+ +

+ Peter's Properties +

+
+
+
- - - Peter's Properties - - - - + + `; diff --git a/packages/marketplace/src/components/ui/app-detail/app-header/__test__/app-header.test.tsx b/packages/marketplace/src/components/ui/app-detail/app-header/__test__/app-header.test.tsx index 2ed1dd3ffc..95404a8253 100644 --- a/packages/marketplace/src/components/ui/app-detail/app-header/__test__/app-header.test.tsx +++ b/packages/marketplace/src/components/ui/app-detail/app-header/__test__/app-header.test.tsx @@ -1,8 +1,10 @@ import * as React from 'react' -import { shallow } from 'enzyme' import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' import AppHeader, { AppHeaderProps } from '../app-header' - +import { mount } from 'enzyme' +import configureStore from 'redux-mock-store' +import { ReduxState } from '@/types/core' +import { Provider } from 'react-redux' const mockProps: AppHeaderProps = { appDetailData: { ...appDetailDataStub.data, @@ -12,6 +14,24 @@ const mockProps: AppHeaderProps = { describe('AppContent', () => { it('should match a snapshot', () => { - expect(shallow()).toMatchSnapshot() + const mockState = { + client: { + webComponent: { + loading: false, + updating: false, + data: null, + isShowModal: true, + }, + }, + } as ReduxState + const mockStore = configureStore() + const store = mockStore(mockState) + expect( + mount( + + + , + ), + ).toMatchSnapshot() }) }) diff --git a/packages/marketplace/src/components/ui/app-detail/app-header/app-header.tsx b/packages/marketplace/src/components/ui/app-detail/app-header/app-header.tsx index 1076d3129e..c41c744cb1 100644 --- a/packages/marketplace/src/components/ui/app-detail/app-header/app-header.tsx +++ b/packages/marketplace/src/components/ui/app-detail/app-header/app-header.tsx @@ -1,8 +1,20 @@ import * as React from 'react' -import { Grid, GridItem, H3 } from '@reapit/elements' +import { Grid, GridItem, H3, Button, H6 } from '@reapit/elements' import styles from '@/styles/pages/developer-app-detail.scss?mod' import { AppDetailModel } from '@reapit/foundations-ts-definitions' +import { useDispatch, useSelector } from 'react-redux' +import { selectIsWebComponentOpen } from '@/selector/client' +import WebComponentModal from '../../web-component-config-modal' +import { clientCloseWebComponentConfig, clientOpenWebComponentConfig } from '@/actions/client' +import { Dispatch } from 'redux' + +export const toggleWebComponentModal = (dispatch: Dispatch) => () => { + dispatch(clientOpenWebComponentConfig()) +} +export const closeWebComponentModal = (dispatch: Dispatch) => () => { + dispatch(clientCloseWebComponentConfig()) +} export type AppHeaderProps = { appDetailData: AppDetailModel & { @@ -12,9 +24,13 @@ export type AppHeaderProps = { } const AppHeader: React.FC = ({ appDetailData, buttonGroup }) => { - const { media, name } = appDetailData - const appIcon = media?.filter(({ type }) => type === 'icon')[0] + const dispatch = useDispatch() + const isWebComponentOpen = useSelector(selectIsWebComponentOpen) + const handleToggleWebComponentModal = toggleWebComponentModal(dispatch) + const handleCloseWebComponentModal = closeWebComponentModal(dispatch) + const { media, name, isWebComponent } = appDetailData + const appIcon = media?.filter(({ type }) => type === 'icon')[0] return ( <> @@ -26,6 +42,23 @@ const AppHeader: React.FC = ({ appDetailData, buttonGroup }) => alt={name} />
+ {!isWebComponent && ( + <> + +
Settings
+ +
+ {isWebComponentOpen && ( + + )} + + )}

{name}

diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal-inner.test.tsx.snap b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal-inner.test.tsx.snap new file mode 100644 index 0000000000..71ace5cb03 --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal-inner.test.tsx.snap @@ -0,0 +1,1024 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config-modal-inner should WebComponentConfigModalBody match a snapshot 1`] = ` + +

+ abc +

+ + +
+
+ + +
+ + + + + + + +
+
+
+
+`; + +exports[`Config-modal-inner should WebComponentConfigModalFooter match a snapshot 1`] = ` + + + + + + + + + + +`; + +exports[`Config-modal-inner should WebComponentConfigModalInner match a snapshot 1`] = ` + + + +
+ + +
+

+ Book a Viewing Configuration +

+
+
+ + } + > +
+ +

+ Please use the following form to configure your diary settings for your ‘Book a Viewing’ widget on your website +

+ + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + +
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + } + > +
+ + + + + + + + +
+
+
+ +
+
+
+`; diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap new file mode 100644 index 0000000000..b6ba57ae65 --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/__snapshots__/config-modal.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config-modal should match snapshot 1`] = ` + + + +`; diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/__test__/config-modal-inner.test.tsx b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/config-modal-inner.test.tsx new file mode 100644 index 0000000000..817cbf6b00 --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/config-modal-inner.test.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import { + updateWebComponentConfig, + handleFetchWebComponentConfig, + WebComponentConfigModalBody, + WebComponentConfigModalFooter, + WebComponentConfigModalInner, +} from '../config-modal-inner' +import { shallow, mount } from 'enzyme' +import configureStore from 'redux-mock-store' +import { ReduxState } from '@/types/core' +import { Provider } from 'react-redux' +import { BOOK_VIEWING_CONSTANT } from '../config-modal' +import { PutWebComponentConfigParams } from '@/services/web-component' +import { clientPutWebComponentConfig, clientFetchWebComponentConfig } from '@/actions/client' + +const params = { + appointmentLength: 1, + appointmentTimeGap: 1, + customerId: 'string', + daysOfWeek: [1, 2], + negotiatorIds: ['1', '2'], +} as PutWebComponentConfigParams + +describe('Config-modal-inner', () => { + const mockState = { + client: { + webComponent: { + loading: false, + updating: false, + data: null, + isShowModal: true, + }, + }, + } as ReduxState + const mockStore = configureStore() + const store = mockStore(mockState) + + it('should WebComponentConfigModalBody match a snapshot', () => { + const mockProps = { + subtext: 'abc', + formikProps: { values: params }, + } + expect(shallow()).toMatchSnapshot() + }) + + it('should WebComponentConfigModalFooter match a snapshot', () => { + const mockProps = { + closeModal: jest.fn(), + } + expect( + mount( + + + , + ), + ).toMatchSnapshot() + }) + + it('should WebComponentConfigModalInner match a snapshot', () => { + const mockState = { + client: { + webComponent: { + loading: false, + updating: false, + data: null, + isShowModal: true, + }, + }, + } as ReduxState + const mockStore = configureStore() + const store = mockStore(mockState) + expect( + mount( + + + , + ), + ).toMatchSnapshot() + }) + + it('should updateWebComponentConfig run correctly', () => { + const dispatch = jest.fn() + + const fn = updateWebComponentConfig(dispatch) + fn(params) + expect(dispatch).toBeCalledWith(clientPutWebComponentConfig(params)) + }) + + it('should handleFetchWebComponentConfig run correctly', () => { + const dispatch = jest.fn() + const customerId = 'string' + + const fn = handleFetchWebComponentConfig(dispatch, customerId) + fn() + expect(dispatch).toBeCalledWith(clientFetchWebComponentConfig({ customerId })) + }) +}) diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/__test__/config-modal.test.tsx b/packages/marketplace/src/components/ui/web-component-config-modal/__test__/config-modal.test.tsx new file mode 100644 index 0000000000..b696c59bd9 --- /dev/null +++ b/packages/marketplace/src/components/ui/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/marketplace/src/components/ui/web-component-config-modal/config-modal-inner.tsx b/packages/marketplace/src/components/ui/web-component-config-modal/config-modal-inner.tsx new file mode 100644 index 0000000000..a103897c50 --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/config-modal-inner.tsx @@ -0,0 +1,127 @@ +import React, { useEffect } from 'react' +import { + ModalHeader, + ModalBody, + ModalFooter, + Button, + Formik, + RadioSelect, + Checkbox, + Form, + Loader, + FormikValues, +} from '@reapit/elements' +import styles from '@/styles/elements/radioselect.scss?mod' +import { useDispatch, useSelector } from 'react-redux' +import { clientFetchWebComponentConfig, clientPutWebComponentConfig } from '@/actions/client' +import { selectIsWebComponentData, selectIsWebComponentLoading, selectIsWebComponentUpdating } from '@/selector/client' +import { PutWebComponentConfigParams } from '@/services/web-component' +import { selectClientId } from '@/selector/auth' +import { Dispatch } from 'redux' + +export const updateWebComponentConfig = (dispatch: Dispatch) => (params: FormikValues) => { + dispatch(clientPutWebComponentConfig(params as PutWebComponentConfigParams)) +} + +export const handleFetchWebComponentConfig = (dispatch: Dispatch, customerId?: string) => () => { + customerId && dispatch(clientFetchWebComponentConfig({ customerId })) +} + +export const WebComponentConfigModalBody = ({ subtext, formikProps }) => { + const { values, setFieldValue } = formikProps + return ( + <> +

{subtext}

+ + +
+
+ + +
+ + + + + + + +
+
+
+ + ) +} + +export const WebComponentConfigModalFooter = ({ closeModal }) => { + const updating = useSelector(selectIsWebComponentUpdating) + return ( + <> + + + + ) +} + +export const WebComponentConfigModalInner = ({ config, closeModal }) => { + const dispatch = useDispatch() + const handleUpdateWebComponentConfig = updateWebComponentConfig(dispatch) + const webComponentData = useSelector(selectIsWebComponentData) + const loading = useSelector(selectIsWebComponentLoading) + const clientId = useSelector(selectClientId) || '' + + const initialFormValues = + webComponentData || + ({ + appointmentLength: 30, + appointmentTimeGap: 30, + daysOfWeek: ['1', '2', '3', '4', '5', '6'], + } as FormikValues) + + useEffect(handleFetchWebComponentConfig(dispatch, clientId), []) + + if (loading) return + return ( + + {formikProps => ( +
+ + } /> + } /> + + )} +
+ ) +} diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/config-modal.tsx b/packages/marketplace/src/components/ui/web-component-config-modal/config-modal.tsx new file mode 100644 index 0000000000..9b6784319b --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/config-modal.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' +import { Modal } from '@reapit/elements' +import { WebComponentConfigModalInner } from './config-modal-inner' + +export const BOOK_VIEWING_CONSTANT = { + title: 'Book a Viewing Configuration', + subtext: + 'Please use the following form to configure your diary settings for your ‘Book a Viewing’ widget on your website', +} + +export const BOOK_VALUATION_CONSTANT = { + title: 'Book a Valuation Configuration', + subtext: + 'Please use the following form to configure your diary settings for your ‘Book a Valuation’ widget on your website', +} + +export type WebComponentModalProps = { + type: 'BOOK_VIEWING' | 'BOOK_VALUATION' + afterClose: () => void + closeModal: () => void +} + +export const WebComponentModal = ({ type, afterClose, closeModal }: WebComponentModalProps) => { + const config = type === 'BOOK_VIEWING' ? BOOK_VIEWING_CONSTANT : BOOK_VALUATION_CONSTANT + + return ( + + + + ) +} +export default WebComponentModal diff --git a/packages/marketplace/src/components/ui/web-component-config-modal/index.tsx b/packages/marketplace/src/components/ui/web-component-config-modal/index.tsx new file mode 100644 index 0000000000..0e6aeee889 --- /dev/null +++ b/packages/marketplace/src/components/ui/web-component-config-modal/index.tsx @@ -0,0 +1 @@ +export { default } from './config-modal' diff --git a/packages/marketplace/src/constants/action-types.ts b/packages/marketplace/src/constants/action-types.ts index 83b5e73dc4..cbf942523a 100644 --- a/packages/marketplace/src/constants/action-types.ts +++ b/packages/marketplace/src/constants/action-types.ts @@ -71,6 +71,13 @@ const ActionTypes = { CLIENT_FETCH_APP_DETAIL_FAILED: 'CLIENT_FETCH_APP_DETAIL_FAILED', CLIENT_FETCH_APP_DETAIL_SUCCESS: 'CLIENT_FETCH_APP_DETAIL_SUCCESS', + // Client Web Component Config + CLIENT_WEB_COMPONENT_CONFIG_OPEN: 'CLIENT_WEB_COMPONENT_CONFIG_OPEN', + CLIENT_WEB_COMPONENT_CONFIG_CLOSE: 'CLIENT_WEB_COMPONENT_CONFIG_CLOSE', + CLIENT_FETCH_WEB_COMPONENT_CONFIG: 'CLIENT_FETCH_WEB_COMPONENT_CONFIG', + CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS: 'CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS', + CLIENT_PUT_WEB_COMPONENT_CONFIG: 'CLIENT_PUT_WEB_COMPONENT_CONFIG', + // Installed apps actions INSTALLED_APPS_REQUEST_DATA: 'INSTALLED_APPS_REQUEST_DATA', INSTALLED_APPS_LOADING: 'INSTALLED_APPS_LOADING', diff --git a/packages/marketplace/src/core/store.ts b/packages/marketplace/src/core/store.ts index 1cfb2437c7..985897ae3c 100644 --- a/packages/marketplace/src/core/store.ts +++ b/packages/marketplace/src/core/store.ts @@ -53,6 +53,7 @@ import adminStatsSaga from '@/sagas/admin-stats' import webhookSubscriptionsSagas from '@/sagas/webhook-subscriptions' import { injectSwitchModeToWindow } from '@reapit/elements' import webhookEditSagas from '../sagas/webhook-edit-modal' +import webComponentSagas from '../sagas/web-component' export class Store { static _instance: Store @@ -124,6 +125,7 @@ export class Store { fork(appHttpTrafficEventSagas), fork(webhookEditSagas), fork(webhookSubscriptionsSagas), + fork(webComponentSagas), ]) } diff --git a/packages/marketplace/src/reducers/__stubs__/app-state.ts b/packages/marketplace/src/reducers/__stubs__/app-state.ts index 805cecb4d4..cbfe4059fb 100644 --- a/packages/marketplace/src/reducers/__stubs__/app-state.ts +++ b/packages/marketplace/src/reducers/__stubs__/app-state.ts @@ -12,6 +12,12 @@ const appState: ReduxState = { data: null, isAppDetailLoading: false, }, + webComponent: { + isShowModal: false, + data: null, + updating: false, + loading: false, + }, }, installedApps: { loading: false, diff --git a/packages/marketplace/src/reducers/client/__test__/web-component.test.ts b/packages/marketplace/src/reducers/client/__test__/web-component.test.ts new file mode 100644 index 0000000000..a2ef7d484f --- /dev/null +++ b/packages/marketplace/src/reducers/client/__test__/web-component.test.ts @@ -0,0 +1,73 @@ +import webComponentReducer, { defaultState } from '../web-component' +import { ActionType } from '../../../types/core' +import ActionTypes from '../../../constants/action-types' + +describe('client app detail reducer', () => { + it('should return default state if action not matched', () => { + const newState = webComponentReducer(undefined, { type: 'UNKNOWN' as ActionType, data: undefined }) + expect(newState).toEqual(defaultState) + }) + + it('should set isModalOpen to true when CLIENT_WEB_COMPONENT_CONFIG_OPEN action is called', () => { + const newState = webComponentReducer(undefined, { + type: ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_OPEN as ActionType, + data: true, + }) + const expected = { + ...defaultState, + isShowModal: true, + loading: true, + } + expect(newState).toEqual(expected) + }) + + it('should set isModalOpen to false when CLIENT_WEB_COMPONENT_CONFIG_CLOSE action is called', () => { + const newState = webComponentReducer(undefined, { + type: ActionTypes.CLIENT_WEB_COMPONENT_CONFIG_CLOSE as ActionType, + data: true, + }) + const expected = { + ...defaultState, + isShowModal: false, + } + expect(newState).toEqual(expected) + }) + + it('should set loading to true when CLIENT_FETCH_WEB_COMPONENT_CONFIG action is called', () => { + const newState = webComponentReducer(undefined, { + type: ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG as ActionType, + data: true, + }) + const expected = { + ...defaultState, + loading: true, + } + expect(newState).toEqual(expected) + }) + + it('should set updating to true when CLIENT_PUT_WEB_COMPONENT_CONFIG action is called', () => { + const newState = webComponentReducer(undefined, { + type: ActionTypes.CLIENT_PUT_WEB_COMPONENT_CONFIG as ActionType, + data: true, + }) + const expected = { + ...defaultState, + updating: true, + } + expect(newState).toEqual(expected) + }) + + it('should set updating to true when CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS action is called', () => { + const newState = webComponentReducer(undefined, { + type: ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG_SUCCESS as ActionType, + data: {}, + }) + const expected = { + ...defaultState, + data: {}, + loading: false, + updating: false, + } + expect(newState).toEqual(expected) + }) +}) diff --git a/packages/marketplace/src/reducers/client/index.ts b/packages/marketplace/src/reducers/client/index.ts index e6181a20ce..97cecbafc9 100644 --- a/packages/marketplace/src/reducers/client/index.ts +++ b/packages/marketplace/src/reducers/client/index.ts @@ -1,13 +1,16 @@ import { combineReducers } from 'redux' import appDetailReducer, { ClientAppDetailState } from './app-detail' import clientReducer, { ClientAppSummaryState } from './app-summary' +import webComponentReducer, { WebComponentState } from './web-component' export interface ClientRootState { appSummary: ClientAppSummaryState appDetail: ClientAppDetailState + webComponent: WebComponentState } export default combineReducers({ appSummary: clientReducer, appDetail: appDetailReducer, + webComponent: webComponentReducer, }) diff --git a/packages/marketplace/src/reducers/client/web-component.ts b/packages/marketplace/src/reducers/client/web-component.ts new file mode 100644 index 0000000000..8d98939d06 --- /dev/null +++ b/packages/marketplace/src/reducers/client/web-component.ts @@ -0,0 +1,64 @@ +import { Action } from '@/types/core' +import { isType } from '@/utils/actions' +import { + clientOpenWebComponentConfig, + clientCloseWebComponentConfig, + clientFetchWebComponentConfig, + clientPutWebComponentConfig, + clientFetchWebComponentConfigSuccess, +} from '@/actions/client' +import { WebComponentConfigResult } from '@/services/web-component' + +export interface WebComponentState { + isShowModal: boolean + data: WebComponentConfigResult + loading: boolean + updating: boolean +} +export const defaultState: WebComponentState = { + isShowModal: false, + data: null, + loading: true, + updating: false, +} + +const webComponentReducer = (state: WebComponentState = defaultState, action: Action): WebComponentState => { + if (isType(action, clientOpenWebComponentConfig)) { + return { + ...state, + isShowModal: true, + loading: true, + } + } + if (isType(action, clientCloseWebComponentConfig)) { + return { + ...state, + isShowModal: false, + } + } + + if (isType(action, clientFetchWebComponentConfig)) { + return { + ...state, + loading: true, + } + } + if (isType(action, clientPutWebComponentConfig)) { + return { + ...state, + updating: true, + } + } + if (isType(action, clientFetchWebComponentConfigSuccess)) { + return { + ...state, + data: action.data, + loading: false, + updating: false, + } + } + + return state +} + +export default webComponentReducer diff --git a/packages/marketplace/src/sagas/__tests__/web-component.ts b/packages/marketplace/src/sagas/__tests__/web-component.ts new file mode 100644 index 0000000000..0cc4b2506e --- /dev/null +++ b/packages/marketplace/src/sagas/__tests__/web-component.ts @@ -0,0 +1,127 @@ +import webComponentSagas, { + putWebComponentConfigListen, + fetchWebComponentConfigListen, + putWebComponentConfigSaga, + fetchWebComponentConfigSaga, +} from '../web-component' +import { call, put, all, fork, takeLatest } from '@redux-saga/core/effects' +import { Action } from '@/types/core' +import { + PutWebComponentConfigParams, + FetchWebComponentConfigParams, + putWebComponentConfig, + fetchWebComponentConfig, + WebComponentConfigResult, +} from '@/services/web-component' +import ActionTypes from '@/constants/action-types' +import { cloneableGenerator } from '@redux-saga/testing-utils' +import { clientFetchWebComponentConfigSuccess, clientCloseWebComponentConfig } from '@/actions/client' +import { errorThrownServer } from '@/actions/error' +import errorMessages from '../../../../elements/src/utils/validators/error-messages' + +describe('webComponentSagas', () => { + it('should listen request data', () => { + const gen = webComponentSagas() + + expect(gen.next().value).toEqual(all([fork(fetchWebComponentConfigListen), fork(putWebComponentConfigListen)])) + + expect(gen.next().done).toBe(true) + }) +}) + +describe('putWebComponentConfigListen', () => { + it('should editWebhook when called', () => { + const gen = putWebComponentConfigListen() + expect(gen.next().value).toEqual( + takeLatest>( + ActionTypes.CLIENT_PUT_WEB_COMPONENT_CONFIG, + putWebComponentConfigSaga, + ), + ) + expect(gen.next().done).toBe(true) + }) +}) + +describe('fetchWebComponentConfigListen', () => { + it('should editWebhook when called', () => { + const gen = fetchWebComponentConfigListen() + expect(gen.next().value).toEqual( + takeLatest>( + ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG, + fetchWebComponentConfigSaga, + ), + ) + expect(gen.next().done).toBe(true) + }) +}) + +describe('putWebComponentConfigSaga', () => { + const params = { + appointmentLength: 12, + appointmentTimeGap: 21, + customerId: 'string', + daysOfWeek: [1], + negotiatorIds: ['1'], + } as PutWebComponentConfigParams + + const gen = cloneableGenerator(putWebComponentConfigSaga as any)({ data: params }) + expect(gen.next().value).toEqual(call(putWebComponentConfig, params)) + + it('api call success', () => { + const clone = gen.clone() + expect(clone.next(params as any).value).toEqual(put(clientFetchWebComponentConfigSuccess(params))) + expect(clone.next().done).toBe(true) + }) + + test('api call fail', () => { + const clone = gen.clone() + // @ts-ignore + expect(clone.throw(new Error('Call API Failed')).value).toEqual( + put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ), + ) + expect(clone.next().done).toBe(true) + }) +}) + +describe('fetchWebComponentConfigSaga', () => { + const params = { + customerId: 'string', + } as FetchWebComponentConfigParams + + const respone = { + appointmentLength: 12, + appointmentTimeGap: 21, + customerId: 'string', + daysOfWeek: [1], + negotiatorIds: ['1'], + } as WebComponentConfigResult + + const gen = cloneableGenerator(fetchWebComponentConfigSaga as any)({ data: params }) + expect(gen.next().value).toEqual(call(fetchWebComponentConfig, params)) + + it('api call success', () => { + const clone = gen.clone() + expect(clone.next(respone as any).value).toEqual(put(clientFetchWebComponentConfigSuccess(respone))) + expect(clone.next().done).toBe(true) + }) + + test('api call fail', () => { + const clone = gen.clone() + // @ts-ignore + expect(clone.throw(new Error('Call API Failed')).value).toEqual(put(clientCloseWebComponentConfig())) + expect(clone.next().value).toEqual( + put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ), + ) + expect(clone.next().done).toBe(true) + }) +}) diff --git a/packages/marketplace/src/sagas/web-component.ts b/packages/marketplace/src/sagas/web-component.ts new file mode 100644 index 0000000000..e8ce8e33bb --- /dev/null +++ b/packages/marketplace/src/sagas/web-component.ts @@ -0,0 +1,61 @@ +import { put, fork, takeLatest, all, call } from '@redux-saga/core/effects' +import { Action } from '../types/core' +import ActionTypes from '../constants/action-types' +import { + fetchWebComponentConfig, + FetchWebComponentConfigParams, + PutWebComponentConfigParams, + putWebComponentConfig, +} from '@/services/web-component' +import errorMessages from '../../../elements/src/utils/validators/error-messages' +import { errorThrownServer } from '@/actions/error' +import { clientFetchWebComponentConfigSuccess, clientCloseWebComponentConfig } from '@/actions/client' + +export const fetchWebComponentConfigSaga = function*({ data }: Action) { + try { + const respone = yield call(fetchWebComponentConfig, data) + if (respone.message) throw Error(respone.message) + yield put(clientFetchWebComponentConfigSuccess(respone)) + } catch (err) { + yield put(clientCloseWebComponentConfig()) + yield put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ) + } +} + +export const putWebComponentConfigSaga = function*({ data }: Action) { + try { + const respone = yield call(putWebComponentConfig, data) + yield put(clientFetchWebComponentConfigSuccess(respone)) + } catch (err) { + yield put( + errorThrownServer({ + type: 'SERVER', + message: errorMessages.DEFAULT_SERVER_ERROR, + }), + ) + } +} + +export const fetchWebComponentConfigListen = function*() { + yield takeLatest>( + ActionTypes.CLIENT_FETCH_WEB_COMPONENT_CONFIG, + fetchWebComponentConfigSaga, + ) +} +export const putWebComponentConfigListen = function*() { + yield takeLatest>( + ActionTypes.CLIENT_PUT_WEB_COMPONENT_CONFIG, + putWebComponentConfigSaga, + ) +} + +const webComponentSagas = function*() { + yield all([fork(fetchWebComponentConfigListen), fork(putWebComponentConfigListen)]) +} + +export default webComponentSagas diff --git a/packages/marketplace/src/selector/__tests__/auth.ts b/packages/marketplace/src/selector/__tests__/auth.ts index f09d828386..48840777f7 100644 --- a/packages/marketplace/src/selector/__tests__/auth.ts +++ b/packages/marketplace/src/selector/__tests__/auth.ts @@ -1,5 +1,5 @@ import { ReduxState } from '@/types/core' -import { selectLoginType, selectIsAdmin, selectLoginIdentity, selectLoginSession } from '../auth' +import { selectLoginType, selectIsAdmin, selectLoginIdentity, selectLoginSession, selectClientId } from '../auth' const mockState = { auth: { @@ -7,6 +7,7 @@ const mockState = { loginSession: { loginIdentity: { isAdmin: true, + clientId: 'DCX', }, }, }, @@ -39,3 +40,10 @@ describe('selectLoginSession', () => { expect(result).toEqual(mockState.auth.loginSession) }) }) + +describe('selectClientId', () => { + it('should run correctly', () => { + const result = selectClientId(mockState) + expect(result).toEqual(mockState.auth.loginSession?.loginIdentity?.clientId) + }) +}) diff --git a/packages/marketplace/src/selector/__tests__/client.ts b/packages/marketplace/src/selector/__tests__/client.ts index 5d3a7c3353..87ed181316 100644 --- a/packages/marketplace/src/selector/__tests__/client.ts +++ b/packages/marketplace/src/selector/__tests__/client.ts @@ -1,5 +1,13 @@ import { ReduxState } from '@/types/core' -import { selectClientId, selectLoggedUserEmail, selectFeaturedApps } from '../client' +import { + selectClientId, + selectLoggedUserEmail, + selectFeaturedApps, + selectIsWebComponentOpen, + selectIsWebComponentData, + selectIsWebComponentLoading, + selectIsWebComponentUpdating, +} from '../client' import { featuredAppsDataStub } from '@/sagas/__stubs__/apps' describe('selectClientId', () => { @@ -74,4 +82,52 @@ describe('selectFeaturedApps', () => { const result = selectFeaturedApps(input) expect(result).toEqual([]) }) + + it('should selectIsWebComponentOpen run correctly and return true', () => { + const input = { + client: { + webComponent: { + isShowModal: true, + }, + }, + } as ReduxState + const result = selectIsWebComponentOpen(input) + expect(result).toEqual(true) + }) + + it('should selectIsWebComponentData run correctly and return {}', () => { + const input = { + client: { + webComponent: { + data: {}, + }, + }, + } as ReduxState + const result = selectIsWebComponentData(input) + expect(result).toEqual({}) + }) + + it('should selectIsWebComponentLoading run correctly and return true', () => { + const input = { + client: { + webComponent: { + loading: true, + }, + }, + } as ReduxState + const result = selectIsWebComponentLoading(input) + expect(result).toEqual(true) + }) + + it('should selectIsWebComponentUpdating run correctly and return true', () => { + const input = { + client: { + webComponent: { + updating: true, + }, + }, + } as ReduxState + const result = selectIsWebComponentUpdating(input) + expect(result).toEqual(true) + }) }) diff --git a/packages/marketplace/src/selector/auth.ts b/packages/marketplace/src/selector/auth.ts index 0dac32570b..115d3f0d44 100644 --- a/packages/marketplace/src/selector/auth.ts +++ b/packages/marketplace/src/selector/auth.ts @@ -15,3 +15,7 @@ export const selectLoginIdentity = (state: ReduxState) => { export const selectLoginSession = (state: ReduxState) => { return state.auth?.loginSession } + +export const selectClientId = (state: ReduxState) => { + return state.auth?.loginSession?.loginIdentity?.clientId +} diff --git a/packages/marketplace/src/selector/client.ts b/packages/marketplace/src/selector/client.ts index 90d70e11e7..2bfb2d5d7d 100644 --- a/packages/marketplace/src/selector/client.ts +++ b/packages/marketplace/src/selector/client.ts @@ -15,3 +15,19 @@ export const selectAppSummary = (state: ReduxState) => { export const selectFeaturedApps = (state: ReduxState) => { return state?.client.appSummary.data?.featuredApps || [] } + +export const selectIsWebComponentOpen = (state: ReduxState) => { + return state?.client.webComponent?.isShowModal +} + +export const selectIsWebComponentData = (state: ReduxState) => { + return state?.client.webComponent?.data +} + +export const selectIsWebComponentLoading = (state: ReduxState) => { + return state?.client.webComponent?.loading +} + +export const selectIsWebComponentUpdating = (state: ReduxState) => { + return state?.client.webComponent?.updating +} diff --git a/packages/marketplace/src/services/web-component.ts b/packages/marketplace/src/services/web-component.ts new file mode 100644 index 0000000000..95ef4d63ee --- /dev/null +++ b/packages/marketplace/src/services/web-component.ts @@ -0,0 +1,62 @@ +import { fetcher } from '@reapit/elements' +import { generateHeader } from './utils' +import { logger } from 'logger' + +export interface FetchWebComponentConfigParams { + customerId: string +} + +export interface PutWebComponentConfigParams { + appointmentLength: number + appointmentTimeGap: number + appointmentTypes: any + customerId: string + daysOfWeek: number[] + negotiatorIds: string[] +} + +export type WebComponentConfigResult = { + appointmentLength: number + appointmentTimeGap: number + appointmentTypes: any + customerId: string + daysOfWeek: number[] + negotiatorIds: string[] +} | null + +const API = '/dev/v1/web-components-config' + +export const fetchWebComponentConfig = async ( + params: FetchWebComponentConfigParams, +): Promise => { + try { + const { customerId } = params + const response = await fetcher({ + url: `${API}/${customerId}`, + api: 'http://localhost:3000', + method: 'GET', + headers: generateHeader(window.reapit.config.marketplaceApiKey), + }) + return response + } catch (error) { + logger(error) + throw new Error(error) + } +} + +export const putWebComponentConfig = async (params: PutWebComponentConfigParams) => { + try { + const { customerId = 'DXX', ...rest } = params + const response = await fetcher({ + url: `${API}/${customerId}`, + api: 'http://localhost:3000', + method: 'PATCH', + headers: generateHeader(window.reapit.config.marketplaceApiKey), + body: rest, + }) + return response + } catch (error) { + logger(error) + throw new Error(error) + } +} diff --git a/packages/marketplace/src/styles/elements/radioselect.scss b/packages/marketplace/src/styles/elements/radioselect.scss new file mode 100644 index 0000000000..82e02b3318 --- /dev/null +++ b/packages/marketplace/src/styles/elements/radioselect.scss @@ -0,0 +1,3 @@ +.radioselect { + margin-top: 0.5em; +} diff --git a/packages/marketplace/src/tests/badges/badge-branches.svg b/packages/marketplace/src/tests/badges/badge-branches.svg index 27f77ced28..7ce51f6372 100644 --- a/packages/marketplace/src/tests/badges/badge-branches.svg +++ b/packages/marketplace/src/tests/badges/badge-branches.svg @@ -1 +1 @@ -Coverage:branchesCoverage:branches69.19%69.19% \ No newline at end of file +Coverage:branchesCoverage:branches69.53%69.53% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-functions.svg b/packages/marketplace/src/tests/badges/badge-functions.svg index 163abe6863..2bbb3fc8a1 100644 --- a/packages/marketplace/src/tests/badges/badge-functions.svg +++ b/packages/marketplace/src/tests/badges/badge-functions.svg @@ -1 +1 @@ -Coverage:functionsCoverage:functions76.29%76.29% \ No newline at end of file +Coverage:functionsCoverage:functions76.58%76.58% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-lines.svg b/packages/marketplace/src/tests/badges/badge-lines.svg index 28f152bf6d..1730635bec 100644 --- a/packages/marketplace/src/tests/badges/badge-lines.svg +++ b/packages/marketplace/src/tests/badges/badge-lines.svg @@ -1 +1 @@ -Coverage:linesCoverage:lines89.74%89.74% \ No newline at end of file +Coverage:linesCoverage:lines89.91%89.91% \ No newline at end of file diff --git a/packages/marketplace/src/tests/badges/badge-statements.svg b/packages/marketplace/src/tests/badges/badge-statements.svg index 529e100f44..0ff47bb5c6 100644 --- a/packages/marketplace/src/tests/badges/badge-statements.svg +++ b/packages/marketplace/src/tests/badges/badge-statements.svg @@ -1 +1 @@ -Coverage:statementsCoverage:statements88.9%88.9% \ No newline at end of file +Coverage:statementsCoverage:statements89.05%89.05% \ No newline at end of file