diff --git a/package-lock.json b/package-lock.json index b78ba0a8..81dcbd82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20119,7 +20119,7 @@ "kleur": "^4.1.4", "lodash.debounce": "^4.0.8", "redlock": "^4.2.0", - "uuid": "^10.0.0" + "uuid": "^11.0.3" }, "devDependencies": { "@types/ioredis": "^4.28.7", @@ -20132,15 +20132,16 @@ } }, "packages/extension-redis/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "packages/extension-sqlite": { @@ -20222,7 +20223,7 @@ "async-lock": "^1.3.1", "kleur": "^4.1.4", "lib0": "^0.2.47", - "uuid": "^10.0.0", + "uuid": "^11.0.3", "ws": "^8.5.0" }, "devDependencies": { @@ -20236,15 +20237,16 @@ } }, "packages/server/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "packages/server/node_modules/ws": { @@ -21797,13 +21799,13 @@ "kleur": "^4.1.4", "lodash.debounce": "^4.0.8", "redlock": "^4.2.0", - "uuid": "^10.0.0" + "uuid": "^11.0.3" }, "dependencies": { "uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==" } } }, @@ -21947,14 +21949,14 @@ "async-lock": "^1.3.1", "kleur": "^4.1.4", "lib0": "^0.2.47", - "uuid": "^10.0.0", + "uuid": "^11.0.3", "ws": "^8.5.0" }, "dependencies": { "uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==" }, "ws": { "version": "8.5.0", diff --git a/packages/provider/src/TiptapCollabProvider.ts b/packages/provider/src/TiptapCollabProvider.ts index d8ca7310..38c1aece 100644 --- a/packages/provider/src/TiptapCollabProvider.ts +++ b/packages/provider/src/TiptapCollabProvider.ts @@ -7,9 +7,11 @@ import { } from './HocuspocusProvider.js' import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js' -import type { - DeleteCommentOptions, - TCollabComment, TCollabThread, THistoryVersion, +import { + type DeleteCommentOptions, + type DeleteThreadOptions, + type GetThreadsOptions, + type TCollabComment, type TCollabThread, type THistoryVersion, } from './types.js' const defaultDeleteCommentOptions: DeleteCommentOptions = { @@ -17,6 +19,15 @@ const defaultDeleteCommentOptions: DeleteCommentOptions = { deleteThread: false, } +const defaultGetThreadsOptions: GetThreadsOptions = { + types: ['unarchived'], +} + +const defaultDeleteThreadOptions: DeleteThreadOptions = { + deleteComments: false, + force: false, +} + export type TiptapCollabProviderConfiguration = Required> & Partial & @@ -128,10 +139,29 @@ export class TiptapCollabProvider extends HocuspocusProvider { /** * Finds all threads in the document and returns them as JSON objects + * @options Options to control the output of the threads (e.g. include deleted threads) * @returns An array of threads as JSON objects */ - getThreads(): TCollabThread[] { - return this.getYThreads().toJSON() as TCollabThread[] + getThreads(options?: GetThreadsOptions): TCollabThread[] { + const { types } = { ...defaultGetThreadsOptions, ...options } as GetThreadsOptions + + const threads = this.getYThreads().toJSON() as TCollabThread[] + + if (types?.includes('archived') && types?.includes('unarchived')) { + return threads + } + + return threads.filter(currentThead => { + if (types?.includes('archived') && currentThead.deletedAt) { + return true + } + + if (types?.includes('unarchived') && !currentThead.deletedAt) { + return true + } + + return false + }) } /** @@ -144,7 +174,7 @@ export class TiptapCollabProvider extends HocuspocusProvider { let i = 0 // eslint-disable-next-line no-restricted-syntax - for (const thread of this.getThreads()) { + for (const thread of this.getThreads({ types: ['archived', 'unarchived'] })) { if (thread.id === id) { index = i break @@ -190,7 +220,7 @@ export class TiptapCollabProvider extends HocuspocusProvider { * @param data The thread data * @returns The created thread */ - createThread(data: Omit) { + createThread(data: Omit) { let createdThread: TCollabThread = {} as TCollabThread this.document.transact(() => { @@ -199,6 +229,7 @@ export class TiptapCollabProvider extends HocuspocusProvider { thread.set('createdAt', (new Date()).toISOString()) thread.set('comments', new Y.Array()) thread.set('deletedComments', new Y.Array()) + thread.set('deletedAt', null) this.getYThreads().push([thread]) createdThread = this.updateThread(String(thread.get('id')), data) @@ -242,18 +273,57 @@ export class TiptapCollabProvider extends HocuspocusProvider { } /** - * Delete a specific thread and all its comments + * Handle the deletion of a thread. By default, the thread and it's comments are not deleted, but marked as deleted + * via the `deletedAt` property. Forceful deletion can be enabled by setting the `force` option to `true`. + * + * If you only want to delete the comments of a thread, you can set the `deleteComments` option to `true`. * @param id The thread id - * @returns void + * @param options A set of options that control how the thread is deleted + * @returns The deleted thread or null if the thread is not found */ - deleteThread(id: TCollabThread['id']) { + deleteThread(id: TCollabThread['id'], options?: DeleteThreadOptions) { + const { deleteComments, force } = { ...defaultDeleteThreadOptions, ...options } + const index = this.getThreadIndex(id) if (index === null) { + return null + } + + if (force) { + this.getYThreads().delete(index, 1) return } - this.getYThreads().delete(index, 1) + const thread = this.getYThreads().get(index) + + thread.set('deletedAt', (new Date()).toISOString()) + + if (deleteComments) { + thread.set('comments', new Y.Array()) + thread.set('deletedComments', new Y.Array()) + } + + return thread.toJSON() as TCollabThread + } + + /** + * Tries to restore a deleted thread + * @param id The thread id + * @returns The restored thread or null if the thread is not found + */ + restoreThread(id: TCollabThread['id']) { + const index = this.getThreadIndex(id) + + if (index === null) { + return null + } + + const thread = this.getYThreads().get(index) + + thread.set('deletedAt', null) + + return thread.toJSON() as TCollabThread } /** diff --git a/packages/provider/src/types.ts b/packages/provider/src/types.ts index 07c44732..fda75863 100644 --- a/packages/provider/src/types.ts +++ b/packages/provider/src/types.ts @@ -110,6 +110,7 @@ export type TCollabThread = { id: string; createdAt: number; updatedAt: number; + deletedAt: number | null; resolvedAt?: string; // (new Date()).toISOString() comments: TCollabComment[]; deletedComments: TCollabComment[]; @@ -197,3 +198,34 @@ export type DeleteCommentOptions = { */ deleteContent?: boolean } + +export type DeleteThreadOptions = { + /** + * If `true`, will remove the comments on the thread, + * otherwise will only mark the thread as deleted + * and keep the comments + * @default false + */ + deleteComments?: boolean + + /** + * If `true`, will forcefully remove the thread and all comments, + * otherwise will only mark the thread as deleted + * and keep the comments + * @default false + */ + force?: boolean, +} + +/** + * The type of thread + */ +export type ThreadType = 'archived' | 'unarchived' + +export type GetThreadsOptions = { + /** + * The types of threads to get + * @default ['unarchived'] + */ + types?: Array +}