mirror of
https://ghfast.top/https://github.com/discourse/discourse-shared-edits.git
synced 2026-07-15 11:17:01 +08:00
Some checks failed
Discourse Plugin / ci (push) Has been cancelled
Allow sites to choose which groups can enable or disable shared edits instead of hardcoding staff and TL4 users. Expose the permission on the current user serializer so the admin menu can hide toggle actions for unauthorized users. Add rate limits to privileged reset and recover endpoints, and update specs, acceptance tests, docs, and settings copy for the new permission model. * Update config/settings.yml Co-authored-by: Martin Brennan <martin@discourse.org> * fix spec --------- Co-authored-by: Martin Brennan <martin@discourse.org>
9.2 KiB
9.2 KiB
Discourse Shared Edits Plugin – AI Coding Agent Guide
- Always start by reading ../../AGENTS.md to understand Discourse-wide conventions.
- While working on the plugin always feel free to consult Discourse source for best practices, patterns, and utilities.
- NEVER make commits to the repo, always leave it to humans to commit the code.
Linting
- run support/lint to lint files
- run support/lint --fix to attempt fixes
Scope & Feature Flags
- Lives at
plugins/discourse-shared-edits; everything here only runs whenSiteSetting.shared_edits_enabled(defined inconfig/settings.yml) is true and the per-post custom fieldshared_edits_enabledhas been toggled viaSharedEditRevision.toggle_shared_edits!. - Guardian hook (
lib/discourse_shared_edits/guardian_extension.rb) restricts enable/disable/reset/recover endpoints to users inSiteSetting.shared_edits_toggle_allowed_groups(defaults to staff and trust level 4). Reuseguardian.ensure_can_toggle_shared_edits!for any new privileged action. Privileged endpoints must also callguardian.ensure_can_see!(@post)to prevent access to posts in restricted categories. - API routes live under
/shared_edits(plugin.rb). Do not rename them without updating the Ember service and the Pretender fixtures intest/javascripts.
Backend Architecture & Expectations
app/controllers/discourse_shared_edits/revision_controller.rbis the only HTTP surface. Every new server feature must enforcerequires_plugin,requires_login, andensure_shared_editsguards, and must return JSON (never 204 when clients expect a body). Respond withmessage_bus_last_idwhenever clients need to subscribe after fetching state.app/services/discourse_shared_edits/revise.rbowns the non-recoveryreviseorchestration (client validation +SharedEditRevision.revise!+ commit scheduling). State-vector divergence checks run insideSharedEditRevision.revise!under the post lock. Keep controller responses and rescue behavior compatible when changing this flow. Validation failures intentionally raiseStateValidator::*exceptions that are rescued in the controller.app/models/shared_edit_revision.rbstores every Yjs update. Treatrawas the authoritative, base64-encoded document snapshot andrevisionas the individual update payload. Always use the provided class methods (init!,revise!,commit!,toggle_shared_edits!,reset_history!, etc.) so Redis scheduling (ensure_will_commit+Jobs::CommitSharedRevision), message bus fan-out, editor attribution, and compaction invariants stay intact.lib/discourse_shared_edits/state_validator.rbis the gatekeeper for base64/Yjs safety,max_post_length, health reports, and corruption recovery. Any code that manipulates Yjs blobs must run through the validator helpers (or add new helpers here) so that errors surface asStateCorruptionErrorand can trigger automatic recovery.lib/discourse_shared_edits/yjs.rbwraps a sharedMiniRacer::Contextthat executes the bundledpublic/javascripts/yjs-dist.js. Never eval ad-hoc scripts elsewhere; if you need a new primitive, add it to this wrapper so both Ruby and Ember flows stay aligned on how docs are encoded.- Protocol literals are centralized in
lib/discourse_shared_edits/protocol.rb(Ruby) andassets/javascripts/discourse/lib/shared-edits/protocol.js(Ember). Reuse these constants for message actions/error codes instead of hardcoded strings. - Background commits: updates are throttled client-side, but the server still schedules
Jobs::CommitSharedRevision10 seconds out using a Redis key per post. If you change commit timing, update bothensure_will_commitand the job to avoid duplicate commits or missed flushes. - Recovery + maintenance endpoints:
health,recover, andresetall useStateValidatorand emit/shared_edits/:post_idmessage-bus resync events. When adding new maintenance operations, emit the same payload shape ({ action: "resync", version: <int> }) so the Ember service understands it. - Database: migrations live in
db/migrate. The original table creation (20200721001123_migrate_shared_edits.rb) plus the column resize (20251124000123_resize_shared_edit_columns.rb) show expectations: always providedownpaths, mark large operationsalgorithm: :concurrentlywhen indexing, and protect edits on large tables.
Frontend Architecture & Expectations
assets/javascripts/discourse/services/shared-edit-manager.jsis the heart of the client: it lazy-loads core Yjs via the URLs inlib/shared-edits/bundle-paths.js, loads the ProseMirror bundle when rich mode is active, mirrors composer text into a sharedY.Doc, throttles PUTs to/shared_edits/p/:post_id, and subscribes to/shared_edits/:post_idonmessageBus. Preserve: message payload keys (version,update,client_id,user_id,username), selection/cursor broadcasting, throttling constants (THROTTLE_SAVE,THROTTLE_SELECTION), and cleanup of DOM listeners/cursor overlays to avoid leaks.- Composer integration lives in
assets/javascripts/discourse/initializers/shared-edits-init.jsandextend-composer-service.js. Always guard new behavior withsiteSettings.shared_edits_enabled, register hooks viawithPluginApi, and respectcreatingSharedEdit/editingPostsemantics so we never leave the composer in a half-shared state. - UI pieces: the post action replacement is in
components/shared-edit-button.gjs; the composer “Done” button lives inconnectors/composer-fields-below/shared-edit-buttons.gjs; shared styles are underassets/stylesheets/common/discourse-shared-edits.scss; cursor rendering utilities are inassets/javascripts/discourse/lib/{caret-coordinates,cursor-overlay}.js. Keep strings translatable (shared_edits.*keys exist on both client and server locales). - Asset bundling:
public/javascripts/yjs-dist.js(core Yjs) andpublic/javascripts/yjs-prosemirror.js(y-prosemirror plugins) are generated viabin/rake shared_edits:yjs:build(lib/tasks/yjs.rakewrapspnpm exec esbuild …). The build also copies hashed variants (e.g.yjs-dist-<sha>.js) and updatesassets/javascripts/discourse/lib/shared-edits/bundle-paths.jsso the client always loads cache-busted URLs. Never hand-edit bundled files; re-bundle wheneveryjs/y-prosemirrorchanges and commit the new artifacts.
Testing, Linting & Tooling
- Ruby specs cover validators, model behavior, controller endpoints, and basic system flows. Run
bin/rspec plugins/discourse-shared-edits/spec/<area>(requiresLOAD_PLUGINS=1when running outside the full suite).spec/systemrelies on page objects; avoid raw Capybara finders for new tests. - Ember acceptance tests live at
plugins/discourse-shared-edits/test/javascripts/acceptance. Execute them withbin/qunit plugins/discourse-shared-edits/test/javascripts/acceptance/composer-test.js(or the directory to run them all). - Lint every file you touch:
bin/lint plugins/discourse-shared-edits/<path>for Ruby/JS/SCSS andpnpm --filter discourse-shared-edits lintif you need the plugin-level configs frompackage.json. Stylelint and template lint configs already live alongside the plugin—respect them when adding files. - Node tooling: the plugin pins Node ≥ 22 and pnpm 9 (
package.json). Usepnpm installinside the plugin when you add JS dependencies so lockfiles stay inplugins/discourse-shared-edits/pnpm-lock.yaml.
Operational Tips & Utilities
- Manual QA:
plugins/discourse-shared-edits/support/fake_writeruses Playwright to simulate concurrent editors. Runsupport/fake_writer POST_ID --speed=fast --headless=falseagainst a dev instance to reproduce race conditions before shipping protocol changes. - Message bus hygiene:
SharedEditRevision::MESSAGE_BUS_MAX_BACKLOG_*caps backlog size/age. Keep any new channels under the same limits or we risk unbounded Redis usage. - Edit reasons:
SharedEditRevision.update_edit_reasonbuildsshared_edits.reasonstrings listing everyone who contributed between commits. Editor usernames are stored structurally in theshared_edits_editor_usernamesJSON post custom field (registered inplugin.rb) rather than parsed from the localized edit reason string. If you change commit batching or editor attribution, update both the method and translations. - Recovery workflow: corruption is surfaced in logs and bubbled to the client via
state_recovered/state_corruptederror codes. When adding new error states, expose translated messaging inconfig/locales/client.*and wire them into the composer UI. - Selection sharing: the Ember service currently attempts to PUT
/shared_edits/p/:post_id/selection. The endpoint is not implemented yet, so requests are best-effort and errors are ignored; reuse that route if you decide to ship cursor/selection sync so the client code does not need changing. - Knowledge sharing: keep this file current whenever you add new entry points, commands, or conventions. After completing any task that touches this plugin, spawn a review agent to compare your diff against
plugins/discourse-shared-edits/AGENTS.mdand confirm the instructions remain accurate. - Tip: Run
plugins/discourse-shared-edits/support/lintfrom the repo root (add--fix/-fto trigger auto-fix variants) to execute the full GitHub lint suite without guessing binaries.