@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 +23 -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/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -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/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- 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/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- 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-email-password/web/auth-gate.tsx +24 -5
- 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 +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- 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 +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
- 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-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- 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 +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- 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 +135 -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/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- 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 +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -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 +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- 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 +34 -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-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -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/changes.json +6 -0
- 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-access-token.ts +13 -8
- 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 +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -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 +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- 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/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- 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 +42 -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/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- 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 +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -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 +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- 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 +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -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 +135 -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 +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- 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 +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -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/pdf-constants.ts +12 -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 +34 -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 +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- 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-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -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 +176 -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/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- 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/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -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/changes.json +1 -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/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- 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 +239 -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 +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -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 +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- 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,114 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type {
|
|
3
|
+
HandlerContext,
|
|
4
|
+
SessionUser,
|
|
5
|
+
TenantId,
|
|
6
|
+
WriteResult,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
9
|
+
import { createFileRefFromBytes, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
10
|
+
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
11
|
+
import { requireForTenant } from "../shared/index.js";
|
|
12
|
+
import { executor } from "./handlers/shared.js";
|
|
13
|
+
import type { LinkedResourceEntry } from "./linked-resources.js";
|
|
14
|
+
import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
|
|
15
|
+
|
|
16
|
+
export type TemplateResourceFile = {
|
|
17
|
+
readonly fileName: string;
|
|
18
|
+
readonly contentType: string;
|
|
19
|
+
readonly bytes: Uint8Array;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type FileRefRow = {
|
|
23
|
+
readonly storageKey: string;
|
|
24
|
+
readonly fileName: string;
|
|
25
|
+
readonly mimeType: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Reads the file behind a template resource. Null when the fileRef is unknown
|
|
29
|
+
// to the caller's tenant or has no bytes.
|
|
30
|
+
export async function readTemplateResource(
|
|
31
|
+
ctx: Pick<HandlerContext, "db" | "files" | "user">,
|
|
32
|
+
fileRefId: string,
|
|
33
|
+
): Promise<TemplateResourceFile | null> {
|
|
34
|
+
if (!ctx.files) return null;
|
|
35
|
+
const row = await fetchOne<FileRefRow>(ctx.db, fileRefsTable, {
|
|
36
|
+
id: fileRefId,
|
|
37
|
+
isDeleted: false,
|
|
38
|
+
});
|
|
39
|
+
if (!row) return null;
|
|
40
|
+
const handle = ctx.files.refFromFileRef(row);
|
|
41
|
+
if (!(await handle.exists())) return null;
|
|
42
|
+
return { fileName: row.fileName, contentType: row.mimeType, bytes: await handle.read() };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Gives the target tenant its own copy of every file behind the entries, so a template that was
|
|
46
|
+
// copied from the system default keeps working once the tenant exports it or measures its
|
|
47
|
+
// attachments. Rendered-PDF links carry no file and pass through. An entry whose source file is
|
|
48
|
+
// gone is left out: it could not be resolved at render time either.
|
|
49
|
+
export async function duplicateLinkedResourceFiles(
|
|
50
|
+
ctx: HandlerContext,
|
|
51
|
+
args: {
|
|
52
|
+
readonly user: SessionUser;
|
|
53
|
+
readonly tenantId: TenantId;
|
|
54
|
+
readonly resources: Readonly<Record<string, LinkedResourceEntry>>;
|
|
55
|
+
},
|
|
56
|
+
): Promise<WriteResult<Record<string, LinkedResourceEntry>>> {
|
|
57
|
+
const { user, tenantId } = args;
|
|
58
|
+
const { db } = requireForTenant(ctx, tenantId);
|
|
59
|
+
const copies: Record<string, LinkedResourceEntry> = {};
|
|
60
|
+
for (const [key, entry] of Object.entries(args.resources)) {
|
|
61
|
+
if (typeof entry !== "string" && entry.kind === "rendered-pdf") {
|
|
62
|
+
copies[key] = entry;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const sourceFileRefId = typeof entry === "string" ? entry : entry.fileRefId;
|
|
66
|
+
const file = await readTemplateResource(ctx, sourceFileRefId);
|
|
67
|
+
// skip: the source file is gone, there is nothing to copy
|
|
68
|
+
if (file === null) continue;
|
|
69
|
+
const created = await createFileRefFromBytes(
|
|
70
|
+
{
|
|
71
|
+
db,
|
|
72
|
+
files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
|
|
73
|
+
registry: ctx.registry,
|
|
74
|
+
user: { ...user, tenantId },
|
|
75
|
+
},
|
|
76
|
+
{ fileName: file.fileName, mimeType: file.contentType, data: file.bytes },
|
|
77
|
+
);
|
|
78
|
+
if (!created.isSuccess) return created;
|
|
79
|
+
copies[key] =
|
|
80
|
+
typeof entry === "string" ? created.data.id : { ...entry, fileRefId: created.data.id };
|
|
81
|
+
}
|
|
82
|
+
return { isSuccess: true, data: copies };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Replaces the whole key map of one row with files that are already stored (bundle import,
|
|
86
|
+
// copy-locale). Single keys go through storeTemplateResource and resource-remove.
|
|
87
|
+
export async function attachTemplateResources(
|
|
88
|
+
ctx: Pick<HandlerContext, "forTenant">,
|
|
89
|
+
args: {
|
|
90
|
+
readonly user: SessionUser;
|
|
91
|
+
readonly tenantId: TenantId;
|
|
92
|
+
readonly templateId: string;
|
|
93
|
+
readonly resources: Readonly<Record<string, LinkedResourceEntry>>;
|
|
94
|
+
},
|
|
95
|
+
): Promise<WriteResult<null>> {
|
|
96
|
+
if (Object.keys(args.resources).length === 0) return { isSuccess: true, data: null };
|
|
97
|
+
const { db, streamTenantId } = requireForTenant(ctx, args.tenantId);
|
|
98
|
+
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
99
|
+
id: args.templateId,
|
|
100
|
+
tenantId: args.tenantId,
|
|
101
|
+
});
|
|
102
|
+
if (!row) return writeFailure(new NotFoundError("template-resource", args.templateId));
|
|
103
|
+
const result = await executor.update(
|
|
104
|
+
{
|
|
105
|
+
id: row.id,
|
|
106
|
+
version: row.version,
|
|
107
|
+
changes: { linkedResources: JSON.stringify(args.resources) },
|
|
108
|
+
},
|
|
109
|
+
args.user,
|
|
110
|
+
db,
|
|
111
|
+
{ streamTenantId },
|
|
112
|
+
);
|
|
113
|
+
return result.isSuccess ? { isSuccess: true, data: null } : result;
|
|
114
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type NotificationDefinition,
|
|
3
|
+
parseQn,
|
|
4
|
+
type Registry,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { DEFAULT_MAIL_LAYOUT_SLUG, type TemplateKind } from "./constants.js";
|
|
7
|
+
|
|
8
|
+
type UsageRole = "template" | "layout";
|
|
9
|
+
|
|
10
|
+
export type TemplateUsageEntry = {
|
|
11
|
+
readonly qualifiedName: string;
|
|
12
|
+
readonly feature: string;
|
|
13
|
+
readonly notification: string;
|
|
14
|
+
readonly trigger: string | null;
|
|
15
|
+
readonly role: UsageRole;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Layout slug of the effective stored mail template, by notification qualified
|
|
19
|
+
* name and locale. The renderer reads the layout from the stored row's meta,
|
|
20
|
+
* so it wins over the code-registered notification meta. */
|
|
21
|
+
export type EffectiveMailLayoutSlugs = ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
22
|
+
|
|
23
|
+
function layoutSlugsOf(
|
|
24
|
+
qualifiedName: string,
|
|
25
|
+
notification: NotificationDefinition,
|
|
26
|
+
effectiveLayouts: EffectiveMailLayoutSlugs,
|
|
27
|
+
): string[] {
|
|
28
|
+
const stored = effectiveLayouts.get(qualifiedName);
|
|
29
|
+
const locales = new Set([
|
|
30
|
+
...Object.keys(notification.template ?? {}),
|
|
31
|
+
...Object.keys(notification.meta ?? {}),
|
|
32
|
+
...(stored?.keys() ?? []),
|
|
33
|
+
]);
|
|
34
|
+
return [...locales].map(
|
|
35
|
+
(locale) =>
|
|
36
|
+
stored?.get(locale) ?? notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function collectTemplateUsage(
|
|
41
|
+
registry: Registry,
|
|
42
|
+
slug: string,
|
|
43
|
+
kind: TemplateKind,
|
|
44
|
+
effectiveLayouts: EffectiveMailLayoutSlugs = new Map(),
|
|
45
|
+
): TemplateUsageEntry[] {
|
|
46
|
+
const usedBy: TemplateUsageEntry[] = [];
|
|
47
|
+
if (kind === "mail" || kind === "mail-layout") {
|
|
48
|
+
for (const [qualifiedName, notification] of registry.getAllNotifications()) {
|
|
49
|
+
const role: UsageRole = kind === "mail" ? "template" : "layout";
|
|
50
|
+
// Notifications without a code template are not rendered through the resolver.
|
|
51
|
+
const isUsed =
|
|
52
|
+
kind === "mail"
|
|
53
|
+
? qualifiedName === slug && notification.template !== undefined
|
|
54
|
+
: layoutSlugsOf(qualifiedName, notification, effectiveLayouts).includes(slug);
|
|
55
|
+
if (!isUsed) continue;
|
|
56
|
+
const parsed = parseQn(qualifiedName);
|
|
57
|
+
usedBy.push({
|
|
58
|
+
qualifiedName,
|
|
59
|
+
feature: parsed.scope,
|
|
60
|
+
notification: parsed.name,
|
|
61
|
+
trigger: notification.trigger?.on ?? null,
|
|
62
|
+
role,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
usedBy.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
|
|
67
|
+
return usedBy;
|
|
68
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
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({ ...variableCommon, type: z.literal("richText"), example: textLikeExample }),
|
|
51
|
+
z.object({
|
|
52
|
+
...variableCommon,
|
|
53
|
+
type: z.literal("money"),
|
|
54
|
+
example: z
|
|
55
|
+
.union([z.number(), z.object({ amount: z.number(), currency: z.string() })])
|
|
56
|
+
.optional(),
|
|
57
|
+
}),
|
|
58
|
+
z.object({ ...variableCommon, type: z.literal("date"), example: textLikeExample }),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const variableDefSchema = z.union([
|
|
62
|
+
scalarVariableSchema,
|
|
63
|
+
z.object({
|
|
64
|
+
...variableCommon,
|
|
65
|
+
type: z.literal("list"),
|
|
66
|
+
item: z.record(z.string(), scalarVariableSchema),
|
|
67
|
+
example: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// A stored variableSchema value is either a bare string (legacy "this is a text
|
|
72
|
+
// variable") or a TemplateVariableDef object; anything else is not declared and
|
|
73
|
+
// therefore shows up as an unknown placeholder instead of being guessed at.
|
|
74
|
+
function toVariableDef(raw: unknown): TemplateVariableDef | null {
|
|
75
|
+
if (typeof raw === "string") return { type: "text" };
|
|
76
|
+
const parsed = variableDefSchema.safeParse(raw);
|
|
77
|
+
return parsed.success ? parsed.data : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveTemplateVariables(
|
|
81
|
+
registry: Registry,
|
|
82
|
+
slug: string,
|
|
83
|
+
variableSchema: Readonly<Record<string, unknown>>,
|
|
84
|
+
): TemplateVariables {
|
|
85
|
+
const declared = registry.getAllNotifications().get(slug)?.variables;
|
|
86
|
+
return declared ?? variablesFromSchema(variableSchema);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function variablesFromSchema(
|
|
90
|
+
variableSchema: Readonly<Record<string, unknown>>,
|
|
91
|
+
): TemplateVariables {
|
|
92
|
+
const variables: Record<string, TemplateVariableDef> = {};
|
|
93
|
+
for (const [name, raw] of Object.entries(variableSchema)) {
|
|
94
|
+
const def = toVariableDef(raw);
|
|
95
|
+
if (def !== null) variables[name] = def;
|
|
96
|
+
}
|
|
97
|
+
return variables;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type Resolution = { readonly known: boolean; readonly used: string };
|
|
101
|
+
|
|
102
|
+
function rootName(name: string): string {
|
|
103
|
+
return name.split(".")[0] ?? name;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type PlaceholderScope = {
|
|
107
|
+
readonly variables: TemplateVariables;
|
|
108
|
+
readonly systemPlaceholders: ReadonlySet<string>;
|
|
109
|
+
readonly resourceKeys: ReadonlySet<string>;
|
|
110
|
+
readonly used: Set<string>;
|
|
111
|
+
readonly unknown: Set<string>;
|
|
112
|
+
readonly unknownResources: Set<string>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function listItemFields(
|
|
116
|
+
variables: TemplateVariables,
|
|
117
|
+
listName: string | null,
|
|
118
|
+
): Readonly<Record<string, TemplateScalarVariableDef>> {
|
|
119
|
+
const def =
|
|
120
|
+
listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
|
|
121
|
+
return def?.type === "list" ? def.item : {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isKnownName(
|
|
125
|
+
name: string,
|
|
126
|
+
scope: PlaceholderScope,
|
|
127
|
+
itemFields: Readonly<Record<string, unknown>>,
|
|
128
|
+
): Resolution {
|
|
129
|
+
const { variables } = scope;
|
|
130
|
+
const root = rootName(name);
|
|
131
|
+
if (Object.hasOwn(itemFields, name) || Object.hasOwn(itemFields, root)) {
|
|
132
|
+
return { known: true, used: "" };
|
|
133
|
+
}
|
|
134
|
+
if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
|
|
135
|
+
if (Object.hasOwn(variables, name)) return { known: true, used: name };
|
|
136
|
+
if (Object.hasOwn(variables, root)) return { known: true, used: root };
|
|
137
|
+
return { known: false, used: "" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function recordName(
|
|
141
|
+
name: string,
|
|
142
|
+
itemFields: Readonly<Record<string, unknown>>,
|
|
143
|
+
scope: PlaceholderScope,
|
|
144
|
+
) {
|
|
145
|
+
const resolution = isKnownName(name, scope, itemFields);
|
|
146
|
+
if (!resolution.known) scope.unknown.add(name);
|
|
147
|
+
else if (resolution.used !== "") scope.used.add(resolution.used);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function enclosingEachName(element: ScanElement): string | null {
|
|
151
|
+
return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
|
|
155
|
+
withParsedDocument(body, (root) => {
|
|
156
|
+
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
157
|
+
const listName = element.getAttribute("data-each") ?? "";
|
|
158
|
+
const def = Object.hasOwn(scope.variables, listName) ? scope.variables[listName] : undefined;
|
|
159
|
+
if (def?.type === "list") scope.used.add(listName);
|
|
160
|
+
else scope.unknown.add(listName);
|
|
161
|
+
}
|
|
162
|
+
for (const element of Array.from(
|
|
163
|
+
root.querySelectorAll("span[data-variable], a[href], img[data-resource]"),
|
|
164
|
+
)) {
|
|
165
|
+
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
166
|
+
const tag = element.tagName.toLowerCase();
|
|
167
|
+
if (tag === "span") {
|
|
168
|
+
recordName(element.getAttribute("data-variable") ?? "", itemFields, scope);
|
|
169
|
+
} else if (tag === "a") {
|
|
170
|
+
const href = element.getAttribute("href") ?? "";
|
|
171
|
+
if (isDocumentPlaceholderHref(href)) recordName(href.slice(2, -2), itemFields, scope);
|
|
172
|
+
} else {
|
|
173
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
174
|
+
if (!scope.resourceKeys.has(key)) scope.unknownResources.add(key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
|
|
181
|
+
|
|
182
|
+
// Meta text supports only plain `{{name}}` / `{{name.field}}`; every other
|
|
183
|
+
// Handlebars construct is reported as unknown so it can never reach the compiler.
|
|
184
|
+
function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
|
|
185
|
+
const leftover = text.replace(HANDLEBARS_EXPRESSION, (_match, inner: string) => {
|
|
186
|
+
const name = inner.trim();
|
|
187
|
+
if (isDocumentReferenceName(name)) recordName(name, {}, scope);
|
|
188
|
+
else scope.unknown.add(`{{${inner}}}`);
|
|
189
|
+
return "";
|
|
190
|
+
});
|
|
191
|
+
if (/[{}]/.test(leftover)) scope.unknown.add("{");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function metaTexts(meta: TemplateMeta | null): readonly string[] {
|
|
195
|
+
if (meta === null || !("subject" in meta)) return [];
|
|
196
|
+
return [meta.subject, ...(meta.previewText === undefined ? [] : [meta.previewText])];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function validateTemplatePlaceholders(input: {
|
|
200
|
+
readonly kind: string;
|
|
201
|
+
readonly body: string;
|
|
202
|
+
readonly meta: TemplateMeta | null;
|
|
203
|
+
readonly variables: TemplateVariables;
|
|
204
|
+
/** data-resource keys with an uploaded file besides the built-in `logo`. */
|
|
205
|
+
readonly resourceKeys?: readonly string[];
|
|
206
|
+
}): TemplateValidation {
|
|
207
|
+
const scope: PlaceholderScope = {
|
|
208
|
+
variables: input.variables,
|
|
209
|
+
systemPlaceholders: systemPlaceholdersFor(input.kind),
|
|
210
|
+
resourceKeys: new Set([...BASE_RESOURCE_KEYS, ...(input.resourceKeys ?? [])]),
|
|
211
|
+
used: new Set(),
|
|
212
|
+
unknown: new Set(),
|
|
213
|
+
unknownResources: new Set(),
|
|
214
|
+
};
|
|
215
|
+
scanBodyPlaceholders(input.body, scope);
|
|
216
|
+
for (const text of metaTexts(input.meta)) scanMetaPlaceholders(text, scope);
|
|
217
|
+
const missingRequired = Object.entries(input.variables)
|
|
218
|
+
.filter(([name, def]) => def.required === true && !scope.used.has(name))
|
|
219
|
+
.map(([name]) => name);
|
|
220
|
+
return {
|
|
221
|
+
unknown: [...scope.unknown].sort(),
|
|
222
|
+
missingRequired: missingRequired.sort(),
|
|
223
|
+
used: [...scope.used].sort(),
|
|
224
|
+
unknownResources: [...scope.unknownResources].sort(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function hasPlaceholderProblems(validation: TemplateValidation): boolean {
|
|
229
|
+
return validation.unknown.length > 0 || validation.missingRequired.length > 0;
|
|
230
|
+
}
|
|
@@ -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,8 @@ import {
|
|
|
40
40
|
TemplateResolverHandlers,
|
|
41
41
|
TemplateResolverQueries,
|
|
42
42
|
} from "../qualified-names.js";
|
|
43
|
+
import { DocumentBlocksContext, DocumentContentEditor } from "./document-editor.js";
|
|
44
|
+
import { documentBlocksForKind } from "./document-toolbar-blocks.js";
|
|
43
45
|
import { defaultTranslations } from "./i18n.js";
|
|
44
46
|
|
|
45
47
|
// Exported for the unit test — groupBlocksByFolder is a pure function.
|
|
@@ -256,21 +258,22 @@ type TextBlock = {
|
|
|
256
258
|
readonly modifiedAt: string | null;
|
|
257
259
|
};
|
|
258
260
|
|
|
259
|
-
// The collection's declared contentFormat ("plain" | "rich" | "markdown"
|
|
260
|
-
// see ContentCollectionDefinition.contentFormat) uses a different vocabulary
|
|
261
|
+
// The collection's declared contentFormat ("plain" | "rich" | "markdown" |
|
|
262
|
+
// "document", see ContentCollectionDefinition.contentFormat) uses a different vocabulary
|
|
261
263
|
// than the stored entry's contentFormat ("plain" | "html" | "markdown" |
|
|
262
|
-
// "mjml", see CONTENT_FORMATS) — "rich" always
|
|
264
|
+
// "mjml", see CONTENT_FORMATS) — "rich" and "document" always store HTML. Without this,
|
|
263
265
|
// handleSave omitting contentFormat let the write schema's
|
|
264
266
|
// contentFormatSchema.default("markdown") silently stamp every save as
|
|
265
267
|
// markdown, clobbering e.g. a "plain" entry. Undefined (no collection, or
|
|
266
268
|
// the collection didn't declare a format) keeps whatever was already saved
|
|
267
269
|
// instead of guessing.
|
|
268
|
-
|
|
270
|
+
// Exported for the unit test — resolveWriteContentFormat is a pure function.
|
|
271
|
+
export function resolveWriteContentFormat(
|
|
269
272
|
collectionContentFormat: string | undefined,
|
|
270
273
|
loadedContentFormat: string | undefined,
|
|
271
274
|
): string | undefined {
|
|
272
275
|
if (collectionContentFormat === "plain") return "plain";
|
|
273
|
-
if (collectionContentFormat === "rich") return "html";
|
|
276
|
+
if (collectionContentFormat === "rich" || collectionContentFormat === "document") return "html";
|
|
274
277
|
if (collectionContentFormat === "markdown") return "markdown";
|
|
275
278
|
return loadedContentFormat;
|
|
276
279
|
}
|
|
@@ -309,6 +312,7 @@ function TextBlockEditor({
|
|
|
309
312
|
const contentFormat = findContentFormat(features, collectionId);
|
|
310
313
|
const variableExamples = findVariableExamples(features, collectionId);
|
|
311
314
|
const variables = Object.keys(variableExamples);
|
|
315
|
+
const documentBlocks = documentBlocksForKind(findCollection(features, collectionId)?.kind);
|
|
312
316
|
const ContentEditor = useContentEditor(contentFormat);
|
|
313
317
|
const canWrite =
|
|
314
318
|
user?.roles.includes("TenantAdmin") === true || user?.roles.includes("SystemAdmin") === true;
|
|
@@ -446,13 +450,15 @@ function TextBlockEditor({
|
|
|
446
450
|
contentFormat={contentFormat}
|
|
447
451
|
/>
|
|
448
452
|
) : (
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
<DocumentBlocksContext.Provider value={documentBlocks}>
|
|
454
|
+
<ContentEditor
|
|
455
|
+
id={contentEditorId}
|
|
456
|
+
value={content}
|
|
457
|
+
onChange={setContent}
|
|
458
|
+
variables={variables}
|
|
459
|
+
readOnly={disabled}
|
|
460
|
+
/>
|
|
461
|
+
</DocumentBlocksContext.Provider>
|
|
456
462
|
)}
|
|
457
463
|
</Field>
|
|
458
464
|
{saveError !== null && <Banner variant="error">{saveError}</Banner>}
|
|
@@ -499,12 +505,13 @@ export function textBlocksClient(opts?: {
|
|
|
499
505
|
resolvers: {
|
|
500
506
|
"template-resolver:edit": TextBlockEditor,
|
|
501
507
|
},
|
|
502
|
-
// The default for every "plain" collection (ai-prompt, mail
|
|
508
|
+
// The default for every "plain" collection (ai-prompt, mail without
|
|
503
509
|
// an explicit rich override) — an app-registered "plain" editor on
|
|
504
510
|
// another clientFeature still wins, last-wins same as columnRenderers.
|
|
505
511
|
contentEditors: {
|
|
506
512
|
plain: PlainContentEditor,
|
|
507
513
|
rich: RichContentEditor,
|
|
514
|
+
document: DocumentContentEditor,
|
|
508
515
|
// ponytail: same textarea+chips as "plain" — markdown is stored as
|
|
509
516
|
// text either way. Shares PlainContentEditor's fixed-DOM-id ceiling
|
|
510
517
|
// (see its own file comment); swap for a live-preview widget or a
|