diff --git a/application/cypress/e2e/method-details-icon.cy.ts b/application/cypress/e2e/method-details-icon.cy.ts new file mode 100644 index 0000000..cb7f182 --- /dev/null +++ b/application/cypress/e2e/method-details-icon.cy.ts @@ -0,0 +1,36 @@ +/// +/// +/// + +import { + entryPointUriPath, + APPLICATION_BASE_ROUTE, +} from '../support/constants'; + +beforeEach(() => { + cy.loginToMerchantCenter({ + entryPointUriPath, + initialRoute: APPLICATION_BASE_ROUTE, + }); + + cy.fixture('forward-to').then((response) => { + cy.intercept('GET', '/proxy/forward-to', { + statusCode: 200, + body: response, + }); + }); +}); + +describe('Test method details - Icon tab', () => { + it('should be fully functional', () => { + const paymentMethod = 'PayPal'; + + cy.findByText(paymentMethod).click(); + + cy.findByRole('tab', { name: 'Icon' }).click(); + cy.url().should('contain', 'icon'); + + cy.findByTestId('image-url-input').should('be.visible'); + cy.findByTestId('image-preview').should('be.visible'); + }); +}); diff --git a/application/cypress/e2e/method-details.cy.ts b/application/cypress/e2e/method-details.cy.ts index 069e4b8..9cee950 100644 --- a/application/cypress/e2e/method-details.cy.ts +++ b/application/cypress/e2e/method-details.cy.ts @@ -42,7 +42,7 @@ describe('Test welcome.cy.ts', () => { }); it('should update display order successfully', () => { - const paymentMethodIds = ['paypal', 'ideal', 'bancontact']; + const paymentMethodIds = ['paypal']; cy.findByTestId(`display-order-column-${paymentMethodIds[0]}`).click(); cy.url().should('contain', 'general'); diff --git a/application/cypress/e2e/welcome.cy.ts b/application/cypress/e2e/welcome.cy.ts index 6133421..d508851 100644 --- a/application/cypress/e2e/welcome.cy.ts +++ b/application/cypress/e2e/welcome.cy.ts @@ -24,10 +24,10 @@ describe('Test welcome.cy.ts', () => { const paymentMethods = [ 'PayPal', - 'iDEAL Pay in 3 instalments, 0% interest', 'iDEAL', 'Bancontact', 'Blik', + 'Bank transfer', ]; const headers = ['Payment method', 'Active', 'Icon', 'Display order']; diff --git a/application/src/components/channel-details/channel-details-form.tsx b/application/src/components/channel-details/channel-details-form.tsx deleted file mode 100644 index d309c14..0000000 --- a/application/src/components/channel-details/channel-details-form.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { ReactElement } from 'react'; -import { useFormik, type FormikHelpers } from 'formik'; -import { useIntl } from 'react-intl'; -import LocalizedTextField from '@commercetools-uikit/localized-text-field'; -import TextField from '@commercetools-uikit/text-field'; -import Spacings from '@commercetools-uikit/spacings'; -import SelectField from '@commercetools-uikit/select-field'; -import type { TFormValues } from '../../types'; -import { CHANNEL_ROLES } from './constants'; -import validate from './validate'; -import messages from './messages'; - -type TChannelRole = keyof typeof CHANNEL_ROLES; -type Formik = ReturnType; -type FormProps = { - formElements: ReactElement; - values: Formik['values']; - isDirty: Formik['dirty']; - isSubmitting: Formik['isSubmitting']; - submitForm: Formik['handleSubmit']; - handleReset: Formik['handleReset']; -}; -type TChannelDetailsFormProps = { - onSubmit: ( - values: TFormValues, - formikHelpers: FormikHelpers - ) => void | Promise; - initialValues: TFormValues; - isReadOnly: boolean; - dataLocale: string; - children: (formProps: FormProps) => JSX.Element; -}; - -const getRoleOptions = Object.keys(CHANNEL_ROLES).map((key) => ({ - label: CHANNEL_ROLES[key as TChannelRole], - value: CHANNEL_ROLES[key as TChannelRole], -})); - -const ChannelDetailsForm = (props: TChannelDetailsFormProps) => { - const intl = useIntl(); - const formik = useFormik({ - initialValues: props.initialValues, - onSubmit: props.onSubmit, - validate, - enableReinitialize: true, - }); - - const formElements = ( - - (formik.errors).key} - touched={formik.touched.key} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - isReadOnly={props.isReadOnly} - renderError={(errorKey) => { - if (errorKey === 'duplicate') { - return intl.formatMessage(messages.duplicateKey); - } - return null; - }} - isRequired - horizontalConstraint={13} - /> - (formik.errors).name} - touched={Boolean(formik.touched.name)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - selectedLanguage={props.dataLocale} - isReadOnly={props.isReadOnly} - horizontalConstraint={13} - /> - (formik.errors).roles} - touched={formik.touched.roles} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - isMulti - options={getRoleOptions} - isReadOnly={props.isReadOnly} - isRequired - horizontalConstraint={13} - /> - - ); - - return props.children({ - formElements, - values: formik.values, - isDirty: formik.dirty, - isSubmitting: formik.isSubmitting, - submitForm: formik.handleSubmit, - handleReset: formik.handleReset, - }); -}; -ChannelDetailsForm.displayName = 'ChannelDetailsForm'; - -export default ChannelDetailsForm; diff --git a/application/src/components/channel-details/channel-details.spec.tsx.invalidate b/application/src/components/channel-details/channel-details.spec.tsx.invalidate deleted file mode 100644 index 6332cca..0000000 --- a/application/src/components/channel-details/channel-details.spec.tsx.invalidate +++ /dev/null @@ -1,322 +0,0 @@ -import { graphql, type GraphQLHandler } from 'msw'; -import { setupServer } from 'msw/node'; -import { - fireEvent, - screen, - waitFor, - within, - mapResourceAccessToAppliedPermissions, - type TRenderAppWithReduxOptions, -} from '@commercetools-frontend/application-shell/test-utils'; -import { buildGraphqlList } from '@commercetools-test-data/core'; -import type { TChannel } from '@commercetools-test-data/channel'; -import * as Channel from '@commercetools-test-data/channel'; -import { LocalizedString } from '@commercetools-test-data/commons'; -import { renderApplicationWithRoutesAndRedux } from '../../test-utils'; -import { entryPointUriPath, PERMISSIONS } from '../../constants'; - -const mockServer = setupServer(); -afterEach(() => mockServer.resetHandlers()); -beforeAll(() => { - mockServer.listen({ - // for debugging reasons we force an error when the test fires a request with a query or mutation which is not mocked - // more: https://mswjs.io/docs/api/setup-worker/start#onunhandledrequest - onUnhandledRequest: 'error', - }); -}); -afterAll(() => { - mockServer.close(); -}); - -const TEST_CHANNEL_ID = 'b8a40b99-0c11-43bc-8680-fc570d624747'; -const TEST_CHANNEL_KEY = 'test-key'; -const TEST_CHANNEL_NEW_KEY = 'new-test-key'; - -const renderApp = ( - options: Partial = {}, - includeManagePermissions = true -) => { - const route = - options.route || - `/my-project/${entryPointUriPath}/channels/${TEST_CHANNEL_ID}`; - const { history } = renderApplicationWithRoutesAndRedux({ - route, - project: { - allAppliedPermissions: mapResourceAccessToAppliedPermissions( - [ - PERMISSIONS.View, - includeManagePermissions ? PERMISSIONS.Manage : '', - ].filter(Boolean) - ), - }, - ...options, - }); - return { history }; -}; - -const fetchChannelDetailsQueryHandler = graphql.query( - 'FetchChannelDetails', - (_req, res, ctx) => { - return res( - ctx.data({ - channel: Channel.random() - .name(LocalizedString.random()) - .key(TEST_CHANNEL_KEY) - .buildGraphql(), - }) - ); - } -); - -const fetchChannelDetailsQueryHandlerWithNullData = graphql.query( - 'FetchChannelDetails', - (_req, res, ctx) => { - return res(ctx.data({ channel: null })); - } -); - -const fetchChannelDetailsQueryHandlerWithError = graphql.query( - 'FetchChannelDetails', - (_req, res, ctx) => { - return res( - ctx.data({ channel: null }), - ctx.errors([ - { - message: "Field '$channelId' has wrong value: Invalid ID.", - }, - ]) - ); - } -); - -const updateChannelDetailsHandler = graphql.mutation( - 'UpdateChannelDetails', - (_req, res, ctx) => { - return res( - ctx.data({ - updateChannel: Channel.random() - .name(LocalizedString.random()) - .key(TEST_CHANNEL_KEY) - .buildGraphql(), - }) - ); - } -); - -const updateChannelDetailsHandlerWithDuplicateFieldError = graphql.mutation( - 'UpdateChannelDetails', - (_req, res, ctx) => { - return res( - ctx.data({ updateChannel: null }), - ctx.errors([ - { - message: "A duplicate value '\"test-key\"' exists for field 'key'.", - extensions: { - code: 'DuplicateField', - duplicateValue: 'test-key', - field: 'key', - }, - }, - ]) - ); - } -); - -const updateChannelDetailsHandlerWithARandomError = graphql.mutation( - 'UpdateChannelDetails', - (_req, res, ctx) => { - return res( - ctx.data({ updateChannel: null }), - ctx.errors([ - { - message: 'Some fake error message.', - code: 'SomeFakeErrorCode', - }, - ]) - ); - } -); - -const useMockServerHandlers = (handlers: GraphQLHandler[]) => { - mockServer.use( - graphql.query('FetchChannels', (_req, res, ctx) => { - const totalItems = 2; - - return res( - ctx.data({ - channels: buildGraphqlList( - Array.from({ length: totalItems }).map((_, index) => - Channel.random() - .name(LocalizedString.random()) - .key(`channel-key-${index}`) - ), - { - name: 'Channel', - total: totalItems, - } - ), - }) - ); - }), - ...handlers - ); -}; - -describe('rendering', () => { - it('should render channel details', async () => { - useMockServerHandlers([fetchChannelDetailsQueryHandler]); - renderApp(); - - const keyInput: HTMLInputElement = await screen.findByLabelText( - /channel key/i - ); - expect(keyInput.value).toBe(TEST_CHANNEL_KEY); - - screen.getByRole('combobox', { name: /channel roles/i }); - expect(screen.getByDisplayValue(/primary/i)).toBeInTheDocument(); - }); - it('should reset form values on "revert" button click', async () => { - useMockServerHandlers([fetchChannelDetailsQueryHandler]); - renderApp(); - - const resetButton = await screen.findByRole('button', { - name: /revert/i, - }); - expect(resetButton).toBeDisabled(); - - const keyInput: HTMLInputElement = await screen.findByLabelText( - /channel key/i - ); - expect(keyInput.value).toBe(TEST_CHANNEL_KEY); - - fireEvent.change(keyInput, { - target: { value: TEST_CHANNEL_NEW_KEY }, - }); - expect(keyInput.value).toBe(TEST_CHANNEL_NEW_KEY); - - fireEvent.click(resetButton); - - await waitFor(() => { - expect(keyInput.value).toBe(TEST_CHANNEL_KEY); - }); - }); - describe('when user has no manage permission', () => { - it('should render the form as read-only and keep the "save" button "disabled"', async () => { - useMockServerHandlers([ - fetchChannelDetailsQueryHandler, - updateChannelDetailsHandler, - ]); - renderApp({}, false); - - const keyInput = await screen.findByLabelText(/channel key/i); - expect(keyInput.hasAttribute('readonly')).toBeTruthy(); - - const nameInput = screen.getByLabelText(/en/i, { selector: 'input' }); - expect(nameInput.hasAttribute('readonly')).toBeTruthy(); - - const rolesSelect = screen.getByRole('combobox', { - name: /channel roles/i, - }); - expect(rolesSelect.hasAttribute('readonly')).toBeTruthy(); - - const saveButton = screen.getByRole('button', { name: /save/i }); - expect(saveButton).toBeDisabled(); - }); - }); - it('should display a "page not found" information if the fetched channel details data is null (without an error)', async () => { - useMockServerHandlers([fetchChannelDetailsQueryHandlerWithNullData]); - renderApp(); - - await screen.findByRole('heading', { - name: /we could not find what you are looking for/i, - }); - }); - it('should display a key field validation message if the submitted key value is duplicated', async () => { - useMockServerHandlers([ - fetchChannelDetailsQueryHandler, - updateChannelDetailsHandlerWithDuplicateFieldError, - ]); - renderApp(); - - const keyInput: HTMLInputElement = await screen.findByLabelText( - /channel key/i - ); - - fireEvent.change(keyInput, { - target: { value: TEST_CHANNEL_NEW_KEY }, - }); - expect(keyInput.value).toBe(TEST_CHANNEL_NEW_KEY); - - // updating channel details - const saveButton = screen.getByRole('button', { name: /save/i }); - fireEvent.click(saveButton); - - await screen.findByText(/a channel with this key already exists/i); - }); -}); -describe('notifications', () => { - it('should render a success notification after an update', async () => { - useMockServerHandlers([ - fetchChannelDetailsQueryHandler, - updateChannelDetailsHandler, - ]); - renderApp(); - - const keyInput: HTMLInputElement = await screen.findByLabelText( - /channel key/i - ); - expect(keyInput.value).toBe(TEST_CHANNEL_KEY); - - fireEvent.change(keyInput, { - target: { value: TEST_CHANNEL_NEW_KEY }, - }); - expect(keyInput.value).toBe(TEST_CHANNEL_NEW_KEY); - - const rolesSelect = screen.getByRole('combobox', { - name: /channel roles/i, - }); - fireEvent.focus(rolesSelect); - fireEvent.keyDown(rolesSelect, { key: 'ArrowDown' }); - const inventorySupplyOption = await screen.findByText('InventorySupply'); - - inventorySupplyOption.click(); - expect(screen.getByDisplayValue(/InventorySupply/i)).toBeInTheDocument(); - - // updating channel details - const saveButton = screen.getByRole('button', { name: /save/i }); - fireEvent.click(saveButton); - const notification = await screen.findByRole('alertdialog'); - within(notification).getByText(/channel .+ updated/i); - }); - it('should render an error notification if fetching channel details resulted in an error', async () => { - useMockServerHandlers([fetchChannelDetailsQueryHandlerWithError]); - renderApp(); - await screen.findByText( - /please check your connection, the provided channel ID and try again/i - ); - }); - it('should display an error notification if an update resulted in an unmapped error', async () => { - // Mock error log - jest.spyOn(console, 'error').mockImplementation(); - - useMockServerHandlers([ - fetchChannelDetailsQueryHandler, - updateChannelDetailsHandlerWithARandomError, - ]); - renderApp(); - - const keyInput = await screen.findByLabelText(/channel key/i); - - // we're firing the input change to enable the save button, the value itself is not relevant - fireEvent.change(keyInput, { - target: { value: 'not relevant' }, - }); - - // updating channel details - const saveButton = screen.getByRole('button', { name: /save/i }); - fireEvent.click(saveButton); - - const notification = await screen.findByRole('alertdialog'); - within(notification).getByText(/some fake error message/i); - }); -}); diff --git a/application/src/components/channel-details/channel-details.tsx b/application/src/components/channel-details/channel-details.tsx deleted file mode 100644 index 643c31f..0000000 --- a/application/src/components/channel-details/channel-details.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useIntl } from 'react-intl'; -import { useParams } from 'react-router-dom'; -import { - PageNotFound, - FormModalPage, -} from '@commercetools-frontend/application-components'; -import { ContentNotification } from '@commercetools-uikit/notifications'; -import Text from '@commercetools-uikit/text'; -import Spacings from '@commercetools-uikit/spacings'; -import LoadingSpinner from '@commercetools-uikit/loading-spinner'; -import { useCallback } from 'react'; -import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; -import { formatLocalizedString } from '@commercetools-frontend/l10n'; -import { DOMAINS, NO_VALUE_FALLBACK } from '@commercetools-frontend/constants'; -import { useIsAuthorized } from '@commercetools-frontend/permissions'; -import { - useShowNotification, - useShowApiErrorNotification, - type TApiErrorNotificationOptions, -} from '@commercetools-frontend/actions-global'; -import { PERMISSIONS } from '../../constants'; -import { - useChannelDetailsUpdater, - useChannelDetailsFetcher, -} from '../../hooks/use-channels-connector'; -import { docToFormValues, formValuesToDoc } from './conversions'; -import ChannelsDetailsForm from './channel-details-form'; -import { transformErrors } from './transform-errors'; -import messages from './messages'; -import { ApplicationPageTitle } from '@commercetools-frontend/application-shell'; - -type TChannelDetailsProps = { - onClose: () => void; -}; - -const ChannelDetails = (props: TChannelDetailsProps) => { - const intl = useIntl(); - const params = useParams<{ id: string }>(); - const { loading, error, channel } = useChannelDetailsFetcher(params.id); - const { dataLocale, projectLanguages } = useApplicationContext((context) => ({ - dataLocale: context.dataLocale ?? '', - projectLanguages: context.project?.languages ?? [], - })); - const canManage = useIsAuthorized({ - demandedPermissions: [PERMISSIONS.Manage], - }); - const showNotification = useShowNotification(); - const showApiErrorNotification = useShowApiErrorNotification(); - const channelDetailsUpdater = useChannelDetailsUpdater(); - const handleSubmit = useCallback( - async (formikValues, formikHelpers) => { - const data = formValuesToDoc(formikValues); - try { - await channelDetailsUpdater.execute({ - originalDraft: channel!, - nextDraft: data, - }); - showNotification({ - kind: 'success', - domain: DOMAINS.SIDE, - text: intl.formatMessage(messages.channelUpdated, { - channelName: formatLocalizedString(formikValues, { - key: 'name', - locale: dataLocale, - fallbackOrder: projectLanguages, - }), - }), - }); - } catch (error) { - const transformedErrors = transformErrors(error); - if (transformedErrors.unmappedErrors.length > 0) { - showApiErrorNotification({ - errors: - transformedErrors.unmappedErrors as TApiErrorNotificationOptions['errors'], - }); - } - - formikHelpers.setErrors(transformedErrors.formErrors); - } - }, - [ - channel, - channelDetailsUpdater, - dataLocale, - intl, - projectLanguages, - showApiErrorNotification, - showNotification, - ] - ); - return ( - - {(formProps) => { - const channelName = formatLocalizedString( - { - name: formProps.values?.name, - }, - { - key: 'name', - locale: dataLocale, - fallbackOrder: projectLanguages, - fallback: NO_VALUE_FALLBACK, - } - ); - return ( - formProps.submitForm()} - labelPrimaryButton={FormModalPage.Intl.save} - labelSecondaryButton={FormModalPage.Intl.revert} - > - {loading && ( - - - - )} - {error && ( - - - {intl.formatMessage(messages.channelDetailsErrorMessage)} - - - )} - {channel && formProps.formElements} - {channel && ( - - )} - {channel === null && } - - ); - }} - - ); -}; -ChannelDetails.displayName = 'ChannelDetails'; - -export default ChannelDetails; diff --git a/application/src/components/channel-details/constants.ts b/application/src/components/channel-details/constants.ts deleted file mode 100644 index c6c3378..0000000 --- a/application/src/components/channel-details/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -// https://docs.commercetools.com/api/projects/channels#channelroleenum -export const CHANNEL_ROLES = { - inventorySupply: 'InventorySupply', - productDistribution: 'ProductDistribution', - orderExport: 'OrderExport', - orderImport: 'OrderImport', - primary: 'Primary', -}; diff --git a/application/src/components/channel-details/conversions.ts b/application/src/components/channel-details/conversions.ts deleted file mode 100644 index 2e4b083..0000000 --- a/application/src/components/channel-details/conversions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import LocalizedTextInput from '@commercetools-uikit/localized-text-input'; -import { transformLocalizedFieldToLocalizedString } from '@commercetools-frontend/l10n'; -import type { TFetchChannelDetailsQuery } from '../../types/generated/ctp'; -import type { TFormValues } from '../../types'; - -export const docToFormValues = ( - channel: TFetchChannelDetailsQuery['channel'], - languages: string[] -): TFormValues => ({ - key: channel?.key ?? '', - roles: channel?.roles ?? [], - name: LocalizedTextInput.createLocalizedString( - languages, - transformLocalizedFieldToLocalizedString(channel?.nameAllLocales ?? []) ?? - {} - ), -}); - -export const formValuesToDoc = (formValues: TFormValues) => ({ - name: LocalizedTextInput.omitEmptyTranslations(formValues.name), - key: formValues.key, - roles: formValues.roles, -}); diff --git a/application/src/components/channel-details/index.ts b/application/src/components/channel-details/index.ts deleted file mode 100644 index a9dd420..0000000 --- a/application/src/components/channel-details/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { lazy } from 'react'; - -const ChannelDetails = lazy( - () => import('./channel-details' /* webpackChunkName: "channel-details" */) -); - -export default ChannelDetails; diff --git a/application/src/components/channel-details/messages.ts b/application/src/components/channel-details/messages.ts deleted file mode 100644 index 1b80e1d..0000000 --- a/application/src/components/channel-details/messages.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { defineMessages } from 'react-intl'; - -export default defineMessages({ - backToChannelsList: { - id: 'ChannelDetails.backToChannelsList', - defaultMessage: 'Back to channels list', - }, - duplicateKey: { - id: 'ChannelDetails.duplicateKey', - defaultMessage: 'A channel with this key already exists.', - }, - channelUpdated: { - id: 'ChannelDetails.channelUpdated', - defaultMessage: 'Channel {channelName} updated', - }, - channelKeyLabel: { - id: 'ChannelDetails.channelKeyLabel', - defaultMessage: 'Channel key', - }, - channelNameLabel: { - id: 'ChannelDetails.channelNameLabel', - defaultMessage: 'Channel name', - }, - channelRolesLabel: { - id: 'ChannelDetails.channelRolesLabel', - defaultMessage: 'Channel roles', - }, - hint: { - id: 'ChannelDetails.hint', - defaultMessage: - 'This page demonstrates for instance how to use forms, notifications and how to update data using GraphQL, etc.', - }, - modalTitle: { - id: 'ChannelDetails.modalTitle', - defaultMessage: 'Edit channel', - }, - channelDetailsErrorMessage: { - id: 'ChannelDetails.errorMessage', - defaultMessage: - 'We were unable to fetch the channel details. Please check your connection, the provided channel ID and try again.', - }, -}); diff --git a/application/src/components/channel-details/transform-errors.ts b/application/src/components/channel-details/transform-errors.ts deleted file mode 100644 index 3b3152b..0000000 --- a/application/src/components/channel-details/transform-errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import omitEmpty from 'omit-empty-es'; - -const DUPLICATE_FIELD_ERROR_CODE = 'DuplicateField'; - -type TransformedErrors = { - unmappedErrors: unknown[]; - formErrors: Record; -}; - -export const transformErrors = (error: unknown): TransformedErrors => { - const errorsToMap = Array.isArray(error) ? error : [error]; - - const { formErrors, unmappedErrors } = errorsToMap.reduce( - (transformedErrors, graphQLError) => { - const errorCode = graphQLError?.extensions?.code ?? graphQLError.code; - const fieldName = graphQLError?.extensions?.field ?? graphQLError.field; - - if (errorCode === DUPLICATE_FIELD_ERROR_CODE) { - transformedErrors.formErrors[fieldName] = { duplicate: true }; - } else { - transformedErrors.unmappedErrors.push(graphQLError); - } - return transformedErrors; - }, - { - formErrors: {}, // will be mappped to form field error messages - unmappedErrors: [], // will result in dispatching `showApiErrorNotification` - } - ); - - return { - formErrors: omitEmpty(formErrors), - unmappedErrors, - }; -}; diff --git a/application/src/components/channel-details/validate.ts b/application/src/components/channel-details/validate.ts deleted file mode 100644 index 6333a13..0000000 --- a/application/src/components/channel-details/validate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import TextInput from '@commercetools-uikit/text-input'; -import omitEmpty from 'omit-empty-es'; -import type { FormikErrors } from 'formik'; -import type { TFormValues } from '../../types'; - -type TErrors = { - key: { missing?: boolean }; - roles: { missing?: boolean }; -}; - -const validate = (formikValues: TFormValues): FormikErrors => { - const errors: TErrors = { - key: {}, - roles: {}, - }; - - if (TextInput.isEmpty(formikValues.key)) { - errors.key.missing = true; - } - if (Array.isArray(formikValues.roles) && formikValues.roles.length === 0) { - errors.roles.missing = true; - } - return omitEmpty(errors); -}; - -export default validate; diff --git a/application/src/components/channels/channels.spec.tsx.invalidate b/application/src/components/channels/channels.spec.tsx.invalidate deleted file mode 100644 index 907ab2a..0000000 --- a/application/src/components/channels/channels.spec.tsx.invalidate +++ /dev/null @@ -1,81 +0,0 @@ -import { graphql } from 'msw'; -import { setupServer } from 'msw/node'; -import { - fireEvent, - screen, - mapResourceAccessToAppliedPermissions, - type TRenderAppWithReduxOptions, -} from '@commercetools-frontend/application-shell/test-utils'; -import { buildGraphqlList } from '@commercetools-test-data/core'; -import type { TChannel } from '@commercetools-test-data/channel'; -import * as Channel from '@commercetools-test-data/channel'; -import { LocalizedString } from '@commercetools-test-data/commons'; -import { renderApplicationWithRedux } from '../../test-utils'; -import { entryPointUriPath, PERMISSIONS } from '../../constants'; -import ApplicationRoutes from '../../routes'; - -const mockServer = setupServer(); -afterEach(() => mockServer.resetHandlers()); -beforeAll(() => { - mockServer.listen({ - // for debugging reasons we force an error when the test fires a request with a query or mutation which is not mocked - // more: https://mswjs.io/docs/api/setup-worker/start#onunhandledrequest - onUnhandledRequest: 'error', - }); -}); -afterAll(() => { - mockServer.close(); -}); - -const renderApp = (options: Partial = {}) => { - const route = options.route || `/my-project/${entryPointUriPath}/channels`; - const { history } = renderApplicationWithRedux(, { - route, - project: { - allAppliedPermissions: mapResourceAccessToAppliedPermissions([ - PERMISSIONS.View, - ]), - }, - ...options, - }); - return { history }; -}; - -it('should render channels and paginate to second page', async () => { - mockServer.use( - graphql.query('FetchChannels', (req, res, ctx) => { - // Simulate a server side pagination. - const { offset } = req.variables; - const totalItems = 25; // 2 pages - const itemsPerPage = offset === 0 ? 20 : 5; - - return res( - ctx.data({ - channels: buildGraphqlList( - Array.from({ length: itemsPerPage }).map((_, index) => - Channel.random() - .name(LocalizedString.random()) - .key(`channel-key-${offset === 0 ? index : 20 + index}`) - ), - { - name: 'Channel', - total: totalItems, - } - ), - }) - ); - }) - ); - renderApp(); - - // First page - await screen.findByText('channel-key-0'); - expect(screen.queryByText('channel-key-22')).not.toBeInTheDocument(); - - // Go to second page - fireEvent.click(screen.getByLabelText('Next page')); - - // Second page - await screen.findByText('channel-key-22'); - expect(screen.queryByText('channel-key-0')).not.toBeInTheDocument(); -}); diff --git a/application/src/components/channels/channels.tsx b/application/src/components/channels/channels.tsx deleted file mode 100644 index c41d3ff..0000000 --- a/application/src/components/channels/channels.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useIntl } from 'react-intl'; -import { - Link as RouterLink, - Switch, - useHistory, - useRouteMatch, -} from 'react-router-dom'; -import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; -import { NO_VALUE_FALLBACK } from '@commercetools-frontend/constants'; -import { - usePaginationState, - useDataTableSortingState, -} from '@commercetools-uikit/hooks'; -import { BackIcon } from '@commercetools-uikit/icons'; -import Constraints from '@commercetools-uikit/constraints'; -import FlatButton from '@commercetools-uikit/flat-button'; -import LoadingSpinner from '@commercetools-uikit/loading-spinner'; -import DataTable from '@commercetools-uikit/data-table'; -import { ContentNotification } from '@commercetools-uikit/notifications'; -import { Pagination } from '@commercetools-uikit/pagination'; -import Spacings from '@commercetools-uikit/spacings'; -import Text from '@commercetools-uikit/text'; -import { SuspendedRoute } from '@commercetools-frontend/application-shell'; -import { - formatLocalizedString, - transformLocalizedFieldToLocalizedString, -} from '@commercetools-frontend/l10n'; -import type { TFetchChannelsQuery } from '../../types/generated/ctp'; -import { useChannelsFetcher } from '../../hooks/use-channels-connector'; -import { getErrorMessage } from '../../helpers'; -import messages from './messages'; -import ChannelDetails from '../channel-details'; - -const columns = [ - { key: 'name', label: 'Channel name' }, - { key: 'key', label: 'Channel key', isSortable: true }, - { key: 'roles', label: 'Roles' }, -]; - -type TChannelsProps = { - linkToWelcome: string; -}; - -const Channels = (props: TChannelsProps) => { - const intl = useIntl(); - const match = useRouteMatch(); - const { push } = useHistory(); - const { page, perPage } = usePaginationState(); - const tableSorting = useDataTableSortingState({ key: 'key', order: 'asc' }); - const { dataLocale, projectLanguages } = useApplicationContext((context) => ({ - dataLocale: context.dataLocale, - projectLanguages: context.project?.languages, - })); - const { channelsPaginatedResult, error, loading } = useChannelsFetcher({ - page, - perPage, - tableSorting, - }); - - if (error) { - return ( - - {getErrorMessage(error)} - - ); - } - - return ( - - - } - /> - - - - - - - - - - {loading && } - - {channelsPaginatedResult ? ( - - [0]> - isCondensed - columns={columns} - rows={channelsPaginatedResult.results} - itemRenderer={(item, column) => { - switch (column.key) { - case 'key': - return item.key; - case 'roles': - return item.roles.join(', '); - case 'name': - return formatLocalizedString( - { - name: transformLocalizedFieldToLocalizedString( - item.nameAllLocales ?? [] - ), - }, - { - key: 'name', - locale: dataLocale, - fallbackOrder: projectLanguages, - fallback: NO_VALUE_FALLBACK, - } - ); - default: - return null; - } - }} - sortedBy={tableSorting.value.key} - sortDirection={tableSorting.value.order} - onSortChange={tableSorting.onChange} - onRowClick={(row) => push(`${match.url}/${row.id}`)} - /> - - - - push(`${match.url}`)} /> - - - - ) : null} - - ); -}; -Channels.displayName = 'Channels'; - -export default Channels; diff --git a/application/src/components/channels/index.ts b/application/src/components/channels/index.ts deleted file mode 100644 index 648cf1c..0000000 --- a/application/src/components/channels/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { lazy } from 'react'; - -const Channels = lazy( - () => import('./channels' /* webpackChunkName: "channels" */) -); - -export default Channels; diff --git a/application/src/components/channels/messages.ts b/application/src/components/channels/messages.ts deleted file mode 100644 index b6fcd42..0000000 --- a/application/src/components/channels/messages.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineMessages } from 'react-intl'; - -export default defineMessages({ - backToWelcome: { - id: 'Channels.backToWelcome', - defaultMessage: 'Back to Welcome page', - }, - title: { - id: 'Channels.title', - defaultMessage: 'Channels list', - }, - demoHint: { - id: 'Channels.demoHint', - defaultMessage: - 'This page demonstrates how you can develop a component following some of the Merchant Center UX Guidelines and development best practices. For instance, fetching data using GraphQL, displaying data in a paginated table, writing functional tests, etc.', - }, - noResults: { - id: 'Channels.noResults', - defaultMessage: 'There are no channels available in this project.', - }, -}); diff --git a/application/src/components/method-details/messages.ts b/application/src/components/method-details/messages.ts index b6cfd58..a8d22f3 100644 --- a/application/src/components/method-details/messages.ts +++ b/application/src/components/method-details/messages.ts @@ -78,4 +78,8 @@ export default defineMessages({ id: 'MethodDetails.fieldMaxAmountInvalidValue', defaultMessage: 'Maximum amount has to be higher then minimum amount.', }, + fieldImageUrl: { + id: 'MethodDetails.fieldImageUrl', + defaultMessage: 'Image URL', + }, }); diff --git a/application/src/components/method-details/method-details-form.tsx b/application/src/components/method-details/method-details-form.tsx index 37523b4..85ad1dc 100644 --- a/application/src/components/method-details/method-details-form.tsx +++ b/application/src/components/method-details/method-details-form.tsx @@ -17,6 +17,7 @@ import validate from './validate'; type Formik = ReturnType; type FormProps = { formElements: ReactElement; + iconElements: ReactElement; values: Formik['values']; isDirty: Formik['dirty']; isSubmitting: Formik['isSubmitting']; @@ -172,8 +173,47 @@ const MethodDetailsForm = (props: TCustomObjectDetailsFormProps) => { ); + const iconElements = ( + + icon + (formik.errors) + .imageUrl + } + touched={Boolean(formik.touched.displayOrder)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + isReadOnly={props.isReadOnly} + horizontalConstraint={13} + onInfoButtonClick={() => { + infoModalState.openModal(); + }} + renderError={(errorKey) => { + if (errorKey === 'isNotInteger') { + return intl.formatMessage( + messages.fieldMethodDisplayOrderIsNotInteger + ); + } + return null; + }} + data-testid="image-url-input" + > + + ); + return props.children({ formElements, + iconElements, values: formik.values, isDirty: formik.dirty, isSubmitting: formik.isSubmitting, diff --git a/application/src/components/method-details/method-details.tsx b/application/src/components/method-details/method-details.tsx index 2eb2adc..f10f7af 100644 --- a/application/src/components/method-details/method-details.tsx +++ b/application/src/components/method-details/method-details.tsx @@ -158,9 +158,9 @@ const MethodDetails = (props: TMethodDetailsProps) => { exactPathMatch={true} /> { {method && formProps.formElements} -
Icon
+ {method && formProps.iconElements}
{ 'en-GB': '', }, name: { + 'de-DE': method.description, 'en-GB': method.description, }, status: method.status === 'active' ? 'Active' : 'Inactive', diff --git a/application/src/components/welcome/welcome.tsx b/application/src/components/welcome/welcome.tsx index bb00482..bcd4e80 100644 --- a/application/src/components/welcome/welcome.tsx +++ b/application/src/components/welcome/welcome.tsx @@ -30,9 +30,7 @@ import { getErrorMessage } from '../../helpers'; import { SuspendedRoute } from '@commercetools-frontend/application-shell'; import MethodDetails from '../method-details'; import { useIntl } from 'react-intl'; -import { formatLocalizedString } from '@commercetools-frontend/l10n'; import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; -import { NO_VALUE_FALLBACK } from '@commercetools-frontend/constants'; const Welcome = () => { const intl = useIntl(); @@ -179,7 +177,11 @@ const Welcome = () => { ); case 'name': - return item.technicalName; + return ( +
+ {item.technicalName} +
+ ); case 'image': return ( { > ); case 'order': - return item.displayOrder ?? '-'; + return ( +
+ {item.displayOrder ?? '-'} +
+ ); default: return null; } diff --git a/application/src/hooks/use-mollie-connector/use-mollie-connector.ts b/application/src/hooks/use-mollie-connector/use-mollie-connector.ts index 2662c05..ba85ba4 100644 --- a/application/src/hooks/use-mollie-connector/use-mollie-connector.ts +++ b/application/src/hooks/use-mollie-connector/use-mollie-connector.ts @@ -56,7 +56,7 @@ const convertMollieMethodToCustomMethod = ( acc[lang] = ''; return acc; }, {} as Record), - imageUrl: method.image.svg, + imageUrl: method.image.svg || '', status: 'Inactive', displayOrder: 0, })); diff --git a/processor/package-lock.json b/processor/package-lock.json index 66b2637..9e068d6 100644 --- a/processor/package-lock.json +++ b/processor/package-lock.json @@ -1,12 +1,12 @@ { "name": "shopmacher-mollie-processor", - "version": "1.2.0-alpha+build.10.10.24", + "version": "1.2.0-alpha+build.15.11.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shopmacher-mollie-processor", - "version": "1.2.0-alpha+build.10.10.24", + "version": "1.2.0-alpha+build.15.11.24", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -386,14 +386,6 @@ "node": ">=4" } }, - "node_modules/@commercetools-backend/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", @@ -2126,11 +2118,6 @@ "node": ">=0.10.0" } }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -2187,18 +2174,6 @@ "integrity": "sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==", "dev": true }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -2324,15 +2299,6 @@ "node": ">=10" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -3379,14 +3345,6 @@ "semver": "bin/semver.js" } }, - "node_modules/winston-transport/node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4549,11 +4507,6 @@ "node": ">= 0.8" } }, - "node_modules/@commercetools/connect-payments-sdk/node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" - }, "node_modules/shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -4846,11 +4799,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -5165,18 +5113,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6921,21 +6857,6 @@ "node": ">= 0.6" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ngrok": { "version": "5.0.0-beta.2", "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-5.0.0-beta.2.tgz", @@ -8222,12 +8143,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9458,4 +9373,4 @@ } } } -} \ No newline at end of file +} diff --git a/processor/package.json b/processor/package.json index 3182309..8b09276 100644 --- a/processor/package.json +++ b/processor/package.json @@ -1,7 +1,7 @@ { "name": "shopmacher-mollie-processor", "description": "Integration between commercetools and mollie payment service provider", - "version": "1.2.0-alpha+build.10.10.24", + "version": "1.2.0-alpha+build.15.11.24", "main": "index.js", "private": true, "scripts": {