diff --git a/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.scss b/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.scss deleted file mode 100644 index 917271c691..0000000000 --- a/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import '../../../../scss/styles.scss'; - -@layer payload-default { - .reset-preferences-modal { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(2); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(1); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - margin-block: 0; - } - } - } -} diff --git a/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.tsx b/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.tsx deleted file mode 100644 index d4757f7922..0000000000 --- a/packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' -import { Button, Modal, useModal, useTranslation } from '@payloadcms/ui' - -import './index.scss' - -const baseClass = 'reset-preferences-modal' - -export const ConfirmResetModal: React.FC<{ - readonly onConfirm: () => void - readonly slug: string -}> = ({ slug, onConfirm }) => { - const { closeModal } = useModal() - const { t } = useTranslation() - - const handleClose = () => closeModal(slug) - - const handleConfirm = () => { - handleClose() - if (typeof onConfirm === 'function') { - onConfirm() - } - } - - return ( - -
-
-

{t('general:resetPreferences')}?

-

{t('general:resetPreferencesDescription')}

-
-
- - -
-
-
- ) -} diff --git a/packages/next/src/views/Account/ResetPreferences/index.tsx b/packages/next/src/views/Account/ResetPreferences/index.tsx index 55079433b4..60ea5d8f92 100644 --- a/packages/next/src/views/Account/ResetPreferences/index.tsx +++ b/packages/next/src/views/Account/ResetPreferences/index.tsx @@ -1,11 +1,10 @@ 'use client' +import type { OnConfirm } from '@payloadcms/ui' import type { User } from 'payload' -import { Button, LoadingOverlay, toast, useModal, useTranslation } from '@payloadcms/ui' +import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui' import * as qs from 'qs-esm' -import { Fragment, useCallback, useState } from 'react' - -import { ConfirmResetModal } from './ConfirmResetModal/index.js' +import { Fragment, useCallback } from 'react' const confirmResetModalSlug = 'confirm-reset-modal' @@ -16,51 +15,54 @@ export const ResetPreferences: React.FC<{ const { openModal } = useModal() const { t } = useTranslation() - const [loading, setLoading] = useState(false) + const handleResetPreferences: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + if (!user) { + setConfirming(false) + closeConfirmationModal() + return + } - const handleResetPreferences = useCallback(async () => { - if (!user || loading) { - return - } - setLoading(true) - - const stringifiedQuery = qs.stringify( - { - depth: 0, - where: { - user: { - id: { - equals: user.id, + const stringifiedQuery = qs.stringify( + { + depth: 0, + where: { + user: { + id: { + equals: user.id, + }, }, }, }, - }, - { addQueryPrefix: true }, - ) + { addQueryPrefix: true }, + ) - try { - const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - method: 'DELETE', - }) + try { + const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + method: 'DELETE', + }) - const json = await res.json() - const message = json.message + const json = await res.json() + const message = json.message - if (res.ok) { - toast.success(message) - } else { - toast.error(message) + if (res.ok) { + toast.success(message) + } else { + toast.error(message) + } + } catch (_err) { + // swallow error + } finally { + setConfirming(false) + closeConfirmationModal() } - } catch (e) { - // swallow error - } finally { - setLoading(false) - } - }, [apiRoute, loading, user]) + }, + [apiRoute, user], + ) return ( @@ -69,8 +71,13 @@ export const ResetPreferences: React.FC<{ {t('general:resetPreferences')} - - {loading && } + ) } diff --git a/packages/next/src/views/Version/Restore/index.tsx b/packages/next/src/views/Version/Restore/index.tsx index ec042c3ae7..33d9993e5d 100644 --- a/packages/next/src/views/Version/Restore/index.tsx +++ b/packages/next/src/views/Version/Restore/index.tsx @@ -1,11 +1,10 @@ 'use client' +import type { OnConfirm } from '@payloadcms/ui' + import { getTranslation } from '@payloadcms/translations' import { Button, - ChevronIcon, - Modal, - Pill, - Popup, + ConfirmationModal, PopupList, useConfig, useModal, @@ -45,7 +44,6 @@ const Restore: React.FC = ({ const collectionConfig = getEntityConfig({ collectionSlug }) const { toggleModal } = useModal() - const [processing, setProcessing] = useState(false) const router = useRouter() const { i18n, t } = useTranslation() const [draft, setDraft] = useState(false) @@ -77,23 +75,27 @@ const Restore: React.FC = ({ }) } - const handleRestore = useCallback(async () => { - setProcessing(true) + const handleRestore: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + const res = await requests.post(fetchURL, { + headers: { + 'Accept-Language': i18n.language, + }, + }) - const res = await requests.post(fetchURL, { - headers: { - 'Accept-Language': i18n.language, - }, - }) + setConfirming(false) + closeConfirmationModal() - if (res.status === 200) { - const json = await res.json() - toast.success(json.message) - startRouteTransition(() => router.push(redirectURL)) - } else { - toast.error(t('version:problemRestoringVersion')) - } - }, [fetchURL, redirectURL, t, i18n, router, startRouteTransition]) + if (res.status === 200) { + const json = await res.json() + toast.success(json.message) + startRouteTransition(() => router.push(redirectURL)) + } else { + toast.error(t('version:problemRestoringVersion')) + } + }, + [fetchURL, redirectURL, t, i18n, router, startRouteTransition], + ) return ( @@ -118,27 +120,13 @@ const Restore: React.FC = ({ {t('version:restoreThisVersion')} - -
-
-

{t('version:confirmVersionRestoration')}

-

{restoreMessage}

