@cosmicdrift/kumiko-bundled-features 0.364.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/dist/audit/feature.js +1 -1
- package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
- package/dist/audit/run-escape-hatch-retention.js +39 -12
- package/dist/auth-email-password/bootstrap.d.ts +1 -3
- package/dist/auth-email-password/feature.js +2 -0
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -3
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
- package/dist/auth-email-password/magic-link-mail.js +6 -3
- package/dist/auth-email-password/mail-i18n.d.ts +3 -0
- package/dist/auth-email-password/mail-i18n.js +10 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +8 -1
- package/dist/billing-foundation/i18n.js +6 -0
- package/dist/billing-foundation/subscription-tier-sync.js +8 -2
- package/dist/cap-counter/book-cap-usage.js +8 -0
- package/dist/cap-counter/cap-reservation.js +7 -3
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
- package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
- package/dist/channel-discord/discord-channel.js +3 -2
- package/dist/channel-email/email-channel.js +24 -19
- package/dist/channel-email/smtp-transport.js +6 -1
- package/dist/channel-slack/slack-channel.js +3 -2
- package/dist/channel-teams/teams-channel.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.js +2 -2
- package/dist/custom-fields/db/queries/retention.d.ts +1 -1
- package/dist/custom-fields/db/queries/retention.js +2 -1
- package/dist/data-retention/run-retention-cleanup.js +4 -5
- package/dist/delivery/attachment-ref.js +16 -1
- package/dist/delivery/delivery-service.js +71 -82
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
- package/dist/feature-toggles/toggle-runtime.js +25 -12
- package/dist/files-tenant-data/index.d.ts +1 -0
- package/dist/files-tenant-data/index.js +2 -1
- package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/jobs/job-run-logger.js +5 -2
- package/dist/metrics/filters.d.ts +16 -1
- package/dist/metrics/filters.js +27 -11
- package/dist/personal-access-tokens/revoke-for-user.js +3 -7
- package/dist/secrets/handlers/delete.write.js +3 -1
- package/dist/secrets/write-gate.d.ts +3 -1
- package/dist/secrets/write-gate.js +6 -3
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/resource-image-formats.d.ts +23 -0
- package/dist/shared/resource-image-formats.js +21 -0
- package/dist/shared/sharp-input-limits.js +7 -8
- package/dist/step-dispatcher/webhook-runner.js +2 -2
- package/dist/template-resolver/compile-document-body.d.ts +3 -0
- package/dist/template-resolver/compile-document-body.js +63 -12
- package/dist/template-resolver/compile-steps-block.d.ts +10 -0
- package/dist/template-resolver/compile-steps-block.js +45 -0
- package/dist/template-resolver/constants.d.ts +4 -1
- package/dist/template-resolver/constants.js +10 -1
- package/dist/template-resolver/document-sanitizer.js +3 -0
- package/dist/template-resolver/document-schema.d.ts +2 -1
- package/dist/template-resolver/document-schema.js +6 -1
- package/dist/template-resolver/document-steps.d.ts +17 -0
- package/dist/template-resolver/document-steps.js +93 -0
- package/dist/template-resolver/feature.js +9 -0
- package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
- package/dist/template-resolver/handlers/bundle-import.js +63 -38
- package/dist/template-resolver/handlers/collection-set.write.js +4 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +83 -55
- package/dist/template-resolver/handlers/diff-to-system.query.js +1 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +9 -1
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
- package/dist/template-resolver/handlers/resource-upload.write.js +35 -5
- package/dist/template-resolver/handlers/validate.query.js +4 -1
- package/dist/template-resolver/index.d.ts +3 -3
- package/dist/template-resolver/index.js +3 -3
- package/dist/template-resolver/layout-appearance.d.ts +1 -1
- package/dist/template-resolver/layout-appearance.js +19 -9
- package/dist/template-resolver/mail-constants.d.ts +2 -0
- package/dist/template-resolver/mail-constants.js +3 -0
- package/dist/template-resolver/resource-image.d.ts +2 -9
- package/dist/template-resolver/resource-image.js +2 -11
- package/dist/template-resolver/seeding.js +2 -0
- package/dist/template-resolver/store-template-resource.d.ts +53 -9
- package/dist/template-resolver/store-template-resource.js +250 -97
- package/dist/template-resolver/template-bundle.js +4 -0
- package/dist/template-resolver/template-draft.d.ts +3 -0
- package/dist/template-resolver/template-draft.js +1 -1
- package/dist/template-resolver/template-meta.d.ts +5 -1
- package/dist/template-resolver/template-meta.js +7 -0
- package/dist/template-resolver/template-preview.js +1 -2
- package/dist/template-resolver/template-resources.d.ts +0 -5
- package/dist/template-resolver/template-resources.js +2 -34
- package/dist/template-resolver/template-validation.js +12 -9
- package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
- package/dist/template-resolver/web/document-editor-extensions.js +26 -1
- package/dist/template-resolver/web/document-editor.js +5 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +30 -7
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +5 -5
- package/dist/template-resolver/web/document-toolbar-blocks.js +2 -0
- package/dist/template-resolver/web/i18n.js +5 -0
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +4 -2
- package/dist/template-resolver/web/placeholder-line-editor.js +8 -6
- package/dist/tenant/handlers/update-member-roles.write.js +3 -6
- package/dist/tenant/seeding.js +23 -24
- package/dist/tenant-handover/move-entity-graph.js +14 -4
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
- package/dist/tenant-lifecycle/stages.js +3 -2
- package/dist/tier-engine/constants.d.ts +2 -0
- package/dist/tier-engine/constants.js +4 -0
- package/dist/tier-engine/entity.d.ts +15 -14
- package/dist/tier-engine/entity.js +4 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +5 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
- package/dist/tier-engine/index.d.ts +1 -1
- package/dist/tier-engine/index.js +1 -1
- package/dist/tier-engine/tier-resolver.d.ts +13 -13
- package/dist/user/constants.d.ts +1 -0
- package/dist/user/invited-global-roles.d.ts +2 -4
- package/dist/user-data-rights/email-templates.js +0 -1
- package/dist/user-data-rights/i18n.js +5 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/waitlist/feature.js +1 -1
- package/dist/waitlist/handlers/invite.write.js +21 -3
- package/dist/waitlist/handlers/reject.write.js +3 -16
- package/dist/waitlist/handlers/submit.write.js +29 -7
- package/dist/waitlist/i18n.js +9 -0
- package/dist/waitlist/lib.d.ts +2 -1
- package/dist/waitlist/lib.js +15 -0
- package/dist/workflow-runner/feature.js +1 -1
- package/package.json +10 -10
- package/src/audit/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/billing-foundation/changes.json +12 -0
- package/src/cap-counter/changes.json +13 -0
- package/src/cap-overview/changes.json +6 -0
- package/src/channel-email/changes.json +18 -0
- package/src/delivery/changes.json +12 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/feature-toggles/changes.json +7 -0
- package/src/jobs/changes.json +12 -0
- package/src/metrics/changes.json +6 -0
- package/src/personal-access-tokens/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/template-resolver/changes.json +81 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +12 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
package/dist/shared/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
|
18
18
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
19
19
|
export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
|
|
20
20
|
export { requireForTenant } from "./require-for-tenant.js";
|
|
21
|
+
export { extensionForResourceContentType, isResourceImageContentType, isResourceImageFormat, RESOURCE_IMAGE_FORMATS, type ResourceImageContentType, type ResourceImageFormat, } from "./resource-image-formats.js";
|
|
21
22
|
export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
22
23
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
23
24
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
|
18
18
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
19
19
|
export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
|
|
20
20
|
export { requireForTenant } from "./require-for-tenant.js";
|
|
21
|
+
export { extensionForResourceContentType, isResourceImageContentType, isResourceImageFormat, RESOURCE_IMAGE_FORMATS, } from "./resource-image-formats.js";
|
|
21
22
|
export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
22
23
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
23
24
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const RESOURCE_IMAGE_FORMATS: {
|
|
2
|
+
readonly png: {
|
|
3
|
+
readonly contentType: "image/png";
|
|
4
|
+
readonly extension: "png";
|
|
5
|
+
};
|
|
6
|
+
readonly jpeg: {
|
|
7
|
+
readonly contentType: "image/jpeg";
|
|
8
|
+
readonly extension: "jpg";
|
|
9
|
+
};
|
|
10
|
+
readonly gif: {
|
|
11
|
+
readonly contentType: "image/gif";
|
|
12
|
+
readonly extension: "gif";
|
|
13
|
+
};
|
|
14
|
+
readonly webp: {
|
|
15
|
+
readonly contentType: "image/webp";
|
|
16
|
+
readonly extension: "webp";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type ResourceImageFormat = keyof typeof RESOURCE_IMAGE_FORMATS;
|
|
20
|
+
export type ResourceImageContentType = (typeof RESOURCE_IMAGE_FORMATS)[ResourceImageFormat]["contentType"];
|
|
21
|
+
export declare function isResourceImageFormat(format: string | undefined): format is ResourceImageFormat;
|
|
22
|
+
export declare function isResourceImageContentType(contentType: string): contentType is ResourceImageContentType;
|
|
23
|
+
export declare function extensionForResourceContentType(contentType: string): string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Single source for the raster formats a template resource image may have: the normalizer, the
|
|
2
|
+
// stored file extension, the data-URI embedding and the inline mail attachment name all derive
|
|
3
|
+
// from it, so a new format cannot be accepted in one place and dropped in another.
|
|
4
|
+
export const RESOURCE_IMAGE_FORMATS = {
|
|
5
|
+
png: { contentType: "image/png", extension: "png" },
|
|
6
|
+
jpeg: { contentType: "image/jpeg", extension: "jpg" },
|
|
7
|
+
gif: { contentType: "image/gif", extension: "gif" },
|
|
8
|
+
webp: { contentType: "image/webp", extension: "webp" },
|
|
9
|
+
};
|
|
10
|
+
export function isResourceImageFormat(format) {
|
|
11
|
+
return format !== undefined && Object.hasOwn(RESOURCE_IMAGE_FORMATS, format);
|
|
12
|
+
}
|
|
13
|
+
function formatForContentType(contentType) {
|
|
14
|
+
return Object.values(RESOURCE_IMAGE_FORMATS).find((entry) => entry.contentType === contentType);
|
|
15
|
+
}
|
|
16
|
+
export function isResourceImageContentType(contentType) {
|
|
17
|
+
return formatForContentType(contentType) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
export function extensionForResourceContentType(contentType) {
|
|
20
|
+
return formatForContentType(contentType)?.extension;
|
|
21
|
+
}
|
|
@@ -4,19 +4,18 @@
|
|
|
4
4
|
// to be an OOM primitive for a single request.
|
|
5
5
|
export const MAX_INPUT_PIXELS = 100_000_000;
|
|
6
6
|
// sequentialRead trades random pixel access for lower peak memory on
|
|
7
|
-
// streamed formats
|
|
8
|
-
//
|
|
9
|
-
// against a single already-buffered region per call).
|
|
7
|
+
// streamed formats. Shared by every sharp caller since none of them need
|
|
8
|
+
// non-sequential access to the original bytes.
|
|
10
9
|
export const SHARP_INPUT_OPTIONS = {
|
|
11
10
|
limitInputPixels: MAX_INPUT_PIXELS,
|
|
12
11
|
sequentialRead: true,
|
|
13
12
|
};
|
|
14
|
-
// Shared trust boundary for every image byte source
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
13
|
+
// Shared trust boundary for every image byte source decoded through sharp:
|
|
14
|
+
// uploads, overlay layers and template resources alike. A base64 string in a
|
|
15
|
+
// field declaration isn't more trustworthy than an upload just because it
|
|
16
|
+
// came from app code.
|
|
18
17
|
export function assertNotSvg(format) {
|
|
19
18
|
if (format === "svg") {
|
|
20
|
-
throw new Error("
|
|
19
|
+
throw new Error("SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this code doesn't need to cross.");
|
|
21
20
|
}
|
|
22
21
|
}
|
|
@@ -79,8 +79,8 @@ let fetchImpl = globalThis.fetch.bind(globalThis);
|
|
|
79
79
|
export function setWebhookFetch(fn) {
|
|
80
80
|
fetchImpl = fn;
|
|
81
81
|
}
|
|
82
|
-
//
|
|
83
|
-
//
|
|
82
|
+
// Bounds a single hanging tenant-controlled receiver to this long. The consumer
|
|
83
|
+
// is sequential and shared, so N hanging requests still delay other tenants by N times this.
|
|
84
84
|
export const WEBHOOK_REQUEST_TIMEOUT_MS = 10_000;
|
|
85
85
|
// Never includes the secret name or value — spec.auth.secret is a
|
|
86
86
|
// tenant-chosen name, but the error still reaches the tenant via the
|
|
@@ -2,10 +2,13 @@ import type { TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
export type CompileDocumentBodyOptions = {
|
|
3
3
|
readonly resourceSrc?: (key: string) => string | undefined;
|
|
4
4
|
readonly resourceImageMaxWidthPx?: number;
|
|
5
|
+
readonly resourceImageMaxHeightPx?: number;
|
|
5
6
|
readonly alignmentAsInlineStyle?: boolean;
|
|
6
7
|
readonly buttonAsInlineStyle?: boolean;
|
|
7
8
|
readonly tableAsInlineStyle?: boolean;
|
|
8
9
|
readonly buttonInlineStyle?: string;
|
|
10
|
+
readonly headingAsInlineStyle?: boolean;
|
|
11
|
+
readonly stepsColor?: string;
|
|
9
12
|
};
|
|
10
13
|
export declare function collectDocumentResourceKeys(html: string): readonly string[];
|
|
11
14
|
export declare function toResourceDataUri(contentType: string, bytes: Uint8Array): string;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { isResourceImageContentType } from "../shared/index.js";
|
|
3
|
+
import { rewriteStepsBlock } from "./compile-steps-block.js";
|
|
2
4
|
import { withParsedDocument } from "./document-sanitizer.js";
|
|
3
5
|
import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
4
6
|
// Private-use characters mark where a Handlebars expression goes. Author text
|
|
@@ -52,6 +54,33 @@ function pathAndDef(name, element, state) {
|
|
|
52
54
|
const { path, def } = resolveReference(name, rowScopeOf(element, state), state.variables);
|
|
53
55
|
return [path, def];
|
|
54
56
|
}
|
|
57
|
+
const VARIABLE_ONLY_BLOCK_SELECTOR = "p, h1, h2, h3, h4";
|
|
58
|
+
function soleVariableName(block) {
|
|
59
|
+
const names = new Set(Array.from(block.querySelectorAll("span[data-variable]")).map((span) => span.getAttribute("data-variable") ?? ""));
|
|
60
|
+
const [name] = [...names];
|
|
61
|
+
if (names.size !== 1 || name === undefined || !isDocumentReferenceName(name))
|
|
62
|
+
return null;
|
|
63
|
+
const withoutVariables = block.cloneNode(true);
|
|
64
|
+
for (const span of Array.from(withoutVariables.querySelectorAll("span[data-variable]"))) {
|
|
65
|
+
span.remove();
|
|
66
|
+
}
|
|
67
|
+
const hasStaticContent = (withoutVariables.textContent ?? "").trim() !== "" ||
|
|
68
|
+
withoutVariables.querySelector("img") !== null;
|
|
69
|
+
return hasStaticContent ? null : name;
|
|
70
|
+
}
|
|
71
|
+
// A line that holds nothing but one optional placeholder would otherwise leave
|
|
72
|
+
// an empty paragraph (a visible gap) when the value is missing. includeZero
|
|
73
|
+
// keeps an amount of 0.
|
|
74
|
+
function wrapVariableOnlyBlock(block, state) {
|
|
75
|
+
const name = soleVariableName(block);
|
|
76
|
+
// skip: a line with static content or several placeholders stays as it is
|
|
77
|
+
if (name === null)
|
|
78
|
+
return;
|
|
79
|
+
const [path] = pathAndDef(name, block, state);
|
|
80
|
+
const document = block.ownerDocument;
|
|
81
|
+
block.before(document.createTextNode(marker(state, `{{#if ${path} includeZero=true}}`)));
|
|
82
|
+
block.after(document.createTextNode(marker(state, "{{/if}}")));
|
|
83
|
+
}
|
|
55
84
|
function rewriteLink(element, state) {
|
|
56
85
|
const href = element.getAttribute("href") ?? "";
|
|
57
86
|
if (isDocumentPlaceholderHref(href)) {
|
|
@@ -94,18 +123,17 @@ const MAIL_BUTTON_STYLE = "display:inline-block;padding:10px 20px;border-radius:
|
|
|
94
123
|
const MAIL_TABLE_STYLE = "width:100%;border-collapse:collapse";
|
|
95
124
|
const MAIL_TABLE_CELL_STYLE = "padding:6px 12px;vertical-align:top;border-bottom:1px solid #e5e5e5";
|
|
96
125
|
const MAIL_TABLE_HEAD_STYLE = "font-size:12px;font-weight:600;color:#6b7280;text-align:left";
|
|
97
|
-
const MAIL_SMALL_STYLE = "font-size:
|
|
126
|
+
const MAIL_SMALL_STYLE = "font-size:13px;color:#4A515E";
|
|
127
|
+
const MAIL_HEADING_STYLES = {
|
|
128
|
+
h1: "margin:0 0 12px 0;font-size:30px;line-height:1.15;font-weight:700;letter-spacing:-0.035em;color:#0B0D12",
|
|
129
|
+
h2: "margin:0 0 12px 0;font-size:24px;line-height:1.2;font-weight:700;letter-spacing:-0.02em;color:#0B0D12",
|
|
130
|
+
h3: "margin:20px 0 8px 0;font-size:17px;line-height:1.3;font-weight:600;color:#0B0D12",
|
|
131
|
+
};
|
|
98
132
|
// Appended after an existing style (data-align) so the alignment survives.
|
|
99
133
|
function appendStyle(element, style) {
|
|
100
134
|
const existing = element.getAttribute("style");
|
|
101
135
|
element.setAttribute("style", existing === null ? style : `${existing};${style}`);
|
|
102
136
|
}
|
|
103
|
-
const RESOURCE_IMAGE_CONTENT_TYPES = new Set([
|
|
104
|
-
"image/png",
|
|
105
|
-
"image/jpeg",
|
|
106
|
-
"image/gif",
|
|
107
|
-
"image/webp",
|
|
108
|
-
]);
|
|
109
137
|
// Unique valid resource keys in document order.
|
|
110
138
|
export function collectDocumentResourceKeys(html) {
|
|
111
139
|
return withParsedDocument(html, (root) => {
|
|
@@ -119,7 +147,7 @@ export function collectDocumentResourceKeys(html) {
|
|
|
119
147
|
});
|
|
120
148
|
}
|
|
121
149
|
export function toResourceDataUri(contentType, bytes) {
|
|
122
|
-
if (!
|
|
150
|
+
if (!isResourceImageContentType(contentType)) {
|
|
123
151
|
throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
|
|
124
152
|
}
|
|
125
153
|
return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
@@ -130,7 +158,7 @@ function rewriteAlignment(element) {
|
|
|
130
158
|
if (isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align))
|
|
131
159
|
element.setAttribute("style", `text-align:${align}`);
|
|
132
160
|
}
|
|
133
|
-
function rewriteResourceImage(element, resourceSrc, maxWidthPx) {
|
|
161
|
+
function rewriteResourceImage(element, resourceSrc, maxWidthPx, maxHeightPx) {
|
|
134
162
|
const key = element.getAttribute("data-resource") ?? "";
|
|
135
163
|
const src = isDocumentResourceKey(key) ? resourceSrc?.(key) : undefined;
|
|
136
164
|
if (src === undefined) {
|
|
@@ -140,14 +168,18 @@ function rewriteResourceImage(element, resourceSrc, maxWidthPx) {
|
|
|
140
168
|
}
|
|
141
169
|
element.setAttribute("src", src);
|
|
142
170
|
element.removeAttribute("data-resource");
|
|
143
|
-
|
|
144
|
-
|
|
171
|
+
const caps = [
|
|
172
|
+
...(maxWidthPx === undefined ? [] : [`max-width:${maxWidthPx}px`]),
|
|
173
|
+
...(maxHeightPx === undefined ? [] : [`max-height:${maxHeightPx}px;width:auto`]),
|
|
174
|
+
];
|
|
175
|
+
if (caps.length > 0)
|
|
176
|
+
element.setAttribute("style", `${caps.join(";")};height:auto`);
|
|
145
177
|
}
|
|
146
178
|
function stripScriptsAndActiveContent(root, options) {
|
|
147
179
|
for (const element of Array.from(root.querySelectorAll("script")))
|
|
148
180
|
element.remove();
|
|
149
181
|
for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
|
|
150
|
-
rewriteResourceImage(element, options.resourceSrc, options.resourceImageMaxWidthPx);
|
|
182
|
+
rewriteResourceImage(element, options.resourceSrc, options.resourceImageMaxWidthPx, options.resourceImageMaxHeightPx);
|
|
151
183
|
}
|
|
152
184
|
for (const element of Array.from(root.querySelectorAll("*")))
|
|
153
185
|
stripActiveContent(element);
|
|
@@ -161,6 +193,13 @@ function applyAlignmentInlineStyles(root) {
|
|
|
161
193
|
appendStyle(element, MAIL_SMALL_STYLE);
|
|
162
194
|
}
|
|
163
195
|
}
|
|
196
|
+
function applyHeadingInlineStyles(root) {
|
|
197
|
+
for (const heading of Array.from(root.querySelectorAll("h1, h2, h3"))) {
|
|
198
|
+
const style = MAIL_HEADING_STYLES[heading.tagName.toLowerCase()];
|
|
199
|
+
if (style !== undefined)
|
|
200
|
+
appendStyle(heading, style);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
164
203
|
function applyTableInlineStyles(root) {
|
|
165
204
|
for (const table of Array.from(root.querySelectorAll("table"))) {
|
|
166
205
|
appendStyle(table, MAIL_TABLE_STYLE);
|
|
@@ -190,8 +229,20 @@ export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
|
|
|
190
229
|
stripScriptsAndActiveContent(root, options);
|
|
191
230
|
if (options.alignmentAsInlineStyle === true)
|
|
192
231
|
applyAlignmentInlineStyles(root);
|
|
232
|
+
if (options.headingAsInlineStyle === true)
|
|
233
|
+
applyHeadingInlineStyles(root);
|
|
193
234
|
if (options.tableAsInlineStyle === true)
|
|
194
235
|
applyTableInlineStyles(root);
|
|
236
|
+
for (const element of Array.from(root.querySelectorAll("div[data-steps]"))) {
|
|
237
|
+
rewriteStepsBlock(element, {
|
|
238
|
+
expression: (source) => marker(state, source),
|
|
239
|
+
pathOf: (name, at) => pathAndDef(name, at, state)[0],
|
|
240
|
+
color: options.stepsColor,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
for (const block of Array.from(root.querySelectorAll(VARIABLE_ONLY_BLOCK_SELECTOR))) {
|
|
244
|
+
wrapVariableOnlyBlock(block, state);
|
|
245
|
+
}
|
|
195
246
|
for (const element of Array.from(root.querySelectorAll("span[data-variable]"))) {
|
|
196
247
|
rewriteVariable(element, state);
|
|
197
248
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Element as ScanElement } from "happy-dom";
|
|
2
|
+
export declare const DEFAULT_STEPS_COLOR = "#1f5468";
|
|
3
|
+
export type StepsCompileHooks = {
|
|
4
|
+
/** Registers a Handlebars expression and returns the marker that stands for it. */
|
|
5
|
+
readonly expression: (source: string) => string;
|
|
6
|
+
/** Resolves a variable name to its Handlebars path at the element's position. */
|
|
7
|
+
readonly pathOf: (name: string, element: ScanElement) => string;
|
|
8
|
+
readonly color: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare function rewriteStepsBlock(element: ScanElement, hooks: StepsCompileHooks): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isDocumentReferenceName } from "./document-schema.js";
|
|
2
|
+
import { readDocumentSteps } from "./document-steps.js";
|
|
3
|
+
import { isHexColor } from "./layout-appearance.js";
|
|
4
|
+
export const DEFAULT_STEPS_COLOR = "#1f5468";
|
|
5
|
+
const TODO_BAR_COLOR = "#ECEDEA";
|
|
6
|
+
const REACHED_LABEL_COLOR = "#0B0D12";
|
|
7
|
+
const TODO_LABEL_COLOR = "#5A6170";
|
|
8
|
+
// Inline-styled table in every output (mail, PDF, preview): mail clients ignore
|
|
9
|
+
// document CSS, and one rendering keeps the three outputs identical. The labels
|
|
10
|
+
// stay static text; only the reached/current switch is a Handlebars block, and
|
|
11
|
+
// its arguments are a validated path and validated keys.
|
|
12
|
+
export function rewriteStepsBlock(element, hooks) {
|
|
13
|
+
const name = element.getAttribute("data-steps") ?? "";
|
|
14
|
+
const steps = readDocumentSteps(element);
|
|
15
|
+
if (!isDocumentReferenceName(name) || steps === null) {
|
|
16
|
+
element.remove();
|
|
17
|
+
// skip: an invalid steps block renders nothing
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const path = hooks.pathOf(name, element);
|
|
21
|
+
const keys = steps.map((step) => step.key).join(",");
|
|
22
|
+
const color = hooks.color !== undefined && isHexColor(hooks.color) ? hooks.color : DEFAULT_STEPS_COLOR;
|
|
23
|
+
const when = (index, state, yes, no) => `${hooks.expression(`{{#stepIs ${path} ${index} "${keys}" "${state}"}}`)}${yes}${hooks.expression("{{else}}")}${no}${hooks.expression("{{/stepIs}}")}`;
|
|
24
|
+
const document = element.ownerDocument;
|
|
25
|
+
const table = document.createElement("table");
|
|
26
|
+
table.setAttribute("role", "presentation");
|
|
27
|
+
table.setAttribute("width", "100%");
|
|
28
|
+
table.setAttribute("cellpadding", "0");
|
|
29
|
+
table.setAttribute("cellspacing", "0");
|
|
30
|
+
table.setAttribute("style", "table-layout:fixed;border-collapse:collapse;margin:4px 0 24px 0");
|
|
31
|
+
const row = document.createElement("tr");
|
|
32
|
+
steps.forEach((step, index) => {
|
|
33
|
+
const cell = document.createElement("td");
|
|
34
|
+
cell.setAttribute("style", `padding:0 ${index === steps.length - 1 ? 0 : 4}px 0 0;vertical-align:top`);
|
|
35
|
+
const bar = document.createElement("div");
|
|
36
|
+
bar.setAttribute("style", `height:4px;line-height:4px;font-size:0;border-radius:2px;background-color:${when(index, "reached", color, TODO_BAR_COLOR)}`);
|
|
37
|
+
const label = document.createElement("div");
|
|
38
|
+
label.setAttribute("style", `padding-top:8px;font-size:12px;line-height:1.3;color:${when(index, "reached", REACHED_LABEL_COLOR, TODO_LABEL_COLOR)};font-weight:${when(index, "current", "600", "400")}`);
|
|
39
|
+
label.textContent = step.label;
|
|
40
|
+
cell.append(bar, label);
|
|
41
|
+
row.append(cell);
|
|
42
|
+
});
|
|
43
|
+
table.append(row);
|
|
44
|
+
element.replaceWith(table);
|
|
45
|
+
}
|
|
@@ -9,16 +9,19 @@ export type TemplateKind = (typeof TEMPLATE_KINDS)[number];
|
|
|
9
9
|
export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snapshot", "ai-prompt", "mail-layout", "pdf-layout"];
|
|
10
10
|
export type UpsertKind = (typeof UPSERT_KINDS)[number];
|
|
11
11
|
export declare const TEXT_BLOCK_KIND: "text-block";
|
|
12
|
-
export declare const COLLECTION_REFUSED_KINDS: readonly
|
|
12
|
+
export declare const COLLECTION_REFUSED_KINDS: readonly ["mail", "mail-layout", "pdf-layout"];
|
|
13
|
+
export declare function isCollectionRefusedKind(kind: string): boolean;
|
|
13
14
|
export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
|
|
14
15
|
export type ContentFormat = (typeof CONTENT_FORMATS)[number];
|
|
15
16
|
export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
|
|
16
17
|
export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
|
|
17
18
|
export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
|
|
18
19
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
20
|
+
export declare const ACTIVE_TEMPLATE_STATUS = "active";
|
|
19
21
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, } from "./pdf-constants.js";
|
|
20
22
|
export declare const HEX_COLOR_PATTERN: RegExp;
|
|
21
23
|
export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
24
|
+
export declare const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
22
25
|
export declare const MAIL_PREVIEW_MAX_BYTES: number;
|
|
23
26
|
export declare const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
|
|
24
27
|
export declare const TEST_MAIL_SUBJECT_PREFIX = "[TEST] ";
|
|
@@ -40,14 +40,23 @@ export const TEXT_BLOCK_KIND = "text-block";
|
|
|
40
40
|
// Kinds a content collection must not write: a tenant row of these kinds would
|
|
41
41
|
// shadow the system notification template or layout without the meta and
|
|
42
42
|
// placeholder gates that upsertTenant applies.
|
|
43
|
-
export const COLLECTION_REFUSED_KINDS = [
|
|
43
|
+
export const COLLECTION_REFUSED_KINDS = [
|
|
44
|
+
"mail",
|
|
45
|
+
"mail-layout",
|
|
46
|
+
"pdf-layout",
|
|
47
|
+
];
|
|
48
|
+
export function isCollectionRefusedKind(kind) {
|
|
49
|
+
return COLLECTION_REFUSED_KINDS.some((refused) => refused === kind);
|
|
50
|
+
}
|
|
44
51
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
|
|
45
52
|
export const TEMPLATE_SCOPES = ["system", "tenant"];
|
|
46
53
|
export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
|
|
54
|
+
export const ACTIVE_TEMPLATE_STATUS = "active";
|
|
47
55
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
|
|
48
56
|
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
49
57
|
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
50
58
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
59
|
+
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
51
60
|
// The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
|
|
52
61
|
export const MAIL_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
|
|
53
62
|
export const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
|
|
@@ -3,6 +3,7 @@ import { generateHTML, generateJSON } from "@tiptap/html/server";
|
|
|
3
3
|
import { Window } from "happy-dom";
|
|
4
4
|
import { DOCUMENT_HTML_KINDS } from "./constants.js";
|
|
5
5
|
import { documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
|
+
import { isDocumentStepKey } from "./document-steps.js";
|
|
6
7
|
export function violation(code, detail, message) {
|
|
7
8
|
return { code, detail, message };
|
|
8
9
|
}
|
|
@@ -56,6 +57,8 @@ const REFERENCE_ATTRIBUTES = [
|
|
|
56
57
|
["data-variable", isDocumentReferenceName],
|
|
57
58
|
["data-each", isDocumentReferenceName],
|
|
58
59
|
["data-resource", isDocumentResourceKey],
|
|
60
|
+
["data-steps", isDocumentReferenceName],
|
|
61
|
+
["data-step", isDocumentStepKey],
|
|
59
62
|
];
|
|
60
63
|
function scanReferences(tag, element, out) {
|
|
61
64
|
for (const [attribute, isValid] of REFERENCE_ATTRIBUTES) {
|
|
@@ -3,12 +3,13 @@ export declare function isDocumentPlaceholderHref(href: string): boolean;
|
|
|
3
3
|
export declare function isAllowedDocumentLinkHref(href: string): boolean;
|
|
4
4
|
export declare function isDocumentReferenceName(value: string): boolean;
|
|
5
5
|
export declare function isDocumentResourceKey(value: string): boolean;
|
|
6
|
-
export declare const DOCUMENT_NODE_NAMES: readonly ["variable", "resource", "button", "divider", "pageBreak"];
|
|
6
|
+
export declare const DOCUMENT_NODE_NAMES: readonly ["variable", "resource", "button", "divider", "pageBreak", "steps"];
|
|
7
7
|
export declare const DocumentVariable: Node<any, any>;
|
|
8
8
|
export declare const DocumentResource: Node<any, any>;
|
|
9
9
|
export declare const DocumentButton: Node<any, any>;
|
|
10
10
|
export declare const DocumentDivider: Node<any, any>;
|
|
11
11
|
export declare const DocumentPageBreak: Node<any, any>;
|
|
12
|
+
export declare const DocumentSteps: Node<any, any>;
|
|
12
13
|
export declare const DocumentTableRow: Node<import("@tiptap/extension-table").TableRowOptions, any>;
|
|
13
14
|
export declare const DocumentTable: Node<import("@tiptap/extension-table").TableOptions, any>;
|
|
14
15
|
export declare const DocumentTableCell: Node<import("@tiptap/extension-table").TableCellOptions, any>;
|
|
@@ -8,6 +8,7 @@ import { Extension, Node } from "@tiptap/core";
|
|
|
8
8
|
import { Link } from "@tiptap/extension-link";
|
|
9
9
|
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
10
10
|
import StarterKit from "@tiptap/starter-kit";
|
|
11
|
+
import { createDocumentStepsNode } from "./document-steps.js";
|
|
11
12
|
const PLACEHOLDER_HREF = /^\{\{(.+)\}\}$/;
|
|
12
13
|
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
13
14
|
const RESOURCE_KEY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
@@ -63,6 +64,7 @@ export const DOCUMENT_NODE_NAMES = [
|
|
|
63
64
|
"button",
|
|
64
65
|
"divider",
|
|
65
66
|
"pageBreak",
|
|
67
|
+
"steps",
|
|
66
68
|
];
|
|
67
69
|
export const DocumentVariable = Node.create({
|
|
68
70
|
name: "variable",
|
|
@@ -121,7 +123,8 @@ export const DocumentResource = Node.create({
|
|
|
121
123
|
export const DocumentButton = Node.create({
|
|
122
124
|
name: "button",
|
|
123
125
|
group: "block",
|
|
124
|
-
|
|
126
|
+
// A placeholder in the label ("View {{pageName}}") must survive the round trip.
|
|
127
|
+
content: "(text | variable)*",
|
|
125
128
|
marks: "",
|
|
126
129
|
defining: true,
|
|
127
130
|
addAttributes() {
|
|
@@ -166,6 +169,7 @@ export const DocumentPageBreak = Node.create({
|
|
|
166
169
|
return ["div", { "data-page-break": "" }];
|
|
167
170
|
},
|
|
168
171
|
});
|
|
172
|
+
export const DocumentSteps = createDocumentStepsNode(isDocumentReferenceName);
|
|
169
173
|
// A repeat row is a table row carrying `each`: prosemirror-tables requires
|
|
170
174
|
// the tableRow node type, so a separate node type is not an option.
|
|
171
175
|
export const DocumentTableRow = TableRow.extend({
|
|
@@ -311,6 +315,7 @@ export const documentExtensions = [
|
|
|
311
315
|
DocumentButton,
|
|
312
316
|
DocumentDivider,
|
|
313
317
|
DocumentPageBreak,
|
|
318
|
+
DocumentSteps,
|
|
314
319
|
DocumentTextAlign,
|
|
315
320
|
DocumentTextSize,
|
|
316
321
|
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
export declare const DOCUMENT_STEPS_MAX = 8;
|
|
3
|
+
export type DocumentStep = {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function isDocumentStepKey(value: string): boolean;
|
|
8
|
+
export declare function documentStepKeyOf(label: string): string;
|
|
9
|
+
export declare function parseDocumentStepsInput(text: string): readonly DocumentStep[] | null;
|
|
10
|
+
export declare function readDocumentSteps(element: {
|
|
11
|
+
querySelectorAll(selector: string): ArrayLike<{
|
|
12
|
+
getAttribute(name: string): string | null;
|
|
13
|
+
textContent: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
}): readonly DocumentStep[] | null;
|
|
16
|
+
export declare function readDocumentStepsAttribute(value: unknown): readonly DocumentStep[];
|
|
17
|
+
export declare function createDocumentStepsNode(isReferenceName: (value: string) => boolean): Node<any, any>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// A progress row: fixed stages stored in the template, the reached stage read
|
|
3
|
+
// from a variable at render time. Universal like document-schema.ts.
|
|
4
|
+
import { Node } from "@tiptap/core";
|
|
5
|
+
// Keys end up as Handlebars string literals in the compiled body, so they are
|
|
6
|
+
// restricted to characters that cannot close or escape a literal.
|
|
7
|
+
const STEP_KEY = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
|
|
8
|
+
export const DOCUMENT_STEPS_MAX = 8;
|
|
9
|
+
const STEP_LABEL_MAX_LENGTH = 40;
|
|
10
|
+
export function isDocumentStepKey(value) {
|
|
11
|
+
return STEP_KEY.test(value);
|
|
12
|
+
}
|
|
13
|
+
export function documentStepKeyOf(label) {
|
|
14
|
+
return label
|
|
15
|
+
.normalize("NFKD")
|
|
16
|
+
.replace(/\p{M}/gu, "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "")
|
|
20
|
+
.slice(0, 40);
|
|
21
|
+
}
|
|
22
|
+
// Editor input: "value=Label" entries separated by commas; a bare label derives its value.
|
|
23
|
+
export function parseDocumentStepsInput(text) {
|
|
24
|
+
const steps = text
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter((entry) => entry !== "")
|
|
28
|
+
.map((entry) => {
|
|
29
|
+
const separator = entry.indexOf("=");
|
|
30
|
+
const label = (separator < 0 ? entry : entry.slice(separator + 1)).trim();
|
|
31
|
+
const key = separator < 0 ? documentStepKeyOf(entry) : entry.slice(0, separator).trim();
|
|
32
|
+
return { key, label: label.slice(0, STEP_LABEL_MAX_LENGTH) };
|
|
33
|
+
});
|
|
34
|
+
return areValidSteps(steps) ? steps : null;
|
|
35
|
+
}
|
|
36
|
+
function areValidSteps(steps) {
|
|
37
|
+
return (steps.length > 0 &&
|
|
38
|
+
steps.length <= DOCUMENT_STEPS_MAX &&
|
|
39
|
+
new Set(steps.map((step) => step.key)).size === steps.length &&
|
|
40
|
+
steps.every((step) => isDocumentStepKey(step.key) && step.label !== ""));
|
|
41
|
+
}
|
|
42
|
+
// Duplicate or invalid keys would make the reached stage ambiguous, so the whole
|
|
43
|
+
// list is refused rather than repaired.
|
|
44
|
+
export function readDocumentSteps(element) {
|
|
45
|
+
const steps = Array.from(element.querySelectorAll("span[data-step]")).map((span) => ({
|
|
46
|
+
key: span.getAttribute("data-step") ?? "",
|
|
47
|
+
label: (span.textContent ?? "").trim().slice(0, STEP_LABEL_MAX_LENGTH),
|
|
48
|
+
}));
|
|
49
|
+
return areValidSteps(steps) ? steps : null;
|
|
50
|
+
}
|
|
51
|
+
function isDocumentStep(step) {
|
|
52
|
+
return (typeof step === "object" &&
|
|
53
|
+
step !== null &&
|
|
54
|
+
"key" in step &&
|
|
55
|
+
"label" in step &&
|
|
56
|
+
typeof step.key === "string" &&
|
|
57
|
+
typeof step.label === "string");
|
|
58
|
+
}
|
|
59
|
+
// Node attrs are untyped JSON; anything that is not a step entry is dropped.
|
|
60
|
+
export function readDocumentStepsAttribute(value) {
|
|
61
|
+
return Array.isArray(value) ? value.filter(isDocumentStep) : [];
|
|
62
|
+
}
|
|
63
|
+
export function createDocumentStepsNode(isReferenceName) {
|
|
64
|
+
return Node.create({
|
|
65
|
+
name: "steps",
|
|
66
|
+
group: "block",
|
|
67
|
+
atom: true,
|
|
68
|
+
addAttributes() {
|
|
69
|
+
return { variable: { default: null }, steps: { default: [] } };
|
|
70
|
+
},
|
|
71
|
+
parseHTML() {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
tag: "div[data-steps]",
|
|
75
|
+
getAttrs: (element) => {
|
|
76
|
+
const variable = element.getAttribute("data-steps");
|
|
77
|
+
const steps = readDocumentSteps(element);
|
|
78
|
+
if (variable === null || !isReferenceName(variable) || steps === null)
|
|
79
|
+
return false;
|
|
80
|
+
return { variable, steps };
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
},
|
|
85
|
+
renderHTML({ node }) {
|
|
86
|
+
return [
|
|
87
|
+
"div",
|
|
88
|
+
{ "data-steps": node.attrs["variable"] },
|
|
89
|
+
...readDocumentStepsAttribute(node.attrs["steps"]).map((step) => ["span", { "data-step": step.key }, step.label]),
|
|
90
|
+
];
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSystemConfig, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { isCollectionRefusedKind } from "./constants.js";
|
|
2
3
|
import { bySlugQuery } from "./handlers/by-slug.query.js";
|
|
3
4
|
import { byTenantQuery } from "./handlers/by-tenant.query.js";
|
|
4
5
|
import { makeCollectionItemQuery } from "./handlers/collection-item.query.js";
|
|
@@ -29,8 +30,16 @@ import { usageQuery } from "./handlers/usage.query.js";
|
|
|
29
30
|
import { validateQuery } from "./handlers/validate.query.js";
|
|
30
31
|
import { templateResourceEntity } from "./table.js";
|
|
31
32
|
import { userContentEntryEntity } from "./user-content-table.js";
|
|
33
|
+
function assertCollectionKindsAllowed(collections) {
|
|
34
|
+
for (const collection of collections) {
|
|
35
|
+
if (isCollectionRefusedKind(collection.kind)) {
|
|
36
|
+
throw new Error(`template-resolver: collection "${collection.id}" uses kind "${collection.kind}", which collections may not write`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
export function createTemplateResolverFeature(opts = {}) {
|
|
33
41
|
const collections = opts.collections ?? [];
|
|
42
|
+
assertCollectionKindsAllowed(collections);
|
|
34
43
|
const hasUserOwned = collections.some((c) => c.ownership === "user");
|
|
35
44
|
return defineFeature("template-resolver", (r) => {
|
|
36
45
|
r.describe([
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { HandlerContext, SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import type { BeforeTransactionResult, HandlerContext, SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { type FieldIssue } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import type { TemplateWriteScope } from "../layout-regions.js";
|
|
4
|
+
import { type PreparedTemplateFile } from "../store-template-resource.js";
|
|
4
5
|
import type { TemplateBundle } from "../template-bundle.js";
|
|
5
6
|
export declare function targetExistsIssue(locale: string): FieldIssue;
|
|
6
7
|
type BundleImport = {
|
|
@@ -12,8 +13,13 @@ type BundleImport = {
|
|
|
12
13
|
readonly overwrite: boolean;
|
|
13
14
|
};
|
|
14
15
|
type BundleImportStatus = "draft" | "active";
|
|
16
|
+
export type PreparedBundleResources = ReadonlyMap<string, PreparedTemplateFile>;
|
|
17
|
+
export declare function prepareTemplateBundleBeforeTransaction(args: BundleImport & {
|
|
18
|
+
readonly status: BundleImportStatus;
|
|
19
|
+
}): Promise<BeforeTransactionResult<PreparedBundleResources>>;
|
|
15
20
|
export declare function importTemplateBundle(args: BundleImport & {
|
|
16
21
|
readonly status: BundleImportStatus;
|
|
22
|
+
readonly prepared: PreparedBundleResources;
|
|
17
23
|
}): Promise<WriteResult<{
|
|
18
24
|
ids: Record<string, string>;
|
|
19
25
|
}>>;
|