Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
881ce5b
chore(migrate): useEditorMixin to useEditor composable
max-nextcloud Jun 16, 2025
8d998f0
chore(migrate): setContent mixin...
max-nextcloud Jun 17, 2025
13a5ce1
chore(migrate): to useEditorFlags composable
max-nextcloud Jun 18, 2025
d958a4d
chore(cleanup): fix small review remarks
max-nextcloud Jun 18, 2025
b0790dc
chore(migrate): use.find instead of deprecated .contains
max-nextcloud Jun 19, 2025
8b4e635
enh(editor): store session in separate extension
max-nextcloud Jun 20, 2025
9d3cc87
chore(refactor): configure mention in rich text extension
max-nextcloud Jun 20, 2025
c4f863f
chore(types): collaborationCursor extension to typescript
max-nextcloud Jun 20, 2025
354ad41
chore(simplify): rely on updateUser command
max-nextcloud Jun 20, 2025
022e483
chore(simplify): replace computed fileExtension with temp
max-nextcloud Jun 20, 2025
0809c8e
enh(code): start to load syntax highlighting during setup
max-nextcloud Jun 20, 2025
b326875
chore(refactor): load editor in mounted
max-nextcloud Jun 20, 2025
3e5cfd8
chore(refactor): create editor in created instead of mounted
max-nextcloud Jun 26, 2025
ab26032
chore(refactor): create ydoc in setup
max-nextcloud Jun 26, 2025
2831462
fix(character-count): always provide the current editors doc
max-nextcloud Jun 26, 2025
d7df4c0
fix(character-count): use the NcActionTexts name prop
max-nextcloud Jun 26, 2025
e24d90a
fix(loading): only show main container when content loaded
max-nextcloud Jun 26, 2025
0d5f4f7
fix(mention): use shallowRef for connection
max-nextcloud Jun 27, 2025
057a682
fix(load): create initial YjsState with dir
max-nextcloud Jun 27, 2025
fae62fc
chore(refactor): extract useEditor into its own file
max-nextcloud Jun 28, 2025
17f0d62
chore(refactor): extract useEditorFlags into its own file
max-nextcloud Jun 28, 2025
90b1d84
test(RichTextReader): basic test
max-nextcloud Jun 28, 2025
fabc79e
test(RichTextReader): update content
max-nextcloud Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
enh(editor): store session in separate extension
The `Session` extension provides the `setSession` and `clearSession` commands.
Session data is made available via `editor.storage.session`.

This way session data can be provided after initializing the editor.

Signed-off-by: Max <[email protected]>
  • Loading branch information
max-nextcloud committed Jun 28, 2025
commit 8b4e6359d0829a0e0a505b44caed85184c228c81
6 changes: 2 additions & 4 deletions src/EditorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const editorProps = {
scrollThreshold: 50,
}

const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded = false } = {}) => {
const createRichEditor = ({ extensions = [], relativePath, isEmbedded = false } = {}) => {
return new Editor({
editorProps,
extensions: [
Expand All @@ -47,9 +47,7 @@ const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded =
component: this,
extensions: [
Mention.configure({
suggestion: MentionSuggestion({
session,
}),
suggestion: MentionSuggestion()
}),
],
}),
Expand Down
5 changes: 4 additions & 1 deletion src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { fetchNode } from '../services/WebdavClient.ts'
import SuggestionsBar from './SuggestionsBar.vue'
import { useDelayedFlag } from './Editor/useDelayedFlag.ts'
import { useEditorMethods } from '../composables/useEditorMethods.ts'
import { Session } from '../extensions/Session.ts'

export default {
name: 'Editor',
Expand Down Expand Up @@ -598,21 +599,23 @@ export default {
clientId: this.$ydoc.clientID,
},
}),
Session,
]
if (this.isRichEditor) {
this.editor = createRichEditor({
relativePath: this.relativePath,
session,
extensions,
isEmbedded: this.isEmbedded,
})
this.listenEditorEvents()
this.editor.commands.setSession(this.currentSession)
} else {
const language =
extensionHighlight[this.fileExtension] || this.fileExtension
loadSyntaxHighlight(language).then(() => {
this.editor = createPlainEditor({ language, extensions })
this.listenEditorEvents()
this.editor.commands.setSession(this.currentSession)
})
}
},
Expand Down
33 changes: 16 additions & 17 deletions src/components/Suggestion/Mention/suggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,41 @@ const USERS_LIST_ENDPOINT_URL = generateUrl('apps/text/api/v1/users')

