@cosmicdrift/kumiko-samples 0.374.0 → 0.378.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 +6 -4
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/constants.ts +1 -0
- package/packages/bundled-features/src/account-security/index.ts +1 -0
- package/packages/bundled-features/src/admin-shell/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/audit/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/web/default-topbar-actions.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/tenant-menu-items.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/tenant-switcher.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
- package/packages/bundled-features/src/auth-foundation/changes.json +6 -0
- package/packages/bundled-features/src/auth-foundation/env.ts +27 -0
- package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/contact-card.ts +14 -0
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/index.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +16 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-contact-plan.tsx +32 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +5 -15
- package/packages/bundled-features/src/billing-foundation/web/cancel-contract-dialog.tsx +2 -2
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +1 -1
- package/packages/bundled-features/src/billing-foundation/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/billing-foundation/web/localized-params.ts +14 -0
- package/packages/bundled-features/src/cap-counter/changes.json +12 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +9 -0
- package/packages/bundled-features/src/cap-overview/web/cap-cards-panel.tsx +3 -3
- package/packages/bundled-features/src/cap-overview/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +2 -1
- package/packages/bundled-features/src/compliance-profiles/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/config/feature.ts +2 -2
- package/packages/bundled-features/src/config/web/i18n.ts +1 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +1 -1
- package/packages/bundled-features/src/delivery/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/delivery/web/delivery-status-cell.tsx +1 -1
- package/packages/bundled-features/src/feature-toggles/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/file-provider-s3/changes.json +6 -0
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +45 -0
- package/packages/bundled-features/src/file-provider-s3/env-schema.ts +20 -13
- package/packages/bundled-features/src/file-provider-s3/index.ts +4 -0
- package/packages/bundled-features/src/folders/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/folders/web/folder-section.tsx +3 -4
- package/packages/bundled-features/src/host-routing/README.md +7 -0
- package/packages/bundled-features/src/host-routing/changes.json +6 -0
- package/packages/bundled-features/src/host-routing/constants.ts +4 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +34 -26
- package/packages/bundled-features/src/host-routing/feature.ts +122 -41
- package/packages/bundled-features/src/host-routing/host.ts +28 -1
- package/packages/bundled-features/src/host-routing/index.ts +3 -0
- package/packages/bundled-features/src/host-routing/options.ts +45 -2
- package/packages/bundled-features/src/host-routing/redirects.ts +33 -0
- package/packages/bundled-features/src/host-routing/tenant-lookup.ts +65 -0
- package/packages/bundled-features/src/host-routing/validate-options.ts +48 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +8 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +11 -4
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +100 -0
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +2 -2
- package/packages/bundled-features/src/jobs/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/legal-pages/changes.json +6 -0
- package/packages/bundled-features/src/legal-pages/feature.ts +14 -1
- package/packages/bundled-features/src/legal-pages/layout.ts +6 -6
- package/packages/bundled-features/src/legal-pages/web/client-plugin.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +21 -22
- package/packages/bundled-features/src/mail-foundation/i18n.ts +3 -2
- package/packages/bundled-features/src/mail-transport-smtp/i18n.ts +1 -1
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/notes-history/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/page-render/branding.ts +1 -1
- package/packages/bundled-features/src/page-render/layout.ts +2 -2
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/i18n.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +4 -4
- package/packages/bundled-features/src/seo/sitemap.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +1 -1
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +127 -2
- package/packages/bundled-features/src/tags/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/template-resolver/changes.json +38 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -3
- package/packages/bundled-features/src/template-resolver/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-blocks.ts +36 -0
- package/packages/bundled-features/src/template-resolver/document-images.ts +29 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +515 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +44 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +5 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +79 -34
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +2 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +0 -6
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +67 -58
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +36 -13
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +86 -0
- package/packages/bundled-features/src/template-resolver/handlers/document-blocks.query.ts +48 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +11 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +37 -28
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +66 -62
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +134 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +2 -1
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +62 -49
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +44 -29
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +52 -36
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +126 -106
- package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +38 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +105 -110
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +110 -103
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +22 -1
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +56 -28
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +38 -29
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +24 -9
- package/packages/bundled-features/src/template-resolver/index.ts +8 -1
- package/packages/bundled-features/src/template-resolver/{web/placeholder-text.ts → placeholder-text.ts} +1 -1
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +3 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +6 -1
- package/packages/bundled-features/src/template-resolver/template-edit-policy.ts +91 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +32 -10
- package/packages/bundled-features/src/template-resolver/template-validation.ts +32 -11
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +3 -3
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +29 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +43 -5
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +9 -33
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +2 -2
- package/packages/bundled-features/src/tenant/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/tenant/web/member-status-cell.tsx +1 -1
- package/packages/bundled-features/src/tier-engine/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/env.ts +15 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -2
- package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -2
- package/packages/bundled-features/src/waitlist/changes.json +6 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +51 -2
- package/samples/apps/admin-console/package.json +0 -3
- package/samples/apps/admin-console/src/app/client.tsx +1 -1
- package/samples/apps/admin-console/src/app/shell.tsx +1 -1
- package/samples/apps/config-demo/package.json +1 -3
- package/samples/apps/config-demo/src/app/client.tsx +1 -1
- package/samples/apps/config-demo/src/app/shell.tsx +2 -2
- package/samples/apps/config-demo/src/features/demo/web/index.ts +1 -1
- package/samples/apps/config-demo/tsconfig.json +0 -12
- package/samples/apps/marketing-demo/package.json +1 -3
- package/samples/apps/marketing-demo/src/app/client.tsx +1 -1
- package/samples/apps/marketing-demo/src/app/shell.tsx +1 -1
- package/samples/apps/marketing-demo/src/features/assets/web/index.ts +1 -1
- package/samples/apps/marketing-demo/src/features/helpdesk/web/index.ts +1 -1
- package/samples/apps/showcase/package.json +1 -3
- package/samples/apps/showcase/src/app/client.tsx +1 -1
- package/samples/apps/showcase/src/app/shell.tsx +1 -1
- package/samples/apps/showcase/src/features/demos/web/index.ts +1 -1
- package/samples/apps/showcase/src/features/demos/web/pages/sidebar-panel.tsx +1 -1
- package/samples/apps/showcase/src/features/demos/web/pages/toast.tsx +1 -1
- package/samples/apps/showcase/src/features/items/web/index.ts +1 -1
- package/samples/apps/styleguide/package.json +1 -3
- package/samples/apps/styleguide/src/app/client.tsx +1 -1
- package/samples/apps/styleguide/src/app/shell.tsx +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/web.ts +1 -1
- package/samples/apps/styleguide/src/features/content/web.tsx +2 -2
- package/samples/apps/styleguide/src/features/demo/web.ts +1 -1
- package/samples/apps/styleguide/src/features/examples/web.ts +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/index.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/lib/format.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/FormExamples.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/index.ts +1 -1
- package/samples/apps/styleguide/tsconfig.json +0 -12
- package/samples/apps/ui-walkthrough/package.json +1 -3
- package/samples/apps/ui-walkthrough/src/app/client.tsx +1 -1
- package/samples/apps/ui-walkthrough/src/app/help-action.tsx +37 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +7 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +65 -1
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +70 -0
- package/samples/apps/ui-walkthrough/src/features/rental/invoice-receive.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/rental/lease-support.ts +15 -0
- package/samples/apps/ui-walkthrough/src/features/rental/qualified-names.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +79 -0
- package/samples/apps/ui-walkthrough/src/features/rental/web/index.ts +3 -1
- package/samples/apps/ui-walkthrough/src/features/rental/web/upload-standard-layout.tsx +61 -0
- package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
- package/samples/apps/ui-walkthrough/src/features/tasks/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/features/vehicles/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +32 -3
- package/samples/apps/use-all-bundled/bin/main.ts +3 -2
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +12 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +1 -1
- package/samples/apps/use-all-bundled/feature-manifest.json +3 -2
- package/samples/apps/use-all-bundled/package.json +1 -3
- package/samples/apps/use-all-bundled/src/app/client.tsx +1 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +5 -4
- package/samples/apps/use-all-bundled/src/app/shell.tsx +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -1
- package/samples/apps/workspaces/package.json +1 -3
- package/samples/apps/workspaces/src/app/client.tsx +1 -1
- package/samples/apps/workspaces/src/app/shell.tsx +1 -1
- package/samples/apps/workspaces/src/features/demo/web/index.ts +1 -1
- package/samples/apps/workspaces/src/features/demo-driver/web/index.ts +1 -1
- package/samples/recipes/apex-landing/package.json +1 -1
- package/samples/recipes/apex-landing/src/feature.ts +1 -1
- package/samples/recipes/apex-surface-auth/README.md +1 -1
- package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
- package/samples/recipes/compose-stacks/README.md +7 -4
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/embedded/README.md +8 -0
- package/samples/recipes/embedded/src/feature.ts +8 -0
- package/samples/recipes/embedded/src/handlers/invoice-receive.write.ts +38 -0
- package/samples/recipes/embedded/src/screens/invoice-receive.screen.ts +59 -0
- package/samples/recipes/embedded-entity-form/package.json +0 -1
- package/samples/recipes/embedded-entity-form/src/web/accept-suggestion-drawer.tsx +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/record-detail-layout/package.json +0 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/user-profile/README.md +1 -1
- package/samples/recipes/wizard-form/package.json +0 -1
- package/samples/recipes/wizard-form/src/web/index.ts +2 -2
- package/samples/recipes/wizard-form/src/web/listing-review-section.tsx +2 -2
- package/samples/recipes/writeform-section/package.json +0 -1
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Email sending offers a platform default and an own SMTP server",
|
|
6
|
+
"detail": "The provider field is labelled \"Send via\" with the options \"Platform sending (default)\" (empty, saved as \"\") and \"Own SMTP server\"; the SMTP fields show only while the own server is picked."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.370.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -58,34 +58,33 @@ export const MAIL_TRANSPORT_EXTENSION = "mailTransport" as const;
|
|
|
58
58
|
// =============================================================================
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* read-Felder die fuer Tenant-Config + Secret-Lookup noetig sind.
|
|
61
|
+
* Narrow surface type for transport plugins, mirroring file-foundation's
|
|
62
|
+
* FileProviderContext. HandlerContext carries tx, actor, signal and more;
|
|
63
|
+
* plugins only get the read fields needed for tenant config and secret lookup.
|
|
65
64
|
*
|
|
66
|
-
* **
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* ConfigAccessor
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
65
|
+
* **Why not the full HandlerContext?** On the worker path (transport builds
|
|
66
|
+
* triggered by r.job, such as the user-data-rights forget/export cron) there
|
|
67
|
+
* is no per-request `config`/`tx`/`actor`; the wrapper builds the per-tenant
|
|
68
|
+
* ConfigAccessor from `ctx.configResolver`. A plugin reading `ctx.tx` or other
|
|
69
|
+
* request-only fields would break the worker path at runtime, and that would
|
|
70
|
+
* only show up with real SMTP in production. Casting
|
|
71
|
+
* `ctx as unknown as HandlerContext` satisfies the compiler but fails at
|
|
72
|
+
* runtime in the worker. A plugin that needs more extends MailTransportContext
|
|
73
|
+
* explicitly (a visible breaking change) instead of casting ctx.
|
|
75
74
|
*
|
|
76
|
-
* **
|
|
77
|
-
* config
|
|
78
|
-
* registry
|
|
79
|
-
* secrets
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
75
|
+
* **Fields:**
|
|
76
|
+
* config — tenant config reads (the plugin's host/port/from/...)
|
|
77
|
+
* registry — extension lookup in the factory (not plugin-internal)
|
|
78
|
+
* secrets — tenant secret reads (smtp.password)
|
|
79
|
+
* auditUserId — audit identity for secret reads. Handler path: the
|
|
80
|
+
* dispatcher sets the caller's user id; worker path: the
|
|
81
|
+
* r.job wrap sets a system identity.
|
|
83
82
|
*/
|
|
84
83
|
export type MailTransportContext = {
|
|
85
84
|
readonly config?: ConfigAccessor;
|
|
86
85
|
readonly registry?: Registry;
|
|
87
86
|
readonly secrets?: import("@cosmicdrift/kumiko-framework/secrets").SecretsContext;
|
|
88
|
-
readonly
|
|
87
|
+
readonly auditUserId?: string | undefined;
|
|
89
88
|
};
|
|
90
89
|
|
|
91
90
|
/**
|
|
@@ -283,7 +282,7 @@ export function tenantMailTransport(
|
|
|
283
282
|
config: scope.config,
|
|
284
283
|
registry: scope.registry,
|
|
285
284
|
secrets: scope.secrets,
|
|
286
|
-
|
|
285
|
+
auditUserId: createSystemUser(scope.tenantId).id,
|
|
287
286
|
},
|
|
288
287
|
scope.tenantId,
|
|
289
288
|
"channel-email",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export const MAIL_FOUNDATION_I18N = {
|
|
2
2
|
"screen:mail-foundation-tenant.title": { en: "Email sending" },
|
|
3
3
|
"mail-foundation.settings": { en: "Email sending" },
|
|
4
|
-
"mail-foundation.provider": { en: "
|
|
4
|
+
"mail-foundation.provider": { en: "Send via" },
|
|
5
5
|
"mail-foundation.provider.description": {
|
|
6
|
-
en: "
|
|
6
|
+
en: "Choose who sends your emails. The platform default needs no settings of your own.",
|
|
7
7
|
},
|
|
8
|
+
"mail-foundation.provider.platformDefault": { en: "Platform sending (default)" },
|
|
8
9
|
} as const;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const MAIL_TRANSPORT_SMTP_I18N = {
|
|
2
2
|
// Also the option label of mail-foundation's provider select.
|
|
3
|
-
"mail-transport-smtp.settings": { en: "SMTP server" },
|
|
3
|
+
"mail-transport-smtp.settings": { en: "Own SMTP server" },
|
|
4
4
|
"mail-transport-smtp.settings.secrets": { en: "SMTP sign-in" },
|
|
5
5
|
"mail-transport-smtp.settings.description": {
|
|
6
6
|
en: "The connection details come from your mail provider, for example Brevo, Postmark, Amazon SES or your own server.",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// [managedPagesClient()] }), damit die Admin-Labels übersetzt rendern.
|
|
7
7
|
|
|
8
8
|
import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
|
-
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer
|
|
9
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
|
|
10
10
|
import { defaultTranslations } from "./i18n.ts";
|
|
11
11
|
|
|
12
12
|
export type ManagedPagesClientOptions = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
|
|
3
|
-
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer
|
|
3
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
4
|
import { NOTES_HISTORY_FEATURE_NAME, NOTES_SECTION_EXTENSION_NAME } from "../constants.ts";
|
|
5
5
|
import { defaultTranslations } from "./i18n.ts";
|
|
6
6
|
import { NotesSection } from "./notes-section.tsx";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
2
|
|
|
3
3
|
// Tenant-branding tokens resolved at render time (managed-pages reads them
|
|
4
4
|
// from config — see managed-pages/branding.ts). Empty string = "unset, use
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import { type ApexHead, renderApexHeadTags } from "@cosmicdrift/kumiko-headless/apex";
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
|
+
import { type ApexHead, renderApexHeadTags } from "@cosmicdrift/kumiko-renderer/headless/apex";
|
|
3
3
|
import { type BrandingTokens, brandingHeaderHtml, brandingStyleBlock } from "./branding.ts";
|
|
4
4
|
import { sanitizeTenantCss } from "./css-sanitize.ts";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import { fieldLabelKey, fieldOptionLabelKey } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { fieldLabelKey, fieldOptionLabelKey } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
3
|
import { PAT_FEATURE } from "./constants.ts";
|
|
4
4
|
import type { PatScopeConfig } from "./scopes.ts";
|
|
5
5
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type SecretMintScreenDefinition,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
8
|
-
import { fieldOptionLabelKeyPrefix } from "@cosmicdrift/kumiko-headless";
|
|
8
|
+
import { fieldOptionLabelKeyPrefix } from "@cosmicdrift/kumiko-renderer/headless";
|
|
9
9
|
import {
|
|
10
10
|
PAT_FEATURE,
|
|
11
11
|
PAT_MINT_SCREEN_ID,
|
|
@@ -79,11 +79,11 @@ export const JOIN_REQUEST_TRANSACTION_REASON =
|
|
|
79
79
|
// FileProviderContext-Pfade (S3-Plugin im Worker) liefern dieselben
|
|
80
80
|
// zwei Felder, also reicht die schmale ctx-shape — kein voller
|
|
81
81
|
// HandlerContext-Import noetig.
|
|
82
|
-
// Handler contexts carry the caller as ctx.user, provider contexts set
|
|
82
|
+
// Handler contexts carry the caller as ctx.user, provider contexts set auditUserId (takes precedence).
|
|
83
83
|
export function requireSecretsContext(
|
|
84
84
|
ctx: {
|
|
85
85
|
readonly secrets?: SecretsContext;
|
|
86
|
-
readonly
|
|
86
|
+
readonly auditUserId?: string | undefined;
|
|
87
87
|
readonly user?: { readonly id: string };
|
|
88
88
|
},
|
|
89
89
|
handlerName: string,
|
|
@@ -95,10 +95,10 @@ export function requireSecretsContext(
|
|
|
95
95
|
"createSecretsContext({db, masterKeyProvider, registry}) via extraContext.secrets at boot.",
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
-
const userId = ctx.
|
|
98
|
+
const userId = ctx.auditUserId ?? ctx.user?.id;
|
|
99
99
|
if (!userId) {
|
|
100
100
|
throw new InternalError({
|
|
101
|
-
message: `[${handlerName}] ctx.
|
|
101
|
+
message: `[${handlerName}] ctx.auditUserId missing (and no ctx.user) — cannot audit secret reads without a caller identity.`,
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
const withAudit = (raw: SecretsContext): SecretsContext => ({
|
|
@@ -16,9 +16,14 @@ import {
|
|
|
16
16
|
type EntityDefinition,
|
|
17
17
|
parseTenantId,
|
|
18
18
|
type Registry,
|
|
19
|
+
SYSTEM_USER_ID,
|
|
19
20
|
type TenantId,
|
|
20
21
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
FILE_REF_ENTITY_NAME,
|
|
24
|
+
fileRefEntity,
|
|
25
|
+
fileRefsTable,
|
|
26
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
22
27
|
import {
|
|
23
28
|
collectSingleFileHolders,
|
|
24
29
|
type FileRefPurgeOutcome,
|
|
@@ -155,7 +160,8 @@ export async function purgeFilesReachingSubject(args: {
|
|
|
155
160
|
const storageByTenant = new Map<TenantId, Promise<DeleteFileBytes | undefined>>();
|
|
156
161
|
let firstFailure: FileSweepError | undefined;
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
const fileEntities = collectFileEntities(registry);
|
|
164
|
+
for (const fileEntity of fileEntities) {
|
|
159
165
|
const seenRowIds = new Set<string>();
|
|
160
166
|
for (const where of candidateWheres(subject, recordTenantId, fileEntity)) {
|
|
161
167
|
const rows = await selectMany<Record<string, unknown>>(db, fileEntity.table, where);
|
|
@@ -202,5 +208,124 @@ export async function purgeFilesReachingSubject(args: {
|
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
}
|
|
211
|
+
// Runs even after an entity-sweep failure: one failed row must not leave the pending uploads.
|
|
212
|
+
const pendingFailure = await purgePendingUploadsOfUser({
|
|
213
|
+
db,
|
|
214
|
+
registry,
|
|
215
|
+
subject,
|
|
216
|
+
fileFieldKeys: pendingFieldKeys(fileEntities),
|
|
217
|
+
fileHolders,
|
|
218
|
+
storageByTenant,
|
|
219
|
+
resolveDeleteBytes,
|
|
220
|
+
missingStorage,
|
|
221
|
+
});
|
|
222
|
+
firstFailure ??= pendingFailure;
|
|
205
223
|
if (firstFailure) throw firstFailure;
|
|
206
224
|
}
|
|
225
|
+
|
|
226
|
+
const pendingFieldKey = (entityType: unknown, fieldName: unknown) =>
|
|
227
|
+
`${entityType}\u0000${fieldName}`;
|
|
228
|
+
|
|
229
|
+
function pendingFieldKeys(fileEntities: readonly FileEntity[]): ReadonlySet<string> {
|
|
230
|
+
const keys = new Set<string>();
|
|
231
|
+
for (const { entityName, fileFields } of fileEntities) {
|
|
232
|
+
for (const fieldName of fileFields) keys.add(pendingFieldKey(entityName, fieldName));
|
|
233
|
+
}
|
|
234
|
+
return keys;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function isFileRefHeldByRecord(
|
|
238
|
+
db: DbRunner,
|
|
239
|
+
holders: readonly SingleFileHolder[],
|
|
240
|
+
tenantId: TenantId,
|
|
241
|
+
fileRefId: string,
|
|
242
|
+
): Promise<boolean> {
|
|
243
|
+
for (const holder of holders) {
|
|
244
|
+
for (const field of holder.singleFields) {
|
|
245
|
+
const where: WhereObject = { [field]: fileRefId };
|
|
246
|
+
if (holder.tableHasTenantId) where["tenantId"] = tenantId;
|
|
247
|
+
if ((await selectMany(db, holder.table, where, { limit: 1 })).length > 0) return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// An upload the user made but never bound to a record has no entity row to find it through, so
|
|
254
|
+
// the uploader id is the only link. Only refs that originate from a registered entity file
|
|
255
|
+
// field count as pending uploads: config saves (a tenant logo) and server-made files never
|
|
256
|
+
// bind and must outlive the uploader. A legacy unbound ref a record's single file column still
|
|
257
|
+
// names is not pending but in use and stays with that record.
|
|
258
|
+
async function purgePendingUploadsOfUser(args: {
|
|
259
|
+
readonly db: DbRunner;
|
|
260
|
+
readonly registry: Registry;
|
|
261
|
+
readonly subject: SubjectId;
|
|
262
|
+
readonly fileFieldKeys: ReadonlySet<string>;
|
|
263
|
+
readonly fileHolders: readonly SingleFileHolder[];
|
|
264
|
+
readonly storageByTenant: Map<TenantId, Promise<DeleteFileBytes | undefined>>;
|
|
265
|
+
readonly resolveDeleteBytes: (tenantId: TenantId) => Promise<DeleteFileBytes | undefined>;
|
|
266
|
+
readonly missingStorage: MissingStoragePolicy;
|
|
267
|
+
}): Promise<FileSweepError | undefined> {
|
|
268
|
+
const { db, registry, subject, fileFieldKeys } = args;
|
|
269
|
+
// skip: only a user uploads files without binding them to a record
|
|
270
|
+
if (subject.kind !== "user") return undefined;
|
|
271
|
+
// skip: the system user is not a person; its server-made files are not pending uploads
|
|
272
|
+
if (subject.userId === SYSTEM_USER_ID) return undefined;
|
|
273
|
+
// skip: without the files feature there is no file_refs table to sweep
|
|
274
|
+
if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined) return undefined;
|
|
275
|
+
const pending = await selectMany<Record<string, unknown>>(db, fileRefsTable, {
|
|
276
|
+
insertedById: subject.userId,
|
|
277
|
+
entityId: null,
|
|
278
|
+
});
|
|
279
|
+
let firstFailure: FileSweepError | undefined;
|
|
280
|
+
for (const fileRef of pending) {
|
|
281
|
+
// skip: not uploaded for a registered entity file field (config, server-made, unknown origin)
|
|
282
|
+
if (!fileFieldKeys.has(pendingFieldKey(fileRef["entityType"], fileRef["fieldName"]))) continue;
|
|
283
|
+
firstFailure ??= await purgePendingFileRef(args, fileRef);
|
|
284
|
+
}
|
|
285
|
+
return firstFailure;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function purgePendingFileRef(
|
|
289
|
+
args: {
|
|
290
|
+
readonly db: DbRunner;
|
|
291
|
+
readonly fileHolders: readonly SingleFileHolder[];
|
|
292
|
+
readonly storageByTenant: Map<TenantId, Promise<DeleteFileBytes | undefined>>;
|
|
293
|
+
readonly resolveDeleteBytes: (tenantId: TenantId) => Promise<DeleteFileBytes | undefined>;
|
|
294
|
+
readonly missingStorage: MissingStoragePolicy;
|
|
295
|
+
},
|
|
296
|
+
fileRef: Record<string, unknown>,
|
|
297
|
+
): Promise<FileSweepError | undefined> {
|
|
298
|
+
const { db, fileHolders, storageByTenant, resolveDeleteBytes, missingStorage } = args;
|
|
299
|
+
const tenantId = parseTenantId(fileRef["tenantId"]);
|
|
300
|
+
const fileRefId = String(fileRef["id"]);
|
|
301
|
+
const storageKey = fileRef["storageKey"];
|
|
302
|
+
// skip: a ref without tenant or key has no bytes this sweep could address
|
|
303
|
+
if (tenantId === null || typeof storageKey !== "string" || storageKey === "") return undefined;
|
|
304
|
+
// skip: a record still uses this file
|
|
305
|
+
if (await isFileRefHeldByRecord(db, fileHolders, tenantId, fileRefId)) return undefined;
|
|
306
|
+
|
|
307
|
+
let storagePromise = storageByTenant.get(tenantId);
|
|
308
|
+
if (storagePromise === undefined) {
|
|
309
|
+
storagePromise = resolveDeleteBytes(tenantId);
|
|
310
|
+
storageByTenant.set(tenantId, storagePromise);
|
|
311
|
+
}
|
|
312
|
+
const deleteBytes = await storagePromise;
|
|
313
|
+
if (deleteBytes === undefined) {
|
|
314
|
+
if (missingStorage.mode === "skip") {
|
|
315
|
+
missingStorage.warn(tenantId);
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return new FileSweepError("missing_file_storage", FILE_REF_ENTITY_NAME);
|
|
319
|
+
}
|
|
320
|
+
if ((await deleteBytesFailingClosed(deleteBytes, storageKey)).length > 0) {
|
|
321
|
+
return new FileSweepError("file_delete_failed", FILE_REF_ENTITY_NAME);
|
|
322
|
+
}
|
|
323
|
+
const forgotten = await fileRefExecutor.forget(
|
|
324
|
+
{ id: fileRefId },
|
|
325
|
+
createSystemUser(tenantId),
|
|
326
|
+
createTenantDb({ db, tenantId, mode: "system" }),
|
|
327
|
+
);
|
|
328
|
+
return forgotten.isSuccess
|
|
329
|
+
? undefined
|
|
330
|
+
: new FileSweepError("file_delete_failed", FILE_REF_ENTITY_NAME);
|
|
331
|
+
}
|
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Document PDFs embed the images of a record",
|
|
6
|
+
"detail": "A template variable of type images (templateVariable.images()) holds FileRef ids of the record. The new Images block (div[data-images], PDF templates only, rejected for mail kinds) renders them as data-URI images in the real PDF render. Only files of the rendering tenant are read, only png, jpeg, gif and webp, at most 24 images and 20 MB per document; missing, foreign, oversized or non-image ids are dropped silently. Previews and draft PDFs never read files and show one placeholder box per block. An images variable never renders as inline text. i18n keys template-resolver.documentEditor.images, template-resolver.documentEditor.imagesVariable and templateResolver.errors.document.imagesBlockPdfOnly ship in de, es, fr and it.",
|
|
7
|
+
"migration": "Declare a template variable with type `images`, place the Images block, and pass FileRef ids of the record in `variables`. Images resolve only in the real PDF render, previews show placeholders. The ids must come from the record on the server, never from client input: a resolved id is read for the whole tenant without an entity access check."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.378.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "Template editing is SystemAdmin-only unless the app unlocks it",
|
|
13
|
+
"detail": "Every tenant write handler, the overview and resource upload accept SystemAdmin only, plus the roles of the tenantEditable entries. A write on a template that is not unlocked fails with templateNotTenantEditable. The test send follows the same rule. For a non-SystemAdmin an unknown template id answers 403 like a locked one, so the response does not reveal whether it exists. Collections without an access rule default to SystemAdmin.",
|
|
14
|
+
"migration": "Where TenantAdmins must keep editing a template, pass tenantEditable to createTemplateResolverFeature, for example tenantEditable: [{ kind: \"mail\", slug: \"billing:notify:invoice-issued\", access: { roles: [\"TenantAdmin\"] } }]. Give template collections an explicit access rule if tenants should manage them."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.378.0",
|
|
18
|
+
"type": "improvement",
|
|
19
|
+
"title": "send-test-status query for the template test mail",
|
|
20
|
+
"detail": "template-resolver:query:send-test-status answers whether a test mail can be sent (canSend) and, if not, why (no_channel or no_address), so clients can disable the action instead of failing on send."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.377.0",
|
|
24
|
+
"type": "improvement",
|
|
25
|
+
"title": "seedSystemTemplateBundle sets a title per template",
|
|
26
|
+
"detail": "Bundle locale entries accept an optional title, written to the template resource and exported again by the bundle export. SeedSystemTemplateOptions gets title. The template overview shows it instead of the slug."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.375.0",
|
|
30
|
+
"type": "improvement",
|
|
31
|
+
"title": "Preview and test mail render with the values of a picked record",
|
|
32
|
+
"detail": "`template-resolver:query:preview` and `template-resolver:write:send-test` accept an optional `variables` object that replaces the declared example values for the named variables. The values are checked against the template's variable schema like a real send (unknown names and wrong types are rejected as `variables.<name>` validation errors); variables not passed keep their example. The mail editor now passes the values picked through the preview record picker into the preview and the test mail instead of only listing them."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"version": "0.375.0",
|
|
36
|
+
"type": "improvement",
|
|
37
|
+
"title": "Read a mail or PDF template as blocks and change it with structured operations",
|
|
38
|
+
"detail": "The query `template-resolver:query:document-blocks` returns a document template as blocks with stable ids (b0, b1, ...), its meta, variables, allowed block types and uploaded image keys, plus a `base` (scope and version). The write `template-resolver:write:patch-draft` applies up to 50 operations (insertBlock, replaceText, removeBlock, setResource, setMeta, setVariable) to that state and stores the result as the caller's tenant draft; an active version keeps being served until publish. Block ids refer to the state that was read. If the template changed since `base` was read, the call fails with `version_conflict` and writes nothing; a patch that removes a required placeholder or sets variables a notification declares is refused. `writeTemplateRow` accepts an optional `expectedVersion`. The universal helpers `placeholder-text` and the toolbar block sets moved out of `web/` without changing the web exports."
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "0.372.0",
|
|
4
42
|
"type": "improvement",
|
|
@@ -78,10 +78,15 @@ function valueExpression(
|
|
|
78
78
|
return `{{${path}}}`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Never interpolated as text: its value is a list of file ids.
|
|
82
|
+
function isImagesVariable(def: TemplateVariableDef | undefined): boolean {
|
|
83
|
+
return def?.type === "images";
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
function rewriteVariable(element: ScanElement, state: CompileState): void {
|
|
82
87
|
const name = element.getAttribute("data-variable") ?? "";
|
|
83
88
|
const replacement = element.ownerDocument.createTextNode(
|
|
84
|
-
isDocumentReferenceName(name)
|
|
89
|
+
isDocumentReferenceName(name) && !isImagesVariable(pathAndDef(name, element, state)[1])
|
|
85
90
|
? marker(
|
|
86
91
|
state,
|
|
87
92
|
valueExpression(
|
|
@@ -131,6 +136,8 @@ function wrapVariableOnlyBlock(block: ScanElement, state: CompileState): void {
|
|
|
131
136
|
// skip: a line with static content or several placeholders stays as it is
|
|
132
137
|
if (name === null) return;
|
|
133
138
|
const [path, def] = pathAndDef(name, block, state);
|
|
139
|
+
// skip: images blocks are rewritten by rewriteImagesBlock
|
|
140
|
+
if (isImagesVariable(def)) return;
|
|
134
141
|
const host =
|
|
135
142
|
def?.type === "richText" && block.tagName === "P" ? richTextBlockHost(block, state) : block;
|
|
136
143
|
const document = host.ownerDocument;
|
|
@@ -158,8 +165,8 @@ function rewriteLink(element: ScanElement, state: CompileState): void {
|
|
|
158
165
|
if (isDocumentPlaceholderHref(href)) {
|
|
159
166
|
const name = href.slice(2, -2);
|
|
160
167
|
if (isDocumentReferenceName(name)) {
|
|
161
|
-
const [path] = pathAndDef(name, element, state);
|
|
162
|
-
element.setAttribute("href", marker(state, `{{link ${path}}}`));
|
|
168
|
+
const [path, def] = pathAndDef(name, element, state);
|
|
169
|
+
element.setAttribute("href", isImagesVariable(def) ? "#" : marker(state, `{{link ${path}}}`));
|
|
163
170
|
} else {
|
|
164
171
|
element.setAttribute("href", "#");
|
|
165
172
|
}
|
|
@@ -225,8 +232,14 @@ export type CompileDocumentBodyOptions = {
|
|
|
225
232
|
readonly headingAsInlineStyle?: boolean;
|
|
226
233
|
// Bar color of reached stages in a steps block; ignored unless it is a #rrggbb hex.
|
|
227
234
|
readonly stepsColor?: string;
|
|
235
|
+
// Data URIs of the record's images for the images block bound to `variable`.
|
|
236
|
+
readonly recordImagesSrc?: (variable: string) => readonly string[];
|
|
237
|
+
// Previews and drafts never read the record's files; each images block shows one neutral box.
|
|
238
|
+
readonly recordImagesPlaceholder?: boolean;
|
|
228
239
|
};
|
|
229
240
|
|
|
241
|
+
const IMAGES_PLACEHOLDER_STYLE = "height:96px;margin:8px 0;border-radius:4px;background:#e5e7eb";
|
|
242
|
+
|
|
230
243
|
const MAIL_BUTTON_STYLE =
|
|
231
244
|
"display:inline-block;padding:10px 20px;border-radius:6px;background-color:#1f5468;color:#ffffff;font-weight:600;text-decoration:none";
|
|
232
245
|
|
|
@@ -258,6 +271,48 @@ export function collectDocumentResourceKeys(html: string): readonly string[] {
|
|
|
258
271
|
});
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
// Unique valid names of the images blocks in document order.
|
|
275
|
+
export function collectDocumentImageVariables(html: string): readonly string[] {
|
|
276
|
+
return withParsedDocument(html, (root) => {
|
|
277
|
+
const names = new Set<string>();
|
|
278
|
+
for (const element of Array.from(root.querySelectorAll("div[data-images]"))) {
|
|
279
|
+
const name = element.getAttribute("data-images") ?? "";
|
|
280
|
+
if (isDocumentReferenceName(name)) names.add(name);
|
|
281
|
+
}
|
|
282
|
+
return [...names];
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function rewriteImagesBlock(
|
|
287
|
+
element: ScanElement,
|
|
288
|
+
state: CompileState,
|
|
289
|
+
options: CompileDocumentBodyOptions,
|
|
290
|
+
): void {
|
|
291
|
+
const name = element.getAttribute("data-images") ?? "";
|
|
292
|
+
const declared =
|
|
293
|
+
isDocumentReferenceName(name) && isImagesVariable(ownVariable(state.variables, name));
|
|
294
|
+
if (!declared) {
|
|
295
|
+
element.remove();
|
|
296
|
+
// skip: an images block bound to anything but a declared images variable renders nothing
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const document = element.ownerDocument;
|
|
300
|
+
element.removeAttribute("data-images");
|
|
301
|
+
if (options.recordImagesPlaceholder === true) {
|
|
302
|
+
element.setAttribute("class", "kumiko-doc-image-placeholder");
|
|
303
|
+
element.setAttribute("style", IMAGES_PLACEHOLDER_STYLE);
|
|
304
|
+
// skip: preview and drafts never resolve record images
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
element.setAttribute("class", "kumiko-doc-images");
|
|
308
|
+
for (const src of options.recordImagesSrc?.(name) ?? []) {
|
|
309
|
+
const image = document.createElement("img");
|
|
310
|
+
image.setAttribute("src", src);
|
|
311
|
+
image.setAttribute("alt", "");
|
|
312
|
+
element.append(image);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
261
316
|
export function toResourceDataUri(contentType: string, bytes: Uint8Array): string {
|
|
262
317
|
if (!isResourceImageContentType(contentType)) {
|
|
263
318
|
throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
|
|
@@ -374,6 +429,9 @@ export function compileDocumentBodyToHandlebars(
|
|
|
374
429
|
color: options.stepsColor,
|
|
375
430
|
});
|
|
376
431
|
}
|
|
432
|
+
for (const element of Array.from(root.querySelectorAll("div[data-images]"))) {
|
|
433
|
+
rewriteImagesBlock(element, state, options);
|
|
434
|
+
}
|
|
377
435
|
for (const block of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
378
436
|
rewriteConditionalBlock(block, state);
|
|
379
437
|
}
|
|
@@ -99,3 +99,6 @@ export { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
99
99
|
// Default-Locale wenn Tenant keinen eigenen Default konfiguriert. Resolver
|
|
100
100
|
// fällt darauf zurück wenn requested locale + tenant-default fehlen.
|
|
101
101
|
export const FALLBACK_LOCALE = "de";
|
|
102
|
+
|
|
103
|
+
// A collection mounted without `access` is maintained by the platform only; TenantAdmin access is an explicit mount decision.
|
|
104
|
+
export const DEFAULT_COLLECTION_ACCESS = { roles: ["SystemAdmin"] } as const;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
export type DocumentToolbarBlock =
|
|
3
|
+
| "image"
|
|
4
|
+
| "table"
|
|
5
|
+
| "button"
|
|
6
|
+
| "divider"
|
|
7
|
+
| "repeat"
|
|
8
|
+
| "pageBreak"
|
|
9
|
+
| "align"
|
|
10
|
+
| "steps"
|
|
11
|
+
| "images";
|
|
12
|
+
|
|
13
|
+
export const MAIL_DOCUMENT_BLOCKS = [
|
|
14
|
+
"image",
|
|
15
|
+
"table",
|
|
16
|
+
"button",
|
|
17
|
+
"divider",
|
|
18
|
+
"repeat",
|
|
19
|
+
"steps",
|
|
20
|
+
] as const satisfies readonly DocumentToolbarBlock[];
|
|
21
|
+
|
|
22
|
+
export const PDF_DOCUMENT_BLOCKS = [
|
|
23
|
+
"align",
|
|
24
|
+
"image",
|
|
25
|
+
"table",
|
|
26
|
+
"divider",
|
|
27
|
+
"repeat",
|
|
28
|
+
"steps",
|
|
29
|
+
"images",
|
|
30
|
+
"pageBreak",
|
|
31
|
+
] as const satisfies readonly DocumentToolbarBlock[];
|
|
32
|
+
|
|
33
|
+
/** Block set for a template-resource kind; every kind except "document-pdf" gets the mail set. */
|
|
34
|
+
export function documentBlocksForKind(kind: string | undefined): readonly DocumentToolbarBlock[] {
|
|
35
|
+
return kind === "document-pdf" ? PDF_DOCUMENT_BLOCKS : MAIL_DOCUMENT_BLOCKS;
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// The record's photos: a block bound to an `images` variable whose FileRef ids the
|
|
3
|
+
// app passes at render time. Universal like document-schema.ts.
|
|
4
|
+
import { Node } from "@tiptap/core";
|
|
5
|
+
|
|
6
|
+
export function createDocumentImagesNode(isReferenceName: (value: string) => boolean) {
|
|
7
|
+
return Node.create({
|
|
8
|
+
name: "images",
|
|
9
|
+
group: "block",
|
|
10
|
+
atom: true,
|
|
11
|
+
addAttributes() {
|
|
12
|
+
return { variable: { default: null } };
|
|
13
|
+
},
|
|
14
|
+
parseHTML() {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
tag: "div[data-images]",
|
|
18
|
+
getAttrs: (element) => {
|
|
19
|
+
const variable = element.getAttribute("data-images");
|
|
20
|
+
return variable !== null && isReferenceName(variable) ? { variable } : false;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
renderHTML({ node }) {
|
|
26
|
+
return ["div", { "data-images": node.attrs["variable"] }];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|