@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
112 lines (91 loc) • 6.35 kB
Markdown
# Synced Blocks Plugin — Developer Agent Guide
> **For workflow guidance, debugging, and cross-package task guides, load the `synced-blocks`
> skill:**
> `get_skill(skill_name_or_path="platform/packages/editor/.agents/skills/synced-blocks/SKILL.md")`
## Quick Context
**Synced Blocks** lets users create reusable content blocks (source) that can be referenced across
Confluence pages and Jira issue descriptions. This package is the core editor plugin — it registers
ADF nodes, toolbar/menu integration, commands, and ProseMirror plugins.
**Two ADF node types:**
- `bodiedSyncBlock` — **Source** sync block (contains the editable content)
- `syncBlock` — **Reference** sync block (renders content fetched from Block Service)
## Plugin Internals (`src/`)
```
src/
├── index.ts # Re-exports plugin + type
├── syncedBlockPlugin.tsx # Top-level: registers nodes, commands, UI, pm-plugins
├── syncedBlockPluginType.ts # TypeScript interfaces for options, shared state, dependencies
├── editor-actions/
│ └── index.ts # flushBodiedSyncBlocks, flushSyncBlocks,
│ discardUnpublishedSyncBlocks (EDITOR-6473)
├── editor-commands/
│ └── index.ts # createSyncedBlock, copySyncedBlockReferenceToClipboardEditorCommand,
│ copySyncedBlockReferenceToClipboard, editSyncedBlockSource,
│ removeSyncedBlock, removeSyncedBlockAtPos, unsync
├── nodeviews/
│ ├── syncedBlock.tsx # NodeView for reference (syncBlock) — read-only, fetches from BE
│ ├── lazySyncedBlock.tsx # Lazy-loaded wrapper for syncedBlock (EDITOR-6928)
│ └── bodiedSyncedBlock.tsx # NodeView for source (bodiedSyncBlock) — nested editor with content
├── pm-plugins/
│ ├── main.ts # Core state machine: lifecycle, creation, deletion, cache,
│ │ status decoration apply path
│ ├── menu-and-toolbar-experiences.ts # Experience tracking for menu/toolbar interactions
│ └── utils/
│ ├── track-sync-blocks.ts # Tracks mutations, updates shared state
│ ├── handle-bodied-sync-block-creation.ts # Creation flow, local cache, retry logic
│ ├── handle-bodied-sync-block-removal.ts # Deletion flow, BE synchronization
│ ├── has-synced-blocks.ts # O(childCount) presence check (EDITOR-6928 lazy init)
│ ├── transaction-inserts-synced-block.ts # Detect tr inserts a synced block (lazy init)
│ ├── selection-decorations.ts # Selection decoration helpers
│ ├── ignore-dom-event.ts # DOM event guard
│ └── utils.ts # Misc shared helpers
├── ui/ # (grep the dir for the full current list — it grows often)
│ ├── toolbar-components.tsx # Primary toolbar button ("Create Synced Block")
│ ├── CreateSyncedBlockButton.tsx / CreateSyncedBlockDropdownItem.tsx # Toolbar/menu entry points
│ ├── floating-toolbar.tsx # Node-level actions: delete, unsync, copy link, view locations
│ ├── block-menu-components.tsx # Block menu entry
│ ├── quick-insert.tsx # Slash command / quick insert config
│ ├── SyncedLocationDropdown.tsx # "View synced locations" dropdown
│ ├── DeleteConfirmationModal.tsx # Deletion confirmation dialog
│ ├── SyncBlockRefresher.tsx # Periodic data refresh from backend
│ ├── SyncBlockLabel.tsx # Source/reference label chrome
│ ├── SyncBlockRendererWrapper.tsx # Node-view wrapper
│ ├── SyncBlockSSRReactContextsProvider.tsx # Supplies React contexts during SSR
│ └── Flag.tsx # Error/info flags (offline, copy notifications)
└── types/
└── index.ts # FLAG_ID, SyncedBlockSharedState, BodiedSyncBlockDeletionStatus
```
### Editor Actions
This package exposes top-level **editor actions** (in `editor-actions/index.ts`) that products call
from outside the plugin lifecycle:
- `flushBodiedSyncBlocks(store)` — flush all dirty source blocks
- `flushSyncBlocks(store)` — flush reference manager (e.g. on save)
- `discardUnpublishedSyncBlocks(store)` — delete unpublished blocks on cancel (added in EDITOR-6473;
used by Confluence's editor cancel flow)
### Lazy Init & Perf (EDITOR-6928 / EDITOR-6930)
`main.ts`:
- Skips creating synced-block plugin state and node-views for documents with no synced blocks
(`hasSyncedBlocks(doc)`).
- Computes `statusDecorationSet` inside `apply()` and stores it on plugin state, then exposes it via
an O(1) `decorations` prop instead of an O(n) `doc.descendants()` walk on every transaction.
- Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1) pending-creation early return
in `buildStatusDecorations()`.
### Key Code Patterns
**Creating a sync block** (flow through the code):
1. User triggers via toolbar/block menu/slash command → `ui/toolbar-components.tsx` or
`ui/block-menu-components.tsx`
2. Calls `editor-commands/createSyncedBlock` → inserts `bodiedSyncBlock` node into document (marked
as **pending creation** — it is not persisted yet)
3. `pm-plugins/main.ts` detects new node → `handle-bodied-sync-block-creation.ts` updates
`sourceManager` state. Persistence happens later when the product layer calls `flush()` (via
`flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
identifiers via `commitPendingCreation()`
4. `menu-and-toolbar-experiences.ts` fires the experience event
**Reference rendering** (flow through the code):
1. `nodeviews/syncedBlock.tsx` (or `lazySyncedBlock.tsx`) mounts for each `syncBlock` node
2. Calls `referenceManager.fetchSyncBlocksData(nodes)` → batched/deduped Block Service fetch
3. Renders content via nested renderer from `editor-synced-block-renderer`
4. Subscribes via `referenceManager.subscribeToSyncBlock(...)` — AGG WebSocket (Confluence) or Relay
(Jira) — for real-time updates