fix: flat lists

This commit is contained in:
Palanikannan M
2024-10-25 01:48:54 +05:30
parent 15668299b4
commit cefc1e99b1
7 changed files with 2045 additions and 660 deletions

View File

@@ -16,16 +16,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"@hocuspocus/extension-database": "^2.11.3",
"@hocuspocus/extension-logger": "^2.11.3",
"@hocuspocus/extension-redis": "^2.13.5",
"@hocuspocus/server": "^2.11.3",
"@hocuspocus/extension-database": "^2.13.7",
"@hocuspocus/extension-logger": "^2.13.7",
"@hocuspocus/extension-redis": "^2.13.7",
"@hocuspocus/server": "^2.13.7",
"@plane/editor": "*",
"@plane/types": "*",
"@sentry/node": "^8.28.0",
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "^2.4.0",
"@tiptap/html": "^2.3.0",
"@tiptap/core": "^2.9.1",
"@tiptap/html": "^2.9.1",
"axios": "^1.7.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -39,14 +39,14 @@
"pino-http": "^10.3.0",
"pino-pretty": "^11.2.2",
"uuid": "^10.0.0",
"y-prosemirror": "^1.2.9",
"y-prosemirror": "^1.2.12",
"y-protocols": "^1.0.6",
"yjs": "^13.6.14"
"yjs": "^13.6.20"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@babel/preset-env": "^7.25.9",
"@babel/preset-typescript": "^7.24.7",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",

View File

@@ -36,24 +36,24 @@
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.13.5",
"@hocuspocus/provider": "^2.13.7",
"@plane/ui": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-character-count": "^2.6.5",
"@tiptap/extension-collaboration": "^2.3.2",
"@tiptap/extension-image": "^2.1.13",
"@tiptap/extension-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",
"@tiptap/extension-placeholder": "^2.3.0",
"@tiptap/extension-task-item": "^2.1.13",
"@tiptap/extension-task-list": "^2.1.13",
"@tiptap/extension-text-style": "^2.7.1",
"@tiptap/extension-underline": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.0.13",
"@tiptap/core": "^2.9.1",
"@tiptap/extension-blockquote": "^2.9.1",
"@tiptap/extension-character-count": "^2.9.1",
"@tiptap/extension-collaboration": "^2.9.1",
"@tiptap/extension-image": "^2.9.1",
"@tiptap/extension-list-item": "^2.9.1",
"@tiptap/extension-mention": "^2.9.1",
"@tiptap/extension-placeholder": "^2.9.1",
"@tiptap/extension-task-item": "^2.9.1",
"@tiptap/extension-task-list": "^2.9.1",
"@tiptap/extension-text-style": "^2.9.1",
"@tiptap/extension-underline": "^2.9.1",
"@tiptap/pm": "^2.9.1",
"@tiptap/react": "^2.9.1",
"@tiptap/starter-kit": "^2.9.1",
"@tiptap/suggestion": "^2.9.1",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"highlight.js": "^11.8.0",
@@ -66,12 +66,12 @@
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"tiptap-markdown": "^0.8.10",
"uuid": "^10.0.0",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5",
"y-prosemirror": "^1.2.12",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15"
"yjs": "^13.6.20"
},
"devDependencies": {
"@plane/eslint-config": "*",

View File

@@ -6,14 +6,22 @@ import {
listInputRules,
ListAttributes,
createWrapInListCommand,
DedentListOptions,
IndentListOptions,
createIndentListCommand,
createDedentListCommand,
} from "prosemirror-flat-list";
import { keymap } from "@tiptap/pm/keymap";
import { inputRules } from "@tiptap/pm/inputrules";
import migrationPlugin from "./old-list-migration";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
flatListComponent: {
createList: (attrs: ListAttributes) => ReturnType;
indentList: (attrs: IndentListOptions) => ReturnType;
dedentList: (attrs: DedentListOptions) => ReturnType;
// unwrapList: (attrs: UnwrapListOptions) => ReturnType;
};
}
}
@@ -41,13 +49,52 @@ export const FlatListExtension = Node.create({
return {
createList:
(attrs: ListAttributes) =>
({ editor }) => {
({ state, view }) => {
const wrapInList = createWrapInListCommand<ListAttributes>(attrs);
return wrapInList(editor.state, editor.view.dispatch);
return wrapInList(state, view.dispatch);
},
indentList:
(attrs: IndentListOptions) =>
({ state, view }) => {
const indentList = createIndentListCommand(attrs);
return indentList(state, view.dispatch);
},
dedentList:
(attrs: DedentListOptions) =>
({ state, view }) => {
const dedentList = createDedentListCommand(attrs);
return dedentList(state, view.dispatch);
},
};
},
addKeyboardShortcuts(this) {
return {
Tab: ({ editor }) => {
const { selection } = editor.state;
const { $from } = selection;
if (editor.isActive(this.name)) {
editor.chain().focus().indentList({ from: $from.pos });
return true;
}
return false;
},
"Shift-Tab": ({ editor }) => {
const { selection } = editor.state;
const { $from } = selection;
if (editor.isActive(this.name)) {
editor.chain().focus().dedentList({ from: $from.pos });
return true;
}
return false;
},
};
},
addProseMirrorPlugins() {
return [...createListPlugins({ schema: this.editor.schema }), listKeymapPlugin, listInputRulePlugin];
return [
...createListPlugins({ schema: this.editor.schema }),
listKeymapPlugin,
listInputRulePlugin,
// migrationPlugin,
];
},
});