-
-
- - -
-
-
+
) } diff --git a/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.scss b/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.scss deleted file mode 100644 index a76e060e6f..0000000000 --- a/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .reindex-confirm-modal { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(2); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(1); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.tsx b/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.tsx deleted file mode 100644 index a4d23439f9..0000000000 --- a/packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Button, Modal, useTranslation } from '@payloadcms/ui' - -import './index.scss' - -type Props = { - description: string - onCancel: () => void - onConfirm: () => void - slug: string - title: string -} - -const baseClass = 'reindex-confirm-modal' - -export const ReindexConfirmModal = ({ slug, description, onCancel, onConfirm, title }: Props) => { - const { - i18n: { t }, - } = useTranslation() - return ( - -
-
-

{title}

-

{description}

-
-
- - -
-
-
- ) -} diff --git a/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx b/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx index 56aa99ef4c..cca99f6805 100644 --- a/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx +++ b/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx @@ -1,7 +1,9 @@ 'use client' +import type { OnConfirm } from '@payloadcms/ui' + import { - LoadingOverlay, + ConfirmationModal, Popup, PopupList, toast, @@ -16,7 +18,6 @@ import React, { useCallback, useMemo, useState } from 'react' import type { ReindexButtonProps } from './types.js' import { ReindexButtonLabel } from './ReindexButtonLabel/index.js' -import { ReindexConfirmModal } from './ReindexConfirmModal/index.js' const confirmReindexModalSlug = 'confirm-reindex-modal' @@ -37,45 +38,49 @@ export const ReindexButtonClient: React.FC = ({ const router = useRouter() const [reindexCollections, setReindexCollections] = useState([]) - const [isLoading, setLoading] = useState(false) const openConfirmModal = useCallback(() => openModal(confirmReindexModalSlug), [openModal]) - const closeConfirmModal = useCallback(() => closeModal(confirmReindexModalSlug), [closeModal]) - const handleReindexSubmit = useCallback(async () => { - if (isLoading || !reindexCollections.length) { - return - } - - closeConfirmModal() - setLoading(true) - - try { - const endpointRes = await fetch( - `${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`, - { - body: JSON.stringify({ - collections: reindexCollections, - }), - method: 'POST', - }, - ) - - const { message } = (await endpointRes.json()) as { message: string } - - if (!endpointRes.ok) { - toast.error(message) - } else { - toast.success(message) - router.refresh() + const handleReindexSubmit: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + if (!reindexCollections.length) { + setConfirming(false) + closeConfirmationModal() + return } - } catch (_err: unknown) { - // swallow error, toast shown above - } finally { - setReindexCollections([]) - setLoading(false) - } - }, [closeConfirmModal, isLoading, reindexCollections, router, searchSlug, locale, config]) + + try { + const res = await fetch( + `${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`, + { + body: JSON.stringify({ + collections: reindexCollections, + }), + method: 'POST', + }, + ) + + setConfirming(false) + closeConfirmationModal() + + const { message } = (await res.json()) as { message: string } + + if (!res.ok) { + toast.error(message) + } else { + toast.success(message) + router.refresh() + } + } catch (_err: unknown) { + // swallow error, toast shown above + } finally { + setConfirming(false) + closeConfirmationModal() + setReindexCollections([]) + } + }, + [reindexCollections, router, searchSlug, locale, config], + ) const handleShowConfirmModal = useCallback( (collections: string | string[] = searchCollections) => { @@ -148,14 +153,12 @@ export const ReindexButtonClient: React.FC = ({ size="large" verticalAlign="bottom" /> - - {isLoading && } ) } diff --git a/packages/ui/src/elements/GenerateConfirmation/index.scss b/packages/ui/src/elements/ConfirmationModal/index.scss similarity index 95% rename from packages/ui/src/elements/GenerateConfirmation/index.scss rename to packages/ui/src/elements/ConfirmationModal/index.scss index 0294ef71b2..057b22c7ba 100644 --- a/packages/ui/src/elements/GenerateConfirmation/index.scss +++ b/packages/ui/src/elements/ConfirmationModal/index.scss @@ -1,7 +1,7 @@ @import '../../scss/styles.scss'; @layer payload-default { - .generate-confirmation { + .confirmation-modal { @include blur-bg; display: flex; align-items: center; diff --git a/packages/ui/src/elements/ConfirmationModal/index.tsx b/packages/ui/src/elements/ConfirmationModal/index.tsx new file mode 100644 index 0000000000..2c430e533a --- /dev/null +++ b/packages/ui/src/elements/ConfirmationModal/index.tsx @@ -0,0 +1,104 @@ +'use client' +import { Modal, useModal } from '@faceless-ui/modal' +import React from 'react' + +import { useEditDepth } from '../../providers/EditDepth/index.js' +import { useTranslation } from '../../providers/Translation/index.js' +import { Button } from '../Button/index.js' +import { drawerZBase } from '../Drawer/index.js' +import './index.scss' + +const baseClass = 'confirmation-modal' + +export type OnConfirm = (args: { + closeConfirmationModal: () => void + setConfirming: (state: boolean) => void +}) => Promise | void + +export type OnCancel = () => void + +export type ConfirmationModalProps = { + body: React.ReactNode + cancelLabel?: string + confirmingLabel?: string + confirmLabel?: string + heading: React.ReactNode + modalSlug: string + onCancel?: OnCancel + onConfirm: OnConfirm +} + +export function ConfirmationModal(props: ConfirmationModalProps) { + const { + body, + cancelLabel, + confirmingLabel, + confirmLabel, + heading, + modalSlug, + onCancel, + onConfirm, + } = props + + const editDepth = useEditDepth() + + const [confirming, setConfirming] = React.useState(false) + + const { closeModal } = useModal() + const { t } = useTranslation() + + return ( + +
+
+

{heading}

+

{body}

+
+
+ + +
+
+
+ ) +} diff --git a/packages/ui/src/elements/DeleteDocument/index.scss b/packages/ui/src/elements/DeleteDocument/index.scss index 621028b2bb..671eda49af 100644 --- a/packages/ui/src/elements/DeleteDocument/index.scss +++ b/packages/ui/src/elements/DeleteDocument/index.scss @@ -11,34 +11,5 @@ &__toggle { @extend %btn-reset; } - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - max-width: base(36); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } } } diff --git a/packages/ui/src/elements/DeleteDocument/index.tsx b/packages/ui/src/elements/DeleteDocument/index.tsx index 9d13461881..5651fb9f93 100644 --- a/packages/ui/src/elements/DeleteDocument/index.tsx +++ b/packages/ui/src/elements/DeleteDocument/index.tsx @@ -1,30 +1,27 @@ 'use client' import type { SanitizedCollectionConfig } from 'payload' -import { Modal, useModal } from '@faceless-ui/modal' +import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter } from 'next/navigation.js' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import { useForm } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' -import { useEditDepth } from '../../providers/EditDepth/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' -import { Button } from '../Button/index.js' -import { drawerZBase } from '../Drawer/index.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import { PopupList } from '../Popup/index.js' import { Translation } from '../Translation/index.js' import './index.scss' -const baseClass = 'delete-document' - export type Props = { readonly buttonId?: string readonly collectionSlug: SanitizedCollectionConfig['slug'] @@ -58,109 +55,100 @@ export const DeleteDocument: React.FC = (props) => { const collectionConfig = getEntityConfig({ collectionSlug }) const { setModified } = useForm() - const [deleting, setDeleting] = useState(false) - const { closeModal, toggleModal } = useModal() const router = useRouter() const { i18n, t } = useTranslation() const { title } = useDocumentInfo() - const editDepth = useEditDepth() const { startRouteTransition } = useRouteTransition() + const { openModal } = useModal() const titleToRender = titleFromProps || title || id const modalSlug = `delete-${id}` const addDefaultError = useCallback(() => { - setDeleting(false) toast.error(t('error:deletingTitle', { title })) }, [t, title]) - useEffect(() => { - return () => { - closeModal(modalSlug) - } - }, [closeModal, modalSlug]) + const handleDelete: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + setModified(false) - const handleDelete = useCallback(async () => { - setDeleting(true) - setModified(false) + try { + await requests + .delete(`${serverURL}${api}/${collectionSlug}/${id}`, { + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + }, + }) + .then(async (res) => { + try { + const json = await res.json() + setConfirming(false) + closeConfirmationModal() - try { - await requests - .delete(`${serverURL}${api}/${collectionSlug}/${id}`, { - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', - }, - }) - .then(async (res) => { - try { - const json = await res.json() - - if (res.status < 400) { - setDeleting(false) - toggleModal(modalSlug) - - toast.success( - t('general:titleDeleted', { label: getTranslation(singularLabel, i18n), title }) || - json.message, - ) - - if (redirectAfterDelete) { - return startRouteTransition(() => - router.push( - formatAdminURL({ - adminRoute, - path: `/collections/${collectionSlug}`, - }), - ), + if (res.status < 400) { + toast.success( + t('general:titleDeleted', { + label: getTranslation(singularLabel, i18n), + title, + }) || json.message, ) + + if (redirectAfterDelete) { + return startRouteTransition(() => + router.push( + formatAdminURL({ + adminRoute, + path: `/collections/${collectionSlug}`, + }), + ), + ) + } + + if (typeof onDelete === 'function') { + await onDelete({ id, collectionConfig }) + } + + return } - if (typeof onDelete === 'function') { - await onDelete({ id, collectionConfig }) + if (json.errors) { + json.errors.forEach((error) => toast.error(error.message)) + } else { + addDefaultError() } - toggleModal(modalSlug) - - return + return false + } catch (_err) { + return addDefaultError() } - - toggleModal(modalSlug) - - if (json.errors) { - json.errors.forEach((error) => toast.error(error.message)) - } else { - addDefaultError() - } - return false - } catch (e) { - return addDefaultError() - } - }) - } catch (e) { - addDefaultError() - } - }, [ - setModified, - serverURL, - api, - collectionSlug, - id, - toggleModal, - modalSlug, - t, - singularLabel, - i18n, - title, - router, - adminRoute, - addDefaultError, - redirectAfterDelete, - onDelete, - collectionConfig, - startRouteTransition, - ]) + }) + } catch (_err) { + setConfirming(false) + closeConfirmationModal() + return addDefaultError() + } + }, + [ + setModified, + serverURL, + api, + collectionSlug, + id, + t, + singularLabel, + addDefaultError, + i18n, + title, + router, + adminRoute, + redirectAfterDelete, + onDelete, + collectionConfig, + startRouteTransition, + ], + ) if (id) { return ( @@ -168,60 +156,30 @@ export const DeleteDocument: React.FC = (props) => { { - setDeleting(false) - toggleModal(modalSlug) + openModal(modalSlug) }} > {t('general:delete')} - -
-
-

{t('general:confirmDeletion')}

-

- {children}, - }} - i18nKey="general:aboutToDelete" - t={t} - variables={{ - label: getTranslation(singularLabel, i18n), - title: titleToRender, - }} - /> -

-
-
- - -
-
-
+ {children}, + }} + i18nKey="general:aboutToDelete" + t={t} + variables={{ + label: getTranslation(singularLabel, i18n), + title: titleToRender, + }} + /> + } + confirmingLabel={t('general:deleting')} + heading={t('general:confirmDeletion')} + modalSlug={modalSlug} + onConfirm={handleDelete} + /> ) } diff --git a/packages/ui/src/elements/DeleteMany/index.scss b/packages/ui/src/elements/DeleteMany/index.scss index 2dbdd1c517..4e3cd18744 100644 --- a/packages/ui/src/elements/DeleteMany/index.scss +++ b/packages/ui/src/elements/DeleteMany/index.scss @@ -7,34 +7,5 @@ align-items: center; justify-content: center; height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - max-width: base(36); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } } } diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index e5928d6e8b..e798235258 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -1,13 +1,15 @@ 'use client' import type { ClientCollectionConfig } from 'payload' -import { Modal, useModal } from '@faceless-ui/modal' +import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter, useSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' + import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' @@ -15,7 +17,7 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' -import { Button } from '../Button/index.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import { Pill } from '../Pill/index.js' import './index.scss' @@ -36,10 +38,9 @@ export const DeleteMany: React.FC = (props) => { serverURL, }, } = useConfig() - const { toggleModal } = useModal() + const { openModal } = useModal() const { count, getQueryParams, selectAll, toggleAll } = useSelection() const { i18n, t } = useTranslation() - const [deleting, setDeleting] = useState(false) const router = useRouter() const searchParams = useSearchParams() const { clearRouteCache } = useRouteCache() @@ -53,92 +54,94 @@ export const DeleteMany: React.FC = (props) => { toast.error(t('error:unknown')) }, [t]) - const handleDelete = useCallback(async () => { - setDeleting(true) - - const queryWithSearch = mergeListSearchAndWhere({ - collectionConfig: collection, - search: searchParams.get('search'), - }) - - const queryString = getQueryParams(queryWithSearch) - - await requests - .delete(`${serverURL}${api}/${slug}${queryString}`, { - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', - }, + const handleDelete: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + const queryWithSearch = mergeListSearchAndWhere({ + collectionConfig: collection, + search: searchParams.get('search'), }) - .then(async (res) => { - try { - const json = await res.json() - toggleModal(modalSlug) - const deletedDocs = json?.docs.length || 0 - const successLabel = deletedDocs > 1 ? plural : singular + const queryString = getQueryParams(queryWithSearch) - if (res.status < 400 || deletedDocs > 0) { - toast.success( - t('general:deletedCountSuccessfully', { - count: deletedDocs, - label: getTranslation(successLabel, i18n), - }), - ) + await requests + .delete(`${serverURL}${api}/${slug}${queryString}`, { + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + }, + }) + .then(async (res) => { + try { + const json = await res.json() + setConfirming(false) + closeConfirmationModal() - if (json?.errors.length > 0) { + const deletedDocs = json?.docs.length || 0 + const successLabel = deletedDocs > 1 ? plural : singular + + if (res.status < 400 || deletedDocs > 0) { + toast.success( + t('general:deletedCountSuccessfully', { + count: deletedDocs, + label: getTranslation(successLabel, i18n), + }), + ) + + if (json?.errors.length > 0) { + toast.error(json.message, { + description: json.errors.map((error) => error.message).join('\n'), + }) + } + + toggleAll() + + router.replace( + qs.stringify( + { + page: selectAll ? '1' : undefined, + }, + { addQueryPrefix: true }, + ), + ) + + clearRouteCache() + + return null + } + + if (json.errors) { toast.error(json.message, { description: json.errors.map((error) => error.message).join('\n'), }) + } else { + return addDefaultError() } - - toggleAll() - - router.replace( - qs.stringify( - { - page: selectAll ? '1' : undefined, - }, - { addQueryPrefix: true }, - ), - ) - - clearRouteCache() - - return null + return false + } catch (_err) { + setConfirming(false) + closeConfirmationModal() + return addDefaultError() } - - if (json.errors) { - toast.error(json.message, { - description: json.errors.map((error) => error.message).join('\n'), - }) - } else { - addDefaultError() - } - return false - } catch (_err) { - return addDefaultError() - } - }) - }, [ - searchParams, - addDefaultError, - api, - getQueryParams, - i18n, - modalSlug, - plural, - router, - selectAll, - serverURL, - singular, - slug, - t, - toggleAll, - toggleModal, - clearRouteCache, - collection, - ]) + }) + }, + [ + searchParams, + addDefaultError, + api, + getQueryParams, + i18n, + plural, + router, + selectAll, + serverURL, + singular, + slug, + t, + toggleAll, + clearRouteCache, + collection, + ], + ) if (selectAll === SelectAllStatus.None || !hasDeletePermission) { return null @@ -149,39 +152,21 @@ export const DeleteMany: React.FC = (props) => { { - setDeleting(false) - toggleModal(modalSlug) + openModal(modalSlug) }} > {t('general:delete')} - -
-
-

{t('general:confirmDeletion')}

-

- {t('general:aboutToDeleteCount', { - count, - label: getTranslation(count > 1 ? plural : singular, i18n), - })} -

-
-
- - -
-
-
+ 1 ? plural : singular, i18n), + })} + confirmingLabel={t('general:deleting')} + heading={t('general:confirmDeletion')} + modalSlug={modalSlug} + onConfirm={handleDelete} + /> ) } diff --git a/packages/ui/src/elements/DuplicateDocument/index.scss b/packages/ui/src/elements/DuplicateDocument/index.scss deleted file mode 100644 index ca14d13470..0000000000 --- a/packages/ui/src/elements/DuplicateDocument/index.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .duplicate { - &__modal { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - } - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - max-width: base(36); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/ui/src/elements/DuplicateDocument/index.tsx b/packages/ui/src/elements/DuplicateDocument/index.tsx index 319668d181..e6542e29b3 100644 --- a/packages/ui/src/elements/DuplicateDocument/index.tsx +++ b/packages/ui/src/elements/DuplicateDocument/index.tsx @@ -2,29 +2,25 @@ import type { SanitizedCollectionConfig } from 'payload' -import { Modal, useModal } from '@faceless-ui/modal' +import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter } from 'next/navigation.js' -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import { useForm, useFormModified } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' -import { useEditDepth } from '../../providers/EditDepth/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' -import { Button } from '../Button/index.js' -import { drawerZBase } from '../Drawer/index.js' -import './index.scss' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import { PopupList } from '../Popup/index.js' -const baseClass = 'duplicate' - export type Props = { readonly id: string readonly onDuplicate?: DocumentDrawerContextType['onDuplicate'] @@ -42,7 +38,7 @@ export const DuplicateDocument: React.FC = ({ }) => { const router = useRouter() const modified = useFormModified() - const { toggleModal } = useModal() + const { openModal } = useModal() const locale = useLocale() const { setModified } = useForm() const { startRouteTransition } = useRouteTransition() @@ -57,21 +53,15 @@ export const DuplicateDocument: React.FC = ({ const collectionConfig = getEntityConfig({ collectionSlug: slug }) - const [hasClicked, setHasClicked] = useState(false) + const [renderModal, setRenderModal] = React.useState(false) const { i18n, t } = useTranslation() const modalSlug = `duplicate-${id}` - const editDepth = useEditDepth() + const duplicate = useCallback( + async ({ onResponse }: { onResponse?: () => void } = {}) => { + setRenderModal(true) - const handleClick = useCallback( - async (override = false) => { - setHasClicked(true) - - if (modified && !override) { - toggleModal(modalSlug) - return - } await requests .post( `${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`, @@ -86,6 +76,10 @@ export const DuplicateDocument: React.FC = ({ ) .then(async (res) => { const { doc, errors, message } = await res.json() + if (typeof onResponse === 'function') { + onResponse() + } + if (res.status < 400) { toast.success( message || @@ -119,14 +113,11 @@ export const DuplicateDocument: React.FC = ({ }, [ locale, - modified, serverURL, apiRoute, slug, id, i18n, - toggleModal, - modalSlug, t, singularLabel, onDuplicate, @@ -139,45 +130,43 @@ export const DuplicateDocument: React.FC = ({ ], ) - const confirm = useCallback(async () => { - setHasClicked(false) - await handleClick(true) - }, [handleClick]) + const onConfirm: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + setRenderModal(false) + + await duplicate({ + onResponse: () => { + setConfirming(false) + closeConfirmationModal() + }, + }) + }, + [duplicate], + ) return ( - void handleClick(false)}> + { + if (modified) { + setRenderModal(true) + return openModal(modalSlug) + } + + return duplicate() + }} + > {t('general:duplicate')} - {modified && hasClicked && ( - -
-
-