const emitMention = ({ session, props }) => {
const documentId = session.documentId
if (!session.documentId) {
// TODO: emit the mention on reconnect
console.warn('Disconnected. Could not notify user about mention.', { user: props.id })
return
}
axios.put(generateUrl(`apps/text/session/${documentId}/mention`), {
documentId,
sessionId: session.id,
sessionToken: session.token,
...session,
mention: props.id,
scope: window.location,
})
}

export default ({ session, params }) => createSuggestions({
export default ({ params } = {}) => createSuggestions({
listComponent: MentionList,
items: async ({ query }) => {
items: async ({ editor, query }) => {
const session = editor.storage.session
if (!session.documentId) {
// looks like we're not connected right now.
return []
}
const params = {
documentId: session.documentId,
sessionId: session.id,
sessionToken: session.token,
...session,
filter: query,
}
const response = await axios.post(USERS_LIST_ENDPOINT_URL, params)
const users = JSON.parse(JSON.stringify(response.data))
const result = []

Object.keys(users).map(key => result.push({
id: key,
label: users[key],
}))

return result
return Object.entries(users).map(([id, label]) => ({ id, label }))
},

command: ({ editor, range, props }) => {
if (params?.emitMention) {
params.emitMention({ props })
} else {
emitMention({
session,
session: editor.storage.session,
props,
})
}
Expand Down
55 changes: 55 additions & 0 deletions src/extensions/Session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { Extension } from '@tiptap/core'

declare module '@tiptap/core' {
interface Commands<ReturnType> {
session: {
setSession: (session: {
documentId: number
id: number
token: string
}) => ReturnType
clearSession: () => ReturnType
}
}
}

interface StoredSession {
documentId: number
sessionId: number
sessionToken: string
}

const emptySession = {
documentId: 0,
sessionId: 0,
sessionToken: '',
}

export const Session = Extension.create({
name: 'session',

addStorage(): StoredSession {
return { ...emptySession }
},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh no... this will require tiptap v3 as previously the extension storage is a singleton and therefor used by all loaded editor instances at the same time. ( See ueberdosis/tiptap#6060 )


addCommands() {
return {
setSession: (session) => () => {
this.storage.documentId = session.documentId
this.storage.sessionId = session.id
this.storage.sessionToken = session.token
return true
},
clearSession:
() =>
({ commands }) => {
return commands.setSession({ documentId: 0, id: 0, token: '' })
},
}
},
})
48 changes: 48 additions & 0 deletions src/tests/extensions/Session.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, test as baseTest } from 'vitest'
import createCustomEditor from '../testHelpers/createCustomEditor'
import { Session } from '../../extensions/Session'
import type { Editor } from '@tiptap/core'

interface EditorFixture {
editor: Editor
}

const test = baseTest.extend<EditorFixture>({
editor: async ({ task: _ }, use) => {
const editor = createCustomEditor('', [Session])
await use(editor)
editor.destroy()
}
})

test('start with empty session', ({ editor }) => {
expect(editor.storage.session).toEqual({
"documentId": 0,
"sessionId": 0,
"sessionToken": "",
})
})

test('set a session', ({ editor }) => {
editor.commands.setSession({ documentId: 123, id: 456, token: 'myToken'})
expect(editor.storage.session).toEqual({
"documentId": 123,
"sessionId": 456,
"sessionToken": "myToken",
})
})

test('clear the session', ({ editor }) => {
editor.commands.setSession({ documentId: 123, id: 456, token: 'myToken'})
editor.commands.clearSession()
expect(editor.storage.session).toEqual({
"documentId": 0,
"sessionId": 0,
"sessionToken": "",
})
})