* feat: initial base structure * feat: utils for constructing and flattening collection tree * feat: basic demo to display tree-like structure with virtualization * feat: make it searchable * feat: row component * fix: handle row selection * fix: scroll jitter * fix: popover max-height to eliminate extra scroll * fix: position scrollbar correctly * fix: do not sort to maintain correct tree-like view * feat: footer * fix: scroll to selected item * fix: deselect item * fix: display selected location in footer * fix: deselect item if any upon search trigger * fix: create draft without collection * fix: pass down collectionId to all the nodes * feat: publish document under selected location * fix: move the doc post publish in case it is supposed to be a nested doc * fix: wrap text for selected location * fix: footer background in dark mode and unused css * fix: popover height in small devices * fix: no need to spread * refactor: remove outline * refactor: border-radius is common * refactor: remove active and focus * fix: do not shrink spacer * fix: scroll list padding with correctly adjusted scrolling * refactor: use constants * fix: use padding in favor of spacer * refactor: border attrs not needed * refactor: control title padding and icon size centrally * fix: rename param * fix: import path * fix: refactor styles, avoid magic numbers * fix: type err * feat: make rows collapsible * fix: fully expanded without disclosure upon search * fix: use modal in place of popover * fix: collapse descendants * fix: rename PublishPopover to PublishModal * fix: adjust collapse icon as part of tree view * fix: enable keyboard navigation * not sure why collapse and expand shortcuts are not working * fix: row expansion and search box focus and blur * fix: remove css hover, handle it via active prop * fix: discard tree like view for search results * fix: minor tweaks * refactor: no need to pass onPublish * refactor: remove unnecessary attrs from search component * fix: publish button text * fix: reset intial scroll offset to 0 on search * fix: remove search highlights * fix: clean up search component * refactor: search and row collapse * refactor: PublishLocation * fix: show emoji or star icon if present * fix: shift focus only from top item * fix: leading emoji * fix: baseline text * fix: make path tertiary * fix: do not show path for collections * fix: path text color upon selection * fix: deleted collection case * fix: no results found * fix: space around slash * Refinement, some small refactors * fix: Publish shortcut, use Button action * Allow new document creation from command menu without active collection * fix: duplicate * fix: Unneccessary truncation * fix: Scroll on expand/collapse Remove wraparound * fix: tsc * fix: Horizontal overflow on PublishLocation Remove pointless moveTo method * fix: Missing translation * Remove method indirection Show expanded collection icon in tree when expanded * Shrink font size a point * Remove feature flag * fix: Path color contrast in light mode Remove unused expanded/show attributes * shrink -> collapse, fix expanded disclosure without items after searching * Mobile styles * fix: scroll just into view Co-authored-by: Tom Moor <tom.moor@gmail.com>
305 lines
9.2 KiB
TypeScript
305 lines
9.2 KiB
TypeScript
import { isEmpty } from "lodash";
|
|
import isUUID from "validator/lib/isUUID";
|
|
import { z } from "zod";
|
|
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
|
import BaseSchema from "@server/routes/api/BaseSchema";
|
|
|
|
const DocumentsSortParamsSchema = z.object({
|
|
/** Specifies the attributes by which documents will be sorted in the list */
|
|
sort: z
|
|
.string()
|
|
.refine((val) => ["createdAt", "updatedAt", "index", "title"].includes(val))
|
|
.default("updatedAt"),
|
|
|
|
/** Specifies the sort order with respect to sort field */
|
|
direction: z
|
|
.string()
|
|
.optional()
|
|
.transform((val) => (val !== "ASC" ? "DESC" : val)),
|
|
});
|
|
|
|
const DateFilterSchema = z.object({
|
|
/** Date filter */
|
|
dateFilter: z
|
|
.union([
|
|
z.literal("day"),
|
|
z.literal("week"),
|
|
z.literal("month"),
|
|
z.literal("year"),
|
|
])
|
|
.optional(),
|
|
});
|
|
|
|
const SearchQuerySchema = z.object({
|
|
/** Query for search */
|
|
query: z.string().refine((v) => v.trim() !== ""),
|
|
});
|
|
|
|
const BaseIdSchema = z.object({
|
|
/** Id of the document to be updated */
|
|
id: z.string(),
|
|
});
|
|
|
|
export const DocumentsListSchema = BaseSchema.extend({
|
|
body: DocumentsSortParamsSchema.extend({
|
|
/** Id of the user who created the doc */
|
|
userId: z.string().uuid().optional(),
|
|
|
|
/** Alias for userId - kept for backwards compatibility */
|
|
user: z.string().uuid().optional(),
|
|
|
|
/** Id of the collection to which the document belongs */
|
|
collectionId: z.string().uuid().optional(),
|
|
|
|
/** Alias for collectionId - kept for backwards compatibility */
|
|
collection: z.string().uuid().optional(),
|
|
|
|
/** Id of the backlinked document */
|
|
backlinkDocumentId: z.string().uuid().optional(),
|
|
|
|
/** Id of the parent document to which the document belongs */
|
|
parentDocumentId: z.string().uuid().nullish(),
|
|
|
|
/** Boolean which denotes whether the document is a template */
|
|
template: z.boolean().optional(),
|
|
}),
|
|
// Maintains backwards compatibility
|
|
}).transform((req) => {
|
|
req.body.collectionId = req.body.collectionId || req.body.collection;
|
|
req.body.userId = req.body.userId || req.body.user;
|
|
delete req.body.collection;
|
|
delete req.body.user;
|
|
|
|
return req;
|
|
});
|
|
|
|
export type DocumentsListReq = z.infer<typeof DocumentsListSchema>;
|
|
|
|
export const DocumentsArchivedSchema = BaseSchema.extend({
|
|
body: DocumentsSortParamsSchema.extend({}),
|
|
});
|
|
|
|
export type DocumentsArchivedReq = z.infer<typeof DocumentsArchivedSchema>;
|
|
|
|
export const DocumentsDeletedSchema = BaseSchema.extend({
|
|
body: DocumentsSortParamsSchema.extend({}),
|
|
});
|
|
|
|
export type DocumentsDeletedReq = z.infer<typeof DocumentsDeletedSchema>;
|
|
|
|
export const DocumentsViewedSchema = BaseSchema.extend({
|
|
body: DocumentsSortParamsSchema.extend({}),
|
|
});
|
|
|
|
export type DocumentsViewedReq = z.infer<typeof DocumentsViewedSchema>;
|
|
|
|
export const DocumentsDraftsSchema = BaseSchema.extend({
|
|
body: DocumentsSortParamsSchema.merge(DateFilterSchema).extend({
|
|
/** Id of the collection to which the document belongs */
|
|
collectionId: z.string().uuid().optional(),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsDraftsReq = z.infer<typeof DocumentsDraftsSchema>;
|
|
|
|
export const DocumentsInfoSchema = BaseSchema.extend({
|
|
body: z.object({
|
|
/** Id of the document to be retrieved */
|
|
id: z.string().optional(),
|
|
|
|
/** Share Id, if available */
|
|
shareId: z
|
|
.string()
|
|
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
|
.optional(),
|
|
|
|
/** Version of the API to be used */
|
|
apiVersion: z.number().optional(),
|
|
}),
|
|
}).refine((req) => !(isEmpty(req.body.id) && isEmpty(req.body.shareId)), {
|
|
message: "one of id or shareId is required",
|
|
});
|
|
|
|
export type DocumentsInfoReq = z.infer<typeof DocumentsInfoSchema>;
|
|
|
|
export const DocumentsExportSchema = BaseSchema.extend({
|
|
body: BaseIdSchema,
|
|
});
|
|
|
|
export type DocumentsExportReq = z.infer<typeof DocumentsExportSchema>;
|
|
|
|
export const DocumentsRestoreSchema = BaseSchema.extend({
|
|
body: BaseIdSchema.extend({
|
|
/** Id of the collection to which the document belongs */
|
|
collectionId: z.string().uuid().optional(),
|
|
|
|
/** Id of document revision */
|
|
revisionId: z.string().uuid().optional(),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsRestoreReq = z.infer<typeof DocumentsRestoreSchema>;
|
|
|
|
export const DocumentsSearchSchema = BaseSchema.extend({
|
|
body: SearchQuerySchema.merge(DateFilterSchema).extend({
|
|
/** Whether to include archived docs in results */
|
|
includeArchived: z.boolean().optional(),
|
|
|
|
/** Whether to include drafts in results */
|
|
includeDrafts: z.boolean().optional(),
|
|
|
|
/** Filter results for team based on the collection */
|
|
collectionId: z.string().uuid().optional(),
|
|
|
|
/** Filter results based on user */
|
|
userId: z.string().uuid().optional(),
|
|
|
|
/** Filter results for the team derived from shareId */
|
|
shareId: z
|
|
.string()
|
|
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
|
.optional(),
|
|
|
|
/** Min words to be shown in the results snippets */
|
|
snippetMinWords: z.number().default(20),
|
|
|
|
/** Max words to be accomodated in the results snippets */
|
|
snippetMaxWords: z.number().default(30),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsSearchReq = z.infer<typeof DocumentsSearchSchema>;
|
|
|
|
export const DocumentsTemplatizeSchema = BaseSchema.extend({
|
|
body: BaseIdSchema,
|
|
});
|
|
|
|
export type DocumentsTemplatizeReq = z.infer<typeof DocumentsTemplatizeSchema>;
|
|
|
|
export const DocumentsUpdateSchema = BaseSchema.extend({
|
|
body: BaseIdSchema.extend({
|
|
/** Doc title to be updated */
|
|
title: z.string().optional(),
|
|
|
|
/** Doc text to be updated */
|
|
text: z.string().optional(),
|
|
|
|
/** Boolean to denote if the doc should occupy full width */
|
|
fullWidth: z.boolean().optional(),
|
|
|
|
/** Boolean to denote if the doc should be published */
|
|
publish: z.boolean().optional(),
|
|
|
|
/** Revision to compare against document revision count */
|
|
lastRevision: z.number().optional(),
|
|
|
|
/** Doc template Id */
|
|
templateId: z.string().uuid().nullish(),
|
|
|
|
/** Doc collection Id */
|
|
collectionId: z.string().uuid().nullish(),
|
|
|
|
/** Boolean to denote if text should be appended */
|
|
append: z.boolean().optional(),
|
|
|
|
/** Version of the API to be used */
|
|
apiVersion: z.number().optional(),
|
|
}),
|
|
}).refine((req) => !(req.body.append && !req.body.text), {
|
|
message: "text is required while appending",
|
|
});
|
|
|
|
export type DocumentsUpdateReq = z.infer<typeof DocumentsUpdateSchema>;
|
|
|
|
export const DocumentsMoveSchema = BaseSchema.extend({
|
|
body: BaseIdSchema.extend({
|
|
/** Id of collection to which the doc is supposed to be moved */
|
|
collectionId: z.string().uuid(),
|
|
|
|
/** Parent Id, in case if the doc is moved to a new parent */
|
|
parentDocumentId: z.string().uuid().nullish(),
|
|
|
|
/** Helps evaluate the new index in collection structure upon move */
|
|
index: z.number().gte(0).optional(),
|
|
}),
|
|
}).refine((req) => !(req.body.parentDocumentId === req.body.id), {
|
|
message: "infinite loop detected, cannot nest a document inside itself",
|
|
});
|
|
|
|
export type DocumentsMoveReq = z.infer<typeof DocumentsMoveSchema>;
|
|
|
|
export const DocumentsArchiveSchema = BaseSchema.extend({
|
|
body: BaseIdSchema,
|
|
});
|
|
|
|
export type DocumentsArchiveReq = z.infer<typeof DocumentsArchiveSchema>;
|
|
|
|
export const DocumentsDeleteSchema = BaseSchema.extend({
|
|
body: BaseIdSchema.extend({
|
|
/** Whether to permanently delete the doc as opposed to soft-delete */
|
|
permanent: z.boolean().optional(),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsDeleteReq = z.infer<typeof DocumentsDeleteSchema>;
|
|
|
|
export const DocumentsUnpublishSchema = BaseSchema.extend({
|
|
body: BaseIdSchema.extend({
|
|
/** Version of the API to be used */
|
|
apiVersion: z.number().optional(),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsUnpublishReq = z.infer<typeof DocumentsUnpublishSchema>;
|
|
|
|
export const DocumentsImportSchema = BaseSchema.extend({
|
|
body: z.object({
|
|
/** Whether to publish the imported docs. String as this is always multipart/form-data */
|
|
publish: z.preprocess((val) => val === "true", z.boolean()).optional(),
|
|
|
|
/** Import docs to this collection */
|
|
collectionId: z.string().uuid(),
|
|
|
|
/** Import under this parent doc */
|
|
parentDocumentId: z.string().uuid().nullish(),
|
|
}),
|
|
});
|
|
|
|
export type DocumentsImportReq = z.infer<typeof DocumentsImportSchema>;
|
|
|
|
export const DocumentsCreateSchema = BaseSchema.extend({
|
|
body: z.object({
|
|
/** Doc title */
|
|
title: z.string().default(""),
|
|
|
|
/** Doc text */
|
|
text: z.string().default(""),
|
|
|
|
/** Boolean to denote if the doc should be published */
|
|
publish: z.boolean().optional(),
|
|
|
|
/** Create Doc under this collection */
|
|
collectionId: z.string().uuid().nullish(),
|
|
|
|
/** Create Doc under this parent */
|
|
parentDocumentId: z.string().uuid().nullish(),
|
|
|
|
/** Create doc with this template */
|
|
templateId: z.string().uuid().optional(),
|
|
|
|
/** Whether to create a template doc */
|
|
template: z.boolean().optional(),
|
|
}),
|
|
})
|
|
.refine((req) => !(req.body.parentDocumentId && !req.body.collectionId), {
|
|
message: "collectionId is required to create a nested document",
|
|
})
|
|
.refine((req) => !(req.body.template && !req.body.collectionId), {
|
|
message: "collectionId is required to create a template document",
|
|
})
|
|
.refine((req) => !(req.body.publish && !req.body.collectionId), {
|
|
message: "collectionId is required to publish",
|
|
});
|
|
|
|
export type DocumentsCreateReq = z.infer<typeof DocumentsCreateSchema>;
|