From 6281e63411875ba5c0ce54b5becbb76e2bdcbb3a Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 8 May 2024 11:27:31 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20error=20rendering=20when?= =?UTF-8?q?=20using=20a=20duplicate=20offer=20code=20(#20156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://linear.app/tryghost/issue/ONC-15 - when adding or editing an offer, the backend throws an error if the offer code is already in use. This error was not being surfaced correctly in Admin --- apps/admin-x-framework/src/test/acceptance.ts | 4 ++- .../settings/growth/offers/AddOfferModal.tsx | 25 +++++++++++-- .../settings/growth/offers/EditOfferModal.tsx | 24 +++++++++++-- .../test/acceptance/membership/offers.test.ts | 36 ++++++++++++++++--- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/apps/admin-x-framework/src/test/acceptance.ts b/apps/admin-x-framework/src/test/acceptance.ts index ba5e842223..6bebb95f4e 100644 --- a/apps/admin-x-framework/src/test/acceptance.ts +++ b/apps/admin-x-framework/src/test/acceptance.ts @@ -37,6 +37,7 @@ interface MockRequestConfig { path: string | RegExp; response: unknown; responseStatus?: number; + responseHeaders?: {[key: string]: string}; } interface RequestRecord { @@ -189,7 +190,8 @@ export async function mockApi await route.fulfill({ status: matchingMock.responseStatus || 200, - body: typeof matchingMock.response === 'string' ? matchingMock.response : JSON.stringify(matchingMock.response) + body: typeof matchingMock.response === 'string' ? matchingMock.response : JSON.stringify(matchingMock.response), + headers: matchingMock.responseHeaders || {} }); }); diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx index a180dfcff5..159dba5d2c 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx @@ -1,7 +1,9 @@ import PortalFrame from '../../membership/portal/PortalFrame'; +import toast from 'react-hot-toast'; import {Button} from '@tryghost/admin-x-design-system'; import {ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks'; import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {JSONError} from '@tryghost/admin-x-framework/errors'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; @@ -636,16 +638,35 @@ const AddOfferModal = () => { validate(); const isErrorsEmpty = Object.values(errors).every(error => !error); if (!isErrorsEmpty) { + toast.remove(); showToast({ type: 'pageError', message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' }); return; } - if (!(await handleSave())) { + + try { + if (await handleSave({force: true})) { + return; + } else { + toast.remove(); + showToast({ + type: 'pageError', + message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' + }); + }; + } catch (e) { + let message; + + if (e instanceof JSONError && e.data && e.data.errors[0]) { + message = e.data.errors[0].context || e.data.errors[0].message; + } + + toast.remove(); showToast({ type: 'pageError', - message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields.' + message: message || 'Something went wrong while saving the offer, please try again' }); } }} diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx index cd6534a479..39e4e61b1a 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx @@ -1,8 +1,9 @@ import NiceModal from '@ebay/nice-modal-react'; import PortalFrame from '../../membership/portal/PortalFrame'; -// import useFeatureFlag from '../../../../hooks/useFeatureFlag'; +import toast from 'react-hot-toast'; import {Button, ConfirmationModal, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system'; import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; +import {JSONError} from '@tryghost/admin-x-framework/errors'; import {Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers'; import {createRedemptionFilterUrl} from './OffersIndex'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; @@ -280,10 +281,27 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => { } }} onOk={async () => { - if (!(await handleSave({fakeWhenUnchanged: true}))) { + try { + if (await handleSave({force: true})) { + return; + } else { + toast.remove(); + showToast({ + type: 'pageError', + message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' + }); + } + } catch (e) { + let message; + + if (e instanceof JSONError && e.data && e.data.errors[0]) { + message = e.data.errors[0].context || e.data.errors[0].message; + } + + toast.remove(); showToast({ type: 'pageError', - message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields.' + message: message || 'Something went wrong while saving the offer, please try again' }); } }} /> : null; diff --git a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts index 4bb4db4b03..a9ee04cf1e 100644 --- a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts @@ -3,10 +3,6 @@ import {globalDataRequests} from '../../utils/acceptance'; import {mockApi, responseFixtures, settingsWithStripe} from '@tryghost/admin-x-framework/test/acceptance'; test.describe('Offers Modal', () => { - // test.beforeEach(async () => { - // toggleLabsFlag('adminXOffers', true); - // }); - test('Offers Modal is available', async ({page}) => { await mockApi({page, requests: { ...globalDataRequests, @@ -108,9 +104,41 @@ test.describe('Offers Modal', () => { await modal.getByRole('button', {name: 'New offer'}).click(); const addModal = page.getByTestId('add-offer-modal'); await addModal.getByRole('button', {name: 'Publish'}).click(); + await expect(page.getByTestId('toast-error')).toContainText(/Can't save offer, please double check that you've filled all mandatory fields./); }); + test('Errors if the offer code is already taken', async ({page}) => { + await mockApi({page, requests: { + browseOffers: {method: 'GET', path: '/offers/', response: responseFixtures.offers}, + ...globalDataRequests, + browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe}, + browseOffersById: {method: 'GET', path: `/offers/${responseFixtures.offers.offers![0].id}/`, response: responseFixtures.offers}, + browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers}, + addOffer: {method: 'POST', path: `/offers/`, responseStatus: 400, responseHeaders: {'content-type': 'json'}, response: { + errors: [{ + message: 'Validation error, cannot edit offer.', + context: 'Offer `code` must be unique. Please change and try again.' + }] + }} + }}); + + await page.goto('/'); + const section = page.getByTestId('offers'); + await section.getByRole('button', {name: 'Manage offers'}).click(); + const modal = page.getByTestId('offers-modal'); + await modal.getByRole('button', {name: 'New offer'}).click(); + const addModal = page.getByTestId('add-offer-modal'); + expect(addModal).toBeVisible(); + const sidebar = addModal.getByTestId('add-offer-sidebar'); + expect(sidebar).toBeVisible(); + await sidebar.getByPlaceholder(/^Black Friday$/).fill('Coffee Tuesdays'); + await sidebar.getByLabel('Amount off').fill('10'); + await addModal.getByRole('button', {name: 'Publish'}).click(); + + await expect(page.getByTestId('toast-error')).toContainText(/Offer `code` must be unique. Please change and try again./); + }); + test('Shows validation hints', async ({page}) => { await mockApi({page, requests: { browseOffers: {method: 'GET', path: '/offers/', response: responseFixtures.offers},