feat: Allow creating new doc before/after (#11453)

This commit is contained in:
Tom Moor
2026-02-14 17:24:52 -05:00
committed by GitHub
parent f6709520fa
commit 7824f6b363
9 changed files with 169 additions and 10 deletions
+127 -4
View File
@@ -70,6 +70,7 @@ import {
homePath,
newDocumentPath,
newNestedDocumentPath,
newSiblingDocumentPath,
searchPath,
documentPath,
urlify,
@@ -78,7 +79,12 @@ import {
} from "~/utils/routeHelpers";
import capitalize from "lodash/capitalize";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import type { Action, ActionGroup, ActionSeparator } from "~/types";
import type {
Action,
ActionContext,
ActionGroup,
ActionSeparator,
} from "~/types";
import lazyWithRetry from "~/utils/lazyWithRetry";
import env from "~/env";
@@ -247,12 +253,41 @@ export const createDocumentFromTemplate = createInternalLinkAction({
},
});
/**
* Finds the index of a document among its siblings in the collection tree.
*
* @param stores - the root stores.
* @param document - the document to find the index of.
* @returns the index of the document among its siblings, or -1 if not found.
*/
function findDocumentSiblingIndex(
stores: ActionContext["stores"],
document: {
id: string;
collectionId?: string | null;
parentDocumentId?: string;
}
): number {
if (!document.collectionId) {
return -1;
}
const collection = stores.collections.get(document.collectionId);
if (!collection) {
return -1;
}
const siblings = document.parentDocumentId
? collection.getChildrenForDocument(document.parentDocumentId)
: collection.sortedDocuments;
return siblings?.findIndex((node) => node.id === document.id) ?? -1;
}
export const createNestedDocument = createInternalLinkAction({
name: ({ t }) => t("New nested document"),
name: ({ t }) => t("Nested document"),
analyticsName: "New document",
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
keywords: "create nested",
visible: ({ currentTeamId, activeDocumentId, stores }) =>
!!currentTeamId &&
!!activeDocumentId &&
@@ -270,6 +305,93 @@ export const createNestedDocument = createInternalLinkAction({
},
});
const createDocumentBefore = createInternalLinkAction({
name: ({ t }) => t("Before"),
analyticsName: "New document before",
section: ActiveDocumentSection,
keywords: "create before",
visible: ({ currentTeamId, activeDocumentId, stores }) => {
if (!currentTeamId || !activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
return (
!!document?.collectionId &&
stores.policies.abilities(currentTeamId).createDocument
);
},
to: ({ activeDocumentId, stores, sidebarContext }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return "";
}
const index = findDocumentSiblingIndex(stores, document);
const [pathname, search] = newSiblingDocumentPath({
collectionId: document.collectionId,
parentDocumentId: document.parentDocumentId,
index: Math.max(0, index),
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
const createDocumentAfter = createInternalLinkAction({
name: ({ t }) => t("After"),
analyticsName: "New document after",
section: ActiveDocumentSection,
keywords: "create after",
visible: ({ currentTeamId, activeDocumentId, stores }) => {
if (!currentTeamId || !activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
return (
!!document?.collectionId &&
stores.policies.abilities(currentTeamId).createDocument
);
},
to: ({ activeDocumentId, stores, sidebarContext }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return "";
}
const index = findDocumentSiblingIndex(stores, document);
const [pathname, search] = newSiblingDocumentPath({
collectionId: document.collectionId,
parentDocumentId: document.parentDocumentId,
index: index + 1,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createNewDocument = createActionWithChildren({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, stores }) =>
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument,
children: [createDocumentBefore, createDocumentAfter, createNestedDocument],
});
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
@@ -1456,6 +1578,7 @@ export const rootDocumentActions = [
archiveDocument,
createDocument,
createDraftDocument,
createNewDocument,
createNestedDocument,
createTemplateFromDocument,
deleteDocument,
+3 -3
View File
@@ -11,7 +11,7 @@ import {
unstarDocument,
editDocument,
shareDocument,
createNestedDocument,
createNewDocument,
importDocument,
createTemplateFromDocument,
duplicateDocument,
@@ -94,8 +94,6 @@ export function useDocumentMenuAction({
perform: () => requestAnimationFrame(() => onRename?.()),
}),
shareDocument,
createNestedDocument,
importDocument,
createTemplateFromDocument,
duplicateDocument,
publishDocument,
@@ -104,6 +102,8 @@ export function useDocumentMenuAction({
moveDocument,
moveTemplate,
applyTemplateFactory({ actions: templateMenuActions }),
importDocument,
createNewDocument,
pinDocument,
createDocumentFromTemplate,
ActionSeparator,
+6 -1
View File
@@ -31,6 +31,7 @@ function DocumentNew({ template }: Props) {
useEffect(() => {
async function createDocument() {
const index = parseInt(query.get("index") || "0", 10);
const parentDocumentId = query.get("parentDocumentId") ?? undefined;
const parentDocument = parentDocumentId
? documents.get(parentDocumentId)
@@ -41,6 +42,7 @@ function DocumentNew({ template }: Props) {
if (id) {
collection = await collections.fetch(id);
}
const document = await documents.create(
{
collectionId: collection?.id,
@@ -53,7 +55,10 @@ function DocumentNew({ template }: Props) {
title: query.get("title") ?? "",
data: ProsemirrorHelper.getEmptyDocument(),
},
{ publish: collection?.id || parentDocumentId ? true : undefined }
{
publish: collection?.id || parentDocumentId ? true : undefined,
index,
}
);
if (parentDocumentId) {
+18
View File
@@ -128,6 +128,24 @@ export function newNestedDocumentPath(parentDocumentId?: string): string {
return `/doc/new${search}`;
}
export function newSiblingDocumentPath(params: {
collectionId?: string | null;
parentDocumentId?: string;
index: number;
}): string {
const query: Record<string, string> = {
index: String(params.index),
};
if (params.parentDocumentId) {
query.parentDocumentId = params.parentDocumentId;
}
if (params.collectionId) {
query.collectionId = params.collectionId;
}
return `/doc/new?${queryString.stringify(query)}`;
}
export function searchPath({
query,
collectionId,
+3
View File
@@ -31,6 +31,7 @@ type Props = Optional<
> & {
state?: Buffer;
publish?: boolean;
index?: number;
templateDocument?: Document | null;
};
@@ -45,6 +46,7 @@ export default async function documentCreator(
id,
urlId,
publish,
index,
collectionId,
parentDocumentId,
content,
@@ -152,6 +154,7 @@ export default async function documentCreator(
await document.publish(ctx, {
collectionId,
silent: true,
index,
event: !!document.title,
data: eventData,
});
+3 -1
View File
@@ -1001,11 +1001,13 @@ class Document extends ArchivableModel<
publish = async (
ctx: APIContext,
{
index = 0,
collectionId,
silent = false,
event = true,
data,
}: {
index?: number;
collectionId: string | null | undefined;
silent?: boolean;
event?: boolean;
@@ -1037,7 +1039,7 @@ class Document extends ArchivableModel<
});
if (collection) {
await collection.addDocumentToStructure(this, 0, { transaction });
await collection.addDocumentToStructure(this, index, { transaction });
if (this.collection) {
this.collection.documentStructure = collection.documentStructure;
}
+2
View File
@@ -1661,6 +1661,7 @@ router.post(
icon,
color,
publish,
index,
collectionId,
parentDocumentId,
fullWidth,
@@ -1728,6 +1729,7 @@ router.post(
color,
createdAt,
publish,
index,
collectionId: collection?.id,
parentDocumentId,
templateDocument,
+3
View File
@@ -403,6 +403,9 @@ export const DocumentsCreateSchema = BaseSchema.extend({
/** Collection to create document within */
collectionId: z.string().uuid().nullish(),
/** Index to create the document at within the collection */
index: z.number().optional(),
/** Parent document to create within */
parentDocumentId: z.string().uuid().nullish(),
+4 -1
View File
@@ -54,7 +54,9 @@
"Open document": "Open document",
"New draft": "New draft",
"New from template": "New from template",
"New nested document": "New nested document",
"Nested document": "Nested document",
"Before": "Before",
"After": "After",
"Publish": "Publish",
"Published {{ documentName }}": "Published {{ documentName }}",
"Publish document": "Publish document",
@@ -467,6 +469,7 @@
"Collapse sidebar": "Collapse sidebar",
"Archived collections": "Archived collections",
"New doc": "New doc",
"New nested document": "New nested document",
"Empty": "Empty",
"No collections": "No collections",
"Collapse": "Collapse",