@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,229 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Registry,
|
|
3
|
+
TemplateScalarVariableDef,
|
|
4
|
+
TemplateVariableDef,
|
|
5
|
+
TemplateVariables,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import type { Element as ScanElement } from "happy-dom";
|
|
8
|
+
import * as z from "zod";
|
|
9
|
+
import { withParsedDocument } from "./document-sanitizer.js";
|
|
10
|
+
import { isDocumentPlaceholderHref, isDocumentReferenceName } from "./document-schema.js";
|
|
11
|
+
import type { TemplateMeta } from "./template-meta.js";
|
|
12
|
+
|
|
13
|
+
export const SYSTEM_PLACEHOLDERS: ReadonlySet<string> = new Set([
|
|
14
|
+
"tenantName",
|
|
15
|
+
"signature",
|
|
16
|
+
"imprint",
|
|
17
|
+
"unsubscribeUrl",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const PAGE_PLACEHOLDERS = ["page", "pages"] as const;
|
|
21
|
+
const PAGED_KINDS: ReadonlySet<string> = new Set(["document-pdf", "pdf-layout"]);
|
|
22
|
+
|
|
23
|
+
export function systemPlaceholdersFor(kind: string): ReadonlySet<string> {
|
|
24
|
+
return PAGED_KINDS.has(kind)
|
|
25
|
+
? new Set([...SYSTEM_PLACEHOLDERS, ...PAGE_PLACEHOLDERS])
|
|
26
|
+
: SYSTEM_PLACEHOLDERS;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const BASE_RESOURCE_KEYS = ["logo"] as const;
|
|
30
|
+
|
|
31
|
+
export type TemplateValidation = {
|
|
32
|
+
readonly unknown: readonly string[];
|
|
33
|
+
readonly missingRequired: readonly string[];
|
|
34
|
+
readonly used: readonly string[];
|
|
35
|
+
/** data-resource keys with no uploaded file; a warning, never a blocker. */
|
|
36
|
+
readonly unknownResources: readonly string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Mirrors TemplateVariableDef: a stored or draft schema keeps its label and example, which the
|
|
40
|
+
// preview uses as sample values.
|
|
41
|
+
const variableCommon = {
|
|
42
|
+
label: z.string().optional(),
|
|
43
|
+
required: z.boolean().optional(),
|
|
44
|
+
};
|
|
45
|
+
const textLikeExample = z.string().optional();
|
|
46
|
+
|
|
47
|
+
const scalarVariableSchema = z.discriminatedUnion("type", [
|
|
48
|
+
z.object({ ...variableCommon, type: z.literal("text"), example: textLikeExample }),
|
|
49
|
+
z.object({ ...variableCommon, type: z.literal("link"), example: textLikeExample }),
|
|
50
|
+
z.object({
|
|
51
|
+
...variableCommon,
|
|
52
|
+
type: z.literal("money"),
|
|
53
|
+
example: z
|
|
54
|
+
.union([z.number(), z.object({ amount: z.number(), currency: z.string() })])
|
|
55
|
+
.optional(),
|
|
56
|
+
}),
|
|
57
|
+
z.object({ ...variableCommon, type: z.literal("date"), example: textLikeExample }),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const variableDefSchema = z.union([
|
|
61
|
+
scalarVariableSchema,
|
|
62
|
+
z.object({
|
|
63
|
+
...variableCommon,
|
|
64
|
+
type: z.literal("list"),
|
|
65
|
+
item: z.record(z.string(), scalarVariableSchema),
|
|
66
|
+
example: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// A stored variableSchema value is either a bare string (legacy "this is a text
|
|
71
|
+
// variable") or a TemplateVariableDef object; anything else is not declared and
|
|
72
|
+
// therefore shows up as an unknown placeholder instead of being guessed at.
|
|
73
|
+
function toVariableDef(raw: unknown): TemplateVariableDef | null {
|
|
74
|
+
if (typeof raw === "string") return { type: "text" };
|
|
75
|
+
const parsed = variableDefSchema.safeParse(raw);
|
|
76
|
+
return parsed.success ? parsed.data : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveTemplateVariables(
|
|
80
|
+
registry: Registry,
|
|
81
|
+
slug: string,
|
|
82
|
+
variableSchema: Readonly<Record<string, unknown>>,
|
|
83
|
+
): TemplateVariables {
|
|
84
|
+
const declared = registry.getAllNotifications().get(slug)?.variables;
|
|
85
|
+
return declared ?? variablesFromSchema(variableSchema);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function variablesFromSchema(
|
|
89
|
+
variableSchema: Readonly<Record<string, unknown>>,
|
|
90
|
+
): TemplateVariables {
|
|
91
|
+
const variables: Record<string, TemplateVariableDef> = {};
|
|
92
|
+
for (const [name, raw] of Object.entries(variableSchema)) {
|
|
93
|
+
const def = toVariableDef(raw);
|
|
94
|
+
if (def !== null) variables[name] = def;
|
|
95
|
+
}
|
|
96
|
+
return variables;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type Resolution = { readonly known: boolean; readonly used: string };
|
|
100
|
+
|
|
101
|
+
function rootName(name: string): string {
|
|
102
|
+
return name.split(".")[0] ?? name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type PlaceholderScope = {
|
|
106
|
+
readonly variables: TemplateVariables;
|
|
107
|
+
readonly systemPlaceholders: ReadonlySet<string>;
|
|
108
|
+
readonly resourceKeys: ReadonlySet<string>;
|
|
109
|
+
readonly used: Set<string>;
|
|
110
|
+
readonly unknown: Set<string>;
|
|
111
|
+
readonly unknownResources: Set<string>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function listItemFields(
|
|
115
|
+
variables: TemplateVariables,
|
|
116
|
+
listName: string | null,
|
|
117
|
+
): Readonly<Record<string, TemplateScalarVariableDef>> {
|
|
118
|
+
const def =
|
|
119
|
+
listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
|
|
120
|
+
return def?.type === "list" ? def.item : {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isKnownName(
|
|
124
|
+
name: string,
|
|
125
|
+
scope: PlaceholderScope,
|
|
126
|
+
itemFields: Readonly<Record<string, unknown>>,
|
|
127
|
+
): Resolution {
|
|
128
|
+
const { variables } = scope;
|
|
129
|
+
const root = rootName(name);
|
|
130
|
+
if (Object.hasOwn(itemFields, name) || Object.hasOwn(itemFields, root)) {
|
|
131
|
+
return { known: true, used: "" };
|
|
132
|
+
}
|
|
133
|
+
if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
|
|
134
|
+
if (Object.hasOwn(variables, name)) return { known: true, used: name };
|
|
135
|
+
if (Object.hasOwn(variables, root)) return { known: true, used: root };
|
|
136
|
+
return { known: false, used: "" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordName(
|
|
140
|
+
name: string,
|
|
141
|
+
itemFields: Readonly<Record<string, unknown>>,
|
|
142
|
+
scope: PlaceholderScope,
|
|
143
|
+
) {
|
|
144
|
+
const resolution = isKnownName(name, scope, itemFields);
|
|
145
|
+
if (!resolution.known) scope.unknown.add(name);
|
|
146
|
+
else if (resolution.used !== "") scope.used.add(resolution.used);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function enclosingEachName(element: ScanElement): string | null {
|
|
150
|
+
return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
|
|
154
|
+
withParsedDocument(body, (root) => {
|
|
155
|
+
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
156
|
+
const listName = element.getAttribute("data-each") ?? "";
|
|
157
|
+
const def = Object.hasOwn(scope.variables, listName) ? scope.variables[listName] : undefined;
|
|
158
|
+
if (def?.type === "list") scope.used.add(listName);
|
|
159
|
+
else scope.unknown.add(listName);
|
|
160
|
+
}
|
|
161
|
+
for (const element of Array.from(
|
|
162
|
+
root.querySelectorAll("span[data-variable], a[href], img[data-resource]"),
|
|
163
|
+
)) {
|
|
164
|
+
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
165
|
+
const tag = element.tagName.toLowerCase();
|
|
166
|
+
if (tag === "span") {
|
|
167
|
+
recordName(element.getAttribute("data-variable") ?? "", itemFields, scope);
|
|
168
|
+
} else if (tag === "a") {
|
|
169
|
+
const href = element.getAttribute("href") ?? "";
|
|
170
|
+
if (isDocumentPlaceholderHref(href)) recordName(href.slice(2, -2), itemFields, scope);
|
|
171
|
+
} else {
|
|
172
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
173
|
+
if (!scope.resourceKeys.has(key)) scope.unknownResources.add(key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
|
|
180
|
+
|
|
181
|
+
// Meta text supports only plain `{{name}}` / `{{name.field}}`; every other
|
|
182
|
+
// Handlebars construct is reported as unknown so it can never reach the compiler.
|
|
183
|
+
function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
|
|
184
|
+
const leftover = text.replace(HANDLEBARS_EXPRESSION, (_match, inner: string) => {
|
|
185
|
+
const name = inner.trim();
|
|
186
|
+
if (isDocumentReferenceName(name)) recordName(name, {}, scope);
|
|
187
|
+
else scope.unknown.add(`{{${inner}}}`);
|
|
188
|
+
return "";
|
|
189
|
+
});
|
|
190
|
+
if (/[{}]/.test(leftover)) scope.unknown.add("{");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function metaTexts(meta: TemplateMeta | null): readonly string[] {
|
|
194
|
+
if (meta === null || !("subject" in meta)) return [];
|
|
195
|
+
return [meta.subject, ...(meta.previewText === undefined ? [] : [meta.previewText])];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function validateTemplatePlaceholders(input: {
|
|
199
|
+
readonly kind: string;
|
|
200
|
+
readonly body: string;
|
|
201
|
+
readonly meta: TemplateMeta | null;
|
|
202
|
+
readonly variables: TemplateVariables;
|
|
203
|
+
/** data-resource keys with an uploaded file besides the built-in `logo`. */
|
|
204
|
+
readonly resourceKeys?: readonly string[];
|
|
205
|
+
}): TemplateValidation {
|
|
206
|
+
const scope: PlaceholderScope = {
|
|
207
|
+
variables: input.variables,
|
|
208
|
+
systemPlaceholders: systemPlaceholdersFor(input.kind),
|
|
209
|
+
resourceKeys: new Set([...BASE_RESOURCE_KEYS, ...(input.resourceKeys ?? [])]),
|
|
210
|
+
used: new Set(),
|
|
211
|
+
unknown: new Set(),
|
|
212
|
+
unknownResources: new Set(),
|
|
213
|
+
};
|
|
214
|
+
scanBodyPlaceholders(input.body, scope);
|
|
215
|
+
for (const text of metaTexts(input.meta)) scanMetaPlaceholders(text, scope);
|
|
216
|
+
const missingRequired = Object.entries(input.variables)
|
|
217
|
+
.filter(([name, def]) => def.required === true && !scope.used.has(name))
|
|
218
|
+
.map(([name]) => name);
|
|
219
|
+
return {
|
|
220
|
+
unknown: [...scope.unknown].sort(),
|
|
221
|
+
missingRequired: missingRequired.sort(),
|
|
222
|
+
used: [...scope.used].sort(),
|
|
223
|
+
unknownResources: [...scope.unknownResources].sort(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function hasPlaceholderProblems(validation: TemplateValidation): boolean {
|
|
228
|
+
return validation.unknown.length > 0 || validation.missingRequired.length > 0;
|
|
229
|
+
}
|
|
@@ -79,7 +79,7 @@ export async function assertConsumerHandlesNotFound(
|
|
|
79
79
|
await consumer.resolve({
|
|
80
80
|
tenantId,
|
|
81
81
|
slug,
|
|
82
|
-
kind: "mail
|
|
82
|
+
kind: "mail",
|
|
83
83
|
locale: "de",
|
|
84
84
|
});
|
|
85
85
|
} catch (e) {
|
|
@@ -105,7 +105,7 @@ export async function assertConsumerRespectsLocaleFallback(
|
|
|
105
105
|
await seedTemplate(db, {
|
|
106
106
|
tenantId: SYSTEM_TENANT_ID,
|
|
107
107
|
slug,
|
|
108
|
-
kind: "
|
|
108
|
+
kind: "document-pdf",
|
|
109
109
|
locale: FALLBACK_LOCALE,
|
|
110
110
|
scope: "system",
|
|
111
111
|
content: "conformance-fallback-content",
|
|
@@ -113,7 +113,7 @@ export async function assertConsumerRespectsLocaleFallback(
|
|
|
113
113
|
const result = await consumer.resolve({
|
|
114
114
|
tenantId,
|
|
115
115
|
slug,
|
|
116
|
-
kind: "
|
|
116
|
+
kind: "document-pdf",
|
|
117
117
|
locale: "tr",
|
|
118
118
|
});
|
|
119
119
|
if (result.locale !== FALLBACK_LOCALE) {
|
|
@@ -143,7 +143,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
|
|
|
143
143
|
await seedTemplate(db, {
|
|
144
144
|
tenantId: SYSTEM_TENANT_ID,
|
|
145
145
|
slug,
|
|
146
|
-
kind: "mail
|
|
146
|
+
kind: "mail",
|
|
147
147
|
locale: "de",
|
|
148
148
|
scope: "system",
|
|
149
149
|
linkedResources: { logo: "file_missing" },
|
|
@@ -151,7 +151,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
|
|
|
151
151
|
const template = await consumer.resolve({
|
|
152
152
|
tenantId,
|
|
153
153
|
slug,
|
|
154
|
-
kind: "mail
|
|
154
|
+
kind: "mail",
|
|
155
155
|
locale: "de",
|
|
156
156
|
});
|
|
157
157
|
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
TemplateResolverHandlers,
|
|
41
41
|
TemplateResolverQueries,
|
|
42
42
|
} from "../qualified-names.js";
|
|
43
|
+
import { DocumentContentEditor } from "./document-editor.js";
|
|
43
44
|
import { defaultTranslations } from "./i18n.js";
|
|
44
45
|
|
|
45
46
|
// Exported for the unit test — groupBlocksByFolder is a pure function.
|
|
@@ -256,21 +257,22 @@ type TextBlock = {
|
|
|
256
257
|
readonly modifiedAt: string | null;
|
|
257
258
|
};
|
|
258
259
|
|
|
259
|
-
// The collection's declared contentFormat ("plain" | "rich" | "markdown"
|
|
260
|
-
// see ContentCollectionDefinition.contentFormat) uses a different vocabulary
|
|
260
|
+
// The collection's declared contentFormat ("plain" | "rich" | "markdown" |
|
|
261
|
+
// "document", see ContentCollectionDefinition.contentFormat) uses a different vocabulary
|
|
261
262
|
// than the stored entry's contentFormat ("plain" | "html" | "markdown" |
|
|
262
|
-
// "mjml", see CONTENT_FORMATS) — "rich" always
|
|
263
|
+
// "mjml", see CONTENT_FORMATS) — "rich" and "document" always store HTML. Without this,
|
|
263
264
|
// handleSave omitting contentFormat let the write schema's
|
|
264
265
|
// contentFormatSchema.default("markdown") silently stamp every save as
|
|
265
266
|
// markdown, clobbering e.g. a "plain" entry. Undefined (no collection, or
|
|
266
267
|
// the collection didn't declare a format) keeps whatever was already saved
|
|
267
268
|
// instead of guessing.
|
|
268
|
-
|
|
269
|
+
// Exported for the unit test — resolveWriteContentFormat is a pure function.
|
|
270
|
+
export function resolveWriteContentFormat(
|
|
269
271
|
collectionContentFormat: string | undefined,
|
|
270
272
|
loadedContentFormat: string | undefined,
|
|
271
273
|
): string | undefined {
|
|
272
274
|
if (collectionContentFormat === "plain") return "plain";
|
|
273
|
-
if (collectionContentFormat === "rich") return "html";
|
|
275
|
+
if (collectionContentFormat === "rich" || collectionContentFormat === "document") return "html";
|
|
274
276
|
if (collectionContentFormat === "markdown") return "markdown";
|
|
275
277
|
return loadedContentFormat;
|
|
276
278
|
}
|
|
@@ -499,12 +501,13 @@ export function textBlocksClient(opts?: {
|
|
|
499
501
|
resolvers: {
|
|
500
502
|
"template-resolver:edit": TextBlockEditor,
|
|
501
503
|
},
|
|
502
|
-
// The default for every "plain" collection (ai-prompt, mail
|
|
504
|
+
// The default for every "plain" collection (ai-prompt, mail without
|
|
503
505
|
// an explicit rich override) — an app-registered "plain" editor on
|
|
504
506
|
// another clientFeature still wins, last-wins same as columnRenderers.
|
|
505
507
|
contentEditors: {
|
|
506
508
|
plain: PlainContentEditor,
|
|
507
509
|
rich: RichContentEditor,
|
|
510
|
+
document: DocumentContentEditor,
|
|
508
511
|
// ponytail: same textarea+chips as "plain" — markdown is stored as
|
|
509
512
|
// text either way. Shares PlainContentEditor's fixed-DOM-id ceiling
|
|
510
513
|
// (see its own file comment); swap for a live-preview widget or a
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import type { Extensions, NodeViewRenderer } from "@tiptap/core";
|
|
3
|
+
import { ListKeymap } from "@tiptap/extension-list";
|
|
4
|
+
import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
|
|
5
|
+
import {
|
|
6
|
+
DocumentButton,
|
|
7
|
+
DocumentPageBreak,
|
|
8
|
+
DocumentResource,
|
|
9
|
+
DocumentVariable,
|
|
10
|
+
documentExtensions,
|
|
11
|
+
} from "../document-schema.js";
|
|
12
|
+
import type { DocumentResourcePreview } from "./document-toolbar-blocks.js";
|
|
13
|
+
|
|
14
|
+
export type DocumentEditorLabels = {
|
|
15
|
+
readonly placeholder: string;
|
|
16
|
+
readonly image: string;
|
|
17
|
+
readonly pageBreak: string;
|
|
18
|
+
readonly button?: string;
|
|
19
|
+
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
20
|
+
readonly systemVariables?: readonly string[];
|
|
21
|
+
readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
|
|
22
|
+
/** Canvas chrome labels every block with its type. */
|
|
23
|
+
readonly blockLabels?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function createElementWithClass<K extends keyof HTMLElementTagNameMap>(
|
|
27
|
+
tag: K,
|
|
28
|
+
className: string,
|
|
29
|
+
text?: string,
|
|
30
|
+
): HTMLElementTagNameMap[K] {
|
|
31
|
+
const element = document.createElement(tag);
|
|
32
|
+
element.className = className;
|
|
33
|
+
if (text !== undefined) element.textContent = text;
|
|
34
|
+
return element;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CHIP_CLASS = "mx-0.5 inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-xs";
|
|
38
|
+
const USER_CHIP_CLASS = `${CHIP_CLASS} border-primary/30 bg-primary/10 text-primary`;
|
|
39
|
+
const SYSTEM_CHIP_CLASS = `${CHIP_CLASS} border-input bg-muted text-muted-foreground`;
|
|
40
|
+
|
|
41
|
+
// The chip is display only: the stored node carries just the name, so a
|
|
42
|
+
// label never reaches the document.
|
|
43
|
+
export function createVariableView(
|
|
44
|
+
variableLabels: Readonly<Record<string, string>>,
|
|
45
|
+
systemVariables: readonly string[],
|
|
46
|
+
): NodeViewRenderer {
|
|
47
|
+
return ({ node }) => {
|
|
48
|
+
const name = String(node.attrs["name"]);
|
|
49
|
+
const label = Object.hasOwn(variableLabels, name) ? variableLabels[name] : undefined;
|
|
50
|
+
const dom = createElementWithClass(
|
|
51
|
+
"span",
|
|
52
|
+
systemVariables.includes(name) ? SYSTEM_CHIP_CLASS : USER_CHIP_CLASS,
|
|
53
|
+
);
|
|
54
|
+
dom.contentEditable = "false";
|
|
55
|
+
dom.append(document.createTextNode(label !== undefined && label !== "" ? label : name));
|
|
56
|
+
if (label !== undefined && label !== "" && label !== name) {
|
|
57
|
+
dom.append(createElementWithClass("small", "font-mono text-[10px] opacity-75", name));
|
|
58
|
+
}
|
|
59
|
+
return { dom };
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Same rule the sanitizer's image handling follows: previews never carry a
|
|
64
|
+
// scriptable scheme.
|
|
65
|
+
const PREVIEW_SRC = /^(data:image\/(png|jpeg|gif|webp);|blob:|https:\/\/|\/)/;
|
|
66
|
+
|
|
67
|
+
function createBlockLabel(text: string): HTMLSpanElement {
|
|
68
|
+
const label = createElementWithClass(
|
|
69
|
+
"span",
|
|
70
|
+
"absolute -top-2 left-2 bg-card px-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground",
|
|
71
|
+
text,
|
|
72
|
+
);
|
|
73
|
+
label.contentEditable = "false";
|
|
74
|
+
return label;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createResourceView(labels: DocumentEditorLabels): NodeViewRenderer {
|
|
78
|
+
return ({ node }) => {
|
|
79
|
+
const key = String(node.attrs["resource"]);
|
|
80
|
+
const previews = labels.resourcePreviews ?? {};
|
|
81
|
+
const preview = Object.hasOwn(previews, key) ? previews[key] : undefined;
|
|
82
|
+
const dom = createElementWithClass(
|
|
83
|
+
"div",
|
|
84
|
+
"relative my-3 flex items-center gap-3 rounded border border-dashed border-input px-3 py-3 text-sm text-muted-foreground",
|
|
85
|
+
);
|
|
86
|
+
dom.contentEditable = "false";
|
|
87
|
+
if (labels.blockLabels) dom.append(createBlockLabel(labels.image));
|
|
88
|
+
if (preview !== undefined && PREVIEW_SRC.test(preview.src)) {
|
|
89
|
+
const image = createElementWithClass("img", "max-h-16 max-w-48 rounded");
|
|
90
|
+
image.src = preview.src;
|
|
91
|
+
image.alt = typeof node.attrs["alt"] === "string" ? node.attrs["alt"] : key;
|
|
92
|
+
dom.append(image);
|
|
93
|
+
dom.append(createElementWithClass("span", "text-xs", preview.caption ?? key));
|
|
94
|
+
} else {
|
|
95
|
+
dom.append(
|
|
96
|
+
createElementWithClass("span", "font-medium", labels.image),
|
|
97
|
+
createElementWithClass("code", "text-xs", key),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { dom };
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createButtonView(buttonLabel: string): NodeViewRenderer {
|
|
105
|
+
return () => {
|
|
106
|
+
const dom = createElementWithClass(
|
|
107
|
+
"div",
|
|
108
|
+
"relative my-3 rounded border border-dashed border-input p-3",
|
|
109
|
+
);
|
|
110
|
+
const button = document.createElement("a");
|
|
111
|
+
button.setAttribute("data-button", "");
|
|
112
|
+
dom.append(createBlockLabel(buttonLabel), button);
|
|
113
|
+
return { dom, contentDOM: button };
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createPageBreakView(pageBreakLabel: string): NodeViewRenderer {
|
|
118
|
+
return () => {
|
|
119
|
+
const dom = createElementWithClass(
|
|
120
|
+
"div",
|
|
121
|
+
"my-3 border-t border-dashed border-input pt-1 text-center text-xs text-muted-foreground",
|
|
122
|
+
pageBreakLabel,
|
|
123
|
+
);
|
|
124
|
+
dom.contentEditable = "false";
|
|
125
|
+
return { dom };
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Replaced by name, not appended: the schema must keep exactly one node per
|
|
130
|
+
// name, and only the editor view differs from the shared document schema.
|
|
131
|
+
export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Extensions {
|
|
132
|
+
const base = documentExtensions.map((extension) => {
|
|
133
|
+
switch (extension.name) {
|
|
134
|
+
case DocumentVariable.name:
|
|
135
|
+
return DocumentVariable.extend({
|
|
136
|
+
addNodeView: () =>
|
|
137
|
+
createVariableView(labels.variableLabels ?? {}, labels.systemVariables ?? []),
|
|
138
|
+
});
|
|
139
|
+
case DocumentResource.name:
|
|
140
|
+
return DocumentResource.extend({ addNodeView: () => createResourceView(labels) });
|
|
141
|
+
case DocumentButton.name:
|
|
142
|
+
return labels.blockLabels
|
|
143
|
+
? DocumentButton.extend({ addNodeView: () => createButtonView(labels.button ?? "") })
|
|
144
|
+
: extension;
|
|
145
|
+
case DocumentPageBreak.name:
|
|
146
|
+
return DocumentPageBreak.extend({
|
|
147
|
+
addNodeView: () => createPageBreakView(labels.pageBreak),
|
|
148
|
+
});
|
|
149
|
+
default:
|
|
150
|
+
return extension;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// No TrailingNode: it appends an empty paragraph that breaks the HTML roundtrip.
|
|
154
|
+
return [
|
|
155
|
+
...base,
|
|
156
|
+
UndoRedo,
|
|
157
|
+
Gapcursor,
|
|
158
|
+
Dropcursor,
|
|
159
|
+
ListKeymap,
|
|
160
|
+
Placeholder.configure({ placeholder: labels.placeholder }),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
//
|
|
3
|
+
// Lazy boundary: tiptap only loads when a "document" collection is rendered.
|
|
4
|
+
// Never import document-tiptap-editor or document-editor-extensions statically here.
|
|
5
|
+
|
|
6
|
+
import { TextareaContentEditor } from "@cosmicdrift/kumiko-renderer";
|
|
7
|
+
import { lazy, type ReactNode, Suspense } from "react";
|
|
8
|
+
import type { DocumentEditorProps } from "./document-toolbar-blocks.js";
|
|
9
|
+
|
|
10
|
+
const LazyDocumentTiptapEditor = lazy(() => import("./document-tiptap-editor.js"));
|
|
11
|
+
|
|
12
|
+
export function DocumentContentEditor(props: DocumentEditorProps): ReactNode {
|
|
13
|
+
return (
|
|
14
|
+
<Suspense
|
|
15
|
+
fallback={
|
|
16
|
+
<TextareaContentEditor
|
|
17
|
+
id={props.id}
|
|
18
|
+
value={props.value}
|
|
19
|
+
onChange={props.onChange}
|
|
20
|
+
variables={props.variables}
|
|
21
|
+
readOnly={props.readOnly}
|
|
22
|
+
/>
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<LazyDocumentTiptapEditor {...props} />
|
|
26
|
+
</Suspense>
|
|
27
|
+
);
|
|
28
|
+
}
|