All checks were successful
CD / Build and push images (push) Successful in 2m58s
CI / Lint, typecheck, test (push) Successful in 1m55s
CI / Auth e2e pack (push) Successful in 2m25s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 11s
Revoking write access must terminate live sessions and let a user with pending offline edits export them rather than lose them silently. Backend (generic, reused by M5 grants #53): - packages/shared: POND_ACCESS_CHANGED_CHANNEL, the LISTEN/NOTIFY channel shared by api and collab. - api: PondAccessNotifier emits pg_notify(pond_access_changed, pondId) on a permission-relevant change; the single generic seam for revocation. Wired into pond soft-delete as the interim trigger (see==modify until #53). - collab: a dedicated-connection LISTEN listener (LISTEN is connection- bound, not pooled) that, on a notification, closes every open connection to the pond's open pages. Clients then reconnect and the api re-issues a token reflecting current access (downgrade to ro, or 403/404). Reconnects and re-LISTENs if its connection drops. Frontend: - use-collab-provider: a refused token (403/404) on (re)connect sets accessRevoked and stops the reconnect loop; exposes discardLocal. - AccessRevokedDialog: keeps local content visible and offers Markdown copy/download (derived from the live editor doc, so offline edits are included) and an explicit discard that clears IndexedDB. de+en strings. Tests: collab DB-backed integration test proves a direct NOTIFY closes a live session within seconds (AC1) and leaves unrelated ponds untouched; listener unit tests; api test asserts soft-delete fires the notifier; web test for the export Markdown derivation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
35 lines
1.1 KiB
TypeScript
35 lines
1.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { deriveMarkdownFromEditorJSON } from './derive-markdown';
|
|
|
|
describe('deriveMarkdownFromEditorJSON (issue #39 export)', () => {
|
|
it('converts a TipTap JSON document to Markdown', () => {
|
|
// Shaped like `editor.getJSON()` output, including a mark, so the export
|
|
// that a revoked user triggers reflects the local content faithfully.
|
|
const json = {
|
|
type: 'doc',
|
|
content: [
|
|
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Title' }] },
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'text', text: 'Some ' },
|
|
{ type: 'text', marks: [{ type: 'bold' }], text: 'bold' },
|
|
{ type: 'text', text: ' text.' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const markdown = deriveMarkdownFromEditorJSON(json);
|
|
|
|
expect(markdown).toContain('## Title');
|
|
expect(markdown).toContain('**bold**');
|
|
});
|
|
|
|
it('handles an empty document without throwing', () => {
|
|
const json = { type: 'doc', content: [{ type: 'paragraph' }] };
|
|
expect(() => deriveMarkdownFromEditorJSON(json)).not.toThrow();
|
|
});
|
|
});
|