From 452e702530251de4c32b187311c75539275ef50b Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Wed, 15 Nov 2023 11:52:58 +0700 Subject: [PATCH] Wired up DB to new Offers Modal (#18980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://github.com/TryGhost/Product/issues/4147 --- ### 🤖 Generated by Copilot at b18d725 This pull request adds the functionality to create new offers for members in the admin-x-settings app. It uses custom hooks to handle the form logic and the API mutation, and updates the type definitions and the UI components accordingly. It also modifies the `offers.ts` file in the admin-x-framework to support the new feature. --------- Co-authored-by: Chris Raible Co-authored-by: Sodbileg Gansukh --- apps/admin-x-framework/src/api/offers.ts | 23 ++- .../settings/growth/offers/AddOfferModal.tsx | 163 ++++++++++++------ .../settings/growth/offers/OffersModal.tsx | 8 +- 3 files changed, 136 insertions(+), 58 deletions(-) diff --git a/apps/admin-x-framework/src/api/offers.ts b/apps/admin-x-framework/src/api/offers.ts index 51af4b8e3c..d22ece77ea 100644 --- a/apps/admin-x-framework/src/api/offers.ts +++ b/apps/admin-x-framework/src/api/offers.ts @@ -18,19 +18,27 @@ export type Offer = { redemption_count: number; tier: { id: string; - name: string; + name?: string; } } +export type PartialNewOffer = Omit; +export type NewOffer = Partial> & Omit; + export interface OffersResponseType { meta?: Meta - offers: Offer[] + offers?: Offer[] } export interface OfferEditResponseType extends OffersResponseType { meta?: Meta } +export interface OfferAddResponseType { + meta?: Meta, + offers: NewOffer[] +} + const dataType = 'OffersResponseType'; export const useBrowseOffers = createQuery({ @@ -53,3 +61,14 @@ export const useEditOffer = createMutation({ update: updateQueryCache('offers') } }); + +export const useAddOffer = createMutation({ + method: 'POST', + path: () => '/offers/', + body: offer => ({offers: [offer]}), + updateQueries: { + dataType, + emberUpdateType: 'createOrUpdate', + update: updateQueryCache('offers') + } +}); 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 c6843b88bc..2a7e54cf02 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,10 +1,12 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import PortalFrame from '../../membership/portal/PortalFrame'; import useFeatureFlag from '../../../../hooks/useFeatureFlag'; -import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField} from '@tryghost/admin-x-design-system'; +import useForm from '../../../../hooks/useForm'; +import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system'; import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {getTiersCadences} from '../../../../utils/getTiersCadences'; +import {useAddOffer} from '@tryghost/admin-x-framework/api/offers'; import {useEffect, useState} from 'react'; import {useGlobalData} from '../../../providers/GlobalDataProvider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -206,6 +208,7 @@ const AddOfferModal = () => { const {data: {tiers} = {}} = useBrowseTiers(); const activeTiers = getPaidActiveTiers(tiers || []); const tierCadenceOptions = getTiersCadences(activeTiers); + const {mutateAsync: addOffer} = useAddOffer(); const [selectedTier, setSelectedTier] = useState({ tier: tierCadenceOptions[0] || {}, dataset: { @@ -214,34 +217,76 @@ const AddOfferModal = () => { currency: tierCadenceOptions[0]?.value ? parseData(tierCadenceOptions[0]?.value).currency : '' } }); + const getDiscountAmount = (discount: number, dctype: string) => { + if (dctype === 'percentageOff') { + return discount.toString(); + } + if (dctype === 'currencyOff') { + let calcDiscount = discount * 100; + return calcDiscount.toString(); + } + }; - const [overrides, setOverrides] = useState({ - disableBackground: true, - name: '', - code: { - isDirty: false, - value: '' + const {formState, updateForm, handleSave, saveState, okProps} = useForm({ + initialState: { + disableBackground: true, + name: '', + code: { + isDirty: false, + value: '' + }, + displayTitle: { + isDirty: false, + value: '' + }, + displayDescription: '', + type: 'percent', + cadence: selectedTier?.dataset?.period || '', + trialAmount: 7, + discountAmount: 0, + duration: 'once', + durationInMonths: 0, + currency: selectedTier?.dataset?.currency || '', + status: 'active', + tierId: selectedTier?.dataset?.id || '', + amountType: 'percentageOff' }, - displayTitle: { - isDirty: false, - value: '' + onSave: async () => { + const dataset = { + name: formState.name, + code: formState.code.value, + display_title: formState.displayTitle.value, + display_description: formState.displayDescription, + cadence: formState.cadence, + amount: formState.type === 'trial' ? Number(getDiscountAmount(formState.trialAmount, formState.amountType)) : Number(getDiscountAmount(formState.discountAmount, formState.amountType)), + duration: formState.type === 'trial' ? 'trial' : formState.duration, + duration_in_months: formState.durationInMonths, + currency: formState.currency, + status: formState.status, + tier: { + id: formState.tierId + }, + type: formState.type, + currency_restriction: false + }; + + const response = await addOffer(dataset); + + if (response && response.offers && response.offers.length > 0) { + modal.remove(); + updateRoute(`offers/${response.offers[0].id}`); + } }, - displayDescription: '', - type: 'percent', - cadence: selectedTier?.dataset?.period || '', - trialAmount: 7, - discountAmount: 0, - duration: 'once', - durationInMonths: 0, - currency: selectedTier?.dataset?.currency || '', - status: 'active', - tierId: selectedTier?.dataset?.id || '', - amountType: 'percentageOff' + onSaveError: () => {}, + onValidate: () => { + return {}; + }, + savingDelay: 500 }); const amountOptions = [ {value: 'percentageOff', label: '%'}, - {value: 'currencyOff', label: overrides.currency} + {value: 'currencyOff', label: formState.currency} ]; const handleTierChange = (tier: SelectOption) => { @@ -249,27 +294,26 @@ const AddOfferModal = () => { tier, dataset: parseData(tier.value) }); - - setOverrides({ - ...overrides, - tierId: parseData(tier.value).id, + updateForm(state => ({ + ...state, cadence: parseData(tier.value).period, - currency: parseData(tier.value).currency - }); + currency: parseData(tier.value).currency, + tierId: parseData(tier.value).id + })); }; const handleTypeChange = (type: string) => { - setOverrides({ - ...overrides, + updateForm(state => ({ + ...state, type: type - }); + })); }; const handleAmountTypeChange = (amountType: string) => { - setOverrides({ - ...overrides, + updateForm(state => ({ + ...state, amountType: amountType - }); + })); }; const handleTextInput = ( @@ -277,16 +321,15 @@ const AddOfferModal = () => { key: keyof offerPortalPreviewUrlTypes ) => { const target = e.target as HTMLInputElement | HTMLTextAreaElement; - setOverrides((prevOverrides: offerPortalPreviewUrlTypes) => { + updateForm((state) => { // Extract the current value for the key - const currentValue = prevOverrides[key]; - + const currentValue = (state as offerPortalPreviewUrlTypes)[key]; // Check if the current value is an object and has 'isDirty' and 'value' properties if (currentValue && typeof currentValue === 'object' && 'isDirty' in currentValue && 'value' in currentValue) { // Determine if the field has been modified return { - ...prevOverrides, + ...state, [key]: { ...currentValue, isDirty: true, @@ -296,7 +339,7 @@ const AddOfferModal = () => { } else { // For simple properties, update the value directly return { - ...prevOverrides, + ...state, [key]: target.value }; } @@ -305,8 +348,7 @@ const AddOfferModal = () => { const handleNameInput = (e: React.ChangeEvent) => { const newValue = e.target.value; - - setOverrides((prevOverrides) => { + updateForm((prevOverrides) => { let newOverrides = {...prevOverrides}; newOverrides.name = newValue; if (!prevOverrides.code.isDirty) { @@ -327,17 +369,17 @@ const AddOfferModal = () => { const handleTextAreaInput = (e: React.ChangeEvent) => { const target = e.target as HTMLTextAreaElement; - setOverrides({ - ...overrides, + updateForm(state => ({ + ...state, displayDescription: target.value - }); + })); }; const handleDurationChange = (duration: string) => { - setOverrides({ - ...overrides, + updateForm(state => ({ + ...state, duration: duration - }); + })); }; useEffect(() => { @@ -353,9 +395,9 @@ const AddOfferModal = () => { }; useEffect(() => { - const newHref = getOfferPortalPreviewUrl(overrides, siteData.url); + const newHref = getOfferPortalPreviewUrl(formState, siteData.url); setHref(newHref); - }, [overrides, siteData.url]); + }, [formState, siteData.url]); const sidebar = { handleTextInput={handleTextInput} handleTierChange={handleTierChange} handleTypeChange={handleTypeChange} - overrides={overrides} + overrides={formState} selectedTier={selectedTier.tier} tierOptions={tierCadenceOptions} typeOptions={typeOptions} @@ -376,8 +418,25 @@ const AddOfferModal = () => { const iframe = ; - - return ; + return { + if (!(await handleSave({fakeWhenUnchanged: true}))) { + showToast({ + type: 'pageError', + message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields.' + }); + } + }} />; }; export default NiceModal.create(AddOfferModal); diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/OffersModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/OffersModal.tsx index c2d643126f..e336ed8cce 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/OffersModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/OffersModal.tsx @@ -130,11 +130,11 @@ const OffersModal = () => { currency={offer?.currency || 'USD'} duration={offer?.duration} name={offer?.name} - offerId={offer?.id} + offerId={offer?.id ? offer.id : ''} offerTier={offerTier} - redemptionCount={offer?.redemption_count} + redemptionCount={offer?.redemption_count ? offer.redemption_count : 0} type={offer?.type as OfferType} - onClick={() => handleOfferEdit(offer?.id)} + onClick={() => handleOfferEdit(offer?.id ? offer.id : '')} /> ); })} @@ -156,7 +156,7 @@ const OffersModal = () => { {offerTier.name} {getOfferCadence(offer.cadence)} {discountOffer} {updatedPriceWithCurrency}{originalPriceWithCurrency} - {offer.redemption_count} + {offer.redemption_count} ); })}