mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 05:21:14 +02:00
fix: flat lists
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
82
packages/editor/src/core/extensions/migrationjson.ts
Normal file
82
packages/editor/src/core/extensions/migrationjson.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
() => ({
|
||||
|
||||
Reference in New Issue
Block a user