mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
feat: Allow creating new doc before/after (#11453)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user