{t('general:confirmDuplication')}

-

{t('general:unsavedChangesDuplicate')}

-
-
- - -
-
-
+ {renderModal && ( + )}
) diff --git a/packages/ui/src/elements/GenerateConfirmation/index.tsx b/packages/ui/src/elements/GenerateConfirmation/index.tsx index 04cd9b2db6..952d8d6971 100644 --- a/packages/ui/src/elements/GenerateConfirmation/index.tsx +++ b/packages/ui/src/elements/GenerateConfirmation/index.tsx @@ -1,22 +1,22 @@ 'use client' -import { Modal, useModal } from '@faceless-ui/modal' -import React from 'react' +import { useModal } from '@faceless-ui/modal' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' + import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import { Translation } from '../Translation/index.js' -import './index.scss' - -const baseClass = 'generate-confirmation' export type GenerateConfirmationProps = { highlightField: (Boolean) => void setKey: () => void } -export const GenerateConfirmation: React.FC = (props) => { +export function GenerateConfirmation(props: GenerateConfirmationProps) { const { highlightField, setKey } = props const { id } = useDocumentInfo() @@ -25,12 +25,16 @@ export const GenerateConfirmation: React.FC = (props) const modalSlug = `generate-confirmation-${id}` - const handleGenerate = () => { - setKey() - toggleModal(modalSlug) - toast.success(t('authentication:newAPIKeyGenerated')) - highlightField(true) - } + const handleGenerate: OnConfirm = useCallback( + ({ closeConfirmationModal, setConfirming }) => { + setKey() + toast.success(t('authentication:newAPIKeyGenerated')) + highlightField(true) + setConfirming(false) + closeConfirmationModal() + }, + [highlightField, setKey, t], + ) return ( @@ -43,35 +47,21 @@ export const GenerateConfirmation: React.FC = (props) > {t('authentication:generateNewAPIKey')} - -
-
-

