@cosmicdrift/kumiko-samples 0.362.0 → 0.363.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +17 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
- package/packages/bundled-features/src/delivery/changes.json +48 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +17 -2
- package/packages/bundled-features/src/delivery/jobs.ts +48 -5
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +28 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +63 -7
- package/packages/bundled-features/src/template-resolver/api.ts +83 -8
- package/packages/bundled-features/src/template-resolver/changes.json +118 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +100 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +14 -0
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
- package/samples/apps/ui-walkthrough/package.json +2 -0
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
|
|
4
|
+
export type DocumentToolbarBlock =
|
|
5
|
+
| "image"
|
|
6
|
+
| "table"
|
|
7
|
+
| "button"
|
|
8
|
+
| "divider"
|
|
9
|
+
| "repeat"
|
|
10
|
+
| "pageBreak"
|
|
11
|
+
| "align";
|
|
12
|
+
|
|
13
|
+
/** Imperative access for an outside panel (placeholder list, resource list). */
|
|
14
|
+
export type DocumentEditorHandle = {
|
|
15
|
+
readonly insertPlaceholder: (name: string) => void;
|
|
16
|
+
readonly insertResource: (key: string, alt?: string) => void;
|
|
17
|
+
readonly focus: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type DocumentResourcePreview = {
|
|
21
|
+
/** Shown as the image source in the editor only; never written to the document. */
|
|
22
|
+
readonly src: string;
|
|
23
|
+
readonly caption?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** "default" keeps the compact bordered editor; "canvas" renders a flat text
|
|
27
|
+
* toolbar and a paper-like page with block labels. */
|
|
28
|
+
export type DocumentEditorChrome = "default" | "canvas";
|
|
29
|
+
|
|
30
|
+
export type DocumentEditorProps = ContentEditorProps & {
|
|
31
|
+
readonly blocks?: readonly DocumentToolbarBlock[];
|
|
32
|
+
/** Display label per variable name, as inserted (list item fields use the
|
|
33
|
+
* dotted form, e.g. "lines.description"). A chip shows label and name. */
|
|
34
|
+
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
35
|
+
/** Variable names rendered as system chips (a different tint). */
|
|
36
|
+
readonly systemVariables?: readonly string[];
|
|
37
|
+
/** Image previews by resource key; resources without one show a key placeholder. */
|
|
38
|
+
readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
|
|
39
|
+
readonly chrome?: DocumentEditorChrome;
|
|
40
|
+
/** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
|
|
41
|
+
readonly toolbar?: "full" | "compact";
|
|
42
|
+
/** Called with the handle once the editor exists and with null when it is destroyed. */
|
|
43
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const MAIL_DOCUMENT_BLOCKS = [
|
|
47
|
+
"image",
|
|
48
|
+
"table",
|
|
49
|
+
"button",
|
|
50
|
+
"divider",
|
|
51
|
+
"repeat",
|
|
52
|
+
] as const satisfies readonly DocumentToolbarBlock[];
|
|
53
|
+
|
|
54
|
+
export const PDF_DOCUMENT_BLOCKS = [
|
|
55
|
+
"align",
|
|
56
|
+
"image",
|
|
57
|
+
"table",
|
|
58
|
+
"divider",
|
|
59
|
+
"repeat",
|
|
60
|
+
"pageBreak",
|
|
61
|
+
] as const satisfies readonly DocumentToolbarBlock[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import { isDocumentReferenceName, isDocumentResourceKey } from "../document-schema.js";
|
|
4
|
+
import type { DocumentEditorHandle } from "./document-toolbar-blocks.js";
|
|
5
|
+
|
|
6
|
+
export function createEditorHandle(editor: Editor): DocumentEditorHandle {
|
|
7
|
+
return {
|
|
8
|
+
insertPlaceholder: (name) => {
|
|
9
|
+
// skip: an invalid variable name is not insertable
|
|
10
|
+
if (!isDocumentReferenceName(name)) return;
|
|
11
|
+
editor.chain().focus().insertContent({ type: "variable", attrs: { name } }).run();
|
|
12
|
+
},
|
|
13
|
+
insertResource: (key, alt) => {
|
|
14
|
+
// skip: an invalid resource key is not insertable
|
|
15
|
+
if (!isDocumentResourceKey(key)) return;
|
|
16
|
+
editor
|
|
17
|
+
.chain()
|
|
18
|
+
.focus()
|
|
19
|
+
.insertContent({ type: "resource", attrs: { resource: key, alt: alt ?? null } })
|
|
20
|
+
.run();
|
|
21
|
+
},
|
|
22
|
+
focus: () => {
|
|
23
|
+
editor.commands.focus();
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -9,12 +9,112 @@ import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
|
9
9
|
|
|
10
10
|
export const defaultTranslations: TranslationsByLocale = {
|
|
11
11
|
en: {
|
|
12
|
+
"template-resolver.documentEditor.button": "Button",
|
|
13
|
+
"template-resolver.documentEditor.buttonDefaultText": "Confirm",
|
|
14
|
+
"template-resolver.documentEditor.buttonTarget": "Button target",
|
|
15
|
+
"template-resolver.documentEditor.buttonText": "Button text",
|
|
16
|
+
"template-resolver.documentEditor.cancel": "Cancel",
|
|
17
|
+
"template-resolver.documentEditor.divider": "Divider",
|
|
18
|
+
"template-resolver.documentEditor.heading3": "Heading 3",
|
|
19
|
+
"template-resolver.documentEditor.image": "Image",
|
|
20
|
+
"template-resolver.documentEditor.imageAlt": "Alt text (optional)",
|
|
21
|
+
"template-resolver.documentEditor.insert": "Insert",
|
|
22
|
+
"template-resolver.documentEditor.insertPlaceholder": "Placeholder",
|
|
23
|
+
"template-resolver.documentEditor.invalidImageAlt": "The alt text must not contain < or >.",
|
|
24
|
+
"template-resolver.documentEditor.invalidLink":
|
|
25
|
+
"This link target is not allowed. Use https, http, mailto, tel or a placeholder.",
|
|
26
|
+
"template-resolver.documentEditor.invalidResourceKey": "This resource key is not valid.",
|
|
27
|
+
"template-resolver.documentEditor.link": "Link",
|
|
28
|
+
"template-resolver.documentEditor.linkHref": "Link target",
|
|
29
|
+
"template-resolver.documentEditor.linkHrefPlaceholder": "https://",
|
|
30
|
+
"template-resolver.documentEditor.noVariables": "No matching variables are available here.",
|
|
31
|
+
"template-resolver.documentEditor.pageBreak": "Page break",
|
|
32
|
+
"template-resolver.documentEditor.align": "Align",
|
|
33
|
+
"template-resolver.documentEditor.alignLeft": "Left",
|
|
34
|
+
"template-resolver.documentEditor.alignCenter": "Centered",
|
|
35
|
+
"template-resolver.documentEditor.alignRight": "Right",
|
|
36
|
+
"template-resolver.documentEditor.alignJustify": "Justified",
|
|
37
|
+
"template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
|
|
38
|
+
"template-resolver.documentEditor.repeatHeader": "Repeat header row",
|
|
39
|
+
"template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
|
|
40
|
+
"template-resolver.documentEditor.paragraph": "Paragraph",
|
|
41
|
+
"template-resolver.documentEditor.paragraphStyle": "Paragraph style",
|
|
42
|
+
"template-resolver.documentEditor.placeholder": "Write your text…",
|
|
43
|
+
"template-resolver.documentEditor.removeLink": "Remove link",
|
|
44
|
+
"template-resolver.documentEditor.repeat": "Repeat",
|
|
45
|
+
"template-resolver.documentEditor.repeatList": "List to repeat",
|
|
46
|
+
"template-resolver.documentEditor.resourceKey": "Resource key",
|
|
47
|
+
"template-resolver.documentEditor.table": "Table",
|
|
12
48
|
"template-resolver.editor.titleLabel": "Title",
|
|
13
49
|
"template-resolver.editor.contentLabel": "Content",
|
|
14
50
|
"template-resolver.editor.save": "Save",
|
|
15
51
|
"template-resolver.editor.saving": "Saving…",
|
|
16
52
|
"template-resolver.editor.created": "Created.",
|
|
17
53
|
"template-resolver.editor.saved": "Saved.",
|
|
54
|
+
"templateResolver.errors.document.buttonHrefNotPlaceholder":
|
|
55
|
+
"The button target “{detail}” must be a placeholder.",
|
|
56
|
+
"templateResolver.errors.document.disallowedLinkHref":
|
|
57
|
+
"Link target “{detail}” is not allowed; use https, http, mailto, tel or a placeholder.",
|
|
58
|
+
"templateResolver.errors.document.documentRequiresHtmlFormat":
|
|
59
|
+
"This template kind requires the html format, got “{detail}”.",
|
|
60
|
+
"templateResolver.errors.document.layoutRequiresMjmlFormat":
|
|
61
|
+
"Mail layouts require the mjml format, got “{detail}”.",
|
|
62
|
+
"templateResolver.errors.document.layoutInvalidStructure":
|
|
63
|
+
"The layout may only contain the header, content, signature and footer regions: {detail}.",
|
|
64
|
+
"templateResolver.errors.document.layoutMissingContentSlot":
|
|
65
|
+
"The layout needs its content slot: {detail}.",
|
|
66
|
+
"templateResolver.errors.document.layoutSourceSystemOnly":
|
|
67
|
+
"Only system layouts may use {detail} source; use the structured html layout.",
|
|
68
|
+
"templateResolver.errors.template.targetExists":
|
|
69
|
+
"A template for locale “{detail}” already exists; overwrite it explicitly to replace it.",
|
|
70
|
+
"templateResolver.warnings.resourceNotUploaded": "Image not uploaded: {detail}",
|
|
71
|
+
"templateResolver.checks.resourcesResolved": "All images are available",
|
|
72
|
+
"templateResolver.checks.textPresent": "A plain-text version is present",
|
|
73
|
+
"templateResolver.checks.sizeUnderCap": "The mail is within the 10 MiB size limit",
|
|
74
|
+
"templateResolver.checks.requiredPlaceholders": "All required placeholders are used",
|
|
75
|
+
"templateResolver.errors.preview.renderFailed": "The preview could not be rendered.",
|
|
76
|
+
"templateResolver.errors.testMail.noChannel":
|
|
77
|
+
"No email channel is set up, so no test mail can be sent.",
|
|
78
|
+
"templateResolver.errors.testMail.noAddress":
|
|
79
|
+
"Your account has no email address to send the test mail to.",
|
|
80
|
+
"templateResolver.errors.testMail.failed": "The test mail could not be sent.",
|
|
81
|
+
"templateResolver.errors.resource.tooLarge": "The image is too large.",
|
|
82
|
+
"templateResolver.errors.resource.unsupportedImage":
|
|
83
|
+
"This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
|
|
84
|
+
"templateResolver.errors.template.systemResourcesNeedSystemTenant":
|
|
85
|
+
"Resource files can only be imported as system templates by a user of the system tenant.",
|
|
86
|
+
"templateResolver.errors.collection.kindNotAllowed":
|
|
87
|
+
"“{detail}” templates are written through their own editor, not through a collection.",
|
|
88
|
+
"templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
|
|
89
|
+
"templateResolver.errors.template.invalidMeta": "Invalid template settings: {detail}",
|
|
90
|
+
"templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
|
|
91
|
+
"templateResolver.errors.template.missingRequiredPlaceholders":
|
|
92
|
+
"Required placeholders are not used: {detail}",
|
|
93
|
+
"templateResolver.errors.document.embeddedContent":
|
|
94
|
+
"<{detail}> elements are not allowed in template bodies.",
|
|
95
|
+
"templateResolver.errors.document.unsafeAttributeValue":
|
|
96
|
+
"The attribute value “{detail}” must not contain < or >.",
|
|
97
|
+
"templateResolver.errors.document.eventHandlerAttribute":
|
|
98
|
+
"Inline event handler “{detail}” is not allowed.",
|
|
99
|
+
"templateResolver.errors.document.externalImageSource":
|
|
100
|
+
"Images must reference an uploaded resource; image source “{detail}” is not allowed.",
|
|
101
|
+
"templateResolver.errors.document.imageWithoutResource":
|
|
102
|
+
"Images must reference an uploaded resource.",
|
|
103
|
+
"templateResolver.errors.document.invalidReferenceName": "“{detail}” is not a valid name.",
|
|
104
|
+
"templateResolver.errors.document.scriptElement":
|
|
105
|
+
"<script> elements are not allowed in template bodies.",
|
|
106
|
+
"templateResolver.errors.attachment.unsupportedType":
|
|
107
|
+
"Attachments must be a PDF, PNG or JPEG file.",
|
|
108
|
+
"templateResolver.errors.attachment.limit": "A template can have at most {max} attachments.",
|
|
109
|
+
"templateResolver.errors.attachment.invalid":
|
|
110
|
+
"The attachment needs a template, a key and a valid file name.",
|
|
111
|
+
"templateResolver.errors.attachment.pdfTemplateMissing":
|
|
112
|
+
"There is no PDF template “{slug}” for this tenant or the system.",
|
|
113
|
+
"template-resolver.documentEditor.htmlSource": "HTML source",
|
|
114
|
+
"template-resolver.documentEditor.removeBlock": "Remove block",
|
|
115
|
+
"template-resolver.documentEditor.selectedBlock": "Selected block",
|
|
116
|
+
"template-resolver.documentEditor.showHtml": "HTML",
|
|
117
|
+
"template-resolver.documentEditor.underline": "Underline",
|
|
18
118
|
"template-resolver.editor.saveFailed": "Save failed.",
|
|
19
119
|
"template-resolver.editor.networkError": "Network error while saving.",
|
|
20
120
|
"template-resolver.editor.loading": "Loading current version…",
|
|
@@ -5,4 +5,23 @@
|
|
|
5
5
|
// `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
|
|
6
6
|
// DOM deps.
|
|
7
7
|
|
|
8
|
+
export {
|
|
9
|
+
TemplateResolverErrors,
|
|
10
|
+
TemplateResolverHandlers,
|
|
11
|
+
TemplateResolverQueries,
|
|
12
|
+
} from "../qualified-names.js";
|
|
8
13
|
export { textBlocksClient } from "./client-plugin.js";
|
|
14
|
+
export { DocumentContentEditor } from "./document-editor.js";
|
|
15
|
+
export {
|
|
16
|
+
type DocumentEditorChrome,
|
|
17
|
+
type DocumentEditorHandle,
|
|
18
|
+
type DocumentEditorProps,
|
|
19
|
+
type DocumentResourcePreview,
|
|
20
|
+
type DocumentToolbarBlock,
|
|
21
|
+
MAIL_DOCUMENT_BLOCKS,
|
|
22
|
+
PDF_DOCUMENT_BLOCKS,
|
|
23
|
+
} from "./document-toolbar-blocks.js";
|
|
24
|
+
export {
|
|
25
|
+
PlaceholderLineEditor,
|
|
26
|
+
type PlaceholderLineEditorProps,
|
|
27
|
+
} from "./placeholder-line-editor-lazy.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
//
|
|
3
|
+
// Lazy boundary for the placeholder line editor: tiptap loads with the chunk,
|
|
4
|
+
// the fallback is a plain text input holding the same `{{name}}` text.
|
|
5
|
+
// Never import placeholder-line-editor statically here.
|
|
6
|
+
|
|
7
|
+
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
8
|
+
import { lazy, type ReactNode, Suspense } from "react";
|
|
9
|
+
import type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
|
|
10
|
+
|
|
11
|
+
export type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
|
|
12
|
+
|
|
13
|
+
const LazyPlaceholderLineEditor = lazy(() => import("./placeholder-line-editor.js"));
|
|
14
|
+
|
|
15
|
+
function PlainLineFallback({
|
|
16
|
+
id,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
readOnly = false,
|
|
20
|
+
"aria-label": ariaLabel,
|
|
21
|
+
}: PlaceholderLineEditorProps): ReactNode {
|
|
22
|
+
const { Field, Input } = usePrimitives();
|
|
23
|
+
return (
|
|
24
|
+
<Field id={id} label={ariaLabel} hideLabel>
|
|
25
|
+
<Input kind="text" id={id} name={id} value={value} onChange={onChange} disabled={readOnly} />
|
|
26
|
+
</Field>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PlaceholderLineEditor(props: PlaceholderLineEditorProps): ReactNode {
|
|
31
|
+
return (
|
|
32
|
+
<Suspense fallback={<PlainLineFallback {...props} />}>
|
|
33
|
+
<LazyPlaceholderLineEditor {...props} />
|
|
34
|
+
</Suspense>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
//
|
|
3
|
+
// Single-line editor for plain-text fields that carry {{placeholders}}
|
|
4
|
+
// (subject, preheader). The stored value stays plain text; placeholders show
|
|
5
|
+
// as chips while editing. Lives in the same lazy chunk as the document editor.
|
|
6
|
+
|
|
7
|
+
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
|
+
import { Extension, Node } from "@tiptap/core";
|
|
9
|
+
import { UndoRedo } from "@tiptap/extensions";
|
|
10
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
11
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
12
|
+
import { type ReactNode, useMemo, useState } from "react";
|
|
13
|
+
import { DocumentVariable } from "../document-schema.js";
|
|
14
|
+
import { createVariableView } from "./document-editor-extensions.js";
|
|
15
|
+
import type { DocumentEditorHandle } from "./document-toolbar-blocks.js";
|
|
16
|
+
import { createEditorHandle } from "./editor-handle.js";
|
|
17
|
+
import { contentToLine, lineToContent, singleLine } from "./placeholder-text.js";
|
|
18
|
+
import { useStableByKey } from "./use-stable-by-key.js";
|
|
19
|
+
|
|
20
|
+
export type PlaceholderLineEditorProps = {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** Plain text with `{{name}}` placeholders. */
|
|
23
|
+
readonly value: string;
|
|
24
|
+
readonly onChange: (value: string) => void;
|
|
25
|
+
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
26
|
+
/** Placeholders shown as system chips (a different tint). */
|
|
27
|
+
readonly systemVariables?: readonly string[];
|
|
28
|
+
readonly readOnly?: boolean;
|
|
29
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
|
|
30
|
+
readonly "aria-label": string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const LineDocument = Node.create({ name: "doc", topNode: true, content: "paragraph" });
|
|
34
|
+
|
|
35
|
+
// One line only: Enter must not split the paragraph.
|
|
36
|
+
const BlockLineBreaks = Extension.create({
|
|
37
|
+
name: "blockLineBreaks",
|
|
38
|
+
addKeyboardShortcuts() {
|
|
39
|
+
return { Enter: () => true, "Shift-Enter": () => true, "Mod-Enter": () => true };
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const NO_LABELS: Readonly<Record<string, string>> = {};
|
|
44
|
+
const NO_NAMES: readonly string[] = [];
|
|
45
|
+
|
|
46
|
+
function LineEditorBody({
|
|
47
|
+
id,
|
|
48
|
+
initialValue,
|
|
49
|
+
onChange,
|
|
50
|
+
variableLabels,
|
|
51
|
+
systemVariables,
|
|
52
|
+
readOnly,
|
|
53
|
+
onEditorReady,
|
|
54
|
+
ariaLabel,
|
|
55
|
+
}: {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly initialValue: string;
|
|
58
|
+
readonly onChange: (value: string) => void;
|
|
59
|
+
readonly variableLabels: Readonly<Record<string, string>>;
|
|
60
|
+
readonly systemVariables: readonly string[];
|
|
61
|
+
readonly readOnly: boolean;
|
|
62
|
+
readonly onEditorReady: ((handle: DocumentEditorHandle) => void) | undefined;
|
|
63
|
+
readonly ariaLabel: string;
|
|
64
|
+
}): ReactNode {
|
|
65
|
+
const extensions = useMemo(
|
|
66
|
+
() => [
|
|
67
|
+
LineDocument,
|
|
68
|
+
StarterKit.configure({
|
|
69
|
+
document: false,
|
|
70
|
+
blockquote: false,
|
|
71
|
+
bold: false,
|
|
72
|
+
bulletList: false,
|
|
73
|
+
code: false,
|
|
74
|
+
codeBlock: false,
|
|
75
|
+
dropcursor: false,
|
|
76
|
+
gapcursor: false,
|
|
77
|
+
hardBreak: false,
|
|
78
|
+
heading: false,
|
|
79
|
+
horizontalRule: false,
|
|
80
|
+
italic: false,
|
|
81
|
+
link: false,
|
|
82
|
+
listItem: false,
|
|
83
|
+
listKeymap: false,
|
|
84
|
+
orderedList: false,
|
|
85
|
+
strike: false,
|
|
86
|
+
trailingNode: false,
|
|
87
|
+
underline: false,
|
|
88
|
+
undoRedo: false,
|
|
89
|
+
}),
|
|
90
|
+
DocumentVariable.extend({
|
|
91
|
+
addNodeView: () => createVariableView(variableLabels, systemVariables),
|
|
92
|
+
}),
|
|
93
|
+
UndoRedo,
|
|
94
|
+
BlockLineBreaks,
|
|
95
|
+
],
|
|
96
|
+
[variableLabels, systemVariables],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const editor = useEditor({
|
|
100
|
+
extensions,
|
|
101
|
+
content: {
|
|
102
|
+
type: "doc",
|
|
103
|
+
content: [{ type: "paragraph", content: lineToContent(initialValue) }],
|
|
104
|
+
},
|
|
105
|
+
editable: !readOnly,
|
|
106
|
+
immediatelyRender: false,
|
|
107
|
+
editorProps: {
|
|
108
|
+
attributes: { id, role: "textbox", "aria-label": ariaLabel, "aria-multiline": "false" },
|
|
109
|
+
transformPastedText: singleLine,
|
|
110
|
+
// Rich pastes would bring several paragraphs; only their text is kept.
|
|
111
|
+
transformPastedHTML: (html) => singleLine(html.replace(/<[^>]*>/g, " ")),
|
|
112
|
+
},
|
|
113
|
+
onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
|
|
114
|
+
onUpdate: ({ editor: e }) => onChange(contentToLine(e.getJSON().content?.[0]?.content)),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<EditorContent
|
|
119
|
+
editor={editor}
|
|
120
|
+
className={cn(
|
|
121
|
+
"flex min-h-9 items-center rounded-md border border-input bg-transparent px-3 text-base md:text-sm",
|
|
122
|
+
"[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none",
|
|
123
|
+
"[&_p]:m-0",
|
|
124
|
+
readOnly && "opacity-60",
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default function PlaceholderLineEditor({
|
|
131
|
+
id,
|
|
132
|
+
value,
|
|
133
|
+
onChange,
|
|
134
|
+
variableLabels = NO_LABELS,
|
|
135
|
+
systemVariables = NO_NAMES,
|
|
136
|
+
readOnly = false,
|
|
137
|
+
onEditorReady,
|
|
138
|
+
"aria-label": ariaLabel,
|
|
139
|
+
}: PlaceholderLineEditorProps): ReactNode {
|
|
140
|
+
const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
|
|
141
|
+
const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
|
|
142
|
+
|
|
143
|
+
// The text is the source of truth. Typing reports it through onChange and
|
|
144
|
+
// keeps `emitted` equal to `value`; any other value (async load, reset)
|
|
145
|
+
// remounts the editor body on the new text.
|
|
146
|
+
const [emitted, setEmitted] = useState(value);
|
|
147
|
+
const [epoch, setEpoch] = useState(0);
|
|
148
|
+
if (value !== emitted) {
|
|
149
|
+
setEmitted(value);
|
|
150
|
+
setEpoch((current) => current + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<LineEditorBody
|
|
155
|
+
key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}`}
|
|
156
|
+
id={id}
|
|
157
|
+
initialValue={value}
|
|
158
|
+
onChange={(next) => {
|
|
159
|
+
setEmitted(next);
|
|
160
|
+
onChange(next);
|
|
161
|
+
}}
|
|
162
|
+
variableLabels={stableLabels}
|
|
163
|
+
systemVariables={stableSystem}
|
|
164
|
+
readOnly={readOnly}
|
|
165
|
+
onEditorReady={onEditorReady}
|
|
166
|
+
ariaLabel={ariaLabel}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import type { JSONContent } from "@tiptap/core";
|
|
3
|
+
import { isDocumentReferenceName } from "../document-schema.js";
|
|
4
|
+
|
|
5
|
+
const PLACEHOLDER_TOKEN = /\{\{([^{}]*)\}\}/g;
|
|
6
|
+
|
|
7
|
+
// Names the schema refuses stay plain text, so a typo never becomes a chip
|
|
8
|
+
// that the server would reject.
|
|
9
|
+
export function lineToContent(text: string): JSONContent[] {
|
|
10
|
+
const content: JSONContent[] = [];
|
|
11
|
+
let cursor = 0;
|
|
12
|
+
const pushText = (value: string): void => {
|
|
13
|
+
if (value !== "") content.push({ type: "text", text: value });
|
|
14
|
+
};
|
|
15
|
+
for (const match of text.matchAll(PLACEHOLDER_TOKEN)) {
|
|
16
|
+
const name = match[1] ?? "";
|
|
17
|
+
if (!isDocumentReferenceName(name)) continue;
|
|
18
|
+
pushText(text.slice(cursor, match.index));
|
|
19
|
+
content.push({ type: "variable", attrs: { name } });
|
|
20
|
+
cursor = match.index + match[0].length;
|
|
21
|
+
}
|
|
22
|
+
pushText(text.slice(cursor));
|
|
23
|
+
return content;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function contentToLine(nodes: readonly JSONContent[] | undefined): string {
|
|
27
|
+
return (nodes ?? [])
|
|
28
|
+
.map((node) => {
|
|
29
|
+
if (node.type === "variable") return `{{${String(node.attrs?.["name"])}}}`;
|
|
30
|
+
return node.type === "text" ? (node.text ?? "") : "";
|
|
31
|
+
})
|
|
32
|
+
.join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function singleLine(text: string): string {
|
|
36
|
+
return text.replace(/\s*[\r\n]+\s*/g, " ");
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
// Callers often pass fresh object literals every render; this keeps the first
|
|
5
|
+
// value for as long as the content key is unchanged, so memoized extension
|
|
6
|
+
// lists (and the editor built from them) are not rebuilt on every render.
|
|
7
|
+
export function useStableByKey<T>(value: T, key: string): T {
|
|
8
|
+
const [stored, setStored] = useState({ key, value });
|
|
9
|
+
if (stored.key !== key) {
|
|
10
|
+
setStored({ key, value });
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
return stored.value;
|
|
14
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
9
9
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
|
+
import { errorCodeOf, isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
11
|
import * as z from "zod";
|
|
11
12
|
import { userSessionTable } from "../../sessions/index.js";
|
|
12
13
|
import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
|
|
@@ -37,7 +38,7 @@ const SORT_FIELDS = ["email", "status", "createdAt", "lastSeenAt"] as const;
|
|
|
37
38
|
type SortField = (typeof SORT_FIELDS)[number];
|
|
38
39
|
|
|
39
40
|
function isSortField(value: string): value is SortField {
|
|
40
|
-
return (SORT_FIELDS
|
|
41
|
+
return isOneOf(SORT_FIELDS, value);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Nulls (no session yet, invitation rows) sort last regardless of
|
|
@@ -141,7 +142,7 @@ export const teamListQuery = definePagedQueryHandler({
|
|
|
141
142
|
// ponytail: lean sample apps (admin-console) mount auth without
|
|
142
143
|
// sessions — store_user_sessions never gets pushed. Integration tests
|
|
143
144
|
// may seed the table without mounting the feature; prod mounts both.
|
|
144
|
-
if ((err
|
|
145
|
+
if (errorCodeOf(err) !== "42P01") throw err;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -44,7 +44,10 @@ import {
|
|
|
44
44
|
type TenantId,
|
|
45
45
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
46
46
|
import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
createDeferringNotify,
|
|
49
|
+
createLifecycleHooks,
|
|
50
|
+
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
48
51
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
49
52
|
import { assertAssignableMembershipRoles } from "./membership-roles.js";
|
|
50
53
|
import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
|
|
@@ -118,12 +121,40 @@ export type SeedTenantHooks = {
|
|
|
118
121
|
// dispatcher's sink and flush post-commit. Without a sink (fixture / plain
|
|
119
122
|
// Connection) they still fire immediately — there is no outer TX to wait for.
|
|
120
123
|
//
|
|
124
|
+
function buildSeedHookNotify(
|
|
125
|
+
context: AppContext,
|
|
126
|
+
scopedUser: SessionUser,
|
|
127
|
+
targetTenantId: TenantId,
|
|
128
|
+
notifyTiming: "deferUntilCommit" | "inline",
|
|
129
|
+
): AppContext["notify"] {
|
|
130
|
+
const notifyFactory = context._notifyFactory;
|
|
131
|
+
if (!notifyFactory) return context.notify;
|
|
132
|
+
if (notifyTiming === "inline") return notifyFactory(scopedUser, targetTenantId);
|
|
133
|
+
// Still inside the caller's write tx: delivery DB work must not wait for an app-pool slot.
|
|
134
|
+
const notifyInsideTransaction = (context._notifyInTransactionFactory ?? notifyFactory)(
|
|
135
|
+
scopedUser,
|
|
136
|
+
targetTenantId,
|
|
137
|
+
);
|
|
138
|
+
const scheduleAfterCommit = context.scheduleAfterCommit;
|
|
139
|
+
if (!scheduleAfterCommit) return notifyInsideTransaction;
|
|
140
|
+
return createDeferringNotify({
|
|
141
|
+
registry: context.registry,
|
|
142
|
+
tenantId: targetTenantId,
|
|
143
|
+
notifyAfterCommit: notifyFactory(scopedUser, targetTenantId),
|
|
144
|
+
notifyNow: notifyInsideTransaction,
|
|
145
|
+
scheduleAfterCommit,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
// `runner` comes from the caller, which already holds a declared DbRunner —
|
|
122
150
|
// a TenantDb exposes no runner back out to feature code.
|
|
123
151
|
function scopeSeedHookContext(
|
|
124
152
|
context: AppContext,
|
|
125
153
|
targetTenantId: TenantId,
|
|
126
154
|
runner: DbRunner,
|
|
155
|
+
// The afterCommit sink is snapshotted when it flushes, so a hook scheduled from inside an
|
|
156
|
+
// afterCommit-phase hook would be dropped: that phase must notify inline.
|
|
157
|
+
notifyTiming: "deferUntilCommit" | "inline",
|
|
127
158
|
): AppContext {
|
|
128
159
|
const db = createTenantDb(
|
|
129
160
|
runner,
|
|
@@ -142,10 +173,9 @@ function scopeSeedHookContext(
|
|
|
142
173
|
? { ...baseUser, tenantId: targetTenantId }
|
|
143
174
|
: undefined;
|
|
144
175
|
|
|
145
|
-
const notify =
|
|
146
|
-
context
|
|
147
|
-
|
|
148
|
-
: context.notify;
|
|
176
|
+
const notify = scopedUser
|
|
177
|
+
? buildSeedHookNotify(context, scopedUser, targetTenantId, notifyTiming)
|
|
178
|
+
: context.notify;
|
|
149
179
|
|
|
150
180
|
const features = context.effectiveFeatures as
|
|
151
181
|
| (((tenantId: TenantId) => ReadonlySet<string>) & {
|
|
@@ -188,10 +218,14 @@ export async function fireEntityPostSave(
|
|
|
188
218
|
const scopedContext =
|
|
189
219
|
target === undefined
|
|
190
220
|
? hooks.context
|
|
191
|
-
: scopeSeedHookContext(hooks.context, target.tenantId, target.db);
|
|
221
|
+
: scopeSeedHookContext(hooks.context, target.tenantId, target.db, "deferUntilCommit");
|
|
222
|
+
const afterCommitContext =
|
|
223
|
+
target === undefined
|
|
224
|
+
? hooks.context
|
|
225
|
+
: scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline");
|
|
192
226
|
await lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.inTransaction);
|
|
193
227
|
const runAfterCommit = () =>
|
|
194
|
-
lifecycle.runPostSave(pseudoType, entityData,
|
|
228
|
+
lifecycle.runPostSave(pseudoType, entityData, afterCommitContext, HookPhases.afterCommit);
|
|
195
229
|
if (scopedContext.scheduleAfterCommit) {
|
|
196
230
|
scopedContext.scheduleAfterCommit(runAfterCommit);
|
|
197
231
|
} else {
|
|
@@ -72,12 +72,13 @@ export const cancelDestructionWrite = defineWriteHandler({
|
|
|
72
72
|
payload: { cancelledBy: event.user.id },
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
const gracePeriodEnd: string | null = null;
|
|
75
76
|
return {
|
|
76
77
|
isSuccess: true as const,
|
|
77
78
|
data: {
|
|
78
79
|
tenantId,
|
|
79
80
|
status: "active" as const,
|
|
80
|
-
gracePeriodEnd
|
|
81
|
+
gracePeriodEnd,
|
|
81
82
|
},
|
|
82
83
|
};
|
|
83
84
|
},
|
|
@@ -37,12 +37,12 @@ export type UserStreamBackfillResult = {
|
|
|
37
37
|
export async function backfillUserStreamTenants(
|
|
38
38
|
db: DbConnection,
|
|
39
39
|
): Promise<UserStreamBackfillResult> {
|
|
40
|
-
const candidates =
|
|
40
|
+
const candidates = await unsafeReadRetrying<{ aggregate_id: string }>(
|
|
41
41
|
db,
|
|
42
42
|
`SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
|
|
43
43
|
WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`,
|
|
44
44
|
[SYSTEM_TENANT_ID],
|
|
45
|
-
)
|
|
45
|
+
);
|
|
46
46
|
|
|
47
47
|
let aggregatesMigrated = 0;
|
|
48
48
|
let eventsMigrated = 0;
|
|
@@ -67,13 +67,13 @@ async function migrateAggregate(db: DbConnection, aggregateId: string): Promise<
|
|
|
67
67
|
const raw = asRawClient(tx);
|
|
68
68
|
// Global id order = commit-adjacent replay order; merges a split stream
|
|
69
69
|
// (legacy tenant + SYSTEM) into one consistent version sequence.
|
|
70
|
-
const events =
|
|
70
|
+
const events = await raw.unsafe<{ id: bigint | string }>(
|
|
71
71
|
`SELECT "id" FROM "kumiko_events"
|
|
72
72
|
WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
|
|
73
73
|
ORDER BY "id" ASC
|
|
74
74
|
FOR UPDATE`,
|
|
75
75
|
[aggregateId],
|
|
76
|
-
)
|
|
76
|
+
);
|
|
77
77
|
|
|
78
78
|
// Phase 1: negative interim versions — real versions are always >= 1, so
|
|
79
79
|
// nothing can collide with events_aggregate_version_uq mid-migration.
|