Skip to content

Collaborative editing for other apps (deck) - #9062

Open
max-nextcloud wants to merge 104 commits into
mainfrom
refactor/introduce-icontext
Open

max-nextcloud wants to merge 104 commits into
mainfrom
refactor/introduce-icontext

Conversation

@max-nextcloud

@max-nextcloud max-nextcloud commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Todo

Prerequisits

Backend

  • Handle sync requests with IContext.
  • get text file in AttachmentService based on context.
  • Fix unit tests for ApiService.
  • Fix 412 resonses to push requests.
  • handle saving in IContext.
  • Address refactor/file service #9028 (comment)
  • Address refactor/file service #9028 (comment)
  • check for other requests that rely on the context.
  • Migrate existing documents.
  • Add index for context_type, context_id.
  • Have a lazy context that does not setup the filesystem unless needed.
  • always create the context on the same layer.

Frontend

  • Add context arguments to createCollaborativeEditor.
  • Pass these arguments to the CollaborativeEditor component as props.
  • Use these props to connect.
  • Provide these props when saving. We still use the documentId to save and resolve the context on the backend.
  • Look into attachment handling / attachment resolver - in particular with non-file context

Testing

  • Test with public file shares
  • Check playwright tests
  • Fix playwright
    • print test since 5cca04c shows the placeholder at the bottom
  • Fix playwright (again)
  • Fix cypress sync tests or move it to playwright.
  • Fix collectives tests

Feedback

  • Separate classes
  • specify strict types on new classes
  • double check: a call to removeInactiveSessionsWithoutSteps() within the create flow was removed, and not introduced back elsewhere.
  • change from autoincrement to snowflake ids
  • consider to mark the new event unstable or experimental (not aware of a phpdoc or internal established annotation. )

Documentation

  • document how to implement and register a Context

@max-nextcloud
max-nextcloud force-pushed the refactor/introduce-icontext branch 9 times, most recently from 3e17210 to a2260eb Compare August 17, 2026 10:44
@max-nextcloud max-nextcloud changed the title refactor/introduce icontext refactor/introduce IContext Aug 17, 2026
@max-nextcloud
max-nextcloud force-pushed the refactor/introduce-icontext branch 4 times, most recently from ae7bbf6 to 92e1890 Compare August 17, 2026 13:20
@max-nextcloud
max-nextcloud force-pushed the refactor/file-service branch 4 times, most recently from c9d35f7 to cd2d5b9 Compare August 18, 2026 18:29
@max-nextcloud
max-nextcloud force-pushed the refactor/introduce-icontext branch from b0a7b15 to 7cedf66 Compare August 18, 2026 18:50
@max-nextcloud
max-nextcloud force-pushed the refactor/file-service branch from cd2d5b9 to c4cca9f Compare August 19, 2026 04:32
@max-nextcloud
max-nextcloud force-pushed the refactor/introduce-icontext branch from 7cedf66 to 1bb5586 Compare August 19, 2026 04:34
@max-nextcloud
max-nextcloud force-pushed the refactor/file-service branch 2 times, most recently from 7232808 to 5907ff4 Compare August 19, 2026 05:35
@max-nextcloud
max-nextcloud force-pushed the refactor/introduce-icontext branch 3 times, most recently from 1bc577e to 58fe27f Compare August 19, 2026 09:00
Base automatically changed from refactor/file-service to main August 19, 2026 09:07
@blizzz

blizzz commented Aug 19, 2026

Copy link
Copy Markdown
Member

So far I read up to commit chore(tweak): return first editable file early. I love your iterative style and the discipline you keep with having small, digestible and sensible commits! My impression is also that with having the abstraction layers, there are many places where it just gets cleaner, and having specific logic better capsuled!

What I stumble over is that the IContext file contains the additional classes DocumentData, SessionInfo, NewSessionData. Usually this is something we keep separate and I am not sure it will always works with autoloading? Also about fellow developer expectations. Maybe something to consider.

Maybe this is also OK, just to double check: a call to removeInactiveSessionsWithoutSteps() within the create flow was removed, and not introduced back elsewhere. Alright if intentional of course.

No biggie: new classes miss the declare(strict_types=1); statement.

Can consider to mark the new event unstable or experimental, though I am not aware of a phpdoc or internal established annotation.

@blizzz

blizzz commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

On my dev instance I get an error during Migration:

{
  "reqId": "miQrQfvExjkhgL8Z3V6T",
  "level": 3,
  "time": "2026-08-19T14:14:45+00:00",
  "remoteAddr": "",
  "user": "--",
  "app": "updater",
  "method": "",
  "url": "--",
  "scriptName": "occ",
  "message": "\\OC\\Updater::failure: Exception: Database error when running migration 090000Date20260817110024 for app text\nAn exception occurred while executing a query: SQLSTATE[23502]: Not null violation: 7 ERROR:  column \"context_type\" of relation \"oc_text_documents\" contains null values",
  "userAgent": "--",
  "version": "35.0.0.3",
  "occ_command": [
    "occ",
    "upgrade"
  ],
  "data": {
    "app": "updater"
  }
}