{t('authentication:confirmGeneration')}

-

- {children}, - }} - i18nKey="authentication:generatingNewAPIKeyWillInvalidate" - t={t} - /> -

-
-
- - -
-
-
+ {children}, + }} + i18nKey="authentication:generatingNewAPIKeyWillInvalidate" + t={t} + /> + } + confirmLabel={t('authentication:generate')} + heading={t('authentication:confirmGeneration')} + modalSlug={modalSlug} + onConfirm={handleGenerate} + />
) } diff --git a/packages/ui/src/elements/LeaveWithoutSaving/index.scss b/packages/ui/src/elements/LeaveWithoutSaving/index.scss deleted file mode 100644 index efca3fad57..0000000000 --- a/packages/ui/src/elements/LeaveWithoutSaving/index.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .leave-without-saving { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/ui/src/elements/LeaveWithoutSaving/index.tsx b/packages/ui/src/elements/LeaveWithoutSaving/index.tsx index 5232f8d876..5c7d555c39 100644 --- a/packages/ui/src/elements/LeaveWithoutSaving/index.tsx +++ b/packages/ui/src/elements/LeaveWithoutSaving/index.tsx @@ -1,74 +1,30 @@ 'use client' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' + +import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js' import { useForm, useFormModified } from '../../forms/Form/index.js' import { useAuth } from '../../providers/Auth/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { Button } from '../Button/index.js' -import { Modal, useModal } from '../Modal/index.js' -import './index.scss' +import { ConfirmationModal } from '../ConfirmationModal/index.js' +import { useModal } from '../Modal/index.js' import { usePreventLeave } from './usePreventLeave.js' const modalSlug = 'leave-without-saving' -const baseClass = 'leave-without-saving' - -const Component: React.FC<{ - isActive: boolean - onCancel: () => void - onConfirm: () => void -}> = ({ isActive, onCancel, onConfirm }) => { - const { closeModal, openModal } = useModal() - const { t } = useTranslation() - - // Manually check for modal state as 'esc' key will not trigger the nav inactivity - // useEffect(() => { - // if (!modalState?.[modalSlug]?.isOpen && isActive) { - // onCancel() - // } - // }, [modalState, isActive, onCancel]) - - useEffect(() => { - if (isActive) { - openModal(modalSlug) - } else { - closeModal(modalSlug) - } - }, [isActive, openModal, closeModal]) - - return ( - -
-
-

{t('general:leaveWithoutSaving')}

-

{t('general:changesNotSaved')}

-
-
- - -
-
-
- ) -} - export const LeaveWithoutSaving: React.FC = () => { - const { closeModal } = useModal() + const { closeModal, openModal } = useModal() const modified = useFormModified() const { isValid } = useForm() const { user } = useAuth() - const [show, setShow] = React.useState(false) const [hasAccepted, setHasAccepted] = React.useState(false) + const { t } = useTranslation() const prevent = Boolean((modified || !isValid) && user) const onPrevent = useCallback(() => { - setShow(true) - }, []) + openModal(modalSlug) + }, [openModal]) const handleAccept = useCallback(() => { closeModal(modalSlug) @@ -76,15 +32,25 @@ export const LeaveWithoutSaving: React.FC = () => { usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent }) + const onCancel: OnCancel = useCallback(() => { + closeModal(modalSlug) + }, [closeModal]) + + const onConfirm: OnConfirm = useCallback(({ closeConfirmationModal, setConfirming }) => { + setHasAccepted(true) + setConfirming(false) + closeConfirmationModal() + }, []) + return ( - { - setShow(false) - }} - onConfirm={() => { - setHasAccepted(true) - }} + ) } diff --git a/packages/ui/src/elements/PublishMany/index.scss b/packages/ui/src/elements/PublishMany/index.scss deleted file mode 100644 index 804a464089..0000000000 --- a/packages/ui/src/elements/PublishMany/index.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .publish-many { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/ui/src/elements/PublishMany/index.tsx b/packages/ui/src/elements/PublishMany/index.tsx index 96d685da40..501d5828bc 100644 --- a/packages/ui/src/elements/PublishMany/index.tsx +++ b/packages/ui/src/elements/PublishMany/index.tsx @@ -1,13 +1,15 @@ 'use client' import type { ClientCollectionConfig } from 'payload' -import { Modal, useModal } from '@faceless-ui/modal' +import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter, useSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' + import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' @@ -15,16 +17,15 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' -import { Button } from '../Button/index.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import { Pill } from '../Pill/index.js' -import './index.scss' - -const baseClass = 'publish-many' export type PublishManyProps = { collection: ClientCollectionConfig } +const baseClass = 'publish-many' + export const PublishMany: React.FC = (props) => { const { clearRouteCache } = useRouteCache() @@ -36,13 +37,13 @@ export const PublishMany: React.FC = (props) => { serverURL, }, } = useConfig() + const { permissions } = useAuth() - const { toggleModal } = useModal() const { i18n, t } = useTranslation() const { getQueryParams, selectAll } = useSelection() - const [submitted, setSubmitted] = useState(false) const router = useRouter() const searchParams = useSearchParams() + const { openModal } = useModal() const collectionPermissions = permissions?.collections?.[slug] const hasPermission = collectionPermissions?.update @@ -53,84 +54,87 @@ export const PublishMany: React.FC = (props) => { toast.error(t('error:unknown')) }, [t]) - const handlePublish = useCallback(async () => { - setSubmitted(true) - await requests - .patch( - `${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}&draft=true`, - { - body: JSON.stringify({ - _status: 'published', - }), - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', + const handlePublish: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + await requests + .patch( + `${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}&draft=true`, + { + body: JSON.stringify({ + _status: 'published', + }), + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + }, }, - }, - ) - .then(async (res) => { - try { - const json = await res.json() - toggleModal(modalSlug) + ) + .then(async (res) => { + try { + const json = await res.json() + setConfirming(false) + closeConfirmationModal() - const deletedDocs = json?.docs.length || 0 - const successLabel = deletedDocs > 1 ? plural : singular + const deletedDocs = json?.docs.length || 0 + const successLabel = deletedDocs > 1 ? plural : singular - if (res.status < 400 || deletedDocs > 0) { - toast.success( - t('general:updatedCountSuccessfully', { - count: deletedDocs, - label: getTranslation(successLabel, i18n), - }), - ) + if (res.status < 400 || deletedDocs > 0) { + toast.success( + t('general:updatedCountSuccessfully', { + count: deletedDocs, + label: getTranslation(successLabel, i18n), + }), + ) - if (json?.errors.length > 0) { - toast.error(json.message, { - description: json.errors.map((error) => error.message).join('\n'), - }) + if (json?.errors.length > 0) { + toast.error(json.message, { + description: json.errors.map((error) => error.message).join('\n'), + }) + } + + router.replace( + qs.stringify( + { + ...parseSearchParams(searchParams), + page: selectAll ? '1' : undefined, + }, + { addQueryPrefix: true }, + ), + ) + + clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this + return null } - router.replace( - qs.stringify( - { - ...parseSearchParams(searchParams), - page: selectAll ? '1' : undefined, - }, - { addQueryPrefix: true }, - ), - ) - - clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this - return null + if (json.errors) { + json.errors.forEach((error) => toast.error(error.message)) + } else { + addDefaultError() + } + return false + } catch (_err) { + setConfirming(false) + closeConfirmationModal() + return addDefaultError() } - - if (json.errors) { - json.errors.forEach((error) => toast.error(error.message)) - } else { - addDefaultError() - } - return false - } catch (e) { - return addDefaultError() - } - }) - }, [ - serverURL, - api, - slug, - getQueryParams, - i18n, - toggleModal, - modalSlug, - plural, - singular, - t, - router, - searchParams, - selectAll, - clearRouteCache, - addDefaultError, - ]) + }) + }, + [ + serverURL, + api, + slug, + getQueryParams, + i18n, + plural, + singular, + t, + router, + searchParams, + selectAll, + clearRouteCache, + addDefaultError, + ], + ) if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { return null @@ -141,38 +145,20 @@ export const PublishMany: React.FC = (props) => { { - setSubmitted(false) - toggleModal(modalSlug) + openModal(modalSlug) }} > {t('version:publish')} - -
-
-

{t('version:confirmPublish')}

-

{t('version:aboutToPublishSelection', { label: getTranslation(plural, i18n) })}

-
-
- - -
-
-
+ ) } diff --git a/packages/ui/src/elements/Status/index.scss b/packages/ui/src/elements/Status/index.scss index 8a802c484c..2aa6c29dd0 100644 --- a/packages/ui/src/elements/Status/index.scss +++ b/packages/ui/src/elements/Status/index.scss @@ -17,46 +17,5 @@ &__action { text-decoration: underline; } - - &__modal { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__toggle { - @extend %btn-reset; - } - } - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - max-width: base(36); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } } } diff --git a/packages/ui/src/elements/Status/index.tsx b/packages/ui/src/elements/Status/index.tsx index e139738b9d..51b8180232 100644 --- a/packages/ui/src/elements/Status/index.tsx +++ b/packages/ui/src/elements/Status/index.tsx @@ -1,17 +1,18 @@ 'use client' -import { Modal, useModal } from '@faceless-ui/modal' -import React, { useCallback, useState } from 'react' +import { useModal } from '@faceless-ui/modal' +import React, { useCallback } from 'react' import { toast } from 'sonner' +import type { OnConfirm } from '../ConfirmationModal/index.js' + import { useForm } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' -import { useEditDepth } from '../../providers/EditDepth/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { Button } from '../Button/index.js' -import { drawerZBase } from '../Drawer/index.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' import './index.scss' const baseClass = 'status' @@ -36,13 +37,10 @@ export const Status: React.FC = () => { serverURL, }, } = useConfig() - const [processing, setProcessing] = useState(false) const { reset: resetForm } = useForm() const { code: locale } = useLocale() const { i18n, t } = useTranslation() - const editDepth = useEditDepth() - const unPublishModalSlug = `confirm-un-publish-${id}` const revertModalSlug = `confirm-revert-${id}` @@ -57,13 +55,14 @@ export const Status: React.FC = () => { } const performAction = useCallback( - async (action: 'revert' | 'unpublish') => { + async ( + action: 'revert' | 'unpublish', + { closeConfirmationModal, setConfirming }: Parameters[0], + ) => { let url let method let body - setProcessing(true) - if (action === 'unpublish') { body = { _status: 'draft', @@ -74,6 +73,7 @@ export const Status: React.FC = () => { url = `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}&fallback-locale=null&depth=0` method = 'patch' } + if (globalSlug) { url = `${serverURL}${api}/globals/${globalSlug}?locale=${locale}&fallback-locale=null&depth=0` method = 'post' @@ -100,6 +100,9 @@ export const Status: React.FC = () => { }, }) + setConfirming(false) + closeConfirmationModal() + if (res.status === 200) { let data const json = await res.json() @@ -124,15 +127,6 @@ export const Status: React.FC = () => { } else { toast.error(t('error:unPublishingDocument')) } - - setProcessing(false) - if (action === 'revert') { - toggleModal(revertModalSlug) - } - - if (action === 'unpublish') { - toggleModal(unPublishModalSlug) - } }, [ api, @@ -145,10 +139,8 @@ export const Status: React.FC = () => { resetForm, serverURL, setUnpublishedVersionCount, + setMostRecentVersionIsAutosaved, t, - toggleModal, - revertModalSlug, - unPublishModalSlug, setHasPublishedDoc, ], ) @@ -174,34 +166,13 @@ export const Status: React.FC = () => { > {t('version:unpublish')} - -
-
-

{t('version:confirmUnpublish')}

-

{t('version:aboutToUnpublish')}

-
-
- - -
-
-
+ performAction('unpublish', args)} + /> )} {canUpdate && statusToRender === 'changed' && ( @@ -215,31 +186,13 @@ export const Status: React.FC = () => { > {t('version:revertToPublished')} - -
-
-

{t('version:confirmRevertToSaved')}

-

{t('version:aboutToRevertToPublished')}

-
-
- - -
-
-
+ performAction('revert', args)} + /> )} diff --git a/packages/ui/src/elements/StayLoggedIn/index.scss b/packages/ui/src/elements/StayLoggedIn/index.scss deleted file mode 100644 index 70341adac2..0000000000 --- a/packages/ui/src/elements/StayLoggedIn/index.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .stay-logged-in { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: var(--base); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: var(--base); - max-width: base(36); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: var(--base); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/ui/src/elements/StayLoggedIn/index.tsx b/packages/ui/src/elements/StayLoggedIn/index.tsx index 8e664d9307..a5d31302ca 100644 --- a/packages/ui/src/elements/StayLoggedIn/index.tsx +++ b/packages/ui/src/elements/StayLoggedIn/index.tsx @@ -1,17 +1,15 @@ 'use client' -import { Modal, useModal } from '@faceless-ui/modal' import { useRouter } from 'next/navigation.js' -import React from 'react' +import React, { useCallback } from 'react' + +import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js' -import { Button } from '../../elements/Button/index.js' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' -import './index.scss' - -const baseClass = 'stay-logged-in' +import { ConfirmationModal } from '../ConfirmationModal/index.js' export const stayLoggedInModalSlug = 'stay-logged-in' @@ -28,47 +26,39 @@ export const StayLoggedInModal: React.FC = () => { routes: { admin: adminRoute }, } = config - const { closeModal } = useModal() const { t } = useTranslation() const { startRouteTransition } = useRouteTransition() - return ( - -
-
-

{t('authentication:stayLoggedIn')}

-

{t('authentication:youAreInactive')}

-
-
- - -
-
-
+ startRouteTransition(() => + router.push( + formatAdminURL({ + adminRoute, + path: logoutRoute, + }), + ), + ) + }, + [router, startRouteTransition, adminRoute, logoutRoute], + ) + + const onCancel: OnCancel = useCallback(() => { + refreshCookie() + }, [refreshCookie]) + + return ( + ) } diff --git a/packages/ui/src/elements/UnpublishMany/index.scss b/packages/ui/src/elements/UnpublishMany/index.scss deleted file mode 100644 index 8c3ac9ec2a..0000000000 --- a/packages/ui/src/elements/UnpublishMany/index.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .unpublish-many { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/ui/src/elements/UnpublishMany/index.tsx b/packages/ui/src/elements/UnpublishMany/index.tsx index f869d2fcd4..3c7a52f217 100644 --- a/packages/ui/src/elements/UnpublishMany/index.tsx +++ b/packages/ui/src/elements/UnpublishMany/index.tsx @@ -1,9 +1,14 @@ 'use client' -import { Modal, useModal } from '@faceless-ui/modal' +import type { ClientCollectionConfig } from 'payload' + +import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter, useSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' +import { toast } from 'sonner' + +import type { OnConfirm } from '../ConfirmationModal/index.js' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' @@ -11,22 +16,16 @@ import { useRouteCache } from '../../providers/RouteCache/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' -import { Button } from '../Button/index.js' -import { Pill } from '../Pill/index.js' -import './index.scss' - -const baseClass = 'unpublish-many' - -import type { ClientCollectionConfig } from 'payload' - -import { toast } from 'sonner' - import { parseSearchParams } from '../../utilities/parseSearchParams.js' +import { ConfirmationModal } from '../ConfirmationModal/index.js' +import { Pill } from '../Pill/index.js' export type UnpublishManyProps = { collection: ClientCollectionConfig } +const baseClass = 'unpublish-many' + export const UnpublishMany: React.FC = (props) => { const { collection: { slug, labels: { plural, singular }, versions } = {} } = props @@ -42,7 +41,6 @@ export const UnpublishMany: React.FC = (props) => { const { i18n, t } = useTranslation() const searchParams = useSearchParams() const { getQueryParams, selectAll } = useSelection() - const [submitted, setSubmitted] = useState(false) const router = useRouter() const { clearRouteCache } = useRouteCache() @@ -55,81 +53,87 @@ export const UnpublishMany: React.FC = (props) => { toast.error(t('error:unknown')) }, [t]) - const handleUnpublish = useCallback(async () => { - setSubmitted(true) - await requests - .patch(`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, { - body: JSON.stringify({ - _status: 'draft', - }), - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', - }, - }) - .then(async (res) => { - try { - const json = await res.json() - toggleModal(modalSlug) + const handleUnpublish: OnConfirm = useCallback( + async ({ closeConfirmationModal, setConfirming }) => { + await requests + .patch( + `${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, + { + body: JSON.stringify({ + _status: 'draft', + }), + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + }, + }, + ) + .then(async (res) => { + try { + const json = await res.json() + setConfirming(false) + closeConfirmationModal() - const deletedDocs = json?.docs.length || 0 - const successLabel = deletedDocs > 1 ? plural : singular + const deletedDocs = json?.docs.length || 0 + const successLabel = deletedDocs > 1 ? plural : singular - if (res.status < 400 || deletedDocs > 0) { - toast.success( - t('general:updatedCountSuccessfully', { - count: deletedDocs, - label: getTranslation(successLabel, i18n), - }), - ) + if (res.status < 400 || deletedDocs > 0) { + toast.success( + t('general:updatedCountSuccessfully', { + count: deletedDocs, + label: getTranslation(successLabel, i18n), + }), + ) - if (json?.errors.length > 0) { - toast.error(json.message, { - description: json.errors.map((error) => error.message).join('\n'), - }) + if (json?.errors.length > 0) { + toast.error(json.message, { + description: json.errors.map((error) => error.message).join('\n'), + }) + } + + router.replace( + qs.stringify( + { + ...parseSearchParams(searchParams), + page: selectAll ? '1' : undefined, + }, + { addQueryPrefix: true }, + ), + ) + + clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this + return null } - router.replace( - qs.stringify( - { - ...parseSearchParams(searchParams), - page: selectAll ? '1' : undefined, - }, - { addQueryPrefix: true }, - ), - ) - - clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this - return null + if (json.errors) { + json.errors.forEach((error) => toast.error(error.message)) + } else { + addDefaultError() + } + return false + } catch (_err) { + setConfirming(false) + closeConfirmationModal() + return addDefaultError() } - - if (json.errors) { - json.errors.forEach((error) => toast.error(error.message)) - } else { - addDefaultError() - } - return false - } catch (_err) { - return addDefaultError() - } - }) - }, [ - serverURL, - api, - slug, - getQueryParams, - i18n, - toggleModal, - modalSlug, - plural, - singular, - t, - router, - searchParams, - selectAll, - clearRouteCache, - addDefaultError, - ]) + }) + }, + [ + serverURL, + api, + slug, + getQueryParams, + i18n, + plural, + singular, + t, + router, + searchParams, + selectAll, + clearRouteCache, + addDefaultError, + ], + ) if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { return null @@ -140,38 +144,18 @@ export const UnpublishMany: React.FC = (props) => { { - setSubmitted(false) toggleModal(modalSlug) }} > {t('version:unpublish')} - -
-
-

{t('version:confirmUnpublish')}

-

{t('version:aboutToUnpublishSelection', { label: getTranslation(plural, i18n) })}

-
-
- - -
-
-
+ ) } diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 70c9cc0fb7..58c2eb4bb3 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -23,6 +23,8 @@ export { useEffectEvent } from '../../hooks/useEffectEvent.js' export { useUseTitleField } from '../../hooks/useUseAsTitle.js' // elements +export { ConfirmationModal } from '../../elements/ConfirmationModal/index.js' +export type { OnCancel, OnConfirm } from '../../elements/ConfirmationModal/index.js' export { Link } from '../../elements/Link/index.js' export { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js' export { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js' diff --git a/packages/ui/src/views/Edit/Auth/index.tsx b/packages/ui/src/views/Edit/Auth/index.tsx index 1401464a97..89b9fed06e 100644 --- a/packages/ui/src/views/Edit/Auth/index.tsx +++ b/packages/ui/src/views/Edit/Auth/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import type { Props } from './types.js' @@ -73,7 +73,6 @@ export const Auth: React.FC = (props) => { readOnly || (apiKeyPermissions !== true && !apiKeyPermissions?.update) const canReadApiKey = apiKeyPermissions === true || apiKeyPermissions?.read - const canReadEnableAPIKey = apiKeyPermissions === true || apiKeyPermissions?.read const handleChangePassword = useCallback( (showPasswordFields: boolean) => { @@ -202,18 +201,20 @@ export const Auth: React.FC = (props) => { )} {useAPIKey && (
- {canReadEnableAPIKey && ( - + {canReadApiKey && ( + + + + )} - {canReadApiKey && }
)} {verify && isEditing && ( diff --git a/test/admin/e2e/general/e2e.spec.ts b/test/admin/e2e/general/e2e.spec.ts index 87743aee75..8942df2c55 100644 --- a/test/admin/e2e/general/e2e.spec.ts +++ b/test/admin/e2e/general/e2e.spec.ts @@ -704,7 +704,7 @@ describe('General', () => { await page.goto(postsUrl.edit(id)) await openDocControls(page) await page.locator('#action-delete').click() - await page.locator('#confirm-delete').click() + await page.locator(`[id=delete-${id}] #confirm-action`).click() await expect(page.locator(`text=Post "${title}" successfully deleted.`)).toBeVisible() expect(page.url()).toContain(postsUrl.list) }) @@ -715,7 +715,7 @@ describe('General', () => { await page.goto(postsUrl.list) await page.locator('input#select-all').check() await page.locator('.delete-documents__toggle').click() - await page.locator('#confirm-delete').click() + await page.locator('#delete-posts #confirm-action').click() await expect(page.locator('.payload-toast-container .toast-success')).toHaveText( 'Deleted 3 Posts successfully.', @@ -733,7 +733,7 @@ describe('General', () => { await page.locator('input#select-all').check() await page.locator('button.list-selection__button').click() await page.locator('.delete-documents__toggle').click() - await page.locator('#confirm-delete').click() + await page.locator('#delete-posts #confirm-action').click() await expect(page.locator('.payload-toast-container .toast-success')).toHaveText( 'Deleted 1 Post successfully.', @@ -917,7 +917,9 @@ describe('General', () => { await expect(modalContainer).toBeVisible() // Click the "Leave anyway" button - await page.locator('.leave-without-saving__controls .btn--style-primary').click() + await page + .locator('#leave-without-saving .confirmation-modal__controls .btn--style-primary') + .click() // Assert that the class on the modal container changes to 'payload__modal-container--exitDone' await expect(modalContainer).toHaveClass(/payload__modal-container--exitDone/) diff --git a/test/admin/e2e/list-view/e2e.spec.ts b/test/admin/e2e/list-view/e2e.spec.ts index 366aeb65c2..90c77f9c2b 100644 --- a/test/admin/e2e/list-view/e2e.spec.ts +++ b/test/admin/e2e/list-view/e2e.spec.ts @@ -1006,19 +1006,18 @@ describe('List View', () => { test('should delete many', async () => { await page.goto(postsUrl.list) - await page.waitForURL(new RegExp(postsUrl.list)) // delete should not appear without selection - await expect(page.locator('#confirm-delete')).toHaveCount(0) + await expect(page.locator('#delete-posts #confirm-action')).toHaveCount(0) // select one row await page.locator('.row-1 .cell-_select input').check() // delete button should be present - await expect(page.locator('#confirm-delete')).toHaveCount(1) + await expect(page.locator('#delete-posts #confirm-action')).toHaveCount(1) await page.locator('.row-2 .cell-_select input').check() await page.locator('.delete-documents__toggle').click() - await page.locator('#confirm-delete').click() + await page.locator('#delete-posts #confirm-action').click() await expect(page.locator('.cell-_select')).toHaveCount(1) }) }) diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 9f8ea08910..5b56f309cc 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -64,6 +64,7 @@ export interface Config { auth: { users: UserAuthOperations; }; + blocks: {}; collections: { uploads: Upload; posts: Post; diff --git a/test/fields/collections/Relationship/e2e.spec.ts b/test/fields/collections/Relationship/e2e.spec.ts index 033637b216..3564ec7268 100644 --- a/test/fields/collections/Relationship/e2e.spec.ts +++ b/test/fields/collections/Relationship/e2e.spec.ts @@ -533,7 +533,7 @@ describe('relationship', () => { await drawer1Content.locator('#action-delete').click() await page - .locator('[id^=delete-].payload__modal-item.delete-document[open] button#confirm-delete') + .locator('[id^=delete-].payload__modal-item.confirmation-modal[open] button#confirm-action') .click() await expect(drawer1Content).toBeHidden() diff --git a/test/joins/e2e.spec.ts b/test/joins/e2e.spec.ts index fa5e111ae7..d24ee2223f 100644 --- a/test/joins/e2e.spec.ts +++ b/test/joins/e2e.spec.ts @@ -357,7 +357,7 @@ describe('Join Field', () => { await deleteButton.click() const deleteConfirmModal = page.locator('dialog[id^="delete-"][open]') await expect(deleteConfirmModal).toBeVisible() - const confirmDeleteButton = deleteConfirmModal.locator('button#confirm-delete') + const confirmDeleteButton = deleteConfirmModal.locator('button#confirm-action') await expect(confirmDeleteButton).toBeVisible() await confirmDeleteButton.click() await expect(drawer).toBeHidden() diff --git a/test/locked-documents/e2e.spec.ts b/test/locked-documents/e2e.spec.ts index 3445937f56..1c1f61c554 100644 --- a/test/locked-documents/e2e.spec.ts +++ b/test/locked-documents/e2e.spec.ts @@ -223,7 +223,7 @@ describe('Locked Documents', () => { await page.goto(postsUrl.list) await page.locator('input#select-all').check() await page.locator('.delete-documents__toggle').click() - await expect(page.locator('.delete-documents__content p')).toHaveText( + await expect(page.locator('#delete-posts .confirmation-modal__content p')).toHaveText( 'You are about to delete 2 Posts', ) }) @@ -243,7 +243,7 @@ describe('Locked Documents', () => { await page.locator('input#select-all').check() await page.locator('.list-selection .list-selection__button').click() await page.locator('.delete-documents__toggle').click() - await page.locator('#confirm-delete').click() + await page.locator('#delete-posts #confirm-action').click() await expect(page.locator('.cell-_select')).toHaveCount(1) }) @@ -257,12 +257,11 @@ describe('Locked Documents', () => { await page.reload() await page.goto(postsUrl.list) - await page.waitForURL(new RegExp(postsUrl.list)) await page.locator('input#select-all').check() await page.locator('.list-selection .list-selection__button').click() await page.locator('.publish-many__toggle').click() - await page.locator('#confirm-publish').click() + await page.locator('#publish-posts #confirm-action').click() const paginator = page.locator('.paginator') @@ -273,12 +272,11 @@ describe('Locked Documents', () => { test('should only allow bulk unpublish on unlocked documents on all pages', async () => { await page.goto(postsUrl.list) - await page.waitForURL(new RegExp(postsUrl.list)) await page.locator('input#select-all').check() await page.locator('.list-selection .list-selection__button').click() await page.locator('.unpublish-many__toggle').click() - await page.locator('#confirm-unpublish').click() + await page.locator('#unpublish-posts #confirm-action').click() await expect(page.locator('.payload-toast-container .toast-success')).toHaveText( 'Updated 10 Posts successfully.', ) @@ -568,7 +566,9 @@ describe('Locked Documents', () => { await expect(modalContainer).toBeVisible() // Click the "Leave anyway" button - await page.locator('.leave-without-saving__controls .btn--style-primary').click() + await page + .locator('#leave-without-saving .confirmation-modal__controls .btn--style-primary') + .click() // eslint-disable-next-line payload/no-wait-function await wait(500) @@ -620,7 +620,9 @@ describe('Locked Documents', () => { await expect(modalContainer).toBeVisible() // Click the "Leave anyway" button - await page.locator('.leave-without-saving__controls .btn--style-primary').click() + await page + .locator('#leave-without-saving .confirmation-modal__controls .btn--style-primary') + .click() // eslint-disable-next-line payload/no-wait-function await wait(500) diff --git a/test/versions/.gitignore b/test/versions/.gitignore new file mode 100644 index 0000000000..3f549faf91 --- /dev/null +++ b/test/versions/.gitignore @@ -0,0 +1 @@ +uploads diff --git a/test/versions/collections/uploads/image-1.jpg b/test/versions/collections/uploads/image-1.jpg deleted file mode 100644 index 4d2dfc3457..0000000000 Binary files a/test/versions/collections/uploads/image-1.jpg and /dev/null differ diff --git a/test/versions/collections/uploads/image-1.png b/test/versions/collections/uploads/image-1.png deleted file mode 100644 index 23787ee3d7..0000000000 Binary files a/test/versions/collections/uploads/image-1.png and /dev/null differ diff --git a/test/versions/collections/uploads/image.jpg b/test/versions/collections/uploads/image.jpg deleted file mode 100644 index 4d2dfc3457..0000000000 Binary files a/test/versions/collections/uploads/image.jpg and /dev/null differ diff --git a/test/versions/collections/uploads/image.png b/test/versions/collections/uploads/image.png deleted file mode 100644 index 23787ee3d7..0000000000 Binary files a/test/versions/collections/uploads/image.png and /dev/null differ diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 51043ab75b..f2bf61427e 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -134,7 +134,7 @@ describe('Versions', () => { await rowToDelete.locator('.cell-_select input').check() await page.locator('.delete-documents__toggle').click() - await page.locator('#confirm-delete').click() + await page.locator('#delete-draft-posts #confirm-action').click() await expect(page.locator('.payload-toast-container .toast-success')).toContainText( 'Deleted 1 Draft Post successfully.', @@ -152,7 +152,7 @@ describe('Versions', () => { // Bulk edit the selected rows await page.locator('.publish-many__toggle').click() - await page.locator('#confirm-publish').click() + await page.locator('#publish-draft-posts #confirm-action').click() // Check that the statuses for each row has been updated to `published` await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Published') @@ -176,7 +176,7 @@ describe('Versions', () => { await expect(findTableCell(page, '_status', title)).toContainText('Draft') await selectTableRow(page, title) await page.locator('.publish-many__toggle').click() - await page.locator('#confirm-publish').click() + await page.locator('#publish-autosave-posts #confirm-action').click() await expect(findTableCell(page, '_status', title)).toContainText('Published') }) @@ -189,7 +189,7 @@ describe('Versions', () => { // Bulk edit the selected rows await page.locator('.unpublish-many__toggle').click() - await page.locator('#confirm-unpublish').click() + await page.locator('#unpublish-draft-posts #confirm-action').click() // Check that the statuses for each row has been updated to `draft` await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Draft') @@ -565,7 +565,7 @@ describe('Versions', () => { // revert to last published version await page.locator('#action-revert-to-published').click() - await saveDocAndAssert(page, '#action-revert-to-published-confirm') + await saveDocAndAssert(page, '[id^=confirm-revert-] #confirm-action') // verify that spanish content is reverted correctly await expect(page.locator('#field-title')).toHaveValue(spanishTitle) diff --git a/tsconfig.base.json b/tsconfig.base.json index fba7b319e9..f5fbb754e7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/fields/config.ts"], + "@payload-config": ["./test/plugin-search/config.ts"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],