diff --git a/packages/app-apw/src/plugins/pageBuilder/DecoratePublishActions.tsx b/packages/app-apw/src/plugins/pageBuilder/DecoratePublishActions.tsx index c252b2f7d33..8d285e5b0c0 100644 --- a/packages/app-apw/src/plugins/pageBuilder/DecoratePublishActions.tsx +++ b/packages/app-apw/src/plugins/pageBuilder/DecoratePublishActions.tsx @@ -45,9 +45,9 @@ const PublishRevisionDecorator = createDecorator( ); const PublishPageMenuOptionDecorator = createDecorator( - Components.PageDetails.Toolbar.PublishRevision, + Components.PageDetails.Revisions.Actions.PublishRevision, OriginalRenderer => { - return function PageReview() { + return function PageReview(props) { const { revision } = Components.PageDetails.Revisions.useRevision(); const contentReviewId = useContentReviewId(revision.id); const navigate = useNavigate(); @@ -57,7 +57,7 @@ const PublishPageMenuOptionDecorator = createDecorator( } if (!contentReviewId) { - return ; + return ; } return ( diff --git a/packages/app-page-builder/src/admin/components/BulkActions/ActionDuplicate.tsx b/packages/app-page-builder/src/admin/components/BulkActions/ActionDuplicate.tsx new file mode 100644 index 00000000000..6d971bd9aa0 --- /dev/null +++ b/packages/app-page-builder/src/admin/components/BulkActions/ActionDuplicate.tsx @@ -0,0 +1,68 @@ +import React, { useMemo } from "react"; +import { ReactComponent as Duplicate } from "@material-design-icons/svg/outlined/library_add.svg"; +import { useRecords } from "@webiny/app-aco"; +import { observer } from "mobx-react-lite"; +import { PageListConfig } from "~/admin/config/pages"; +import { getPagesLabel } from "~/admin/components/BulkActions/BulkActions"; +import { useDuplicatePageCase } from "~/admin/views/Pages/hooks/useDuplicatePage"; +import { makeDecoratable } from "@webiny/react-composition"; + +export const ActionDuplicate = makeDecoratable( + "BulkActionDuplicate", + observer(() => { + const { duplicatePage } = useDuplicatePageCase(); + const { getRecord } = useRecords(); + + const { useWorker, useButtons, useDialog } = PageListConfig.Browser.BulkAction; + const { IconButton } = useButtons(); + const worker = useWorker(); + const { showConfirmationDialog, showResultsDialog } = useDialog(); + + const pagesLabel = useMemo(() => { + return getPagesLabel(worker.items.length); + }, [worker.items.length]); + + const openDuplicatePagesDialog = () => + showConfirmationDialog({ + title: "Duplicate pages", + message: `You are about to duplicate ${pagesLabel}. Are you sure you want to continue?`, + loadingLabel: `Processing ${pagesLabel}`, + execute: async () => { + await worker.processInSeries(async ({ item, report }) => { + try { + const data = await duplicatePage({ page: item }); + + await getRecord(data.pid); + + report.success({ + title: `${item.data.title}`, + message: "Page successfully duplicated." + }); + } catch (e) { + report.error({ + title: `${item.data.title}`, + message: e.message + }); + } + }); + + worker.resetItems(); + + showResultsDialog({ + results: worker.results, + title: "Duplicate pages", + message: "Finished duplicating pages! See full report below:" + }); + } + }); + + return ( + } + onAction={openDuplicatePagesDialog} + label={`Duplicate ${pagesLabel}`} + tooltipPlacement={"bottom"} + /> + ); + }) +); diff --git a/packages/app-page-builder/src/admin/components/BulkActions/SecureActionDuplicate.tsx b/packages/app-page-builder/src/admin/components/BulkActions/SecureActionDuplicate.tsx new file mode 100644 index 00000000000..e77633df4df --- /dev/null +++ b/packages/app-page-builder/src/admin/components/BulkActions/SecureActionDuplicate.tsx @@ -0,0 +1,33 @@ +import React, { useMemo } from "react"; +import { useFolders } from "@webiny/app-aco"; +import { observer } from "mobx-react-lite"; +import { PageListConfig } from "~/admin/config/pages"; +import { usePagesPermissions } from "~/hooks/permissions"; +import { ActionDuplicate } from "~/admin/components/BulkActions/ActionDuplicate"; +import { createDecorator } from "@webiny/react-composition"; + +export const SecureActionDuplicate = createDecorator(ActionDuplicate, Original => { + return observer(() => { + const { canWrite: pagesCanWrite } = usePagesPermissions(); + const { folderLevelPermissions: flp } = useFolders(); + + const { useWorker } = PageListConfig.Browser.BulkAction; + const worker = useWorker(); + + const canDuplicateAll = useMemo(() => { + return worker.items.every(item => { + return ( + pagesCanWrite(item.data.createdBy.id) && + flp.canManageContent(item.location?.folderId) + ); + }); + }, [worker.items]); + + if (!canDuplicateAll) { + console.log("You don't have permissions to duplicate pages."); + return null; + } + + return ; + }); +}); diff --git a/packages/app-page-builder/src/admin/components/BulkActions/index.tsx b/packages/app-page-builder/src/admin/components/BulkActions/index.tsx index 68ae030be4e..8abd66adf09 100644 --- a/packages/app-page-builder/src/admin/components/BulkActions/index.tsx +++ b/packages/app-page-builder/src/admin/components/BulkActions/index.tsx @@ -2,6 +2,8 @@ export { ActionDelete } from "./ActionDelete"; export { SecureActionDelete } from "./SecureActionDelete"; export { ActionExport } from "./ActionExport"; export { ActionMove } from "./ActionMove"; +export { ActionDuplicate } from "./ActionDuplicate"; +export { SecureActionDuplicate } from "./SecureActionDuplicate"; export { SecureActionMove } from "./SecureActionMove"; export { ActionPublish } from "./ActionPublish"; export { SecureActionPublish } from "./SecureActionPublish"; diff --git a/packages/app-page-builder/src/admin/components/Table/Table/Actions/DuplicatePage.tsx b/packages/app-page-builder/src/admin/components/Table/Table/Actions/DuplicatePage.tsx new file mode 100644 index 00000000000..1083325c3c4 --- /dev/null +++ b/packages/app-page-builder/src/admin/components/Table/Table/Actions/DuplicatePage.tsx @@ -0,0 +1,25 @@ +import React, { useCallback } from "react"; +import { ReactComponent as Duplicate } from "@material-design-icons/svg/outlined/library_add.svg"; +import { makeDecoratable } from "@webiny/react-composition"; +import { PageListConfig } from "~/admin/config/pages"; +import { usePage } from "~/admin/views/Pages/hooks/usePage"; +import { useDuplicatePage } from "~/admin/views/Pages/hooks/useDuplicatePage"; + +export const DuplicatePage = makeDecoratable("TableActionDuplicatePage", () => { + const { page } = usePage(); + const { duplicatePage } = useDuplicatePage(); + const { OptionsMenuItem } = PageListConfig.Browser.PageAction; + + const onAction = useCallback(async () => { + await duplicatePage({ page }); + }, [page]); + + return ( + } + label={"Duplicate"} + onAction={onAction} + data-testid={"aco.actions.pb.page.duplicate"} + /> + ); +}); diff --git a/packages/app-page-builder/src/admin/components/Table/Table/Actions/SecureDuplicatePage.tsx b/packages/app-page-builder/src/admin/components/Table/Table/Actions/SecureDuplicatePage.tsx new file mode 100644 index 00000000000..e022434c10b --- /dev/null +++ b/packages/app-page-builder/src/admin/components/Table/Table/Actions/SecureDuplicatePage.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from "react"; +import { useFolders } from "@webiny/app-aco"; +import { createDecorator } from "@webiny/react-composition"; +import { usePagesPermissions } from "~/hooks/permissions"; +import { usePage } from "~/admin/views/Pages/hooks/usePage"; +import { DuplicatePage } from "./DuplicatePage"; + +export const SecureDuplicatePage = createDecorator(DuplicatePage, Original => { + return function SecureDuplicatePageRenderer() { + const { page } = usePage(); + const { folderLevelPermissions: flp } = useFolders(); + const { canWrite: pagesCanWrite } = usePagesPermissions(); + + const { folderId } = page.location; + + const canDuplicate = useMemo(() => { + return pagesCanWrite(page.data.createdBy.id) && flp.canManageContent(folderId); + }, [flp, folderId]); + + if (!canDuplicate) { + return null; + } + + return ; + }; +}); diff --git a/packages/app-page-builder/src/admin/components/Table/Table/Actions/index.ts b/packages/app-page-builder/src/admin/components/Table/Table/Actions/index.ts index 2b57a6b4dd4..3018b6e827f 100644 --- a/packages/app-page-builder/src/admin/components/Table/Table/Actions/index.ts +++ b/packages/app-page-builder/src/admin/components/Table/Table/Actions/index.ts @@ -2,6 +2,8 @@ export * from "./ChangePageStatus"; export * from "./SecureChangePageStatus"; export * from "./DeletePage"; export * from "./SecureDeletePage"; +export * from "./DuplicatePage"; +export * from "./SecureDuplicatePage"; export * from "./EditPage"; export * from "./SecureEditPage"; export * from "./MovePage"; diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DefaultDuplicatePage.tsx b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DefaultDuplicatePage.tsx new file mode 100644 index 00000000000..9789b4d367c --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DefaultDuplicatePage.tsx @@ -0,0 +1,27 @@ +import React, { useCallback } from "react"; +import { useRouter } from "@webiny/react-router"; +import { usePage } from "~/admin/views/Pages/PageDetails"; +import { DuplicatePageMenuItem } from "./DuplicatePageMenuItem"; +import { useDuplicatePage } from "~/admin/views/Pages/hooks/useDuplicatePage"; + +interface DefaultDuplicatePageProps { + label: React.ReactNode; + icon: React.ReactElement; +} + +export const DefaultDuplicatePage = (props: DefaultDuplicatePageProps) => { + const { page } = usePage(); + const { duplicatePage } = useDuplicatePage(); + const { history } = useRouter(); + + const onClick = useCallback(async () => { + await duplicatePage({ + page, + onSuccess: page => { + history.push(`/page-builder/pages?id=${encodeURIComponent(page.id)}`); + } + }); + }, [page]); + + return ; +}; diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePage.tsx b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePage.tsx new file mode 100644 index 00000000000..4489e488fd6 --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePage.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { ReactComponent as DuplicateIcon } from "@material-design-icons/svg/filled/library_add.svg"; +import { makeDecoratable } from "@webiny/app-admin"; +import { DefaultDuplicatePage } from "./DefaultDuplicatePage"; +import { DuplicatePageMenuItem } from "./DuplicatePageMenuItem"; + +export interface DuplicatePageProps { + icon?: React.ReactElement; + label?: React.ReactNode; + onClick?: () => void; +} + +export const DuplicatePage = makeDecoratable("DuplicatePage", (props: DuplicatePageProps) => { + const duplicateButtonLabel = "Duplicate"; + + if (!props.onClick) { + return ( + } + /> + ); + } + + return ( + } + onClick={props.onClick} + /> + ); +}); diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePageMenuItem.tsx b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePageMenuItem.tsx new file mode 100644 index 00000000000..817491f22f4 --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/DuplicatePageMenuItem.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { MenuItem } from "@webiny/ui/Menu"; +import { ListItemGraphic } from "@webiny/ui/List"; +import { Icon } from "@webiny/ui/Icon"; +import { usePagesPermissions } from "~/hooks/permissions"; + +export interface DuplicatePageMenuItemProps { + onClick: () => void; + label: React.ReactNode; + icon: React.ReactElement; +} + +export const DuplicatePageMenuItem = (props: DuplicatePageMenuItemProps) => { + const { canWrite } = usePagesPermissions(); + + if (!canWrite) { + return null; + } + + return ( + + + + + {props.label} + + ); +}; diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/SecureDuplicatePage.tsx b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/SecureDuplicatePage.tsx new file mode 100644 index 00000000000..0908ca5ce04 --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/SecureDuplicatePage.tsx @@ -0,0 +1,32 @@ +import React, { useMemo } from "react"; +import { useFolders } from "@webiny/app-aco"; +import { createDecorator } from "@webiny/react-composition"; +import { usePage } from "~/admin/views/Pages/PageDetails"; +import { usePagesPermissions } from "~/hooks/permissions"; +import { DuplicatePage } from "./DuplicatePage"; + +export const SecureDuplicatePage = createDecorator(DuplicatePage, Original => { + return function SecurePageDetailsDuplicatePageRenderer() { + const { page } = usePage(); + const { folderLevelPermissions: flp } = useFolders(); + const { canWrite: pagesCanWrite } = usePagesPermissions(); + + const canDuplicate = useMemo(() => { + if (!page || Object.keys(page).length === 0) { + // Page data is not available yet + return false; + } + + return ( + pagesCanWrite(page.createdBy.id) && + flp.canManageContent(page.wbyAco_location.folderId) + ); + }, [flp, page]); + + if (!canDuplicate) { + return null; + } + + return ; + }; +}); diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/index.ts b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/index.ts new file mode 100644 index 00000000000..6b50089334c --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage/index.ts @@ -0,0 +1,4 @@ +export * from "./DefaultDuplicatePage"; +export * from "./DuplicatePage"; +export * from "./DuplicatePageMenuItem"; +export * from "./SecureDuplicatePage"; diff --git a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/PageOptionsMenu.tsx b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/PageOptionsMenu.tsx index c64706b62d2..50b959c4dfe 100644 --- a/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/PageOptionsMenu.tsx +++ b/packages/app-page-builder/src/admin/plugins/pageDetails/header/pageOptionsMenu/PageOptionsMenu.tsx @@ -1,21 +1,17 @@ import React, { useCallback, useState } from "react"; import { useApolloClient } from "@apollo/react-hooks"; -import { useRouter } from "@webiny/react-router"; import { IconButton } from "@webiny/ui/Button"; import { Icon } from "@webiny/ui/Icon"; import { ReactComponent as MoreVerticalIcon } from "~/admin/assets/more_vert.svg"; import { ReactComponent as HomeIcon } from "~/admin/assets/round-home-24px.svg"; -import { ReactComponent as DuplicateIcon } from "~/editor/assets/icons/round-queue-24px.svg"; import { ReactComponent as GridViewIcon } from "@material-design-icons/svg/outlined/grid_view.svg"; import { ListItemGraphic } from "@webiny/ui/List"; import { MenuItem, Menu } from "@webiny/ui/Menu"; -import { DUPLICATE_PAGE } from "~/admin/graphql/pages"; import { CREATE_TEMPLATE_FROM_PAGE, LIST_PAGE_TEMPLATES } from "~/admin/views/PageTemplates/graphql"; import CreatePageTemplateDialog from "~/admin/views/PageTemplates/CreatePageTemplateDialog"; -import * as GQLCache from "~/admin/views/Pages/cache"; import { usePageBuilderSettings } from "~/admin/hooks/usePageBuilderSettings"; import { css } from "emotion"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; @@ -25,9 +21,10 @@ import { plugins } from "@webiny/plugins"; import { PbPageData, PbPageDetailsHeaderRightOptionsMenuItemPlugin, PbPageTemplate } from "~/types"; import { SecureView } from "@webiny/app-security"; import { useAdminPageBuilder } from "~/admin/hooks/useAdminPageBuilder"; -import { useFolders, useRecords } from "@webiny/app-aco"; -import { usePagesPermissions, useTemplatesPermissions } from "~/hooks/permissions"; +import { useFolders } from "@webiny/app-aco"; +import { useTemplatesPermissions } from "~/hooks/permissions"; import { PreviewPage } from "./PreviewPage"; +import { DuplicatePage } from "./DuplicatePage"; const menuStyles = css({ width: 250, @@ -48,8 +45,6 @@ const PageOptionsMenu = (props: PageOptionsMenuProps) => { const [isCreateTemplateDialogOpen, setIsCreateTemplateDialogOpen] = useState(false); const { settings, isSpecialPage, updateSettingsMutation } = usePageBuilderSettings(); const client = useApolloClient(); - const { history } = useRouter(); - const { getRecord } = useRecords(); const pageBuilder = useAdminPageBuilder(); @@ -66,35 +61,6 @@ const PageOptionsMenu = (props: PageOptionsMenuProps) => { ) }); - const handleDuplicateClick = useCallback(async () => { - try { - await client.mutate({ - mutation: DUPLICATE_PAGE, - variables: { - id: page.id, - meta: { location: { folderId: page.wbyAco_location.folderId } } - }, - async update(cache, { data }) { - if (data.pageBuilder.duplicatePage.error) { - return; - } - - GQLCache.addPageToListCache(cache, data.pageBuilder.duplicatePage.data); - showSnackbar(`The page "${page.title}" was duplicated successfully.`); - history.push( - `/page-builder/pages?id=${encodeURIComponent( - data.pageBuilder.duplicatePage.data.id - )}` - ); - // Sync ACO record - retrieve the most updated record from network - await getRecord(data.pageBuilder.duplicatePage.data.pid); - } - }); - } catch (error) { - showSnackbar(error.message); - } - }, [page]); - const handleCreateTemplateClick = useCallback( async (formData: Pick) => { try { @@ -118,11 +84,9 @@ const PageOptionsMenu = (props: PageOptionsMenuProps) => { [page] ); - const { canWrite: pagesCanWrite } = usePagesPermissions(); const { folderLevelPermissions: flp } = useFolders(); const { canCreate: templatesCanCreate } = useTemplatesPermissions(); - const canDuplicate = pagesCanWrite(); const canCreateTemplate = templatesCanCreate(); const folderId = page.wbyAco_location?.folderId; @@ -193,14 +157,7 @@ const PageOptionsMenu = (props: PageOptionsMenuProps) => { - {canDuplicate && ( - - - } /> - - Duplicate - - )} + {canCreateTemplate && !isTemplatePage && ( setIsCreateTemplateDialogOpen(true)}> diff --git a/packages/app-page-builder/src/admin/views/Pages/PagesModule.tsx b/packages/app-page-builder/src/admin/views/Pages/PagesModule.tsx index 44d83a04cdf..4ab5a9437c1 100644 --- a/packages/app-page-builder/src/admin/views/Pages/PagesModule.tsx +++ b/packages/app-page-builder/src/admin/views/Pages/PagesModule.tsx @@ -6,7 +6,9 @@ import { ActionExport, SecureActionMove, SecureActionPublish, - SecureActionUnpublish + SecureActionUnpublish, + ActionDuplicate, + SecureActionDuplicate as SecureBulkActionDuplicate } from "~/admin/components/BulkActions"; import { DeleteFolder, EditFolder, SetFolderPermissions } from "@webiny/app-aco"; @@ -21,58 +23,71 @@ import { SecureDeletePage, SecureEditPage, SecureMovePage, - PreviewPage + PreviewPage, + SecureDuplicatePage as SecureListDuplicatePage, + DuplicatePage } from "~/admin/components/Table/Table"; +import { SecureDuplicatePage as SecurePageDetailsDuplicatePage } from "~/admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage"; + const { Browser } = PageListConfig; export const PagesModule = () => { return ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - hideable={false} - size={200} - sortable={true} - /> - } /> - } - sortable={true} - /> - } - sortable={true} - /> - } /> - } - size={80} - className={"rmwc-data-table__cell--align-end"} - resizable={false} - hideable={false} - /> - + <> + {/*Page Builder list configs*/} + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + hideable={false} + size={200} + sortable={true} + /> + } /> + } + sortable={true} + /> + } + sortable={true} + /> + } /> + } + size={80} + className={"rmwc-data-table__cell--align-end"} + resizable={false} + hideable={false} + /> + + {/*Page Builder decorated components*/} + + + + ); }; diff --git a/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/index.ts b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/index.ts new file mode 100644 index 00000000000..5cea1c54125 --- /dev/null +++ b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/index.ts @@ -0,0 +1,2 @@ +export * from "./useDuplicatePageCase"; +export * from "./useDuplicatePage"; diff --git a/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/types.ts b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/types.ts new file mode 100644 index 00000000000..0050052e7d6 --- /dev/null +++ b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/types.ts @@ -0,0 +1,25 @@ +import { PbErrorResponse, PbPageData, PbPageDataItem, PbPageTableItem } from "~/types"; +import { SearchRecordItem } from "@webiny/app-aco/table.types"; +import { Location } from "@webiny/app-aco/types"; + +export type PageItem = PbPageTableItem | PbPageData | SearchRecordItem; + +export const isPbPageData = (page: PageItem): page is PbPageData => { + return "wbyAco_location" in page; +}; + +export interface DuplicatePageVariables { + id: string; + meta: { + location: Location; + }; +} + +export interface DuplicatePageResponse { + pageBuilder: { + duplicatePage: { + data: PbPageData; + error: PbErrorResponse; + }; + }; +} diff --git a/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePage.tsx b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePage.tsx new file mode 100644 index 00000000000..ab6480331ed --- /dev/null +++ b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePage.tsx @@ -0,0 +1,37 @@ +import { useRecords } from "@webiny/app-aco"; +import { useSnackbar } from "@webiny/app-admin"; +import { useDuplicatePageCase } from "./useDuplicatePageCase"; +import { PbPageData } from "~/types"; +import { PageItem } from "./types"; + +interface UseDuplicatePageParams { + page: PageItem; + onSuccess?: (page: PbPageData) => void; +} + +export const useDuplicatePage = () => { + const { duplicatePage: duplicatePageMutation } = useDuplicatePageCase(); + const { showSnackbar } = useSnackbar(); + const { getRecord } = useRecords(); + + const duplicatePage = async ({ page, onSuccess }: UseDuplicatePageParams) => { + try { + const data = await duplicatePageMutation({ page }); + + showSnackbar(`The page "${page.title}" was duplicated successfully.`); + + // Sync ACO record - retrieve the new record from network + await getRecord(data.pid); + + if (typeof onSuccess === "function") { + onSuccess(data); + } + } catch (error) { + showSnackbar(error); + } + }; + + return { + duplicatePage + }; +}; diff --git a/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePageCase.ts b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePageCase.ts new file mode 100644 index 00000000000..c43c1e98358 --- /dev/null +++ b/packages/app-page-builder/src/admin/views/Pages/hooks/useDuplicatePage/useDuplicatePageCase.ts @@ -0,0 +1,48 @@ +import { useApolloClient } from "@apollo/react-hooks"; +import { DUPLICATE_PAGE } from "~/admin/graphql/pages"; +import { DuplicatePageResponse, DuplicatePageVariables, isPbPageData, PageItem } from "./types"; + +interface DuplicatePageMutationParams { + page: PageItem; +} + +export const useDuplicatePageCase = () => { + const client = useApolloClient(); + + const duplicatePage = async (params: DuplicatePageMutationParams) => { + let location; + + if (isPbPageData(params.page)) { + location = params.page.wbyAco_location; + } else { + location = params.page.location; + } + + const { data: response } = await client.mutate< + DuplicatePageResponse, + DuplicatePageVariables + >({ + mutation: DUPLICATE_PAGE, + variables: { + id: params.page.id, + meta: { location } + } + }); + + if (!response) { + throw new Error(`Network error while duplicating page "${params.page.id}".`); + } + + const { data, error } = response.pageBuilder.duplicatePage; + + if (!data) { + throw new Error(error?.message || `Could not duplicate page "${params.page.id}".`); + } + + return data; + }; + + return { + duplicatePage + }; +}; diff --git a/packages/app-page-builder/src/components.ts b/packages/app-page-builder/src/components.ts index dcc91475b69..742566f614d 100644 --- a/packages/app-page-builder/src/components.ts +++ b/packages/app-page-builder/src/components.ts @@ -4,6 +4,7 @@ import EditPageInToolbar from "./admin/plugins/pageDetails/header/editRevision/E import * as RevisionsList from "./admin/plugins/pageDetails/pageRevisions/RevisionsList"; import { usePage as usePageDetailsPage } from "./admin/views/Pages/PageDetails"; import { PreviewPage as PreviewPageToolbar } from "./admin/plugins/pageDetails/header/pageOptionsMenu/PreviewPage"; +import { DuplicatePage as DuplicatePageToolbar } from "./admin/plugins/pageDetails/header/pageOptionsMenu/DuplicatePage"; import { PublishPageMenuOption, DeleteRevisionMenuOption, @@ -19,7 +20,8 @@ import { MovePage, EditPage, PreviewPage, - ChangePageStatus + ChangePageStatus, + DuplicatePage } from "~/admin/components/Table/Table"; import { usePage } from "~/admin/views/Pages/hooks/usePage"; @@ -41,6 +43,10 @@ export const Components = { * This component renders the "Edit" page action. */ EditPage, + /** + * This component renders the "Duplicate" page action. + */ + DuplicatePage, /** * This component renders the "Delete" page action. */ @@ -79,7 +85,11 @@ export const Components = { /** * This component renders the "Preview" action in the dropdown menu. */ - PreviewPage: PreviewPageToolbar + PreviewPage: PreviewPageToolbar, + /** + * This component renders the "Duplicate" action in the dropdown menu. + */ + DuplicatePage: DuplicatePageToolbar }, /** * These components are used in the page revisions tab, in the page details drawer.