@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- 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-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- 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 +9 -3
- 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 +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- 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/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- 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/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- 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 +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- 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/read-file-ref-content.ts +6 -3
- 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/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -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/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 +56 -18
- 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/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- 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 +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- 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/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- 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 +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +18 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- 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 +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
|
@@ -24,12 +24,24 @@ const PLACEHOLDER_VALIDATED_KINDS: ReadonlySet<TemplateKind> = new Set([
|
|
|
24
24
|
"pdf-layout",
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
// The parser's wording is English and meant for logs; the UI gets a key per setting instead.
|
|
28
|
+
const META_FIELDS_WITH_MESSAGES: ReadonlySet<string> = new Set([
|
|
29
|
+
"subject",
|
|
30
|
+
"previewText",
|
|
31
|
+
"senderName",
|
|
32
|
+
"replyTo",
|
|
33
|
+
"layoutSlug",
|
|
34
|
+
"fileName",
|
|
35
|
+
]);
|
|
36
|
+
|
|
27
37
|
function metaIssueToFieldIssue(issue: TemplateMetaIssue): FieldIssue {
|
|
38
|
+
const hasMessage = META_FIELDS_WITH_MESSAGES.has(issue.field);
|
|
28
39
|
return {
|
|
29
40
|
path: issue.path,
|
|
30
41
|
code: "invalid_meta",
|
|
31
|
-
i18nKey:
|
|
32
|
-
|
|
42
|
+
i18nKey: hasMessage
|
|
43
|
+
? `templateResolver.errors.template.meta.${issue.field}.${issue.missing ? "required" : "invalid"}`
|
|
44
|
+
: "templateResolver.errors.template.invalidMeta",
|
|
33
45
|
};
|
|
34
46
|
}
|
|
35
47
|
|
|
@@ -17,6 +17,7 @@ import { requireForTenant } from "../../shared/index.js";
|
|
|
17
17
|
import type { ContentFormat, TemplateKind } from "../constants.js";
|
|
18
18
|
import type { TemplateWriteScope } from "../layout-regions.js";
|
|
19
19
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
20
|
+
import { activationChanges, serializeTemplateDraft } from "../template-draft.js";
|
|
20
21
|
import type { TemplateValidation } from "../template-validation.js";
|
|
21
22
|
import { executor } from "./shared.js";
|
|
22
23
|
import {
|
|
@@ -96,6 +97,42 @@ export function gateTemplateWrite(
|
|
|
96
97
|
return { ok: true, evaluated };
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
type TemplateRowFields = {
|
|
101
|
+
readonly content: string | null;
|
|
102
|
+
readonly contentFormat: ContentFormat;
|
|
103
|
+
readonly variableSchema: string;
|
|
104
|
+
readonly meta: string | null;
|
|
105
|
+
readonly title: string | null;
|
|
106
|
+
readonly folder: string | null;
|
|
107
|
+
readonly parentTemplateId: string | null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// A draft over an active row is stored beside it; every other write replaces the main fields.
|
|
111
|
+
function updateChanges(
|
|
112
|
+
existing: TemplateResourceRow,
|
|
113
|
+
status: "draft" | "active",
|
|
114
|
+
fields: TemplateRowFields,
|
|
115
|
+
): Record<string, unknown> {
|
|
116
|
+
if (status === "active") return { ...fields, ...activationChanges(existing) };
|
|
117
|
+
if (existing.status !== "active") return { ...fields, status, draft: null };
|
|
118
|
+
const draft = serializeTemplateDraft({
|
|
119
|
+
content: fields.content,
|
|
120
|
+
contentFormat: fields.contentFormat,
|
|
121
|
+
variableSchema: fields.variableSchema,
|
|
122
|
+
meta: fields.meta,
|
|
123
|
+
title: fields.title,
|
|
124
|
+
folder: fields.folder,
|
|
125
|
+
parentTemplateId: fields.parentTemplateId,
|
|
126
|
+
});
|
|
127
|
+
return existing.activeVersion === null
|
|
128
|
+
? {
|
|
129
|
+
draft,
|
|
130
|
+
activeVersion: existing.version,
|
|
131
|
+
activeSince: existing.modifiedAt ?? existing.insertedAt,
|
|
132
|
+
}
|
|
133
|
+
: { draft };
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
// Shared by every template write: validate through the one pipeline, then
|
|
100
137
|
// create or update the (tenant, slug, kind, locale) row via the event-store
|
|
101
138
|
// executor. linkedResources is never part of an update; resources are attached
|
|
@@ -146,7 +183,11 @@ export async function writeTemplateRow(
|
|
|
146
183
|
|
|
147
184
|
if (existing) {
|
|
148
185
|
const result = await executor.update(
|
|
149
|
-
{
|
|
186
|
+
{
|
|
187
|
+
id: existing.id,
|
|
188
|
+
version: existing.version,
|
|
189
|
+
changes: updateChanges(existing, status, fields),
|
|
190
|
+
},
|
|
150
191
|
user,
|
|
151
192
|
db,
|
|
152
193
|
{ streamTenantId },
|
|
@@ -163,9 +204,18 @@ export async function writeTemplateRow(
|
|
|
163
204
|
};
|
|
164
205
|
}
|
|
165
206
|
|
|
166
|
-
const result = await executor.create(
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
const result = await executor.create(
|
|
208
|
+
{
|
|
209
|
+
...fields,
|
|
210
|
+
linkedResources: "{}",
|
|
211
|
+
tenantId,
|
|
212
|
+
draft: null,
|
|
213
|
+
...(status === "active" ? activationChanges(null) : {}),
|
|
214
|
+
},
|
|
215
|
+
user,
|
|
216
|
+
db,
|
|
217
|
+
{ streamTenantId },
|
|
218
|
+
);
|
|
169
219
|
if (!result.isSuccess) return result;
|
|
170
220
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
171
221
|
// INSERT RETURNING; shape { id } ist garantiert weil PK in der
|
|
@@ -3,14 +3,74 @@ import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framew
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TenantQueries } from "../../tenant/index.js";
|
|
5
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
+
import { servedVersionOf } from "../template-draft.js";
|
|
7
|
+
import { buildOverrideDiff } from "./override-diff.js";
|
|
6
8
|
import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
7
9
|
|
|
8
10
|
const TENANT_OVERRIDES_REASON =
|
|
9
|
-
"SystemAdmin template overview
|
|
11
|
+
"SystemAdmin template overview lists which tenants override one system template; it reads tenant-scope template rows of every tenant (and the system row for the optional per-tenant diff), which the tenant-scoped db cannot reach";
|
|
10
12
|
|
|
11
13
|
const tenantIdsSchema = z.array(z.string());
|
|
14
|
+
const directorySchema = z.object({
|
|
15
|
+
rows: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
type OverrideStatus = "active" | "draft";
|
|
19
|
+
|
|
20
|
+
function labelsOf(raw: unknown): ReadonlyMap<string, string> {
|
|
21
|
+
return new Map(directorySchema.parse(raw).rows.map((row) => [row.id, row.label]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// One row per tenant: its active override wins over a draft, then the newest version.
|
|
25
|
+
function isBetterRow(candidate: TemplateResourceRow, current: TemplateResourceRow): boolean {
|
|
26
|
+
if ((candidate.status === "active") !== (current.status === "active")) {
|
|
27
|
+
return candidate.status === "active";
|
|
28
|
+
}
|
|
29
|
+
return candidate.version > current.version;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statusOf(row: TemplateResourceRow): OverrideStatus {
|
|
33
|
+
return row.status === "active" ? "active" : "draft";
|
|
34
|
+
}
|
|
12
35
|
|
|
13
|
-
|
|
36
|
+
function isCountedTenantRow(
|
|
37
|
+
row: TemplateResourceRow,
|
|
38
|
+
enabledTenantIds: ReadonlySet<string> | null,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (row.tenantId === SYSTEM_TENANT_ID || row.status === "archived") return false;
|
|
41
|
+
return enabledTenantIds === null || enabledTenantIds.has(row.tenantId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickBestRowPerTenant(
|
|
45
|
+
found: readonly TemplateResourceRow[],
|
|
46
|
+
enabledTenantIds: ReadonlySet<string> | null,
|
|
47
|
+
): Map<string, TemplateResourceRow> {
|
|
48
|
+
const bestByTenant = new Map<string, TemplateResourceRow>();
|
|
49
|
+
for (const row of found) {
|
|
50
|
+
if (!isCountedTenantRow(row, enabledTenantIds)) continue;
|
|
51
|
+
const current = bestByTenant.get(row.tenantId);
|
|
52
|
+
if (current === undefined || isBetterRow(row, current)) bestByTenant.set(row.tenantId, row);
|
|
53
|
+
}
|
|
54
|
+
return bestByTenant;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toOverrideRow(
|
|
58
|
+
row: TemplateResourceRow,
|
|
59
|
+
tenantLabels: ReadonlyMap<string, string>,
|
|
60
|
+
userLabels: ReadonlyMap<string, string>,
|
|
61
|
+
) {
|
|
62
|
+
const changedById = row.modifiedById ?? row.insertedById;
|
|
63
|
+
return {
|
|
64
|
+
tenantId: row.tenantId,
|
|
65
|
+
tenantName: tenantLabels.get(row.tenantId) ?? row.tenantId,
|
|
66
|
+
templateId: String(row.id),
|
|
67
|
+
locale: row.locale,
|
|
68
|
+
status: statusOf(row),
|
|
69
|
+
version: servedVersionOf(row),
|
|
70
|
+
changedAt: (row.modifiedAt ?? row.insertedAt).toString(),
|
|
71
|
+
changedBy: changedById === null ? null : (userLabels.get(changedById) ?? null),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
14
74
|
|
|
15
75
|
export const tenantOverridesQuery = defineQueryHandler({
|
|
16
76
|
name: "tenant-overrides",
|
|
@@ -18,14 +78,17 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
18
78
|
slug: templateSlugSchema,
|
|
19
79
|
kind: kindSchema,
|
|
20
80
|
locale: localeSchema.optional(),
|
|
81
|
+
/** Adds the block diff of this tenant's override against the system template. */
|
|
82
|
+
diffTenantId: z.string().min(1).optional(),
|
|
21
83
|
}),
|
|
22
84
|
access: { roles: ["SystemAdmin"] },
|
|
23
85
|
description:
|
|
24
|
-
"
|
|
86
|
+
"Lists the enabled tenants that override one template with an active or a draft version (tenant name, status, version, last change and who made it), next to the counts and the number of enabled tenants; use it before changing a system template to see who keeps their own copy. With diffTenantId it also returns the block diff of that tenant's override against the system template.",
|
|
25
87
|
escapeHatch: { grants: ["unsafeRaw"], reason: TENANT_OVERRIDES_REASON },
|
|
26
88
|
handler: async (query, ctx) => {
|
|
27
|
-
const { slug, kind, locale } = query.payload;
|
|
28
|
-
const
|
|
89
|
+
const { slug, kind, locale, diffTenantId } = query.payload;
|
|
90
|
+
const raw = ctx.db.unsafeRaw();
|
|
91
|
+
const found = await selectMany<TemplateResourceRow>(raw, templateResourcesTable, {
|
|
29
92
|
slug,
|
|
30
93
|
kind,
|
|
31
94
|
scope: "tenant",
|
|
@@ -38,18 +101,37 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
38
101
|
? new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})))
|
|
39
102
|
: null;
|
|
40
103
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
|
|
105
|
+
const best = [...bestByTenant.values()];
|
|
106
|
+
const tenantLabels = hasTenantFeature
|
|
107
|
+
? labelsOf(await ctx.query(TenantQueries.tenantDirectory, {}))
|
|
108
|
+
: new Map<string, string>();
|
|
109
|
+
const userLabels = hasTenantFeature
|
|
110
|
+
? labelsOf(await ctx.query(TenantQueries.memberDirectory, {}))
|
|
111
|
+
: new Map<string, string>();
|
|
112
|
+
|
|
113
|
+
const rows = best
|
|
114
|
+
.map((row) => toOverrideRow(row, tenantLabels, userLabels))
|
|
115
|
+
.sort((a, b) => a.tenantName.localeCompare(b.tenantName));
|
|
116
|
+
|
|
117
|
+
let diff: ReturnType<typeof buildOverrideDiff> | null = null;
|
|
118
|
+
const diffRow = diffTenantId === undefined ? undefined : bestByTenant.get(diffTenantId);
|
|
119
|
+
if (diffRow !== undefined) {
|
|
120
|
+
const [systemRow] = await selectMany<TemplateResourceRow>(raw, templateResourcesTable, {
|
|
121
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
122
|
+
slug,
|
|
123
|
+
kind,
|
|
124
|
+
locale: diffRow.locale,
|
|
125
|
+
});
|
|
126
|
+
if (systemRow !== undefined) diff = buildOverrideDiff(diffRow, systemRow);
|
|
47
127
|
}
|
|
48
|
-
|
|
128
|
+
|
|
49
129
|
return {
|
|
50
|
-
activeOverrides:
|
|
51
|
-
draftOverrides:
|
|
130
|
+
activeOverrides: best.filter((row) => row.status === "active").length,
|
|
131
|
+
draftOverrides: best.filter((row) => row.status !== "active").length,
|
|
52
132
|
totalTenants: enabledTenantIds === null ? null : enabledTenantIds.size,
|
|
133
|
+
rows,
|
|
134
|
+
diff,
|
|
53
135
|
};
|
|
54
136
|
},
|
|
55
137
|
});
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import {
|
|
3
3
|
defineWriteHandler,
|
|
4
|
-
type
|
|
4
|
+
type HandlerContext,
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import {
|
|
8
|
-
type FieldIssue,
|
|
9
8
|
NotFoundError,
|
|
10
9
|
ValidationError,
|
|
10
|
+
type WriteFailure,
|
|
11
11
|
writeFailure,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
13
13
|
import * as z from "zod";
|
|
14
14
|
import { requireForTenant } from "../../shared/index.js";
|
|
15
15
|
import type { TemplateResourceRow } from "../table.js";
|
|
16
16
|
import { templateResourcesTable } from "../table.js";
|
|
17
|
+
import { activationChanges, parseTemplateDraft, toWorkingRow } from "../template-draft.js";
|
|
17
18
|
import { executor } from "./shared.js";
|
|
18
19
|
import { publishGateIssues } from "./template-gates.js";
|
|
19
20
|
|
|
20
21
|
type TemplateStatus = "active" | "archived";
|
|
21
|
-
type StatusTarget = "own" | "system";
|
|
22
|
+
export type StatusTarget = "own" | "system";
|
|
22
23
|
|
|
23
24
|
function isRowOfTarget(row: TemplateResourceRow, target: StatusTarget, callerTenantId: string) {
|
|
24
25
|
return target === "system"
|
|
@@ -26,41 +27,69 @@ function isRowOfTarget(row: TemplateResourceRow, target: StatusTarget, callerTen
|
|
|
26
27
|
: row.tenantId === callerTenantId && row.scope === "tenant";
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export function writeAccessFor(target: StatusTarget) {
|
|
31
|
+
return { roles: target === "system" ? ["SystemAdmin"] : ["TenantAdmin", "SystemAdmin"] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The tenant db also reads system rows; the row's own tenant must match the
|
|
35
|
+
// target or the executor would append to a foreign event stream.
|
|
36
|
+
export async function loadTargetRow(
|
|
37
|
+
ctx: HandlerContext,
|
|
38
|
+
target: StatusTarget,
|
|
39
|
+
user: { readonly tenantId: string },
|
|
40
|
+
id: string,
|
|
41
|
+
) {
|
|
42
|
+
const scoped = target === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID) : null;
|
|
43
|
+
const db = scoped?.db ?? ctx.db;
|
|
44
|
+
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, { id });
|
|
45
|
+
return { db, scoped, row: row && isRowOfTarget(row, target, user.tenantId) ? row : null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type StatusPlan =
|
|
49
|
+
| { readonly ok: true; readonly changes: Record<string, unknown> }
|
|
50
|
+
| { readonly ok: false; readonly failure: WriteFailure };
|
|
51
|
+
|
|
52
|
+
// Publishing promotes an open draft (gated as the version that goes live); a row
|
|
53
|
+
// without one is activated as stored. Archiving leaves the draft standing.
|
|
54
|
+
function planStatusChange(
|
|
55
|
+
ctx: HandlerContext,
|
|
56
|
+
row: TemplateResourceRow,
|
|
57
|
+
status: TemplateStatus,
|
|
58
|
+
): StatusPlan {
|
|
59
|
+
if (status === "archived") return { ok: true, changes: { status } };
|
|
60
|
+
const issues = publishGateIssues(ctx.registry, toWorkingRow(row, row.tenantId));
|
|
61
|
+
if (issues.length > 0) {
|
|
62
|
+
return { ok: false, failure: writeFailure(new ValidationError({ fields: [...issues] })) };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, changes: { ...parseTemplateDraft(row.draft), ...activationChanges(row) } };
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
function createStatusUpdateHandler(
|
|
30
68
|
name: string,
|
|
31
69
|
target: StatusTarget,
|
|
32
70
|
status: TemplateStatus,
|
|
33
71
|
description: string,
|
|
34
|
-
gate?: (registry: Registry, row: TemplateResourceRow) => readonly FieldIssue[],
|
|
35
72
|
) {
|
|
36
73
|
return defineWriteHandler({
|
|
37
74
|
name,
|
|
38
75
|
schema: z.object({ id: z.string().min(1) }),
|
|
39
|
-
access:
|
|
76
|
+
access: writeAccessFor(target),
|
|
40
77
|
// A system template change reaches every tenant without an override.
|
|
41
78
|
...(target === "system" ? { agent: { risk: "high" as const } } : {}),
|
|
42
79
|
description,
|
|
43
80
|
handler: async (event, ctx) => {
|
|
44
|
-
const scoped
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const existing = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
49
|
-
id: event.payload.id,
|
|
50
|
-
});
|
|
51
|
-
if (!existing || !isRowOfTarget(existing, target, event.user.tenantId)) {
|
|
52
|
-
return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
53
|
-
}
|
|
54
|
-
const issues = gate?.(ctx.registry, existing) ?? [];
|
|
55
|
-
if (issues.length > 0) return writeFailure(new ValidationError({ fields: [...issues] }));
|
|
81
|
+
const { db, scoped, row } = await loadTargetRow(ctx, target, event.user, event.payload.id);
|
|
82
|
+
if (!row) return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
83
|
+
const plan = planStatusChange(ctx, row, status);
|
|
84
|
+
if (!plan.ok) return plan.failure;
|
|
56
85
|
const result = await executor.update(
|
|
57
|
-
{ id:
|
|
86
|
+
{ id: row.id, version: row.version, changes: plan.changes },
|
|
58
87
|
event.user,
|
|
59
88
|
db,
|
|
60
89
|
scoped ? { streamTenantId: scoped.streamTenantId } : undefined,
|
|
61
90
|
);
|
|
62
91
|
if (!result.isSuccess) return result;
|
|
63
|
-
return { isSuccess: true as const, data: { id: String(
|
|
92
|
+
return { isSuccess: true as const, data: { id: String(row.id), status } };
|
|
64
93
|
},
|
|
65
94
|
});
|
|
66
95
|
}
|
|
@@ -69,14 +98,13 @@ export const archiveWrite = createStatusUpdateHandler(
|
|
|
69
98
|
"archive",
|
|
70
99
|
"own",
|
|
71
100
|
"archived",
|
|
72
|
-
"Sets one template resource of the caller's tenant to archived so the resolver stops serving it while the row and its content survive; use it to retire a template reversibly instead of deleting it.",
|
|
101
|
+
"Sets one template resource of the caller's tenant to archived so the resolver stops serving it while the row and its content survive; use it to retire a template reversibly instead of deleting it. An open draft stays.",
|
|
73
102
|
);
|
|
74
103
|
export const publishWrite = createStatusUpdateHandler(
|
|
75
104
|
"publish",
|
|
76
105
|
"own",
|
|
77
106
|
"active",
|
|
78
|
-
"Sets one template resource of the caller's tenant to active so the resolver starts serving it; use it to release a draft written by upsert-tenant, or to bring an archived template back; a
|
|
79
|
-
publishGateIssues,
|
|
107
|
+
"Sets one template resource of the caller's tenant to active so the resolver starts serving it; use it to release a draft written by upsert-tenant (a draft saved beside an active version replaces it), or to bring an archived template back; a version with unknown placeholders or unused required variables is refused and the served version stays.",
|
|
80
108
|
);
|
|
81
109
|
export const archiveSystemWrite = createStatusUpdateHandler(
|
|
82
110
|
"archive-system",
|
|
@@ -88,6 +116,5 @@ export const publishSystemWrite = createStatusUpdateHandler(
|
|
|
88
116
|
"publish-system",
|
|
89
117
|
"system",
|
|
90
118
|
"active",
|
|
91
|
-
"Sets one system-default template to active so the resolver serves it again to every tenant that has no override; SystemAdmin only, a
|
|
92
|
-
publishGateIssues,
|
|
119
|
+
"Sets one system-default template to active so the resolver serves it again to every tenant that has no override; an open draft beside the active version replaces it; SystemAdmin only, a version with unknown placeholders or unused required variables is refused.",
|
|
93
120
|
);
|
|
@@ -17,7 +17,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
17
17
|
}),
|
|
18
18
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
19
|
description:
|
|
20
|
-
"Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live; it refuses a system-tenant target, which is what upsert-system is for.",
|
|
20
|
+
"Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live (over an active override the draft is stored beside it, the active version keeps being served until publish); it refuses a system-tenant target, which is what upsert-system is for.",
|
|
21
21
|
handler: async (event, ctx) => {
|
|
22
22
|
// @cast-boundary engine-payload — override aus Zod-parsed string,
|
|
23
23
|
// event.user.tenantId schon TenantId-branded; union als TenantId casten
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import { TEMPLATE_KINDS } from "../constants.js";
|
|
5
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
4
6
|
import { templateSlugSchema } from "../template-meta.js";
|
|
5
7
|
import { collectTemplateUsage } from "../template-usage.js";
|
|
8
|
+
import { effectiveMailLayoutSlugs } from "./overview.query.js";
|
|
9
|
+
|
|
10
|
+
const MAIL_ROW_LIMIT = 5000;
|
|
6
11
|
|
|
7
12
|
export const usageQuery = defineQueryHandler({
|
|
8
13
|
name: "usage",
|
|
9
14
|
schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
|
|
10
15
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
11
16
|
description:
|
|
12
|
-
"Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug, a mail layout by every mail notification
|
|
17
|
+
"Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
|
|
13
18
|
handler: async (query, ctx) => {
|
|
14
19
|
const { slug, kind } = query.payload;
|
|
15
|
-
return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
|
|
20
|
+
if (kind !== "mail-layout") return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
|
|
21
|
+
const mailRows = await selectMany<TemplateResourceRow>(
|
|
22
|
+
ctx.db,
|
|
23
|
+
templateResourcesTable,
|
|
24
|
+
{ kind: "mail" },
|
|
25
|
+
{ limit: MAIL_ROW_LIMIT },
|
|
26
|
+
);
|
|
27
|
+
return {
|
|
28
|
+
usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(mailRows)),
|
|
29
|
+
};
|
|
16
30
|
},
|
|
17
31
|
});
|
|
@@ -10,6 +10,7 @@ import type { ContentFormat, UpsertKind } from "../constants.js";
|
|
|
10
10
|
import type { TemplateWriteScope } from "../layout-regions.js";
|
|
11
11
|
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
12
12
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
13
|
+
import { isSystemAdmin } from "../template-draft.js";
|
|
13
14
|
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
|
|
14
15
|
import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
15
16
|
import { evaluateTemplateWrite } from "./template-gates.js";
|
|
@@ -53,7 +54,9 @@ export async function validateTemplateDraft(
|
|
|
53
54
|
payload: TemplateDraftInput,
|
|
54
55
|
) {
|
|
55
56
|
const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
|
|
56
|
-
const { slug, kind,
|
|
57
|
+
const { slug, kind, locale } = payload;
|
|
58
|
+
// The dry run must not promise what the caller's write handlers would refuse.
|
|
59
|
+
const scope: TemplateWriteScope = isSystemAdmin(user) ? payload.scope : "tenant";
|
|
57
60
|
const known = [
|
|
58
61
|
...(await knownResourceKeys(ctx.db, {
|
|
59
62
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -16,12 +16,18 @@ export {
|
|
|
16
16
|
CONTENT_FORMATS,
|
|
17
17
|
type ContentFormat,
|
|
18
18
|
DEFAULT_MAIL_LAYOUT_SLUG,
|
|
19
|
+
DEFAULT_PDF_LAYOUT_SLUG,
|
|
19
20
|
DOCUMENT_HTML_KINDS,
|
|
20
21
|
type DocumentHtmlKind,
|
|
21
22
|
FALLBACK_LOCALE,
|
|
22
23
|
NON_RENDER_KINDS,
|
|
23
24
|
type NonRenderKind,
|
|
25
|
+
PDF_DEFAULT_MARGIN_MM,
|
|
26
|
+
PDF_FOOTER_MARGIN_MM,
|
|
27
|
+
PDF_HEADER_MARGIN_MM,
|
|
28
|
+
PDF_ORIENTATIONS,
|
|
24
29
|
PDF_PAGE_FORMATS,
|
|
30
|
+
type PdfOrientation,
|
|
25
31
|
type PdfPageFormat,
|
|
26
32
|
RENDER_KINDS,
|
|
27
33
|
type RenderKind,
|
|
@@ -52,7 +58,6 @@ export {
|
|
|
52
58
|
DocumentDivider,
|
|
53
59
|
DocumentLink,
|
|
54
60
|
DocumentPageBreak,
|
|
55
|
-
DocumentRepeatRow,
|
|
56
61
|
DocumentResource,
|
|
57
62
|
DocumentTable,
|
|
58
63
|
DocumentTableCell,
|
|
@@ -77,6 +82,11 @@ export type {
|
|
|
77
82
|
OverviewUsage,
|
|
78
83
|
} from "./handlers/overview.query.js";
|
|
79
84
|
export { localeSchema } from "./handlers/shared.js";
|
|
85
|
+
export {
|
|
86
|
+
isHexColor,
|
|
87
|
+
type MailLayoutAppearance,
|
|
88
|
+
mailLayoutAppearance,
|
|
89
|
+
} from "./layout-appearance.js";
|
|
80
90
|
export {
|
|
81
91
|
type LayoutRegions,
|
|
82
92
|
readLayoutRegions,
|
|
@@ -101,9 +111,11 @@ export {
|
|
|
101
111
|
} from "./qualified-names.js";
|
|
102
112
|
export { mapRecordToVariables } from "./record-mapping.js";
|
|
103
113
|
export {
|
|
114
|
+
type PreparedTemplateImage,
|
|
115
|
+
prepareTemplateResourceBeforeTransaction,
|
|
104
116
|
readTemplateResourceMaxBytes,
|
|
105
117
|
type StoredTemplateResource,
|
|
106
|
-
|
|
118
|
+
storePreparedTemplateResource,
|
|
107
119
|
} from "./store-template-resource.js";
|
|
108
120
|
export {
|
|
109
121
|
type TemplateResourceRow,
|
|
@@ -115,11 +127,15 @@ export {
|
|
|
115
127
|
isMailTemplateMeta,
|
|
116
128
|
type LayoutTemplateMeta,
|
|
117
129
|
MAIL_CATEGORIES,
|
|
130
|
+
MAIL_TONES,
|
|
118
131
|
type MailCategory,
|
|
132
|
+
type MailLayoutMeta,
|
|
119
133
|
type MailTemplateMeta,
|
|
134
|
+
type MailTone,
|
|
120
135
|
type PdfTemplateMeta,
|
|
121
136
|
parseTemplateMeta,
|
|
122
137
|
pdfAttachmentFileNamePatternSchema,
|
|
138
|
+
readMailLayoutMeta,
|
|
123
139
|
readPdfTemplateMeta,
|
|
124
140
|
type TemplateMeta,
|
|
125
141
|
type TemplateMetaIssue,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HEX_COLOR_PATTERN } from "./constants.js";
|
|
2
|
+
import { type MailLayoutMeta, readMailLayoutMeta } from "./template-meta.js";
|
|
3
|
+
|
|
4
|
+
// Every value is a member of a closed set, so it can be written into MJML and inline styles as is.
|
|
5
|
+
export type MailLayoutAppearance = {
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly fontFamily: string;
|
|
8
|
+
readonly outerBackground: string;
|
|
9
|
+
readonly accentLine: boolean;
|
|
10
|
+
readonly buttonStyle: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FONT_FAMILIES = {
|
|
14
|
+
sans: "Arial, Helvetica, sans-serif",
|
|
15
|
+
serif: "Georgia, 'Times New Roman', serif",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_BUTTON_COLOR = "#1f5468";
|
|
19
|
+
const BUTTON_BASE = "display:inline-block;padding:10px 20px;font-weight:600;text-decoration:none";
|
|
20
|
+
|
|
21
|
+
type ButtonStyle = NonNullable<MailLayoutMeta["buttonStyle"]>;
|
|
22
|
+
|
|
23
|
+
function buttonStyleCss(style: ButtonStyle, color: string): string {
|
|
24
|
+
switch (style) {
|
|
25
|
+
case "filled-rounded":
|
|
26
|
+
return `${BUTTON_BASE};border-radius:6px;background-color:${color};color:#ffffff`;
|
|
27
|
+
case "filled-square":
|
|
28
|
+
return `${BUTTON_BASE};background-color:${color};color:#ffffff`;
|
|
29
|
+
case "outline":
|
|
30
|
+
return `${BUTTON_BASE};border:2px solid ${color};border-radius:6px;color:${color}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isHexColor(value: string): boolean {
|
|
35
|
+
return HEX_COLOR_PATTERN.test(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The branding accent colors the buttons only where the layout opted into the branding accent line.
|
|
39
|
+
export function mailLayoutAppearance(
|
|
40
|
+
meta: MailLayoutMeta | unknown,
|
|
41
|
+
brandingAccentColor?: string,
|
|
42
|
+
): MailLayoutAppearance {
|
|
43
|
+
const parsed = readMailLayoutMeta(meta);
|
|
44
|
+
const accentLine = (parsed.accentLine ?? "none") === "branding";
|
|
45
|
+
const buttonColor =
|
|
46
|
+
accentLine && brandingAccentColor !== undefined && isHexColor(brandingAccentColor)
|
|
47
|
+
? brandingAccentColor
|
|
48
|
+
: DEFAULT_BUTTON_COLOR;
|
|
49
|
+
return {
|
|
50
|
+
width: parsed.width ?? 600,
|
|
51
|
+
fontFamily: FONT_FAMILIES[parsed.font ?? "sans"],
|
|
52
|
+
outerBackground: parsed.outerBackground ?? "#f5f5f5",
|
|
53
|
+
accentLine,
|
|
54
|
+
buttonStyle: buttonStyleCss(parsed.buttonStyle ?? "filled-rounded", buttonColor),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// No imports: web/ re-exports these, and constants.ts pulls in the framework engine.
|
|
3
|
+
export const PDF_PAGE_FORMATS = ["A4", "Letter"] as const;
|
|
4
|
+
export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
|
|
5
|
+
|
|
6
|
+
export const PDF_ORIENTATIONS = ["portrait", "landscape"] as const;
|
|
7
|
+
export type PdfOrientation = (typeof PDF_ORIENTATIONS)[number];
|
|
8
|
+
|
|
9
|
+
// Page margins in mm; the header/footer values apply when the layout contributes that region and the template sets no margin itself.
|
|
10
|
+
export const PDF_DEFAULT_MARGIN_MM = 15;
|
|
11
|
+
export const PDF_HEADER_MARGIN_MM = 30;
|
|
12
|
+
export const PDF_FOOTER_MARGIN_MM = 20;
|
|
@@ -9,6 +9,8 @@ export const TemplateResolverHandlers = {
|
|
|
9
9
|
archive: "template-resolver:write:archive",
|
|
10
10
|
publishSystem: "template-resolver:write:publish-system",
|
|
11
11
|
archiveSystem: "template-resolver:write:archive-system",
|
|
12
|
+
discardDraft: "template-resolver:write:discard-draft",
|
|
13
|
+
discardDraftSystem: "template-resolver:write:discard-draft-system",
|
|
12
14
|
resetToSystem: "template-resolver:write:reset-to-system",
|
|
13
15
|
set: "template-resolver:write:set",
|
|
14
16
|
importBundle: "template-resolver:write:import-bundle",
|
|
@@ -53,10 +55,12 @@ export const TemplateResolverErrors = {
|
|
|
53
55
|
invalidSlug: "invalid_slug",
|
|
54
56
|
invalidLocale: "invalid_locale",
|
|
55
57
|
systemAdminRequired: "system_admin_required",
|
|
58
|
+
noDraft: "no_draft",
|
|
56
59
|
alreadyActive: "template_already_active",
|
|
57
60
|
alreadyArchived: "template_already_archived",
|
|
58
61
|
targetExists: "target_exists",
|
|
59
62
|
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
63
|
+
bundleLocalesDiffer: "bundle_locales_differ",
|
|
60
64
|
resourceTooLarge: "template_resource_too_large",
|
|
61
65
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
62
66
|
attachmentUnsupportedType: "template_attachment_unsupported_type",
|