View File

@@ -0,0 +1,82 @@
import type { ListAttributes, ListKind, ProsemirrorNodeJSON } from "./types";
function migrateNodes(nodes: ProsemirrorNodeJSON[]): [ProsemirrorNodeJSON[], boolean] {
const content: ProsemirrorNodeJSON[] = [];
let updated = false;
for (const node of nodes) {
if (node.type === "bullet_list" || node.type === "bulletList") {
updated = true;
for (const child of node.content ?? []) {
const [migratedChild, childUpdated] = migrateNode(child, {
kind: "bullet",
});
content.push(migratedChild);
updated = updated || childUpdated;
}
} else if (node.type === "ordered_list" || node.type === "orderedList") {
updated = true;
for (const child of node.content ?? []) {
const [migratedChild, childUpdated] = migrateNode(child, {
kind: "ordered",
});
content.push(migratedChild);
updated = updated || childUpdated;
}
} else if (node.type === "task_list" || node.type === "taskList") {
updated = true;
for (const child of node.content ?? []) {
const [migratedChild, childUpdated] = migrateNode(child, {
kind: "task",
});
content.push(migratedChild);
updated = updated || childUpdated;
}
} else {
// Handle other node types, including those that may contain list items
const [migratedContent, contentUpdated] = migrateNodes(node.content ?? []);
content.push({ ...node, content: migratedContent });
updated = updated || contentUpdated;
}
}
return [content, updated];
}
function migrateNode(node: ProsemirrorNodeJSON, { kind }: { kind?: ListKind } = {}): [ProsemirrorNodeJSON, boolean] {
// Check if the node is a list item
if (node.type === "list_item" || node.type === "listItem" || node.type === "taskListItem") {
const [content, updated] = migrateNodes(node.content ?? []);
return [
{
...node,
type: "list",
attrs: {
collapsed: Boolean(node.attrs?.closed),
...node.attrs,
kind: kind ?? "bullet",
} satisfies ListAttributes,
content,
},
true,
];
} else if (node.content) {
// If the node has content, we need to check for nested list items
const [content, updated] = migrateNodes(node.content);
return [{ ...node, content }, updated];
} else {
return [node, false];
}
}
/**
* Migrate a ProseMirror document JSON object from the old list structure to the
* new. A new document JSON object is returned if the document is updated,
* otherwise `null` is returned.
*
* @public
*/
export function migrateDocJSON(docJSON: ProsemirrorNodeJSON): ProsemirrorNodeJSON | null {
const [migrated, updated] = migrateNode(docJSON);
return updated ? migrated : null;
}

View File

