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 (
+
+ );
+};
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 && (
-
- )}
+
{canCreateTemplate && !isTemplatePage && (