feat: add "Copy document" dialog (#6009)
This commit is contained in:
@@ -32,6 +32,7 @@ import DocumentMove from "~/scenes/DocumentMove";
|
||||
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
|
||||
import DocumentPublish from "~/scenes/DocumentPublish";
|
||||
import DocumentTemplatizeDialog from "~/components/DocumentTemplatizeDialog";
|
||||
import DuplicateDialog from "~/components/DuplicateDialog";
|
||||
import { createAction } from "~/actions";
|
||||
import { DocumentSection } from "~/actions/sections";
|
||||
import env from "~/env";
|
||||
@@ -420,11 +421,19 @@ export const duplicateDocument = createAction({
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
invariant(document, "Document must exist");
|
||||
const duped = await document.duplicate();
|
||||
// when duplicating, go straight to the duplicated document content
|
||||
history.push(documentPath(duped));
|
||||
stores.toasts.showToast(t("Document duplicated"), {
|
||||
type: "success",
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Copy document"),
|
||||
isCentered: true,
|
||||
content: (
|
||||
<DuplicateDialog
|
||||
document={document}
|
||||
onSubmit={(response) => {
|
||||
stores.dialogs.closeAllModals();
|
||||
history.push(documentPath(response[0]));
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
74
app/components/DuplicateDialog.tsx
Normal file
74
app/components/DuplicateDialog.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DocumentValidation } from "@shared/validations";
|
||||
import Document from "~/models/Document";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import Input from "./Input";
|
||||
import Text from "./Text";
|
||||
|
||||
type Props = {
|
||||
/** The original document to duplicate */
|
||||
document: Document;
|
||||
onSubmit: (documents: Document[]) => void;
|
||||
};
|
||||
|
||||
function DuplicateDialog({ document, onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const defaultTitle = t(`Copy of {{ documentName }}`, {
|
||||
documentName: document.title,
|
||||
});
|
||||
const [recursive, setRecursive] = React.useState<boolean>(true);
|
||||
const [title, setTitle] = React.useState<string>(defaultTitle);
|
||||
|
||||
const handleRecursiveChange = React.useCallback(
|
||||
(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRecursive(ev.target.checked);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTitleChange = React.useCallback(
|
||||
(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(ev.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const result = await document.duplicate({
|
||||
recursive,
|
||||
title,
|
||||
});
|
||||
onSubmit(result);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationDialog onSubmit={handleSubmit} submitText={t("Duplicate")}>
|
||||
<Input
|
||||
autoFocus
|
||||
autoSelect
|
||||
name="title"
|
||||
label={t("Title")}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={DocumentValidation.maxTitleLength}
|
||||
defaultValue={defaultTitle}
|
||||
/>
|
||||
{document.publishedAt && !document.isTemplate && (
|
||||
<label>
|
||||
<Text size="small">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="recursive"
|
||||
checked={recursive}
|
||||
onChange={handleRecursiveChange}
|
||||
/>{" "}
|
||||
{t("Include nested documents")}
|
||||
</Text>
|
||||
</label>
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DuplicateDialog);
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { mergeRefs } from "react-merge-refs";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
@@ -121,6 +122,8 @@ export type Props = React.InputHTMLAttributes<
|
||||
margin?: string | number;
|
||||
error?: string;
|
||||
icon?: React.ReactNode;
|
||||
/** Like autoFocus, but also select any text in the input */
|
||||
autoSelect?: boolean;
|
||||
/** Callback is triggered with the CMD+Enter keyboard combo */
|
||||
onRequestSubmit?: (
|
||||
ev: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
@@ -133,6 +136,7 @@ function Input(
|
||||
props: Props,
|
||||
ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement>
|
||||
) {
|
||||
const internalRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>();
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
|
||||
const handleBlur = (ev: React.SyntheticEvent) => {
|
||||
@@ -165,6 +169,12 @@ function Input(
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.autoSelect && internalRef.current) {
|
||||
internalRef.current.select();
|
||||
}
|
||||
}, [props.autoSelect, internalRef]);
|
||||
|
||||
const {
|
||||
type = "text",
|
||||
icon,
|
||||
@@ -197,7 +207,10 @@ function Input(
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{type === "textarea" ? (
|
||||
<RealTextarea
|
||||
ref={ref as React.RefObject<HTMLTextAreaElement>}
|
||||
ref={mergeRefs([
|
||||
internalRef,
|
||||
ref as React.RefObject<HTMLTextAreaElement>,
|
||||
])}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -206,7 +219,10 @@ function Input(
|
||||
/>
|
||||
) : (
|
||||
<RealInput
|
||||
ref={ref as React.RefObject<HTMLInputElement>}
|
||||
ref={mergeRefs([
|
||||
internalRef,
|
||||
ref as React.RefObject<HTMLInputElement>,
|
||||
])}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -257,7 +257,7 @@ const Small = styled.div`
|
||||
margin: auto auto;
|
||||
width: 30vw;
|
||||
min-width: 350px;
|
||||
max-width: 500px;
|
||||
max-width: 450px;
|
||||
z-index: ${depths.modal};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -399,7 +399,8 @@ export default class Document extends ParanoidModel {
|
||||
move = (collectionId: string, parentDocumentId?: string | undefined) =>
|
||||
this.store.move(this.id, collectionId, parentDocumentId);
|
||||
|
||||
duplicate = () => this.store.duplicate(this);
|
||||
duplicate = (options?: { title?: string; recursive?: boolean }) =>
|
||||
this.store.duplicate(this, options);
|
||||
|
||||
getSummary = (paragraphs = 4) => {
|
||||
const result = this.text.trim().split("\n").slice(0, paragraphs).join("\n");
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DateFilter, NavigationNode, PublicTeam } from "@shared/types";
|
||||
import { subtractDate } from "@shared/utils/date";
|
||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||
import naturalSort from "@shared/utils/naturalSort";
|
||||
import { DocumentValidation } from "@shared/validations";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Store from "~/stores/base/Store";
|
||||
import Document from "~/models/Document";
|
||||
@@ -558,26 +557,21 @@ export default class DocumentsStore extends Store<Document> {
|
||||
};
|
||||
|
||||
@action
|
||||
duplicate = async (document: Document): Promise<Document> => {
|
||||
const append = " (duplicate)";
|
||||
const res = await client.post("/documents.create", {
|
||||
publish: document.isTemplate,
|
||||
parentDocumentId: null,
|
||||
collectionId: document.isTemplate ? document.collectionId : null,
|
||||
template: document.isTemplate,
|
||||
title: `${document.title.slice(
|
||||
0,
|
||||
DocumentValidation.maxTitleLength - append.length
|
||||
)}${append}`,
|
||||
text: document.text,
|
||||
duplicate = async (
|
||||
document: Document,
|
||||
options?: {
|
||||
title?: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
): Promise<Document[]> => {
|
||||
const res = await client.post("/documents.duplicate", {
|
||||
id: document.id,
|
||||
...options,
|
||||
});
|
||||
invariant(res?.data, "Data should be available");
|
||||
const collection = this.getCollectionForDocument(document);
|
||||
if (collection) {
|
||||
await collection.refresh();
|
||||
}
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
return res.data.documents.map(this.add);
|
||||
};
|
||||
|
||||
@action
|
||||
|
||||
@@ -6,7 +6,7 @@ type Props = {
|
||||
id?: string;
|
||||
urlId?: string;
|
||||
title: string;
|
||||
emoji?: string;
|
||||
emoji?: string | null;
|
||||
text?: string;
|
||||
state?: Buffer;
|
||||
publish?: boolean;
|
||||
|
||||
84
server/commands/documentDuplicator.test.ts
Normal file
84
server/commands/documentDuplicator.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { sequelize } from "@server/storage/database";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import documentDuplicator from "./documentDuplicator";
|
||||
|
||||
describe("documentDuplicator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should duplicate existing document", async () => {
|
||||
const user = await buildUser();
|
||||
const original = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const response = await sequelize.transaction((transaction) =>
|
||||
documentDuplicator({
|
||||
document: original,
|
||||
collection: original.collection,
|
||||
transaction,
|
||||
user,
|
||||
ip,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response[0].title).toEqual(original.title);
|
||||
expect(response[0].text).toEqual(original.text);
|
||||
expect(response[0].emoji).toEqual(original.emoji);
|
||||
});
|
||||
|
||||
it("should duplicate document with title override", async () => {
|
||||
const user = await buildUser();
|
||||
const original = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
emoji: "👋",
|
||||
});
|
||||
|
||||
const response = await sequelize.transaction((transaction) =>
|
||||
documentDuplicator({
|
||||
document: original,
|
||||
collection: original.collection,
|
||||
title: "New title",
|
||||
transaction,
|
||||
user,
|
||||
ip,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response[0].title).toEqual("New title");
|
||||
expect(response[0].text).toEqual(original.text);
|
||||
expect(response[0].emoji).toEqual(original.emoji);
|
||||
});
|
||||
|
||||
it("should duplicate child documents with recursive=true", async () => {
|
||||
const user = await buildUser();
|
||||
const original = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
emoji: "👋",
|
||||
});
|
||||
|
||||
await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
parentDocumentId: original.id,
|
||||
collection: original.collection,
|
||||
});
|
||||
|
||||
const response = await sequelize.transaction((transaction) =>
|
||||
documentDuplicator({
|
||||
document: original,
|
||||
collection: original.collection,
|
||||
user,
|
||||
transaction,
|
||||
recursive: true,
|
||||
ip,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
97
server/commands/documentDuplicator.ts
Normal file
97
server/commands/documentDuplicator.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Transaction, Op } from "sequelize";
|
||||
import { User, Collection, Document } from "@server/models";
|
||||
import documentCreator from "./documentCreator";
|
||||
|
||||
type Props = {
|
||||
/** The user who is creating the document */
|
||||
user: User;
|
||||
/** The document to duplicate */
|
||||
document: Document;
|
||||
/** The collection to add the duplicated document to */
|
||||
collection?: Collection | null;
|
||||
/** Override of the parent document to add the duplicate to */
|
||||
parentDocumentId?: string;
|
||||
/** Override of the duplicated document title */
|
||||
title?: string;
|
||||
/** Override of the duplicated document publish state */
|
||||
publish?: boolean;
|
||||
/** Whether to duplicate child documents */
|
||||
recursive?: boolean;
|
||||
/** The database transaction to use for the creation */
|
||||
transaction?: Transaction;
|
||||
/** The IP address of the request */
|
||||
ip: string;
|
||||
};
|
||||
|
||||
export default async function documentDuplicator({
|
||||
user,
|
||||
document,
|
||||
collection,
|
||||
parentDocumentId,
|
||||
title,
|
||||
publish,
|
||||
recursive,
|
||||
transaction,
|
||||
ip,
|
||||
}: Props): Promise<Document[]> {
|
||||
const newDocuments: Document[] = [];
|
||||
const sharedProperties = {
|
||||
user,
|
||||
collectionId: collection?.id,
|
||||
publish: publish ?? !!document.publishedAt,
|
||||
ip,
|
||||
transaction,
|
||||
};
|
||||
|
||||
const duplicated = await documentCreator({
|
||||
parentDocumentId: parentDocumentId ?? document.parentDocumentId,
|
||||
emoji: document.emoji,
|
||||
template: document.template,
|
||||
title: title ?? document.title,
|
||||
text: document.text,
|
||||
...sharedProperties,
|
||||
});
|
||||
|
||||
duplicated.collection = collection;
|
||||
newDocuments.push(duplicated);
|
||||
|
||||
async function duplicateChildDocuments(
|
||||
original: Document,
|
||||
duplicated: Document
|
||||
) {
|
||||
const childDocuments = await original.findChildDocuments(
|
||||
{
|
||||
archivedAt: original.archivedAt
|
||||
? {
|
||||
[Op.ne]: null,
|
||||
}
|
||||
: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
for (const childDocument of childDocuments) {
|
||||
const duplicatedChildDocument = await documentCreator({
|
||||
parentDocumentId: duplicated.id,
|
||||
emoji: childDocument.emoji,
|
||||
title: childDocument.title,
|
||||
text: childDocument.text,
|
||||
...sharedProperties,
|
||||
});
|
||||
|
||||
duplicatedChildDocument.collection = collection;
|
||||
newDocuments.push(duplicatedChildDocument);
|
||||
await duplicateChildDocuments(childDocument, duplicatedChildDocument);
|
||||
}
|
||||
}
|
||||
|
||||
if (recursive && !document.template) {
|
||||
await duplicateChildDocuments(document, duplicated);
|
||||
}
|
||||
|
||||
return newDocuments;
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export default async function loadDocument({
|
||||
}
|
||||
|
||||
const childDocumentIds =
|
||||
(await share.document?.getChildDocumentIds({
|
||||
(await share.document?.findAllChildDocumentIds({
|
||||
archivedAt: {
|
||||
[Op.is]: null,
|
||||
},
|
||||
|
||||
@@ -137,7 +137,7 @@ async function documentMover({
|
||||
if (collectionChanged) {
|
||||
// Efficiently find the ID's of all the documents that are children of
|
||||
// the moved document and update in one query
|
||||
const childDocumentIds = await document.getChildDocumentIds();
|
||||
const childDocumentIds = await document.findAllChildDocumentIds();
|
||||
|
||||
if (collectionId) {
|
||||
// Reload the collection to get relationship data
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("#save", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getChildDocumentIds", () => {
|
||||
describe("#findAllChildDocumentIds", () => {
|
||||
test("should return empty array if no children", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
@@ -124,7 +124,7 @@ describe("#getChildDocumentIds", () => {
|
||||
collectionId: collection.id,
|
||||
title: "test",
|
||||
});
|
||||
const results = await document.getChildDocumentIds();
|
||||
const results = await document.findAllChildDocumentIds();
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("#getChildDocumentIds", () => {
|
||||
parentDocumentId: document2.id,
|
||||
title: "test",
|
||||
});
|
||||
const results = await document.getChildDocumentIds();
|
||||
const results = await document.findAllChildDocumentIds();
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0]).toBe(document2.id);
|
||||
expect(results[1]).toBe(document3.id);
|
||||
|
||||
@@ -537,19 +537,37 @@ class Document extends ParanoidModel {
|
||||
return compact(users);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all of the child documents for this document
|
||||
*
|
||||
* @param options FindOptions
|
||||
* @returns A promise that resolve to a list of documents
|
||||
*/
|
||||
findChildDocuments = async (
|
||||
where?: Omit<WhereOptions<Document>, "parentDocumentId">,
|
||||
options?: FindOptions<Document>
|
||||
): Promise<Document[]> =>
|
||||
await (this.constructor as typeof Document).findAll({
|
||||
where: {
|
||||
parentDocumentId: this.id,
|
||||
...where,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculate all of the document ids that are children of this document by
|
||||
* iterating through parentDocumentId references in the most efficient way.
|
||||
* recursively iterating through parentDocumentId references in the most efficient way.
|
||||
*
|
||||
* @param where query options to further filter the documents
|
||||
* @param options FindOptions
|
||||
* @returns A promise that resolves to a list of document ids
|
||||
*/
|
||||
getChildDocumentIds = async (
|
||||
findAllChildDocumentIds = async (
|
||||
where?: Omit<WhereOptions<Document>, "parentDocumentId">,
|
||||
options?: FindOptions<Document>
|
||||
): Promise<string[]> => {
|
||||
const getChildDocumentIds = async (
|
||||
const findAllChildDocumentIds = async (
|
||||
...parentDocumentId: string[]
|
||||
): Promise<string[]> => {
|
||||
const childDocuments = await (
|
||||
@@ -568,14 +586,14 @@ class Document extends ParanoidModel {
|
||||
if (childDocumentIds.length > 0) {
|
||||
return [
|
||||
...childDocumentIds,
|
||||
...(await getChildDocumentIds(...childDocumentIds)),
|
||||
...(await findAllChildDocumentIds(...childDocumentIds)),
|
||||
];
|
||||
}
|
||||
|
||||
return childDocumentIds;
|
||||
};
|
||||
|
||||
return getChildDocumentIds(this.id);
|
||||
return findAllChildDocumentIds(this.id);
|
||||
};
|
||||
|
||||
archiveWithChildren = async (
|
||||
|
||||
@@ -96,7 +96,7 @@ export default class SearchHelper {
|
||||
const sharedDocument = await options.share.$get("document");
|
||||
invariant(sharedDocument, "Cannot find document for share");
|
||||
|
||||
const childDocumentIds = await sharedDocument.getChildDocumentIds({
|
||||
const childDocumentIds = await sharedDocument.findAllChildDocumentIds({
|
||||
archivedAt: {
|
||||
[Op.is]: null,
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TeamPreference } from "@shared/types";
|
||||
import { subtractDate } from "@shared/utils/date";
|
||||
import slugify from "@shared/utils/slugify";
|
||||
import documentCreator from "@server/commands/documentCreator";
|
||||
import documentDuplicator from "@server/commands/documentDuplicator";
|
||||
import documentImporter from "@server/commands/documentImporter";
|
||||
import documentLoader from "@server/commands/documentLoader";
|
||||
import documentMover from "@server/commands/documentMover";
|
||||
@@ -1011,6 +1012,66 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"documents.duplicate",
|
||||
auth(),
|
||||
validate(T.DocumentsDuplicateSchema),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.DocumentsDuplicateReq>) => {
|
||||
const { transaction } = ctx.state;
|
||||
const { id, title, publish, recursive, collectionId, parentDocumentId } =
|
||||
ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
const document = await Document.findByPk(id, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const collection = collectionId
|
||||
? await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId)
|
||||
: document?.collection;
|
||||
|
||||
if (collection) {
|
||||
authorize(user, "updateDocument", collection);
|
||||
}
|
||||
|
||||
if (parentDocumentId) {
|
||||
const parent = await Document.findByPk(parentDocumentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "update", parent);
|
||||
|
||||
if (!parent.publishedAt) {
|
||||
throw InvalidRequestError("Cannot duplicate document inside a draft");
|
||||
}
|
||||
}
|
||||
|
||||
const response = await documentDuplicator({
|
||||
user,
|
||||
collection,
|
||||
document,
|
||||
title,
|
||||
publish,
|
||||
transaction,
|
||||
recursive,
|
||||
parentDocumentId,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
documents: await Promise.all(
|
||||
response.map((document) => presentDocument(document))
|
||||
),
|
||||
},
|
||||
policies: presentPolicies(user, response),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"documents.move",
|
||||
auth(),
|
||||
@@ -1176,7 +1237,7 @@ router.post(
|
||||
});
|
||||
authorize(user, "unpublish", document);
|
||||
|
||||
const childDocumentIds = await document.getChildDocumentIds();
|
||||
const childDocumentIds = await document.findAllChildDocumentIds();
|
||||
if (childDocumentIds.length > 0) {
|
||||
throw InvalidRequestError(
|
||||
"Cannot unpublish document with child documents"
|
||||
|
||||
@@ -174,6 +174,23 @@ export const DocumentsSearchSchema = BaseSchema.extend({
|
||||
|
||||
export type DocumentsSearchReq = z.infer<typeof DocumentsSearchSchema>;
|
||||
|
||||
export const DocumentsDuplicateSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema.extend({
|
||||
/** New document title */
|
||||
title: z.string().optional(),
|
||||
/** Whether child documents should also be duplicated */
|
||||
recursive: z.boolean().optional(),
|
||||
/** Whether the new document should be published */
|
||||
publish: z.boolean().optional(),
|
||||
/** Id of the collection to which the document should be copied */
|
||||
collectionId: z.string().uuid().optional(),
|
||||
/** Id of the parent document to which the document should be copied */
|
||||
parentDocumentId: z.string().uuid().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type DocumentsDuplicateReq = z.infer<typeof DocumentsDuplicateSchema>;
|
||||
|
||||
export const DocumentsTemplatizeSchema = BaseSchema.extend({
|
||||
body: BaseIdSchema,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"Download document": "Download document",
|
||||
"Duplicate": "Duplicate",
|
||||
"Duplicate document": "Duplicate document",
|
||||
"Document duplicated": "Document duplicated",
|
||||
"Copy document": "Copy document",
|
||||
"collection": "collection",
|
||||
"Pin to {{collectionName}}": "Pin to {{collectionName}}",
|
||||
"Pinned to collection": "Pinned to collection",
|
||||
@@ -169,6 +169,9 @@
|
||||
"Currently editing": "Currently editing",
|
||||
"Currently viewing": "Currently viewing",
|
||||
"Viewed {{ timeAgo }} ago": "Viewed {{ timeAgo }} ago",
|
||||
"Copy of {{ documentName }}": "Copy of {{ documentName }}",
|
||||
"Title": "Title",
|
||||
"Include nested documents": "Include nested documents",
|
||||
"Emoji Picker": "Emoji Picker",
|
||||
"Remove": "Remove",
|
||||
"Module failed to load": "Module failed to load",
|
||||
|
||||
Reference in New Issue
Block a user