@@ -10,6 +10,7 @@ import { useEditor } from "@/hooks/use-editor";
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
import { TCollaborativeEditorProps } from "@/types";
// import { Collaboration } from "@/extensions/collaboration";
export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const {
@@ -33,29 +34,28 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false);
const [hasServerSynced, setHasServerSynced] = useState(false);
// initialize Hocuspocus provider
const provider = useMemo(
() =>
new HocuspocusProvider({
name: id,
parameters: realtimeConfig.queryParams,
// using user id as a token to verify the user on the server
token: user.id,
url: realtimeConfig.url,
onAuthenticationFailed: () => {
const provider = useMemo(() => {
console.log("ran", id, realtimeConfig, serverHandler, user.id);
return new HocuspocusProvider({
name: id,
parameters: realtimeConfig.queryParams,
// using user id as a token to verify the user on the server
token: user.id,
url: realtimeConfig.url,
onAuthenticationFailed: () => {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
},
onConnect: () => serverHandler?.onConnect?.(),
onClose: (data) => {
if (data.event.code === 1006) {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
},
onConnect: () => serverHandler?.onConnect?.(),
onClose: (data) => {
if (data.event.code === 1006) {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
}
},
onSynced: () => setHasServerSynced(true),
}),
[id, realtimeConfig, serverHandler, user.id]
);
}
},
onSynced: () => setHasServerSynced(true),
});
}, [id, realtimeConfig, serverHandler, user.id]);
// destroy and disconnect connection on unmount
useEffect(
@@ -65,13 +65,15 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
},
[provider]
);
// const localProvider = useMemo(() => new IndexeddbPersistence(id, provider.document), [id, provider]);
// indexed db integration for offline support
useLayoutEffect(() => {
const localProvider = new IndexeddbPersistence(id, provider.document);
return () => {
localProvider?.destroy();
};
}, [provider, id]);
// useLayoutEffect(
// () => () => {
// localProvider?.destroy();
// },
// [localProvider, id]
// );
const editor = useEditor({
id,
@@ -101,6 +103,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
mentionHandler,
placeholder,
provider,
// localProvider,
tabIndex,
});

View File

@@ -3,7 +3,8 @@ import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
import { useEditor as useTiptapEditor, Editor, type JSONContent } from "@tiptap/react";
import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs";
// components
import { getEditorMenuItems } from "@/components/menus";
@@ -17,6 +18,8 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
import { CoreEditorProps } from "@/props";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
import { migrateDocJSON } from "@/extensions/migrationjson";
import { type ProsemirrorNodeJSON } from "prosemirror-flat-list";
export interface CustomEditorProps {
editorClassName: string;
@@ -35,6 +38,7 @@ export interface CustomEditorProps {
onChange?: (json: object, html: string) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
provider?: HocuspocusProvider;
localProvider?: IndexeddbPersistence;
tabIndex?: number;
// undefined when prop is not passed, null if intentionally passed to stop
// swr syncing
@@ -56,16 +60,35 @@ export const useEditor = (props: CustomEditorProps) => {
onChange,
placeholder,
provider,
// localProvider,
tabIndex,
value,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
const [isEditorDisabled, setIsEditorDisabled] = useState(false);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor({
immediatelyRender: true,
shouldRerenderOnTransaction: false,
enableContentCheck: true,
onContentError: ({ editor, error, disableCollaboration }) => {
// console.log("ran", editor.getJSON());
if (disableCollaboration) disableCollaboration();
// localProvider.clearData();
// localProvider.destroy();
const emitUpdate = false;
// Disable further user input
// setIsEditorDisabled(true);
editor.setEditable(false, emitUpdate);
console.log("error", error);
},
editorProps: {
...CoreEditorProps({
editorClassName,
@@ -117,6 +140,40 @@ export const useEditor = (props: CustomEditorProps) => {
}
}, [editor, value, id]);
// const [hasMigrated, setHasMigrated] = useState(false);
//
// useEffect(() => {
// if (editor && (!hasMigrated || editor.isActive("listItem")) && !isEditorDisabled) {
// const newJSON = migrateDocJSON(editor.getJSON()) as JSONContent;
//
// if (newJSON) {
// // Create a new transaction
// const transaction = editor.state.tr;
//
// try {
// const node = editor.state.schema.nodeFromJSON(newJSON);
//
// transaction.replaceWith(0, editor.state.doc.content.size, node);
// transaction.setMeta("addToHistory", false);
// editor.view.dispatch(transaction);
// setHasMigrated(true);
//
// // focus user on the current position
// const currentSavedSelection = savedSelectionRef.current;
// if (currentSavedSelection) {
// const docLength = editor.state.doc.content.size;
// const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
// editor.commands.setTextSelection(relativePosition);
// }
//
// console.log("Migration of old lists completed without adding to history");
// } catch (error) {
// console.error("Error during migration:", error);
// }
// }
// }
// }, [editor.getJSON(), editor.isActive("listItem"), hasMigrated]);
useImperativeHandle(
forwardedRef,
() => ({

2396
yarn.lock

File diff suppressed because it is too large Load Diff