mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
refactor(plugins): rename document-mode to doc-mode
Renames the bundled plugin's id (`document-mode` → `doc-mode`), display name (`Document Mode (Alpha)` → `Doc Mode (Alpha)`), package name, directory, and asset path. No migration: the plugin has never been published, so there is no on-disk user data to preserve. Touched everywhere the id was hardcoded: - Plugin manifest, package.json, build/deploy/test scripts, log prefixes - Host BUNDLED_PLUGIN_PATHS entry (src/app/plugins/plugin.service.ts) - Host comments and test fixtures (plugin-hooks, plugin-persistence-key, plugin-persistence.model, conflict-resolution.service.spec) - E2E spec filenames + PLUGIN_ID constant + label assertions - build-all.js plugin orchestrator - build/release-notes.md user-facing entry Test pass: 88/88 plugin specs, 8/8 plugin-hooks, 132/132 conflict-resolution, 15/15 plugin-persistence-key.util. Bundle redeployed to src/assets/bundled-plugins/doc-mode (gitignored). The old `src/assets/bundled-plugins/document-mode/` dir is gitignored and left on disk after this commit; the host loader no longer references it, so it's inert. Remove with `rm -rf src/assets/bundled-plugins/document-mode`.
This commit is contained in:
parent
0df783a669
commit
3443bcacd0
32 changed files with 56 additions and 59 deletions
|
|
@ -33,6 +33,6 @@ For all current downloads, package links, and platform-specific notes: [check th
|
|||
### Plugins & Advanced
|
||||
|
||||
- Added plugin support for work-context header buttons, an embed slot, the WORK_CONTEXT_CHANGE hook, and iframe-only installs.
|
||||
- Added a TipTap-based document-mode plugin and reduced redundant synced chip data.
|
||||
- Added a TipTap-based doc-mode plugin and reduced redundant synced chip data.
|
||||
- Added distribution-target suffixes to Electron version strings.
|
||||
- Fixed SuperSync Caddy healthchecks, PostgreSQL connection headroom, Docker sync package inclusion, and the F-Droid build dependency issue.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import {
|
|||
} from '../../helpers/plugin-test.helpers';
|
||||
|
||||
// Smoke test for the BUNDLED_PLUGIN_PATHS entry added in commit 199e816479.
|
||||
// A typo or accidental removal of 'assets/bundled-plugins/document-mode'
|
||||
// A typo or accidental removal of 'assets/bundled-plugins/doc-mode'
|
||||
// would silently regress this; the test fails loudly instead.
|
||||
test.describe('Document Mode bundled plugin', () => {
|
||||
test.describe('Doc Mode bundled plugin', () => {
|
||||
test('appears in plugin management list', async ({ page, workViewPage }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ test.describe('Document Mode bundled plugin', () => {
|
|||
const pluginReady = await waitForPluginManagementInit(page);
|
||||
expect(pluginReady).toBeTruthy();
|
||||
|
||||
// Manifest declares the user-visible name as "Document Mode (Alpha)".
|
||||
// Manifest declares the user-visible name as "Doc Mode (Alpha)".
|
||||
// Match on the prefix so a future Alpha→Beta label change doesn't break.
|
||||
const titles = await page.evaluate(() => {
|
||||
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
|
||||
|
|
@ -35,7 +35,7 @@ test.describe('Document Mode bundled plugin', () => {
|
|||
.filter(Boolean);
|
||||
});
|
||||
|
||||
const hasDocumentMode = titles.some((t) => t.startsWith('Document Mode'));
|
||||
expect(hasDocumentMode).toBeTruthy();
|
||||
const hasDocMode = titles.some((t) => t.startsWith('Doc Mode'));
|
||||
expect(hasDocMode).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -7,8 +7,8 @@ import {
|
|||
|
||||
/**
|
||||
* End-to-end coverage of the Stage A keyed persistence migration in the
|
||||
* bundled document-mode plugin. The migration logic is unit-tested against
|
||||
* a mock PluginAPI (`packages/plugin-dev/document-mode/src/persistence.spec.ts`),
|
||||
* bundled doc-mode plugin. The migration logic is unit-tested against
|
||||
* a mock PluginAPI (`packages/plugin-dev/doc-mode/src/persistence.spec.ts`),
|
||||
* but those tests can't catch real-iframe quirks: postMessage handling of
|
||||
* `undefined` second args, commit-chain timing across the host's rate
|
||||
* limiter, hydration ordering against the op-log.
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
* `doc:${ctxId}` keyed entries and tombstones the legacy id.
|
||||
*/
|
||||
|
||||
const PLUGIN_ID = 'document-mode';
|
||||
const PLUGIN_ID = 'doc-mode';
|
||||
// Underscore-cased so the @typescript-eslint/naming-convention rule on
|
||||
// object literal keys is happy. The keys are opaque context ids; their
|
||||
// shape doesn't matter to the host.
|
||||
|
|
@ -79,7 +79,7 @@ const findEntry = (
|
|||
id: string,
|
||||
): PluginUserDataEntry | undefined => entries?.find((e) => e.id === id);
|
||||
|
||||
test.describe('Document Mode Stage A migration', () => {
|
||||
test.describe('Doc Mode Stage A migration', () => {
|
||||
test('stamps migration success on a fresh install', async ({ page, workViewPage }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ test.describe('Document Mode Stage A migration', () => {
|
|||
const pluginReady = await waitForPluginManagementInit(page);
|
||||
expect(pluginReady).toBeTruthy();
|
||||
|
||||
const enabled = await enablePluginWithVerification(page, 'Document Mode');
|
||||
const enabled = await enablePluginWithVerification(page, 'Doc Mode');
|
||||
expect(enabled).toBeTruthy();
|
||||
|
||||
// The migration runs once the plugin's background.js executes init().
|
||||
|
|
@ -169,7 +169,7 @@ test.describe('Document Mode Stage A migration', () => {
|
|||
const pluginReady = await waitForPluginManagementInit(page);
|
||||
expect(pluginReady).toBeTruthy();
|
||||
|
||||
const enabled = await enablePluginWithVerification(page, 'Document Mode');
|
||||
const enabled = await enablePluginWithVerification(page, 'Doc Mode');
|
||||
expect(enabled).toBeTruthy();
|
||||
|
||||
// Wait for the migration to stamp success. The full sequence is:
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "document-mode",
|
||||
"name": "doc-mode",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "document-mode",
|
||||
"name": "doc-mode",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@super-productivity/plugin-api": "file:../../plugin-api",
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "document-mode",
|
||||
"name": "doc-mode",
|
||||
"version": "0.1.0",
|
||||
"description": "Document-style editor for projects and Today using TipTap",
|
||||
"private": true,
|
||||
|
|
@ -9,7 +9,7 @@ const SRC_DIR = path.join(ROOT_DIR, 'src');
|
|||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
async function buildPlugin() {
|
||||
console.log('Building document-mode plugin with esbuild...');
|
||||
console.log('Building doc-mode plugin with esbuild...');
|
||||
|
||||
if (fs.existsSync(DIST_DIR)) {
|
||||
fs.rmSync(DIST_DIR, { recursive: true });
|
||||
|
|
@ -13,7 +13,7 @@ const TARGET_DIR = path.join(
|
|||
'src',
|
||||
'assets',
|
||||
'bundled-plugins',
|
||||
'document-mode',
|
||||
'doc-mode',
|
||||
);
|
||||
|
||||
// editor.js is inlined into index.html, so it doesn't need to be deployed.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/**
|
||||
* Unit-test runner for the document-mode plugin.
|
||||
* Unit-test runner for the doc-mode plugin.
|
||||
*
|
||||
* The plugin has no browser-based test setup — its testable logic is the pure
|
||||
* `doc-transform` module. This script transpiles every `*.spec.ts` under
|
||||
|
|
@ -54,7 +54,7 @@ const init = async (): Promise<void> => {
|
|||
try {
|
||||
await migrateToKeyedPersistence(PluginAPI);
|
||||
} catch (err) {
|
||||
PluginAPI.log.err('document-mode: migration failed', err);
|
||||
PluginAPI.log.err('doc-mode: migration failed', err);
|
||||
// Continue anyway: a partial migration leaves the data in place
|
||||
// (corruption guard in persistence.ts) and the next session will retry.
|
||||
}
|
||||
|
|
@ -94,12 +94,12 @@ const onButtonClick = async (ctx: ActiveWorkContext): Promise<void> => {
|
|||
return [...set];
|
||||
});
|
||||
} catch (err) {
|
||||
PluginAPI.log.err('document-mode: failed to persist toggle', err);
|
||||
PluginAPI.log.err('doc-mode: failed to persist toggle', err);
|
||||
}
|
||||
};
|
||||
|
||||
PluginAPI.registerWorkContextHeaderButton({
|
||||
label: 'Document Mode',
|
||||
label: 'Doc Mode',
|
||||
icon: 'description',
|
||||
showFor: ['PROJECT', 'TODAY'],
|
||||
onClick: (ctx) => {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Pure document-transform helpers for Document Mode.
|
||||
* Pure document-transform helpers for Doc Mode.
|
||||
*
|
||||
* Everything here operates on plain ProseMirror-JSON (`PMNode`) and a task
|
||||
* lookup function — no TipTap editor, no DOM, no module-global cache. That
|
||||
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "document-mode",
|
||||
"id": "doc-mode",
|
||||
"manifestVersion": 1,
|
||||
"name": "Document Mode (Alpha)",
|
||||
"name": "Doc Mode (Alpha)",
|
||||
"version": "0.1.0",
|
||||
"description": "Document-style editor for projects and Today, powered by TipTap.",
|
||||
"author": "SuperProductivity",
|
||||
|
|
@ -48,7 +48,7 @@ export const reconcileEnabledIds = async (
|
|||
try {
|
||||
next = new Set(await loadEnabledCtxIds(api));
|
||||
} catch (err) {
|
||||
api.log.err('document-mode: enabled-ids reload failed', err);
|
||||
api.log.err('doc-mode: enabled-ids reload failed', err);
|
||||
return { next: new Set(prev), action: 'noop' };
|
||||
}
|
||||
// Skip the visibility reconcile when membership is identical — most fires
|
||||
|
|
@ -69,7 +69,7 @@ export const reconcileEnabledIds = async (
|
|||
const ctx = await api.getActiveWorkContext();
|
||||
activeId = ctx?.id ?? null;
|
||||
} catch (err) {
|
||||
api.log.err('document-mode: active-ctx read failed', err);
|
||||
api.log.err('doc-mode: active-ctx read failed', err);
|
||||
return { next, action: 'noop' };
|
||||
}
|
||||
if (activeId === null) return { next, action: 'noop' };
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Pure top-level-navigation helpers for the Document Mode editor.
|
||||
* Pure top-level-navigation helpers for the Doc Mode editor.
|
||||
*
|
||||
* Every function here operates on a `DocLike` — the minimal structural
|
||||
* slice of a ProseMirror document node that the drag / move / keyboard
|
||||
|
|
@ -113,7 +113,7 @@ const logErr = (msg: string, err?: unknown): void => {
|
|||
// ignore — fall through to console
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[document-mode]', msg, err);
|
||||
console.error('[doc-mode]', msg, err);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1910,7 +1910,7 @@ const mount = async (): Promise<void> => {
|
|||
try {
|
||||
await migrateToKeyedPersistence(PluginAPI);
|
||||
} catch (err) {
|
||||
logErr('document-mode: migration failed', err);
|
||||
logErr('doc-mode: migration failed', err);
|
||||
}
|
||||
updateDocStatusBanner();
|
||||
const initialCtx = await PluginAPI.getActiveWorkContext();
|
||||
|
|
@ -2149,7 +2149,7 @@ const waitForPluginAPI = (): Promise<void> =>
|
|||
if (attempts >= MAX_ATTEMPTS) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'[document-mode] PluginAPI not injected after',
|
||||
'[doc-mode] PluginAPI not injected after',
|
||||
MAX_ATTEMPTS * INTERVAL_MS,
|
||||
'ms — giving up',
|
||||
);
|
||||
|
|
@ -2173,7 +2173,7 @@ void waitForPluginAPI()
|
|||
const msg = document.createElement('div');
|
||||
msg.className = 'doc-error-state';
|
||||
msg.textContent =
|
||||
'Document Mode could not connect to Super Productivity. ' +
|
||||
'Doc Mode could not connect to Super Productivity. ' +
|
||||
'Try closing and reopening this panel.';
|
||||
root.appendChild(msg);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Inline SVG icons for Document Mode. Kept as a static path map so the
|
||||
* Inline SVG icons for Doc Mode. Kept as a static path map so the
|
||||
* iframe renders icons offline — no Material Icons web font, no Google
|
||||
* Fonts request. Paths are the standard Material Symbols 24×24 outlines.
|
||||
*/
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Document Mode</title>
|
||||
<title>Doc Mode</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
|
@ -347,15 +347,15 @@ const plugins = [
|
|||
},
|
||||
},
|
||||
{
|
||||
name: 'document-mode',
|
||||
path: 'document-mode',
|
||||
name: 'doc-mode',
|
||||
path: 'doc-mode',
|
||||
needsInstall: true,
|
||||
copyToAssets: true,
|
||||
buildCommand: async (pluginPath) => {
|
||||
await execAsync(`cd ${pluginPath} && npm run build`);
|
||||
const targetDir = path.join(
|
||||
__dirname,
|
||||
'../../../src/assets/bundled-plugins/document-mode',
|
||||
'../../../src/assets/bundled-plugins/doc-mode',
|
||||
);
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -1151,7 +1151,7 @@ describe('ConflictResolutionService', () => {
|
|||
const conflicts: EntityConflict[] = [
|
||||
{
|
||||
entityType: 'PLUGIN_USER_DATA',
|
||||
entityId: 'document-mode',
|
||||
entityId: 'doc-mode',
|
||||
localOps: [
|
||||
{
|
||||
id: 'local-plugin-data',
|
||||
|
|
@ -1159,8 +1159,8 @@ describe('ConflictResolutionService', () => {
|
|||
actionType: '[Plugin] Upsert User Data' as ActionType,
|
||||
opType: OpType.Update,
|
||||
entityType: 'PLUGIN_USER_DATA',
|
||||
entityId: 'document-mode',
|
||||
payload: { id: 'document-mode', data: 'local-blob' },
|
||||
entityId: 'doc-mode',
|
||||
payload: { id: 'doc-mode', data: 'local-blob' },
|
||||
vectorClock: { clientA: 1 },
|
||||
timestamp: now - 500,
|
||||
schemaVersion: 1,
|
||||
|
|
@ -1173,8 +1173,8 @@ describe('ConflictResolutionService', () => {
|
|||
actionType: '[Plugin] Upsert User Data' as ActionType,
|
||||
opType: OpType.Update,
|
||||
entityType: 'PLUGIN_USER_DATA',
|
||||
entityId: 'document-mode',
|
||||
payload: { id: 'document-mode', data: 'remote-blob' },
|
||||
entityId: 'doc-mode',
|
||||
payload: { id: 'doc-mode', data: 'remote-blob' },
|
||||
vectorClock: { clientB: 1 },
|
||||
timestamp: now,
|
||||
schemaVersion: 1,
|
||||
|
|
|
|||
|
|
@ -110,27 +110,24 @@ describe('PluginHooksService.dispatchHookToPlugin', () => {
|
|||
});
|
||||
|
||||
it('fires every handler when a plugin registers more than once for the same hook', async () => {
|
||||
// document-mode registers from both its background script and its iframe
|
||||
// doc-mode registers from both its background script and its iframe
|
||||
// editor under the same pluginId — pre-fix the second registration
|
||||
// silently overwrote the first, so iframe registration permanently
|
||||
// shadowed background's enabledIds-reconcile handler.
|
||||
const bgHandler = jasmine.createSpy('bgHandler');
|
||||
const iframeHandler = jasmine.createSpy('iframeHandler');
|
||||
service.registerHookHandler(
|
||||
'document-mode',
|
||||
'doc-mode',
|
||||
PluginHooks.PERSISTED_DATA_CHANGED,
|
||||
bgHandler,
|
||||
);
|
||||
service.registerHookHandler(
|
||||
'document-mode',
|
||||
'doc-mode',
|
||||
PluginHooks.PERSISTED_DATA_CHANGED,
|
||||
iframeHandler,
|
||||
);
|
||||
|
||||
await service.dispatchHookToPlugin(
|
||||
'document-mode',
|
||||
PluginHooks.PERSISTED_DATA_CHANGED,
|
||||
);
|
||||
await service.dispatchHookToPlugin('doc-mode', PluginHooks.PERSISTED_DATA_CHANGED);
|
||||
|
||||
expect(bgHandler).toHaveBeenCalledTimes(1);
|
||||
expect(iframeHandler).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -143,23 +140,23 @@ describe('PluginHooksService.dispatchHookToPlugin', () => {
|
|||
const otherPlugin = jasmine.createSpy('otherPlugin');
|
||||
|
||||
service.registerHookHandler(
|
||||
'document-mode',
|
||||
'doc-mode',
|
||||
PluginHooks.PERSISTED_DATA_CHANGED,
|
||||
dataChanged,
|
||||
);
|
||||
service.registerHookHandler(
|
||||
'document-mode',
|
||||
'doc-mode',
|
||||
PluginHooks.PERSISTED_DATA_CHANGED,
|
||||
dataChanged2,
|
||||
);
|
||||
service.registerHookHandler('document-mode', PluginHooks.TASK_COMPLETE, taskComplete);
|
||||
// Sibling plugin's handler must survive a teardown targeting document-mode.
|
||||
service.registerHookHandler('doc-mode', PluginHooks.TASK_COMPLETE, taskComplete);
|
||||
// Sibling plugin's handler must survive a teardown targeting doc-mode.
|
||||
service.registerHookHandler('other', PluginHooks.TASK_COMPLETE, otherPlugin);
|
||||
|
||||
service.unregisterPluginHooks('document-mode');
|
||||
service.unregisterPluginHooks('doc-mode');
|
||||
|
||||
return Promise.all([
|
||||
service.dispatchHookToPlugin('document-mode', PluginHooks.PERSISTED_DATA_CHANGED),
|
||||
service.dispatchHookToPlugin('doc-mode', PluginHooks.PERSISTED_DATA_CHANGED),
|
||||
service.dispatchHook(PluginHooks.TASK_COMPLETE),
|
||||
]).then(() => {
|
||||
expect(dataChanged).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { PluginLog } from '../core/log';
|
|||
* Each handler has a 5s timeout to prevent hung handlers from blocking others.
|
||||
*
|
||||
* A plugin may register multiple handlers for the same hook under one
|
||||
* pluginId — for example, document-mode registers from both its background
|
||||
* pluginId — for example, doc-mode registers from both its background
|
||||
* script (host page) and its iframe editor. All registered handlers fire on
|
||||
* dispatch.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Maximum size of a single plugin-persistence write, in bytes (256 KB).
|
||||
*
|
||||
* Sized for the realistic upper bound of plugin payloads — a heavy
|
||||
* document-mode TipTap doc is ~30–100 KB, plugin configs are KB-scale,
|
||||
* doc-mode TipTap doc is ~30–100 KB, plugin configs are KB-scale,
|
||||
* automation rules / AI prompts are KB-scale. 256 KB gives several×
|
||||
* headroom while keeping the per-plugin storage growth bounded:
|
||||
* a misbehaving plugin with N keyed entries can still hold N × 256 KB,
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
* The pre-Stage-A cap was 1 MB because one blob held every context's
|
||||
* data; with the keyed split each context gets its own entry, so the
|
||||
* per-write budget can shrink. Legacy migrations skip individual docs
|
||||
* that exceed this (see `document-mode/src/persistence.ts`).
|
||||
* that exceed this (see `doc-mode/src/persistence.ts`).
|
||||
*/
|
||||
export const MAX_PLUGIN_DATA_SIZE = 256 * 1024; // 256 KB
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const BUNDLED_PLUGIN_PATHS = [
|
|||
'assets/bundled-plugins/voice-reminder',
|
||||
'assets/bundled-plugins/google-calendar-provider',
|
||||
'assets/bundled-plugins/caldav-calendar-provider',
|
||||
'assets/bundled-plugins/document-mode',
|
||||
'assets/bundled-plugins/doc-mode',
|
||||
] as const;
|
||||
|
||||
@Injectable({
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const extractOwnerPluginId = (entityId: string): string => {
|
|||
|
||||
/**
|
||||
* Bound on a single plugin's persistence key length. Generous for any
|
||||
* realistic per-plugin keyspace (e.g. document-mode uses `doc:<uuid>`,
|
||||
* realistic per-plugin keyspace (e.g. doc-mode uses `doc:<uuid>`,
|
||||
* well under 100 chars). Prevents a compromised iframe from passing a
|
||||
* multi-megabyte `key` that would be stored verbatim in NgRx state,
|
||||
* IndexedDB, the op-log, and on the sync wire — bypassing the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue