@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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type NotificationDefinition,
|
|
3
|
+
parseQn,
|
|
4
|
+
type Registry,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
6
|
import { DEFAULT_MAIL_LAYOUT_SLUG, type TemplateKind } from "./constants.js";
|
|
3
7
|
|
|
4
8
|
type UsageRole = "template" | "layout";
|
|
@@ -11,27 +15,43 @@ export type TemplateUsageEntry = {
|
|
|
11
15
|
readonly role: UsageRole;
|
|
12
16
|
};
|
|
13
17
|
|
|
18
|
+
/** Layout slug of the effective stored mail template, by notification qualified
|
|
19
|
+
* name and locale. The renderer reads the layout from the stored row's meta,
|
|
20
|
+
* so it wins over the code-registered notification meta. */
|
|
21
|
+
export type EffectiveMailLayoutSlugs = ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
22
|
+
|
|
23
|
+
function layoutSlugsOf(
|
|
24
|
+
qualifiedName: string,
|
|
25
|
+
notification: NotificationDefinition,
|
|
26
|
+
effectiveLayouts: EffectiveMailLayoutSlugs,
|
|
27
|
+
): string[] {
|
|
28
|
+
const stored = effectiveLayouts.get(qualifiedName);
|
|
29
|
+
const locales = new Set([
|
|
30
|
+
...Object.keys(notification.template ?? {}),
|
|
31
|
+
...Object.keys(notification.meta ?? {}),
|
|
32
|
+
...(stored?.keys() ?? []),
|
|
33
|
+
]);
|
|
34
|
+
return [...locales].map(
|
|
35
|
+
(locale) =>
|
|
36
|
+
stored?.get(locale) ?? notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
export function collectTemplateUsage(
|
|
15
41
|
registry: Registry,
|
|
16
42
|
slug: string,
|
|
17
43
|
kind: TemplateKind,
|
|
44
|
+
effectiveLayouts: EffectiveMailLayoutSlugs = new Map(),
|
|
18
45
|
): TemplateUsageEntry[] {
|
|
19
46
|
const usedBy: TemplateUsageEntry[] = [];
|
|
20
47
|
if (kind === "mail" || kind === "mail-layout") {
|
|
21
48
|
for (const [qualifiedName, notification] of registry.getAllNotifications()) {
|
|
22
49
|
const role: UsageRole = kind === "mail" ? "template" : "layout";
|
|
50
|
+
// Notifications without a code template are not rendered through the resolver.
|
|
23
51
|
const isUsed =
|
|
24
52
|
kind === "mail"
|
|
25
|
-
? qualifiedName === slug
|
|
26
|
-
:
|
|
27
|
-
...new Set([
|
|
28
|
-
...Object.keys(notification.template ?? {}),
|
|
29
|
-
...Object.keys(notification.meta ?? {}),
|
|
30
|
-
]),
|
|
31
|
-
].some(
|
|
32
|
-
(locale) =>
|
|
33
|
-
(notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG) === slug,
|
|
34
|
-
);
|
|
53
|
+
? qualifiedName === slug && notification.template !== undefined
|
|
54
|
+
: layoutSlugsOf(qualifiedName, notification, effectiveLayouts).includes(slug);
|
|
35
55
|
if (!isUsed) continue;
|
|
36
56
|
const parsed = parseQn(qualifiedName);
|
|
37
57
|
usedBy.push({
|
|
@@ -47,6 +47,7 @@ const textLikeExample = z.string().optional();
|
|
|
47
47
|
const scalarVariableSchema = z.discriminatedUnion("type", [
|
|
48
48
|
z.object({ ...variableCommon, type: z.literal("text"), example: textLikeExample }),
|
|
49
49
|
z.object({ ...variableCommon, type: z.literal("link"), example: textLikeExample }),
|
|
50
|
+
z.object({ ...variableCommon, type: z.literal("richText"), example: textLikeExample }),
|
|
50
51
|
z.object({
|
|
51
52
|
...variableCommon,
|
|
52
53
|
type: z.literal("money"),
|
|
@@ -127,12 +128,10 @@ function isKnownName(
|
|
|
127
128
|
): Resolution {
|
|
128
129
|
const { variables } = scope;
|
|
129
130
|
const root = rootName(name);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
131
|
+
// Declared variables and item fields are scalars: a dotted path under one never renders a value.
|
|
132
|
+
if (Object.hasOwn(itemFields, name)) return { known: true, used: "" };
|
|
133
133
|
if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
|
|
134
134
|
if (Object.hasOwn(variables, name)) return { known: true, used: name };
|
|
135
|
-
if (Object.hasOwn(variables, root)) return { known: true, used: root };
|
|
136
135
|
return { known: false, used: "" };
|
|
137
136
|
}
|
|
138
137
|
|
|
@@ -150,6 +149,11 @@ function enclosingEachName(element: ScanElement): string | null {
|
|
|
150
149
|
return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
const NAME_ATTRIBUTE_BY_TAG: Readonly<Record<string, string>> = {
|
|
153
|
+
span: "data-variable",
|
|
154
|
+
div: "data-steps",
|
|
155
|
+
};
|
|
156
|
+
|
|
153
157
|
function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
|
|
154
158
|
withParsedDocument(body, (root) => {
|
|
155
159
|
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
@@ -159,12 +163,13 @@ function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
|
|
|
159
163
|
else scope.unknown.add(listName);
|
|
160
164
|
}
|
|
161
165
|
for (const element of Array.from(
|
|
162
|
-
root.querySelectorAll("span[data-variable], a[href], img[data-resource]"),
|
|
166
|
+
root.querySelectorAll("span[data-variable], div[data-steps], a[href], img[data-resource]"),
|
|
163
167
|
)) {
|
|
164
168
|
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
165
169
|
const tag = element.tagName.toLowerCase();
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
const nameAttribute = NAME_ATTRIBUTE_BY_TAG[tag];
|
|
171
|
+
if (nameAttribute !== undefined) {
|
|
172
|
+
recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
|
|
168
173
|
} else if (tag === "a") {
|
|
169
174
|
const href = element.getAttribute("href") ?? "";
|
|
170
175
|
if (isDocumentPlaceholderHref(href)) recordName(href.slice(2, -2), itemFields, scope);
|
|
@@ -178,7 +183,7 @@ function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
|
|
|
178
183
|
|
|
179
184
|
const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
|
|
180
185
|
|
|
181
|
-
// Meta text supports only plain `{{name}}`
|
|
186
|
+
// Meta text supports only plain `{{name}}` references; every other
|
|
182
187
|
// Handlebars construct is reported as unknown so it can never reach the compiler.
|
|
183
188
|
function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
|
|
184
189
|
const leftover = text.replace(HANDLEBARS_EXPRESSION, (_match, inner: string) => {
|
|
@@ -192,7 +197,9 @@ function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
|
|
|
192
197
|
|
|
193
198
|
function metaTexts(meta: TemplateMeta | null): readonly string[] {
|
|
194
199
|
if (meta === null || !("subject" in meta)) return [];
|
|
195
|
-
return [meta.subject,
|
|
200
|
+
return [meta.subject, meta.previewText, meta.headerChip, meta.tone, meta.footerNote].filter(
|
|
201
|
+
(text): text is string => text !== undefined,
|
|
202
|
+
);
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
export function validateTemplatePlaceholders(input: {
|
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
TemplateResolverHandlers,
|
|
41
41
|
TemplateResolverQueries,
|
|
42
42
|
} from "../qualified-names.js";
|
|
43
|
-
import { DocumentContentEditor } from "./document-editor.js";
|
|
43
|
+
import { DocumentBlocksContext, DocumentContentEditor } from "./document-editor.js";
|
|
44
|
+
import { documentBlocksForKind } from "./document-toolbar-blocks.js";
|
|
44
45
|
import { defaultTranslations } from "./i18n.js";
|
|
45
46
|
|
|
46
47
|
// Exported for the unit test — groupBlocksByFolder is a pure function.
|
|
@@ -311,6 +312,7 @@ function TextBlockEditor({
|
|
|
311
312
|
const contentFormat = findContentFormat(features, collectionId);
|
|
312
313
|
const variableExamples = findVariableExamples(features, collectionId);
|
|
313
314
|
const variables = Object.keys(variableExamples);
|
|
315
|
+
const documentBlocks = documentBlocksForKind(findCollection(features, collectionId)?.kind);
|
|
314
316
|
const ContentEditor = useContentEditor(contentFormat);
|
|
315
317
|
const canWrite =
|
|
316
318
|
user?.roles.includes("TenantAdmin") === true || user?.roles.includes("SystemAdmin") === true;
|
|
@@ -448,13 +450,15 @@ function TextBlockEditor({
|
|
|
448
450
|
contentFormat={contentFormat}
|
|
449
451
|
/>
|
|
450
452
|
) : (
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
453
|
+
<DocumentBlocksContext.Provider value={documentBlocks}>
|
|
454
|
+
<ContentEditor
|
|
455
|
+
id={contentEditorId}
|
|
456
|
+
value={content}
|
|
457
|
+
onChange={setContent}
|
|
458
|
+
variables={variables}
|
|
459
|
+
readOnly={disabled}
|
|
460
|
+
/>
|
|
461
|
+
</DocumentBlocksContext.Provider>
|
|
458
462
|
)}
|
|
459
463
|
</Field>
|
|
460
464
|
{saveError !== null && <Banner variant="error">{saveError}</Banner>}
|
|
@@ -2,20 +2,29 @@
|
|
|
2
2
|
import type { Extensions, NodeViewRenderer } from "@tiptap/core";
|
|
3
3
|
import { ListKeymap } from "@tiptap/extension-list";
|
|
4
4
|
import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
|
|
5
|
+
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
5
6
|
import {
|
|
6
7
|
DocumentButton,
|
|
7
8
|
DocumentPageBreak,
|
|
8
9
|
DocumentResource,
|
|
10
|
+
DocumentSteps,
|
|
9
11
|
DocumentVariable,
|
|
10
12
|
documentExtensions,
|
|
11
13
|
} from "../document-schema.js";
|
|
14
|
+
import { readDocumentStepsAttribute } from "../document-steps.js";
|
|
12
15
|
import type { DocumentResourcePreview } from "./document-toolbar-blocks.js";
|
|
13
16
|
|
|
17
|
+
const DOCUMENT_RESOURCE_SIZE_TOKEN = "{size}";
|
|
18
|
+
export const DOCUMENT_BLOCK_ATTRIBUTE = "data-document-block";
|
|
19
|
+
|
|
14
20
|
export type DocumentEditorLabels = {
|
|
15
21
|
readonly placeholder: string;
|
|
16
22
|
readonly image: string;
|
|
17
23
|
readonly pageBreak: string;
|
|
18
24
|
readonly button?: string;
|
|
25
|
+
readonly steps?: string;
|
|
26
|
+
/** Canvas block label of an image, e.g. "Image · inline". */
|
|
27
|
+
readonly imageBlock?: string;
|
|
19
28
|
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
20
29
|
readonly systemVariables?: readonly string[];
|
|
21
30
|
readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
|
|
@@ -38,22 +47,36 @@ const CHIP_CLASS = "mx-0.5 inline-flex items-center gap-1 rounded border px-1.5
|
|
|
38
47
|
const USER_CHIP_CLASS = `${CHIP_CLASS} border-primary/30 bg-primary/10 text-primary`;
|
|
39
48
|
const SYSTEM_CHIP_CLASS = `${CHIP_CLASS} border-input bg-muted text-muted-foreground`;
|
|
40
49
|
|
|
50
|
+
// A chip inside a repeat row names an item field; its label is registered as `list.field`.
|
|
51
|
+
function repeatRowListAt(doc: ProseMirrorNode, position: number | undefined): string | null {
|
|
52
|
+
if (position === undefined) return null;
|
|
53
|
+
const resolved = doc.resolve(position);
|
|
54
|
+
for (let depth = resolved.depth; depth > 0; depth--) {
|
|
55
|
+
const each: unknown = resolved.node(depth).attrs["each"];
|
|
56
|
+
if (typeof each === "string" && each !== "") return each;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
// The chip is display only: the stored node carries just the name, so a
|
|
42
62
|
// label never reaches the document.
|
|
43
63
|
export function createVariableView(
|
|
44
64
|
variableLabels: Readonly<Record<string, string>>,
|
|
45
65
|
systemVariables: readonly string[],
|
|
66
|
+
labelOnly = false,
|
|
46
67
|
): NodeViewRenderer {
|
|
47
|
-
return ({ node }) => {
|
|
68
|
+
return ({ node, editor, getPos }) => {
|
|
48
69
|
const name = String(node.attrs["name"]);
|
|
49
|
-
const
|
|
70
|
+
const listName = repeatRowListAt(editor.state.doc, getPos());
|
|
71
|
+
const labelKey = listName === null ? name : `${listName}.${name}`;
|
|
72
|
+
const label = Object.hasOwn(variableLabels, labelKey) ? variableLabels[labelKey] : undefined;
|
|
50
73
|
const dom = createElementWithClass(
|
|
51
74
|
"span",
|
|
52
75
|
systemVariables.includes(name) ? SYSTEM_CHIP_CLASS : USER_CHIP_CLASS,
|
|
53
76
|
);
|
|
54
77
|
dom.contentEditable = "false";
|
|
55
78
|
dom.append(document.createTextNode(label !== undefined && label !== "" ? label : name));
|
|
56
|
-
if (label !== undefined && label !== "" && label !== name) {
|
|
79
|
+
if (!labelOnly && label !== undefined && label !== "" && label !== name) {
|
|
57
80
|
dom.append(createElementWithClass("small", "font-mono text-[10px] opacity-75", name));
|
|
58
81
|
}
|
|
59
82
|
return { dom };
|
|
@@ -79,18 +102,46 @@ function createResourceView(labels: DocumentEditorLabels): NodeViewRenderer {
|
|
|
79
102
|
const key = String(node.attrs["resource"]);
|
|
80
103
|
const previews = labels.resourcePreviews ?? {};
|
|
81
104
|
const preview = Object.hasOwn(previews, key) ? previews[key] : undefined;
|
|
105
|
+
const hasImage = preview !== undefined && PREVIEW_SRC.test(preview.src);
|
|
106
|
+
// A rendered image needs no frame until the node is selected (ProseMirror marks it).
|
|
82
107
|
const dom = createElementWithClass(
|
|
83
108
|
"div",
|
|
84
|
-
|
|
109
|
+
hasImage
|
|
110
|
+
? "relative my-1 flex items-center gap-2 rounded border border-transparent px-2 py-1 text-sm text-muted-foreground [.ProseMirror-focused_&.ProseMirror-selectednode]:border-dashed [.ProseMirror-focused_&.ProseMirror-selectednode]:border-input"
|
|
111
|
+
: "relative my-3 flex items-center gap-3 rounded border border-dashed border-input px-3 py-3 text-sm text-muted-foreground",
|
|
85
112
|
);
|
|
113
|
+
dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
|
|
86
114
|
dom.contentEditable = "false";
|
|
87
|
-
if (labels.blockLabels)
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
if (labels.blockLabels) {
|
|
116
|
+
const label = createBlockLabel(labels.imageBlock ?? labels.image);
|
|
117
|
+
if (hasImage)
|
|
118
|
+
label.classList.add("hidden", "[.ProseMirror-focused_.ProseMirror-selectednode_&]:block");
|
|
119
|
+
dom.append(label);
|
|
120
|
+
}
|
|
121
|
+
if (hasImage) {
|
|
122
|
+
const image = createElementWithClass("img", "max-w-[180px] rounded");
|
|
90
123
|
image.src = preview.src;
|
|
91
124
|
image.alt = typeof node.attrs["alt"] === "string" ? node.attrs["alt"] : key;
|
|
92
125
|
dom.append(image);
|
|
93
|
-
|
|
126
|
+
// An empty caption means "no caption", not the key.
|
|
127
|
+
const caption = preview.caption ?? key;
|
|
128
|
+
if (caption !== "") {
|
|
129
|
+
const captionElement = createElementWithClass(
|
|
130
|
+
"span",
|
|
131
|
+
"whitespace-nowrap text-[11px]",
|
|
132
|
+
caption.replace(` · ${DOCUMENT_RESOURCE_SIZE_TOKEN}`, ""),
|
|
133
|
+
);
|
|
134
|
+
// The size is only known once the browser decoded the image (possibly already, from cache).
|
|
135
|
+
const showSize = (): void => {
|
|
136
|
+
captionElement.textContent = caption.replace(
|
|
137
|
+
DOCUMENT_RESOURCE_SIZE_TOKEN,
|
|
138
|
+
`${image.naturalWidth} × ${image.naturalHeight} px`,
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
image.addEventListener("load", showSize);
|
|
142
|
+
if (image.complete && image.naturalWidth > 0) showSize();
|
|
143
|
+
dom.append(captionElement);
|
|
144
|
+
}
|
|
94
145
|
} else {
|
|
95
146
|
dom.append(
|
|
96
147
|
createElementWithClass("span", "font-medium", labels.image),
|
|
@@ -107,6 +158,7 @@ function createButtonView(buttonLabel: string): NodeViewRenderer {
|
|
|
107
158
|
"div",
|
|
108
159
|
"relative my-3 rounded border border-dashed border-input p-3",
|
|
109
160
|
);
|
|
161
|
+
dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
|
|
110
162
|
const button = document.createElement("a");
|
|
111
163
|
button.setAttribute("data-button", "");
|
|
112
164
|
dom.append(createBlockLabel(buttonLabel), button);
|
|
@@ -118,7 +170,7 @@ function createPageBreakView(pageBreakLabel: string): NodeViewRenderer {
|
|
|
118
170
|
return () => {
|
|
119
171
|
const dom = createElementWithClass(
|
|
120
172
|
"div",
|
|
121
|
-
"my-3 border-t border-dashed border-input pt-1 text-center text-xs text-muted-foreground",
|
|
173
|
+
"my-3 border-t border-dashed border-input pt-1 text-center text-xs uppercase tracking-wide text-muted-foreground",
|
|
122
174
|
pageBreakLabel,
|
|
123
175
|
);
|
|
124
176
|
dom.contentEditable = "false";
|
|
@@ -126,6 +178,35 @@ function createPageBreakView(pageBreakLabel: string): NodeViewRenderer {
|
|
|
126
178
|
};
|
|
127
179
|
}
|
|
128
180
|
|
|
181
|
+
// A static sketch of the row: which stage is reached is only known at render time.
|
|
182
|
+
function createStepsView(labels: DocumentEditorLabels): NodeViewRenderer {
|
|
183
|
+
return ({ node }) => {
|
|
184
|
+
const dom = createElementWithClass(
|
|
185
|
+
"div",
|
|
186
|
+
"relative my-3 rounded border border-dashed border-input px-3 pb-2 pt-3",
|
|
187
|
+
);
|
|
188
|
+
dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
|
|
189
|
+
dom.contentEditable = "false";
|
|
190
|
+
const variable = String(node.attrs["variable"]);
|
|
191
|
+
const variableLabels = labels.variableLabels ?? {};
|
|
192
|
+
const variableLabel = Object.hasOwn(variableLabels, variable)
|
|
193
|
+
? variableLabels[variable]
|
|
194
|
+
: undefined;
|
|
195
|
+
dom.append(createBlockLabel(`${labels.steps ?? "Steps"} · ${variableLabel ?? variable}`));
|
|
196
|
+
const row = createElementWithClass("div", "flex gap-1.5");
|
|
197
|
+
for (const step of readDocumentStepsAttribute(node.attrs["steps"])) {
|
|
198
|
+
const cell = createElementWithClass("div", "min-w-0 flex-1");
|
|
199
|
+
cell.append(
|
|
200
|
+
createElementWithClass("div", "h-1 rounded-full bg-primary/40"),
|
|
201
|
+
createElementWithClass("div", "truncate pt-1.5 text-xs text-muted-foreground", step.label),
|
|
202
|
+
);
|
|
203
|
+
row.append(cell);
|
|
204
|
+
}
|
|
205
|
+
dom.append(row);
|
|
206
|
+
return { dom };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
129
210
|
// Replaced by name, not appended: the schema must keep exactly one node per
|
|
130
211
|
// name, and only the editor view differs from the shared document schema.
|
|
131
212
|
export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Extensions {
|
|
@@ -134,7 +215,11 @@ export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Ex
|
|
|
134
215
|
case DocumentVariable.name:
|
|
135
216
|
return DocumentVariable.extend({
|
|
136
217
|
addNodeView: () =>
|
|
137
|
-
createVariableView(
|
|
218
|
+
createVariableView(
|
|
219
|
+
labels.variableLabels ?? {},
|
|
220
|
+
labels.systemVariables ?? [],
|
|
221
|
+
labels.blockLabels === true,
|
|
222
|
+
),
|
|
138
223
|
});
|
|
139
224
|
case DocumentResource.name:
|
|
140
225
|
return DocumentResource.extend({ addNodeView: () => createResourceView(labels) });
|
|
@@ -142,6 +227,8 @@ export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Ex
|
|
|
142
227
|
return labels.blockLabels
|
|
143
228
|
? DocumentButton.extend({ addNodeView: () => createButtonView(labels.button ?? "") })
|
|
144
229
|
: extension;
|
|
230
|
+
case DocumentSteps.name:
|
|
231
|
+
return DocumentSteps.extend({ addNodeView: () => createStepsView(labels) });
|
|
145
232
|
case DocumentPageBreak.name:
|
|
146
233
|
return DocumentPageBreak.extend({
|
|
147
234
|
addNodeView: () => createPageBreakView(labels.pageBreak),
|
|
@@ -4,25 +4,35 @@
|
|
|
4
4
|
// Never import document-tiptap-editor or document-editor-extensions statically here.
|
|
5
5
|
|
|
6
6
|
import { TextareaContentEditor } from "@cosmicdrift/kumiko-renderer";
|
|
7
|
-
import { lazy, type ReactNode, Suspense } from "react";
|
|
8
|
-
import type { DocumentEditorProps } from "./document-toolbar-blocks.js";
|
|
7
|
+
import { createContext, lazy, type ReactNode, Suspense, useContext } from "react";
|
|
8
|
+
import type { DocumentEditorProps, DocumentToolbarBlock } from "./document-toolbar-blocks.js";
|
|
9
9
|
|
|
10
10
|
const LazyDocumentTiptapEditor = lazy(() => import("./document-tiptap-editor.js"));
|
|
11
11
|
|
|
12
|
+
// The editor registry only hands ContentEditorProps to an editor, so the
|
|
13
|
+
// collection's block set (mail vs. PDF) reaches it through this context. An
|
|
14
|
+
// explicit `blocks` prop still wins.
|
|
15
|
+
export const DocumentBlocksContext = createContext<readonly DocumentToolbarBlock[] | undefined>(
|
|
16
|
+
undefined,
|
|
17
|
+
);
|
|
18
|
+
|
|
12
19
|
export function DocumentContentEditor(props: DocumentEditorProps): ReactNode {
|
|
20
|
+
const contextBlocks = useContext(DocumentBlocksContext);
|
|
21
|
+
const editorProps = props.blocks === undefined ? { ...props, blocks: contextBlocks } : props;
|
|
13
22
|
return (
|
|
14
23
|
<Suspense
|
|
24
|
+
// Read-only without chips: a textarea would insert {{name}} text and plaintext into a document body.
|
|
15
25
|
fallback={
|
|
16
26
|
<TextareaContentEditor
|
|
17
27
|
id={props.id}
|
|
18
28
|
value={props.value}
|
|
19
29
|
onChange={props.onChange}
|
|
20
|
-
variables={
|
|
21
|
-
readOnly
|
|
30
|
+
variables={[]}
|
|
31
|
+
readOnly
|
|
22
32
|
/>
|
|
23
33
|
}
|
|
24
34
|
>
|
|
25
|
-
<LazyDocumentTiptapEditor {...
|
|
35
|
+
<LazyDocumentTiptapEditor {...editorProps} />
|
|
26
36
|
</Suspense>
|
|
27
37
|
);
|
|
28
38
|
}
|