diff --git a/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx b/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx
index f35be68efb5..3d7d3a21f20 100644
--- a/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx
+++ b/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx
@@ -13,6 +13,8 @@ import {
renderMaybeLocalizedInput,
} from "@/wab/client/components/cms/CmsInputs";
import { isCmsTextLike } from "@/wab/client/components/cms/utils";
+import Button from "@/wab/client/components/widgets/Button";
+import { Modal } from "@/wab/client/components/widgets/Modal";
import { useApi, useAppCtx } from "@/wab/client/contexts/AppContexts";
import {
DefaultCmsEntryDetailsProps,
@@ -33,7 +35,15 @@ import { spawn } from "@/wab/shared/common";
import { DEVFLAGS } from "@/wab/shared/devflags";
import { substituteUrlParams } from "@/wab/shared/utils/url-utils";
import { HTMLElementRefOf } from "@plasmicapp/react-web";
-import { Drawer, Form, Menu, message, notification, Tooltip } from "antd";
+import {
+ Drawer,
+ Form,
+ Input,
+ Menu,
+ message,
+ notification,
+ Tooltip,
+} from "antd";
import { useForm } from "antd/lib/form/Form";
import { isEqual, isNil, mapValues, pickBy } from "lodash";
import * as React from "react";
@@ -175,6 +185,9 @@ function CmsEntryDetailsForm_(
const [hasUnpublishedChanges, setHasUnpublishedChanges] = React.useState(
!!row?.draftData
);
+ const [showCopyModal, setShowCopyModal] = React.useState(false);
+ const [inputCopyIdentifier, setInputCopyIdentifier] = React.useState("");
+
const [revision, setRevision] = React.useState(row.revision);
const [inConflict, setInConflict] = React.useState(false);
const mutateRow_ = useMutateRow();
@@ -572,16 +585,27 @@ function CmsEntryDetailsForm_(
content: "Reverted.",
});
}}
- disabled={isSaving || isPublishing}
+ disabled={isSaving}
>
Revert to published entry
)}
-
>
)}
+ {
+ setInputCopyIdentifier(
+ row.identifier ? `Copy of ${row.identifier}` : ""
+ );
+ setShowCopyModal(true);
+ }}
+ disabled={isSaving || isPublishing}
+ >
+ Duplicate entry
+
{
@@ -632,6 +656,72 @@ function CmsEntryDetailsForm_(
}}
/>
+ {showCopyModal && (
+ setShowCopyModal(false)}
+ >
+
+
+ {`Duplicate the CMS entry with identifier "${row.identifier}"?`}
+
+ {`This will create an unpublished copy of the entry and all its data.`}
+
+ setInputCopyIdentifier(e.target.value)}
+ />
+
+
+
+
+
+
+ )}
>
);
}
diff --git a/platform/wab/src/wab/server/AppServer.ts b/platform/wab/src/wab/server/AppServer.ts
index 778dea11f90..2267c76570c 100644
--- a/platform/wab/src/wab/server/AppServer.ts
+++ b/platform/wab/src/wab/server/AppServer.ts
@@ -61,6 +61,7 @@ import {
import {
cloneDatabase,
cmsFileUpload,
+ copyRow,
createDatabase,
createRows,
createTable,
@@ -826,6 +827,7 @@ export function addCmsEditorRoutes(app: express.Application) {
app.get("/api/v1/cmse/rows/:rowId/revisions", withNext(listRowRevisions));
app.put("/api/v1/cmse/rows/:rowId", withNext(updateRow));
app.delete("/api/v1/cmse/rows/:rowId", withNext(deleteRow));
+ app.post("/api/v1/cmse/rows/:rowId/copy", withNext(copyRow));
app.get("/api/v1/cmse/row-revisions/:revId", withNext(getRowRevision));
app.post(
diff --git a/platform/wab/src/wab/server/db/DbMgr.ts b/platform/wab/src/wab/server/db/DbMgr.ts
index 12327ea3b11..d2ddcf92529 100644
--- a/platform/wab/src/wab/server/db/DbMgr.ts
+++ b/platform/wab/src/wab/server/db/DbMgr.ts
@@ -7342,6 +7342,22 @@ export class DbMgr implements MigrationDbMgr {
await this.entMgr.save(row);
}
+ async copyCmsRow(
+ tableId: CmsTableId,
+ rowId: CmsRowId,
+ opts: {
+ identifier?: string;
+ }
+ ) {
+ await this.checkCmsRowPerms(rowId, "content");
+ const row = await this.getCmsRowById(rowId);
+ const copiedRow = await this.createCmsRow(tableId, {
+ identifier: opts.identifier || undefined,
+ draftData: row.draftData || row.data,
+ });
+ return await this.entMgr.save(copiedRow);
+ }
+
// TODO We are always querying just the default locale.
async queryCmsRows(
tableId: CmsTableId,
diff --git a/platform/wab/src/wab/server/routes/cmse.ts b/platform/wab/src/wab/server/routes/cmse.ts
index b4b49511ab1..0bcc9f1d759 100644
--- a/platform/wab/src/wab/server/routes/cmse.ts
+++ b/platform/wab/src/wab/server/routes/cmse.ts
@@ -269,6 +269,26 @@ export async function deleteRow(req: Request, res: Response) {
res.json({});
}
+export async function copyRow(req: Request, res: Response) {
+ const mgr = userDbMgr(req);
+ const row = await mgr.getCmsRowById(req.params.rowId as CmsRowId);
+ userAnalytics(req).track({
+ event: "Copy cms row",
+ properties: {
+ rowId: row.id as CmsRowId,
+ tableId: row.tableId,
+ tableName: row.table?.name,
+ databaseId: row.table?.databaseId,
+ },
+ });
+ const copiedRow = await mgr.copyCmsRow(
+ row.tableId as CmsTableId,
+ req.params.rowId as CmsRowId,
+ req.body
+ );
+ res.json(copiedRow);
+}
+
export async function updateRow(req: Request, res: Response) {
const mgr = userDbMgr(req);
const row = await mgr.updateCmsRow(req.params.rowId as CmsRowId, req.body);
diff --git a/platform/wab/src/wab/shared/SharedApi.ts b/platform/wab/src/wab/shared/SharedApi.ts
index 3cfa7a2ddae..cba25f95d2e 100644
--- a/platform/wab/src/wab/shared/SharedApi.ts
+++ b/platform/wab/src/wab/shared/SharedApi.ts
@@ -1724,6 +1724,15 @@ export abstract class SharedApi {
return (await this.put(`/cmse/rows/${rowId}`, opts)) as ApiCmseRow;
}
+ async copyCmsRow(
+ rowId: CmsRowId,
+ opts: {
+ identifier: string;
+ }
+ ) {
+ return (await this.post(`/cmse/rows/${rowId}/copy`, opts)) as ApiCmseRow;
+ }
+
async deleteCmsRow(rowId: CmsRowId) {
return await this.delete(`/cmse/rows/${rowId}`);
}