DB is postgres.

Probably happens because the column is added as NOT NULL but the rows would be empty (=> NULL). Maybe set the default to "file"?

UPDATE: same with context_id. Default to 0?

max-nextcloud and others added 30 commits September 30, 2026 20:36
Signed-off-by: Max <max@nextcloud.com>
In some scenarios (MarkdownContentEditor with fileId) there is no session.
Thus we need the file id of the markdown file to access the attachments
as there may not even be a document record in the database.

Get the context inside the AttachmentController using either the session or the fileId
and then hand it on to the AttachmentService.

When getting the attachment list also adjust the urls to include the fileId when needed.

Do not return the documentId in some responses.
The caller provided it in the request already.

Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
The dirname is available both in the `checkAttachment` function
as well as when checking the files later on.

Signed-off-by: Max <max@nextcloud.com>
The loop used $fileId instead of $id, so --all was broken.

Signed-off-by: Max <max@nextcloud.com>
* Cleanup session for files without a mount (e.g. deleted).
* Without --force, only release the lock if the document has been reset.
* Catch NotPermittedException from getFileById().

Signed-off-by: Max <max@nextcloud.com>
This is the fallback for when there is no session.
So use the fileId instead.

Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
AttachmentService had a private copy of FileService::isDownloadDisabled,
and FileService::checkFileAccessFromShare repeated the share attribute
check inline.

Move the attribute check into FileService::isShareDownloadDisabled(IShare).
isDownloadDisabled(File) now uses it for internal shares, and
checkFileAccessFromShare uses it for the link share from the token.
AttachmentService now uses FileService instead of its own copy.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Signed-off-by: Max <max@nextcloud.com>
Rename to match the effect / meaning.

This was named according to an implementation detail:
Federated shares do not have an owner on the instance they were shared to
which prevents attaching files.

Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Doctrine renders the alias as `UPDATE oc_text_documents d SET ...`,
which SQLite rejects, so upgrading a SQLite instance aborted.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
resetDocument() is called with force=true, so it never throws
DocumentHasUnsavedChangesException, and it handles NotFoundException
itself. The DB and NotPermitted exceptions it can throw escaped the
event handler and turned the triggering file write into an error.

Also check isSaveFromText() before loading the document, which skips
a query on every autosave, and drop a redundant IContext check.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
NodeWrittenResetDocumentListener only handles NodeWrittenEvent now.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
The migration creates a unique index, so the AddIndex attribute should
say so.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
resetDocument() refuses to reset a document with unsaved steps. Unlocking
before it left the file unlocked while those sessions kept editing.
Only unlock once the reset went through.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
cleanupAttachments() resolves the context id as a file id. For other
context types, such as deck cards, this could hit an unrelated markdown
file and delete its unreferenced attachments.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Public endpoints build the context from the document's type and pass the
share as auth. Only the file context ties a share to its document, so a
valid share token could reach documents of other types. Reject shares for
non-file contexts in ContextManager, which every public path uses.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
CollaborativeEditor no longer has a fileId prop, so the download
callback never found the editor to save and unsaved changes were
missing from the download. Match on the file context instead.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
The props spread from CollaborativeEditor.props is loosely typed, so
vue-tsc did not see the now required context prop being passed.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Makes sure that vue files get type checked as well.

Signed-off-by: Jonas <jonas@freesources.org>
NodeWrittenResetDocumentListener unlocked the file on every write,
including Text's autosave, so the file stayed unlocked for the rest
of the editing session.

Release the lock through IContext::cleanup() once the document was
actually reset, as the reset ends all editing sessions.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Push and the attachment endpoints let NotFoundException and
NotPermittedException escape when the context became inaccessible.
Map them in SessionMiddleware for all session aware controllers.

Closing the last session no longer fails when the context cleanup does,
as the session is already closed at that point.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
If the outside content could not be loaded for the conflict response,
autosave stored the new checksum and overwrote the outside change.
Skip the save instead and keep the old checksum, so the next save
raises the conflict again.

Also skip the save when the file is locked while autosave reads it,
instead of failing with a 500.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
Typed defineProps made both booleans required, so Viewer logged missing
prop warnings for files it renders without them, like the preloaded
previous and next file. They defaulted to false before.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 🧭 Planning evaluation (don't pick)

Development

Successfully merging this pull request may close these issues.

Collaborative editing for other apps (deck, tables, etc)

4 participants