From a0f90591893190925b448c5027218efaf821d5ad Mon Sep 17 00:00:00 2001 From: Ghost CI <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:25:49 +0000 Subject: [PATCH 001/191] Bumped version to 6.57.2-rc.0 --- apps/ember-admin/package.json | 2 +- ghost/core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ember-admin/package.json b/apps/ember-admin/package.json index 41578db6469..b20705daab9 100644 --- a/apps/ember-admin/package.json +++ b/apps/ember-admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "6.57.1", + "version": "6.57.2-rc.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/package.json b/ghost/core/package.json index 388420ffa26..ed885e49510 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "6.57.1", + "version": "6.57.2-rc.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", From c8be2918b2dd6de3b4daed260bedcca6e71a27ea Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Mon, 10 Aug 2026 16:54:45 -0400 Subject: [PATCH 002/191] Fixed docker build to upload core image as artifact in forks (#29873) no ref - makes the core build consistent with admin/e2e builds --- .github/workflows/ci.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 808b2d63110..f3df26033d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -534,7 +534,8 @@ jobs: name: Performance tests (production image) runs-on: blacksmith-2vcpu-ubuntu-2404 needs: [job_setup, job_docker] - # Registry path only: the core image is pushed to GHCR, never saved as an artifact. + # Registry path only: benchmarks are a canonical-repo series, so this pulls the + # core image from GHCR rather than loading the artifact-path tarball. if: | needs.job_docker.result == 'success' && needs.job_docker.outputs.use-artifact == 'false' && @@ -1399,6 +1400,29 @@ jobs: cache-from: type=registry,ref=${{ steps.strategy.outputs.image-core-name }}:cache-main cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', steps.strategy.outputs.image-core-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} + # Uploaded here, before the full image is even built: on the artifact path + # consumers (Ghost-Moya CD) need the core image — server only, no admin — + # and would otherwise have to fall back to `docker-image-production`. + - name: Save core image as artifact + if: steps.strategy.outputs.use-artifact == 'true' + run: | + IMAGE_TAG=$(echo "${{ steps.meta-core.outputs.tags }}" | head -n1) + echo "Saving image: $IMAGE_TAG" + # Written outside the repo root: a stray tarball there would change the + # `.` context between the core and full builds and bust the deploy-stage + # COPY cache (same reason the admin artifact lands in RUNNER_TEMP below). + docker save "$IMAGE_TAG" | gzip > "${RUNNER_TEMP}/docker-image-core.tar.gz" + ls -lh "${RUNNER_TEMP}/docker-image-core.tar.gz" + + - name: Upload core image artifact + if: steps.strategy.outputs.use-artifact == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: docker-image-core + path: ${{ runner.temp }}/docker-image-core.tar.gz + retention-days: 1 + if-no-files-found: error + # Synchronise with job_build_admin only now, after core is built: the full # image is core + admin. No `needs` edge, so the two jobs run concurrently # and core builds during the wait. From bd615485186704177a96be582c68a67d75440031 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 10 Aug 2026 17:22:42 -0500 Subject: [PATCH 003/191] Changed settings confirmations to a context provider (#29818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref First slice of the settings NiceModal burn-down (follows the router swap #29780 and un-portal #29789). `ConfirmationModal` and `LimitModal` were NiceModal-shown from **54 call sites across 27 files** — the bulk of settings' remaining NiceModal usage. They now go through a settings-local `ConfirmationProvider`: --- apps/admin/src/settings/app/app.tsx | 13 +++-- .../components/confirmation-modal.test.tsx | 25 +++++---- .../app/components/confirmation-modal.tsx | 19 ++++--- .../app/components/limit-modal.test.tsx | 25 +++++---- .../settings/app/components/limit-modal.tsx | 11 ++-- .../providers/confirmation-provider.tsx | 56 +++++++++++++++++++ .../settings/advanced/danger-zone.tsx | 10 ++-- .../settings/advanced/integrations.tsx | 6 +- .../integrations/add-integration-modal.tsx | 8 +-- .../integrations/custom-integration-modal.tsx | 6 +- .../integrations/transistor-modal.tsx | 6 +- .../integrations/webhooks-table.test.tsx | 5 +- .../advanced/integrations/webhooks-table.tsx | 5 +- .../advanced/integrations/zapier-modal.tsx | 6 +- .../universal-import-modal.tsx | 5 +- .../app/components/settings/email/emails.tsx | 9 +-- .../components/settings/email/newsletters.tsx | 23 ++++---- .../newsletters/add-newsletter-modal.tsx | 8 +-- .../newsletters/newsletter-detail-modal.tsx | 11 ++-- .../newsletters/newsletters-tab-content.tsx | 23 ++++---- .../settings/general/user-detail-modal.tsx | 13 ++--- .../settings/general/users/staff-token.tsx | 6 +- .../growth/offers/edit-offer-modal.tsx | 8 +-- .../edit-recommendation-modal.tsx | 6 +- .../custom-fields/custom-field-modal.tsx | 9 +-- .../settings/membership/member-emails.tsx | 9 +-- .../membership/portal/portal-modal.tsx | 12 ++-- .../stripe/stripe-connect-modal.tsx | 12 ++-- .../components/settings/membership/tiers.tsx | 6 +- .../membership/tiers/tier-detail-modal.tsx | 6 +- .../components/settings/site/change-theme.tsx | 8 +-- .../settings/site/design-and-theme-modal.tsx | 8 +-- .../components/settings/site/theme-modal.tsx | 32 ++++++----- .../site/theme/advanced-theme-settings.tsx | 8 +-- .../site/theme/invalid-theme-modal.tsx | 6 +- .../site/theme/theme-installed-modal.tsx | 6 +- 36 files changed, 258 insertions(+), 177 deletions(-) create mode 100644 apps/admin/src/settings/app/components/providers/confirmation-provider.tsx diff --git a/apps/admin/src/settings/app/app.tsx b/apps/admin/src/settings/app/app.tsx index c560ce3737c..5db40d992aa 100644 --- a/apps/admin/src/settings/app/app.tsx +++ b/apps/admin/src/settings/app/app.tsx @@ -1,6 +1,7 @@ import MainContent from './main-content'; import NiceModal from '@ebay/nice-modal-react'; import SettingsAppProvider, {type UpgradeStatusType} from './components/providers/settings-app-provider'; +import {ConfirmationProvider} from './components/providers/confirmation-provider'; import {Outlet, useLocation} from '@tryghost/admin-x-framework'; import {useEffect} from 'react'; import {useScrollSectionContext} from './hooks/use-scroll-section'; @@ -27,11 +28,13 @@ export function App({upgradeStatus}: AppProps) { return (
- - - - - + + + + + + +
); diff --git a/apps/admin/src/settings/app/components/confirmation-modal.test.tsx b/apps/admin/src/settings/app/components/confirmation-modal.test.tsx index 0dcb8d2f5cd..39472b55f48 100644 --- a/apps/admin/src/settings/app/components/confirmation-modal.test.tsx +++ b/apps/admin/src/settings/app/components/confirmation-modal.test.tsx @@ -1,18 +1,19 @@ -import ConfirmationModal, {type ConfirmationModalProps} from '@/settings/app/components/confirmation-modal'; -import NiceModal from '@ebay/nice-modal-react'; -import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {type ConfirmationModalProps} from '@/settings/app/components/confirmation-modal'; +import {useEffect} from 'react'; +import {ConfirmationProvider, useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; describe('ConfirmationModal', () => { - afterEach(() => { - void NiceModal.remove(ConfirmationModal); - }); - const showModal = (props: ConfirmationModalProps) => { - render(); - - act(() => { - void NiceModal.show(ConfirmationModal, props); - }); + const Trigger = () => { + const {confirm} = useConfirmation(); + useEffect(() => { + confirm(props); + + }, [confirm]); + return null; + }; + render(); }; it('renders the supplied content and confirms without closing implicitly', async () => { diff --git a/apps/admin/src/settings/app/components/confirmation-modal.tsx b/apps/admin/src/settings/app/components/confirmation-modal.tsx index 3e408c86b08..892ea1b62dc 100644 --- a/apps/admin/src/settings/app/components/confirmation-modal.tsx +++ b/apps/admin/src/settings/app/components/confirmation-modal.tsx @@ -1,4 +1,3 @@ -import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import { AlertDialog, @@ -31,7 +30,14 @@ export interface ConfirmationModalProps { testId?: string; } -export const ConfirmationModalContent: React.FC = ({ +export type ConfirmationHostProps = { + visible?: boolean; + onRemove: () => void; +}; + +export const ConfirmationModalContent: React.FC = ({ + visible = true, + onRemove, title = 'Are you sure?', prompt, cancelLabel = 'Cancel', @@ -45,7 +51,6 @@ export const ConfirmationModalContent: React.FC = ({ stickyFooter = false, testId = 'confirmation-modal' }) => { - const modal = useModal(); const [taskState, setTaskState] = useState<'running' | ''>(''); const isRunning = taskState === 'running'; const runningLabel = okRunningLabel || okLabel; @@ -58,7 +63,7 @@ export const ConfirmationModalContent: React.FC = ({ if (onCancel) { onCancel(); } else { - modal.remove(); + onRemove(); } }; @@ -66,7 +71,7 @@ export const ConfirmationModalContent: React.FC = ({ setTaskState('running'); try { - await onOk?.(modal); + await onOk?.({remove: onRemove}); } catch (error) { // eslint-disable-next-line no-console console.error('Unhandled Promise Rejection. Make sure you catch errors in your onOk handler.', error); @@ -94,7 +99,7 @@ export const ConfirmationModalContent: React.FC = ({ const footer = customFooter === undefined ? defaultFooter : customFooter; return ( - !open && handleCancel()}> + !open && handleCancel()}> = ({ ); }; - -export default NiceModal.create(ConfirmationModalContent); diff --git a/apps/admin/src/settings/app/components/limit-modal.test.tsx b/apps/admin/src/settings/app/components/limit-modal.test.tsx index 9a18d0ca831..fc3e6058be1 100644 --- a/apps/admin/src/settings/app/components/limit-modal.test.tsx +++ b/apps/admin/src/settings/app/components/limit-modal.test.tsx @@ -1,18 +1,19 @@ -import LimitModal, {type LimitModalProps} from '@/settings/app/components/limit-modal'; -import NiceModal from '@ebay/nice-modal-react'; -import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {type LimitModalProps} from '@/settings/app/components/limit-modal'; +import {useEffect} from 'react'; +import {ConfirmationProvider, useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; describe('LimitModal', () => { - afterEach(() => { - void NiceModal.remove(LimitModal); - }); - const showModal = (props: LimitModalProps) => { - render(); - - act(() => { - void NiceModal.show(LimitModal, props); - }); + const Trigger = () => { + const {showLimit} = useConfirmation(); + useEffect(() => { + showLimit(props); + + }, [showLimit]); + return null; + }; + render(); }; it('preserves the upgrade defaults and renders HTML prompts', async () => { diff --git a/apps/admin/src/settings/app/components/limit-modal.tsx b/apps/admin/src/settings/app/components/limit-modal.tsx index 20f5b4bd033..2f9a0b91c05 100644 --- a/apps/admin/src/settings/app/components/limit-modal.tsx +++ b/apps/admin/src/settings/app/components/limit-modal.tsx @@ -1,7 +1,6 @@ -import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; -import {ConfirmationModalContent} from './confirmation-modal'; +import {type ConfirmationHostProps, ConfirmationModalContent} from './confirmation-modal'; export interface LimitModalProps { title?: string; @@ -13,7 +12,9 @@ export interface LimitModalProps { }) => void | Promise; } -export const LimitModalContent: React.FC = ({ +export const LimitModalContent: React.FC = ({ + visible = true, + onRemove, title = 'Upgrade your plan', prompt, okLabel = 'Upgrade', @@ -31,9 +32,9 @@ export const LimitModalContent: React.FC = ({ prompt={
{promptContent}
} testId='limit-modal' title={title} + visible={visible} onOk={onOk} + onRemove={onRemove} /> ); }; - -export default NiceModal.create(LimitModalContent); diff --git a/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx new file mode 100644 index 00000000000..b22fb12d597 --- /dev/null +++ b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx @@ -0,0 +1,56 @@ +import React, {createContext, useCallback, useContext, useRef, useState} from 'react'; +import {type ConfirmationModalProps, ConfirmationModalContent} from '@/settings/app/components/confirmation-modal'; +import {type LimitModalProps, LimitModalContent} from '@/settings/app/components/limit-modal'; + +export type ConfirmationHandle = {remove: () => void}; + +type ConfirmationRequest = + | {id: number; kind: 'confirm'; props: ConfirmationModalProps} + | {id: number; kind: 'limit'; props: LimitModalProps}; + +type ConfirmationContextType = { + confirm: (props: ConfirmationModalProps) => ConfirmationHandle; + showLimit: (props: LimitModalProps) => ConfirmationHandle; +}; + +const ConfirmationContext = createContext(null); + +export function useConfirmation(): ConfirmationContextType { + const context = useContext(ConfirmationContext); + if (!context) { + throw new Error('useConfirmation must be used inside ConfirmationProvider'); + } + return context; +} + +export const ConfirmationProvider: React.FC<{children: React.ReactNode}> = ({children}) => { + const [requests, setRequests] = useState([]); + const nextId = useRef(0); + + const show = useCallback((request: Omit): ConfirmationHandle => { + nextId.current += 1; + const id = nextId.current; + // One request per kind, matching NiceModal's per-component keying: a + // second show replaces the first instead of stacking (StrictMode + // double-effects depend on this). + setRequests(current => [...current.filter(r => r.kind !== request.kind), {...request, id} as ConfirmationRequest]); + return {remove: () => setRequests(current => current.filter(r => r.id !== id))}; + }, []); + + const confirm = useCallback((props: ConfirmationModalProps) => show({kind: 'confirm', props}), [show]); + const showLimit = useCallback((props: LimitModalProps) => show({kind: 'limit', props}), [show]); + + const contextValue = React.useMemo(() => ({confirm, showLimit}), [confirm, showLimit]); + + return ( + + {children} + {requests.map((request) => { + const remove = () => setRequests(current => current.filter(r => r.id !== request.id)); + return request.kind === 'confirm' ? + : + ; + })} + + ); +}; diff --git a/apps/admin/src/settings/app/components/settings/advanced/danger-zone.tsx b/apps/admin/src/settings/app/components/settings/advanced/danger-zone.tsx index d8c79689ccb..29311eb7a79 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/danger-zone.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/danger-zone.tsx @@ -1,5 +1,3 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; -import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; import TopLevelGroup from '@/settings/app/components/top-level-group'; import trackEvent from '@/settings/app/utils/analytics'; @@ -8,6 +6,7 @@ import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent import {formatNumber} from '@tryghost/shade/utils'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useDeleteAllContent} from '@tryghost/admin-x-framework/api/db'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -24,6 +23,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { const handleError = useHandleError(); const {config} = useGlobalData(); const {totalUsers} = useStaffUsers(); + const {confirm} = useConfirmation(); const resetAuthEnabled = Boolean(config?.labs?.dangerZoneResetAuth); @@ -34,7 +34,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { : 'All staff users, including you, will be signed out and must reset their password before signing back in.'; const handleDeleteAllContent = () => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Would you really like to delete all content from your blog?', prompt: 'This is permanent! No backups, no restores, no magic undo button. We warned you, k?', okVariant: 'destructive', @@ -53,7 +53,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { }; const handleResetAuth = () => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Reset all authentication?', prompt: ( <> @@ -85,7 +85,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { }; const handleRemoveAllGiftLinks = () => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Reset all gift links?', prompt: 'This immediately invalidates every active gift link across your site. Anyone holding one will lose access. New gift links can still be created afterwards.', okLabel: 'Reset all gift links', diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx index b1553e17bb4..2ffba14db5c 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx @@ -1,7 +1,5 @@ import BrandIcon from '@/settings/app/components/icons/brand-icon'; -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import IntegrationsSettingsImg from '@/settings/app/assets/images/integrations-settings.png'; -import NiceModal from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import TopLevelGroup from '@/settings/app/components/top-level-group'; import usePinturaEditor from '@/settings/app/hooks/use-pintura-editor'; @@ -11,6 +9,7 @@ import {LucideIcon} from '@tryghost/shade/utils'; import {Plug} from 'lucide-react'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; @@ -201,6 +200,7 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio const {updateRoute} = useSettingsNavigation(); const {mutateAsync: deleteIntegration} = useDeleteIntegration(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); if (integrations.length) { return ( @@ -220,7 +220,7 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio title={integration.name} custom onDelete={() => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Are you sure?', prompt: 'Deleting this integration will remove all webhooks and api keys associated with it.', okVariant: 'destructive', diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx index c7b382c58da..2c717137192 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx @@ -1,8 +1,7 @@ -import LimitModal from '@/settings/app/components/limit-modal'; -import NiceModal from '@ebay/nice-modal-react'; import {useEffect, useState} from 'react'; import {Field, FieldError, FieldGroup, FieldLabel, Input} from '@tryghost/shade/components'; import {HostLimitError, useLimiter} from '@/settings/app/hooks/use-limiter'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; import {SettingsModal} from '@tryghost/shade/patterns'; import {useCreateIntegration} from '@tryghost/admin-x-framework/api/integrations'; @@ -15,12 +14,13 @@ function AddIntegrationModal() { const {mutateAsync: createIntegration} = useCreateIntegration(); const limiter = useLimiter(); const handleError = useHandleError(); + const {showLimit} = useConfirmation(); useEffect(() => { if (limiter?.isLimited('customIntegrations')) { limiter.errorIfWouldGoOverLimit('customIntegrations').catch((error) => { if (error instanceof HostLimitError) { - NiceModal.show(LimitModal, { + showLimit({ prompt: error.message || `Your current plan doesn't support more custom integrations.`, onOk: () => updateRoute({route: '/pro', isExternal: true}) }); @@ -28,7 +28,7 @@ function AddIntegrationModal() { } }); } - }, [limiter, updateRoute]); + }, [limiter, showLimit, updateRoute]); return = ({in const {mutateAsync: refreshAPIKey} = useRefreshAPIKey(); const {mutateAsync: uploadImage} = useUploadImage(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const {formState, updateForm, handleSave, saveState, errors, clearError, okProps} = useForm({ initialState: integration, @@ -64,7 +64,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in const name = apiKey.type === 'content' ? 'Content' : 'Admin'; - NiceModal.show(ConfirmationModal, { + confirm({ title: `Regenerate ${name} API Key`, prompt: `You can regenerate ${name} API Key any time, but any scripts or applications using it will need to be updated.`, okLabel: `Regenerate ${name} API Key`, diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/transistor-modal.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/transistor-modal.tsx index a9bfc9c2fdc..aa77cacf6bd 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/transistor-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/transistor-modal.tsx @@ -1,14 +1,13 @@ import APIKeys from './api-keys'; import BookmarkThumb from '@/settings/app/assets/images/integrations/ghost-transistor.png'; import BrandIcon from '@/settings/app/components/icons/brand-icon'; -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import IntegrationHeader from './integration-header'; -import NiceModal from '@ebay/nice-modal-react'; import {Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldSet, Switch} from '@tryghost/shade/components'; import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; import {SettingsModal} from '@tryghost/shade/patterns'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useEffect, useState} from 'react'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -23,6 +22,7 @@ function TransistorModal() { const {mutateAsync: refreshAPIKey} = useRefreshAPIKey(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const [regenerated, setRegenerated] = useState(false); const builtInApiIntegrationsDisabled = config.hostSettings?.limits?.customIntegrations?.disabled; @@ -50,7 +50,7 @@ function TransistorModal() { setRegenerated(false); - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Regenerate Admin API Key', prompt: 'You will need to update the API key in your Transistor account settings after regenerating.', okLabel: 'Regenerate Admin API Key', diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.test.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.test.tsx index 1f3e4ad29af..977bb1a35e8 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.test.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.test.tsx @@ -1,4 +1,5 @@ import WebhooksTable from '@/settings/app/components/settings/advanced/integrations/webhooks-table'; +import {ConfirmationProvider} from '@/settings/app/components/providers/confirmation-provider'; import {type Integration} from '@tryghost/admin-x-framework/api/integrations'; import {render, screen} from '@testing-library/react'; @@ -17,7 +18,7 @@ describe('WebhooksTable', () => { webhooks: [] } as unknown as Integration; - const {container} = render(); + const {container} = render(); expect(screen.getByRole('heading', {name: 'No webhooks'})).toBeInTheDocument(); expect(screen.getByText('Add a webhook to send Ghost events to another service.')).toBeInTheDocument(); @@ -40,7 +41,7 @@ describe('WebhooksTable', () => { }] } as unknown as Integration; - const {container} = render(); + const {container} = render(); const table = screen.getByRole('table'); const addButton = screen.getByRole('button', {name: 'Add webhook'}); diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.tsx index 21253318ae4..a3d8717303a 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/webhooks-table.tsx @@ -1,4 +1,3 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import NiceModal from '@ebay/nice-modal-react'; import WebhookModal from './webhook-modal'; import {Button, EmptyIndicator, Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components'; @@ -7,12 +6,14 @@ import {type Integration} from '@tryghost/admin-x-framework/api/integrations'; import {LucideIcon, formatNumber} from '@tryghost/shade/utils'; import {getWebhookEventLabel} from './webhook-event-options'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useDeleteWebhook} from '@tryghost/admin-x-framework/api/webhooks'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => { const {mutateAsync: deleteWebhook} = useDeleteWebhook(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const webhooks = integration.webhooks || []; const showAddWebhookModal = () => { @@ -22,7 +23,7 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => { }; const handleDelete = (id: string) => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Are you sure?', prompt: 'Deleting this webhook may prevent the integration from functioning.', okVariant: 'destructive', diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/zapier-modal.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/zapier-modal.tsx index 7da7fbe9c10..6e32adfaf4a 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/zapier-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/zapier-modal.tsx @@ -1,14 +1,13 @@ import APIKeys from './api-keys'; import BrandIcon from '@/settings/app/components/icons/brand-icon'; -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import IntegrationHeader from './integration-header'; -import NiceModal from '@ebay/nice-modal-react'; import ZapierLogo from '@/settings/app/assets/images/zapier-logo.svg'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button} from '@tryghost/shade/components'; import {LucideIcon} from '@tryghost/shade/utils'; import {SettingsModal} from '@tryghost/shade/patterns'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useEffect, useState} from 'react'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -31,6 +30,7 @@ function ZapierModal() { const {mutateAsync: refreshAPIKey} = useRefreshAPIKey(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const [regenerated, setRegenerated] = useState(false); const zapierDisabled = config.hostSettings?.limits?.customIntegrations?.disabled; @@ -50,7 +50,7 @@ function ZapierModal() { setRegenerated(false); - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Regenerate Admin API Key', prompt: 'You will need to locate the Ghost App within your Zapier account and click on "Reconnect" to enter the new Admin API Key.', okLabel: 'Regenerate Admin API Key', diff --git a/apps/admin/src/settings/app/components/settings/advanced/migration-tools/universal-import-modal.tsx b/apps/admin/src/settings/app/components/settings/advanced/migration-tools/universal-import-modal.tsx index 6798ef96292..63aec562128 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/migration-tools/universal-import-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/migration-tools/universal-import-modal.tsx @@ -1,10 +1,10 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import {Button, Dropzone} from '@tryghost/shade/components'; import {ExternalLink} from 'lucide-react'; import {Inline} from '@tryghost/shade/primitives'; import {SettingsModal} from '@tryghost/shade/patterns'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useImportContent} from '@tryghost/admin-x-framework/api/db'; @@ -13,6 +13,7 @@ const UniversalImportModal: React.FC = () => { const {mutateAsync: importContent} = useImportContent(); const [uploading, setUploading] = useState(false); const handleError = useHandleError(); + const {confirm} = useConfirmation(); return ( { try { await importContent(file); modal.remove(); - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Import in progress', prompt: `Your import is being processed, and you'll receive a confirmation email as soon as it’s complete. Usually this only takes a few minutes, but larger imports may take longer.`, cancelLabel: '', diff --git a/apps/admin/src/settings/app/components/settings/email/emails.tsx b/apps/admin/src/settings/app/components/settings/email/emails.tsx index 9e994664229..673676d185e 100644 --- a/apps/admin/src/settings/app/components/settings/email/emails.tsx +++ b/apps/admin/src/settings/app/components/settings/email/emails.tsx @@ -1,4 +1,3 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import DefaultRecipients from './default-recipients'; import EnableNewsletters from './enable-newsletters'; import MailGun from './mailgun'; @@ -13,6 +12,7 @@ import {APIError} from '@tryghost/admin-x-framework/errors'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; import {LucideIcon} from '@tryghost/shade/utils'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; @@ -68,6 +68,7 @@ const EmailsGroup: React.FC<{ keywords: string[]; newslettersEnabled: boolean }> const verifyEmailToken = useQueryParams().getParam('verifyEmail'); const {mutateAsync: verifySenderUpdate} = useVerifyAutomatedEmailSender(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const submittedTokenRef = useRef(null); const [selectedTab, setSelectedTab] = useState<'newsletters' | 'transactional'>(newslettersEnabled ? 'newsletters' : 'transactional'); const [newslettersFilter, setNewslettersFilter] = useState('active'); @@ -102,7 +103,7 @@ const EmailsGroup: React.FC<{ keywords: string[]; newslettersEnabled: boolean }> } updateRoute('emails'); - NiceModal.show(ConfirmationModal, { + confirm({ title, prompt, okLabel: 'Close', @@ -117,7 +118,7 @@ const EmailsGroup: React.FC<{ keywords: string[]; newslettersEnabled: boolean }> } updateRoute('emails'); - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Error verifying email address', prompt, okLabel: 'Close', @@ -129,7 +130,7 @@ const EmailsGroup: React.FC<{ keywords: string[]; newslettersEnabled: boolean }> }; verify(); - }, [handleError, updateRoute, verifyEmailToken, verifySenderUpdate]); + }, [confirm, handleError, updateRoute, verifyEmailToken, verifySenderUpdate]); const openNewNewsletter = () => { updateRoute('newsletters/new'); diff --git a/apps/admin/src/settings/app/components/settings/email/newsletters.tsx b/apps/admin/src/settings/app/components/settings/email/newsletters.tsx index a72e51c1b1f..400e157bf9f 100644 --- a/apps/admin/src/settings/app/components/settings/email/newsletters.tsx +++ b/apps/admin/src/settings/app/components/settings/email/newsletters.tsx @@ -1,6 +1,4 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import NewslettersList from './newsletters/newsletters-list'; -import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {type ReactNode, useEffect, useState} from 'react'; import TopLevelGroup from '@/settings/app/components/top-level-group'; import useQueryParams from '@/settings/app/hooks/use-query-params'; @@ -10,17 +8,17 @@ import {type InfiniteData, useQueryClient} from '@tryghost/admin-x-framework'; import {type Newsletter, type NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '@tryghost/admin-x-framework/api/newsletters'; import {arrayMove} from '@dnd-kit/sortable'; import {formatNumber} from '@tryghost/shade/utils'; +import {type ConfirmationHandle, useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; import {withErrorBoundary} from '@/settings/app/components/error-boundary'; -const NavigateToNewsletter = ({id, children}: {id: string; children: ReactNode}) => { - const modal = useModal(); +const NavigateToNewsletter = ({id, onNavigate, children}: {id: string; onNavigate: () => void; children: ReactNode}) => { const {updateRoute} = useSettingsNavigation(); return ; }; @@ -37,6 +35,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { const verifyEmailToken = useQueryParams().getParam('verifyEmail'); const {mutateAsync: verifyEmail} = useVerifyNewsletterEmail(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const [newsletters, setNewsletters] = useState(apiNewsletters || []); @@ -52,21 +51,23 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { const verify = async () => { try { const {newsletters: [updatedNewsletter], meta: {email_verified: emailVerified} = {}} = await verifyEmail({token: verifyEmailToken}); + const handleRef: {current: ConfirmationHandle | null} = {current: null}; + const closeConfirmation = () => handleRef.current?.remove(); let title; let prompt; if (emailVerified && emailVerified === 'sender_email') { title = 'Newsletter email verified'; - prompt = <>Newsletter {updatedNewsletter.name} will now be sent from {updatedNewsletter.sender_email}.; + prompt = <>Newsletter {updatedNewsletter.name} will now be sent from {updatedNewsletter.sender_email}.; } else if (emailVerified && emailVerified === 'sender_reply_to') { title = 'Reply-to address verified'; - prompt = <>Newsletter {updatedNewsletter.name} will now use {updatedNewsletter.sender_reply_to} as the reply-to address.; + prompt = <>Newsletter {updatedNewsletter.name} will now use {updatedNewsletter.sender_reply_to} as the reply-to address.; } else { title = 'Email address verified'; - prompt = <>Email address for newsletter {updatedNewsletter.name} has been changed.; + prompt = <>Email address for newsletter {updatedNewsletter.name} has been changed.; } - NiceModal.show(ConfirmationModal, { + handleRef.current = confirm({ title, prompt, okLabel: 'Close', @@ -79,7 +80,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { if (e instanceof APIError && e.message === 'Token expired') { prompt = 'Verification link has expired.'; } - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Error verifying email address', prompt: prompt, okLabel: 'Close', @@ -90,7 +91,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { } }; verify(); - }, [verifyEmailToken, handleError, verifyEmail]); + }, [verifyEmailToken, handleError, verifyEmail, confirm]); const buttons = ( ; }; @@ -44,6 +42,7 @@ const NewslettersTabContent: React.FC = ({filter}) = const verifyEmailToken = useQueryParams().getParam('verifyEmail'); const {mutateAsync: verifyEmail} = useVerifyNewsletterEmail(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const [newsletters, setNewsletters] = useState(apiNewsletters || []); @@ -59,21 +58,23 @@ const NewslettersTabContent: React.FC = ({filter}) = const verify = async () => { try { const {newsletters: [updatedNewsletter], meta: {email_verified: emailVerified} = {}} = await verifyEmail({token: verifyEmailToken}); + const handleRef: {current: ConfirmationHandle | null} = {current: null}; + const closeConfirmation = () => handleRef.current?.remove(); let title; let prompt; if (emailVerified && emailVerified === 'sender_email') { title = 'Newsletter email verified'; - prompt = <>Newsletter {updatedNewsletter.name} will now be sent from {updatedNewsletter.sender_email}.; + prompt = <>Newsletter {updatedNewsletter.name} will now be sent from {updatedNewsletter.sender_email}.; } else if (emailVerified && emailVerified === 'sender_reply_to') { title = 'Reply-to address verified'; - prompt = <>Newsletter {updatedNewsletter.name} will now use {updatedNewsletter.sender_reply_to} as the reply-to address.; + prompt = <>Newsletter {updatedNewsletter.name} will now use {updatedNewsletter.sender_reply_to} as the reply-to address.; } else { title = 'Email address verified'; - prompt = <>Email address for newsletter {updatedNewsletter.name} has been changed.; + prompt = <>Email address for newsletter {updatedNewsletter.name} has been changed.; } - NiceModal.show(ConfirmationModal, { + handleRef.current = confirm({ title, prompt, okLabel: 'Close', @@ -86,7 +87,7 @@ const NewslettersTabContent: React.FC = ({filter}) = if (e instanceof APIError && e.message === 'Token expired') { prompt = 'Verification link has expired.'; } - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Error verifying email address', prompt: prompt, okLabel: 'Close', @@ -97,7 +98,7 @@ const NewslettersTabContent: React.FC = ({filter}) = } }; verify(); - }, [verifyEmailToken, handleError, verifyEmail]); + }, [verifyEmailToken, handleError, verifyEmail, confirm]); const sortedActiveNewsletters = newsletters.filter(n => n.status === 'active').sort((a, b) => a.sort_order - b.sort_order) || []; const archivedNewsletters = newsletters.filter(newsletter => newsletter.status !== 'active'); diff --git a/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx b/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx index b01d8b565d3..ee71145a161 100644 --- a/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx @@ -1,7 +1,4 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; import EmailNotificationsTab from './users/email-notifications-tab'; -import LimitModal from '@/settings/app/components/limit-modal'; -import NiceModal from '@ebay/nice-modal-react'; import ProfileTab from './users/profile-tab'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import SocialLinksTab from './users/social-links-tab'; @@ -24,6 +21,7 @@ import {Text} from '@tryghost/shade/primitives'; import {type User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useGetUserBySlug, useMakeOwner} from '@tryghost/admin-x-framework/api/users'; import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; const validators: Record) => string> = { @@ -91,6 +89,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel const {ownerUser} = useStaffUsers(); const {currentUser} = useGlobalData(); const handleError = useHandleError(); + const {confirm, showLimit} = useConfirmation(); const {formState, setFormState, saveState, handleSave, updateForm, errors, setErrors, clearError, okProps} = useForm({ initialState: user, savingDelay: 500, @@ -172,7 +171,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel await limiter?.errorIfWouldGoOverLimit('staff'); } catch (error) { if (error instanceof HostLimitError) { - NiceModal.show(LimitModal, { + showLimit({ formSheet: true, prompt: error.message || `Your current plan doesn't support more users.`, onOk: () => updateRoute({route: '/pro', isExternal: true}) @@ -188,7 +187,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel if (_user.status === 'inactive') { warningText = 'This user will be able to log in again and will have the same permissions they had previously.'; } - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Are you sure you want to suspend this user?', prompt: ( <> @@ -216,7 +215,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel }; const confirmDelete = (_user: User, {owner}: {owner: User}) => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Are you sure you want to delete this user?', prompt: ( <> @@ -243,7 +242,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel }; const confirmMakeOwner = () => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Transfer Ownership', prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.', okLabel: 'Yep — I\'m sure', diff --git a/apps/admin/src/settings/app/components/settings/general/users/staff-token.tsx b/apps/admin/src/settings/app/components/settings/general/users/staff-token.tsx index b922aaebc43..1bba7534a1b 100644 --- a/apps/admin/src/settings/app/components/settings/general/users/staff-token.tsx +++ b/apps/admin/src/settings/app/components/settings/general/users/staff-token.tsx @@ -1,8 +1,7 @@ import APIKeys from '@/settings/app/components/settings/advanced/integrations/api-keys'; -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; -import NiceModal from '@ebay/nice-modal-react'; import {Text} from '@tryghost/shade/primitives'; import {genStaffToken, getStaffToken} from '@tryghost/admin-x-framework/api/staff-token'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useEffect, useState} from 'react'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -11,6 +10,7 @@ const StaffToken: React.FC = () => { enabled: false }); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const [token, setToken] = useState(''); const {mutateAsync: newApiKey} = genStaffToken(); @@ -25,7 +25,7 @@ const StaffToken: React.FC = () => { } , [apiKey]); const genConfirmation = () => { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Regenerate your Staff Access Token', prompt: 'You can regenerate your Staff Access Token any time, but any scripts or applications using it will need to be updated.', okLabel: 'Regenerate your Staff Access Token', diff --git a/apps/admin/src/settings/app/components/settings/growth/offers/edit-offer-modal.tsx b/apps/admin/src/settings/app/components/settings/growth/offers/edit-offer-modal.tsx index 05bba1ff0f5..d71bc1bbd8a 100644 --- a/apps/admin/src/settings/app/components/settings/growth/offers/edit-offer-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/growth/offers/edit-offer-modal.tsx @@ -1,5 +1,3 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; -import NiceModal from '@ebay/nice-modal-react'; import PortalFrame from '@/settings/app/components/settings/membership/portal/portal-frame'; import SettingsBreadcrumbs from '@/settings/app/components/settings/settings-breadcrumbs'; import {Button, Field, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, Textarea} from '@tryghost/shade/components'; @@ -12,6 +10,7 @@ import {formatNumber} from '@tryghost/shade/utils'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; import {getOfferPortalPreviewUrl, type offerPortalPreviewUrlTypes} from '@/settings/app/utils/get-offers-portal-preview-url'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useEffect, useState} from 'react'; import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; @@ -34,6 +33,7 @@ const Sidebar: React.FC<{ const {siteData} = useGlobalData(); const [isCopied, setIsCopied] = useState(false); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const {mutateAsync: editOffer} = useEditOffer(); const [nameLength, setNameLength] = useState(offer?.name.length || 0); @@ -57,7 +57,7 @@ const Sidebar: React.FC<{ const confirmStatusChange = async () => { if (offer?.status === 'active') { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Archive offer', prompt: <>

New members will no longer be able to subscribe using this offer.

@@ -77,7 +77,7 @@ const Sidebar: React.FC<{ } }); } else { - NiceModal.show(ConfirmationModal, { + confirm({ title: 'Reactivate offer', prompt: <>

Reactivating {offer?.name} will allow new members to subscribe using this offer. Existing members will remain unchanged.

diff --git a/apps/admin/src/settings/app/components/settings/growth/recommendations/edit-recommendation-modal.tsx b/apps/admin/src/settings/app/components/settings/growth/recommendations/edit-recommendation-modal.tsx index 0246fb0969a..44b50f2c728 100644 --- a/apps/admin/src/settings/app/components/settings/growth/recommendations/edit-recommendation-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/growth/recommendations/edit-recommendation-modal.tsx @@ -1,11 +1,10 @@ -import ConfirmationModal from '@/settings/app/components/confirmation-modal'; -import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; import RecommendationDescriptionForm, {validateDescriptionForm} from './recommendation-description-form'; import {Button} from '@tryghost/shade/components'; import {type Recommendation, useDeleteRecommendation, useEditRecommendation} from '@tryghost/admin-x-framework/api/recommendations'; import {SettingsModal} from '@tryghost/shade/patterns'; import {toast} from 'sonner'; +import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; interface EditRecommendationModalProps { @@ -17,6 +16,7 @@ const EditRecommendationModal: React.FC = ({recomm const {mutateAsync: editRecommendation} = useEditRecommendation(); const {mutateAsync: deleteRecommendation} = useDeleteRecommendation(); const handleError = useHandleError(); + const {confirm} = useConfirmation(); const {formState, updateForm, handleSave, errors, clearError, setErrors, okProps} = useForm({ initialState: { @@ -37,7 +37,7 @@ const EditRecommendationModal: React.FC = ({recomm const leftButton = (