🐛 Fixed error rendering when using a duplicate offer code (#20156)
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
This commit is contained in:
parent
e8e3447f15
commit
6281e63411
@ -37,6 +37,7 @@ interface MockRequestConfig {
|
|||||||
path: string | RegExp;
|
path: string | RegExp;
|
||||||
response: unknown;
|
response: unknown;
|
||||||
responseStatus?: number;
|
responseStatus?: number;
|
||||||
|
responseHeaders?: {[key: string]: string};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestRecord {
|
interface RequestRecord {
|
||||||
@ -189,7 +190,8 @@ export async function mockApi<Requests extends Record<string, MockRequestConfig>
|
|||||||
|
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: matchingMock.responseStatus || 200,
|
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 || {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import PortalFrame from '../../membership/portal/PortalFrame';
|
import PortalFrame from '../../membership/portal/PortalFrame';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import {Button} from '@tryghost/admin-x-design-system';
|
import {Button} from '@tryghost/admin-x-design-system';
|
||||||
import {ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
|
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 {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 {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
|
||||||
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
|
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
|
||||||
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
||||||
@ -636,16 +638,35 @@ const AddOfferModal = () => {
|
|||||||
validate();
|
validate();
|
||||||
const isErrorsEmpty = Object.values(errors).every(error => !error);
|
const isErrorsEmpty = Object.values(errors).every(error => !error);
|
||||||
if (!isErrorsEmpty) {
|
if (!isErrorsEmpty) {
|
||||||
|
toast.remove();
|
||||||
showToast({
|
showToast({
|
||||||
type: 'pageError',
|
type: 'pageError',
|
||||||
message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly'
|
message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly'
|
||||||
});
|
});
|
||||||
return;
|
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({
|
showToast({
|
||||||
type: 'pageError',
|
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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import PortalFrame from '../../membership/portal/PortalFrame';
|
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 {Button, ConfirmationModal, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||||
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
|
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 {Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers';
|
||||||
import {createRedemptionFilterUrl} from './OffersIndex';
|
import {createRedemptionFilterUrl} from './OffersIndex';
|
||||||
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
|
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
|
||||||
@ -280,10 +281,27 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onOk={async () => {
|
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({
|
showToast({
|
||||||
type: 'pageError',
|
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;
|
}} /> : null;
|
||||||
|
@ -3,10 +3,6 @@ import {globalDataRequests} from '../../utils/acceptance';
|
|||||||
import {mockApi, responseFixtures, settingsWithStripe} from '@tryghost/admin-x-framework/test/acceptance';
|
import {mockApi, responseFixtures, settingsWithStripe} from '@tryghost/admin-x-framework/test/acceptance';
|
||||||
|
|
||||||
test.describe('Offers Modal', () => {
|
test.describe('Offers Modal', () => {
|
||||||
// test.beforeEach(async () => {
|
|
||||||
// toggleLabsFlag('adminXOffers', true);
|
|
||||||
// });
|
|
||||||
|
|
||||||
test('Offers Modal is available', async ({page}) => {
|
test('Offers Modal is available', async ({page}) => {
|
||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
@ -108,9 +104,41 @@ test.describe('Offers Modal', () => {
|
|||||||
await modal.getByRole('button', {name: 'New offer'}).click();
|
await modal.getByRole('button', {name: 'New offer'}).click();
|
||||||
const addModal = page.getByTestId('add-offer-modal');
|
const addModal = page.getByTestId('add-offer-modal');
|
||||||
await addModal.getByRole('button', {name: 'Publish'}).click();
|
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./);
|
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}) => {
|
test('Shows validation hints', async ({page}) => {
|
||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
browseOffers: {method: 'GET', path: '/offers/', response: responseFixtures.offers},
|
browseOffers: {method: 'GET', path: '/offers/', response: responseFixtures.offers},
|
||||||
|
Loading…
Reference in New Issue
Block a user