@cosmicdrift/kumiko-samples 0.374.0 → 0.378.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +6 -4
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/constants.ts +1 -0
- package/packages/bundled-features/src/account-security/index.ts +1 -0
- package/packages/bundled-features/src/admin-shell/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/audit/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/web/default-topbar-actions.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/tenant-menu-items.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/tenant-switcher.tsx +1 -1
- package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
- package/packages/bundled-features/src/auth-foundation/changes.json +6 -0
- package/packages/bundled-features/src/auth-foundation/env.ts +27 -0
- package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/contact-card.ts +14 -0
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/index.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +16 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-contact-plan.tsx +32 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +5 -15
- package/packages/bundled-features/src/billing-foundation/web/cancel-contract-dialog.tsx +2 -2
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +1 -1
- package/packages/bundled-features/src/billing-foundation/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/billing-foundation/web/localized-params.ts +14 -0
- package/packages/bundled-features/src/cap-counter/changes.json +12 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +9 -0
- package/packages/bundled-features/src/cap-overview/web/cap-cards-panel.tsx +3 -3
- package/packages/bundled-features/src/cap-overview/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +2 -1
- package/packages/bundled-features/src/compliance-profiles/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/config/feature.ts +2 -2
- package/packages/bundled-features/src/config/web/i18n.ts +1 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +1 -1
- package/packages/bundled-features/src/delivery/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/delivery/web/delivery-status-cell.tsx +1 -1
- package/packages/bundled-features/src/feature-toggles/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/file-provider-s3/changes.json +6 -0
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +45 -0
- package/packages/bundled-features/src/file-provider-s3/env-schema.ts +20 -13
- package/packages/bundled-features/src/file-provider-s3/index.ts +4 -0
- package/packages/bundled-features/src/folders/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/folders/web/folder-section.tsx +3 -4
- package/packages/bundled-features/src/host-routing/README.md +7 -0
- package/packages/bundled-features/src/host-routing/changes.json +6 -0
- package/packages/bundled-features/src/host-routing/constants.ts +4 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +34 -26
- package/packages/bundled-features/src/host-routing/feature.ts +122 -41
- package/packages/bundled-features/src/host-routing/host.ts +28 -1
- package/packages/bundled-features/src/host-routing/index.ts +3 -0
- package/packages/bundled-features/src/host-routing/options.ts +45 -2
- package/packages/bundled-features/src/host-routing/redirects.ts +33 -0
- package/packages/bundled-features/src/host-routing/tenant-lookup.ts +65 -0
- package/packages/bundled-features/src/host-routing/validate-options.ts +48 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +8 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +11 -4
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +100 -0
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +2 -2
- package/packages/bundled-features/src/jobs/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/legal-pages/changes.json +6 -0
- package/packages/bundled-features/src/legal-pages/feature.ts +14 -1
- package/packages/bundled-features/src/legal-pages/layout.ts +6 -6
- package/packages/bundled-features/src/legal-pages/web/client-plugin.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +21 -22
- package/packages/bundled-features/src/mail-foundation/i18n.ts +3 -2
- package/packages/bundled-features/src/mail-transport-smtp/i18n.ts +1 -1
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/notes-history/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/page-render/branding.ts +1 -1
- package/packages/bundled-features/src/page-render/layout.ts +2 -2
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/i18n.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +4 -4
- package/packages/bundled-features/src/seo/sitemap.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +1 -1
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +127 -2
- package/packages/bundled-features/src/tags/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/template-resolver/changes.json +38 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -3
- package/packages/bundled-features/src/template-resolver/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-blocks.ts +36 -0
- package/packages/bundled-features/src/template-resolver/document-images.ts +29 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +515 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +44 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +5 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +79 -34
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +2 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +0 -6
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +67 -58
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +36 -13
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +86 -0
- package/packages/bundled-features/src/template-resolver/handlers/document-blocks.query.ts +48 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +11 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +37 -28
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +66 -62
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +134 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +2 -1
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +62 -49
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +44 -29
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +52 -36
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +126 -106
- package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +38 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +105 -110
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +110 -103
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +22 -1
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +56 -28
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +38 -29
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +24 -9
- package/packages/bundled-features/src/template-resolver/index.ts +8 -1
- package/packages/bundled-features/src/template-resolver/{web/placeholder-text.ts → placeholder-text.ts} +1 -1
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +3 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +6 -1
- package/packages/bundled-features/src/template-resolver/template-edit-policy.ts +91 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +32 -10
- package/packages/bundled-features/src/template-resolver/template-validation.ts +32 -11
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +3 -3
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +29 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +43 -5
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +9 -33
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +2 -2
- package/packages/bundled-features/src/tenant/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/tenant/web/member-status-cell.tsx +1 -1
- package/packages/bundled-features/src/tier-engine/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/env.ts +15 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -2
- package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/client-plugin.tsx +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -2
- package/packages/bundled-features/src/waitlist/changes.json +6 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +51 -2
- package/samples/apps/admin-console/package.json +0 -3
- package/samples/apps/admin-console/src/app/client.tsx +1 -1
- package/samples/apps/admin-console/src/app/shell.tsx +1 -1
- package/samples/apps/config-demo/package.json +1 -3
- package/samples/apps/config-demo/src/app/client.tsx +1 -1
- package/samples/apps/config-demo/src/app/shell.tsx +2 -2
- package/samples/apps/config-demo/src/features/demo/web/index.ts +1 -1
- package/samples/apps/config-demo/tsconfig.json +0 -12
- package/samples/apps/marketing-demo/package.json +1 -3
- package/samples/apps/marketing-demo/src/app/client.tsx +1 -1
- package/samples/apps/marketing-demo/src/app/shell.tsx +1 -1
- package/samples/apps/marketing-demo/src/features/assets/web/index.ts +1 -1
- package/samples/apps/marketing-demo/src/features/helpdesk/web/index.ts +1 -1
- package/samples/apps/showcase/package.json +1 -3
- package/samples/apps/showcase/src/app/client.tsx +1 -1
- package/samples/apps/showcase/src/app/shell.tsx +1 -1
- package/samples/apps/showcase/src/features/demos/web/index.ts +1 -1
- package/samples/apps/showcase/src/features/demos/web/pages/sidebar-panel.tsx +1 -1
- package/samples/apps/showcase/src/features/demos/web/pages/toast.tsx +1 -1
- package/samples/apps/showcase/src/features/items/web/index.ts +1 -1
- package/samples/apps/styleguide/package.json +1 -3
- package/samples/apps/styleguide/src/app/client.tsx +1 -1
- package/samples/apps/styleguide/src/app/shell.tsx +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/web.ts +1 -1
- package/samples/apps/styleguide/src/features/content/web.tsx +2 -2
- package/samples/apps/styleguide/src/features/demo/web.ts +1 -1
- package/samples/apps/styleguide/src/features/examples/web.ts +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/index.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/lib/format.ts +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/FormExamples.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +1 -1
- package/samples/apps/styleguide/src/features/widgets/web/index.ts +1 -1
- package/samples/apps/styleguide/tsconfig.json +0 -12
- package/samples/apps/ui-walkthrough/package.json +1 -3
- package/samples/apps/ui-walkthrough/src/app/client.tsx +1 -1
- package/samples/apps/ui-walkthrough/src/app/help-action.tsx +37 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +7 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +65 -1
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +70 -0
- package/samples/apps/ui-walkthrough/src/features/rental/invoice-receive.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/rental/lease-support.ts +15 -0
- package/samples/apps/ui-walkthrough/src/features/rental/qualified-names.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +79 -0
- package/samples/apps/ui-walkthrough/src/features/rental/web/index.ts +3 -1
- package/samples/apps/ui-walkthrough/src/features/rental/web/upload-standard-layout.tsx +61 -0
- package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
- package/samples/apps/ui-walkthrough/src/features/tasks/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/features/vehicles/web/index.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +32 -3
- package/samples/apps/use-all-bundled/bin/main.ts +3 -2
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +12 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +1 -1
- package/samples/apps/use-all-bundled/feature-manifest.json +3 -2
- package/samples/apps/use-all-bundled/package.json +1 -3
- package/samples/apps/use-all-bundled/src/app/client.tsx +1 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +5 -4
- package/samples/apps/use-all-bundled/src/app/shell.tsx +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -1
- package/samples/apps/workspaces/package.json +1 -3
- package/samples/apps/workspaces/src/app/client.tsx +1 -1
- package/samples/apps/workspaces/src/app/shell.tsx +1 -1
- package/samples/apps/workspaces/src/features/demo/web/index.ts +1 -1
- package/samples/apps/workspaces/src/features/demo-driver/web/index.ts +1 -1
- package/samples/recipes/apex-landing/package.json +1 -1
- package/samples/recipes/apex-landing/src/feature.ts +1 -1
- package/samples/recipes/apex-surface-auth/README.md +1 -1
- package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
- package/samples/recipes/compose-stacks/README.md +7 -4
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/embedded/README.md +8 -0
- package/samples/recipes/embedded/src/feature.ts +8 -0
- package/samples/recipes/embedded/src/handlers/invoice-receive.write.ts +38 -0
- package/samples/recipes/embedded/src/screens/invoice-receive.screen.ts +59 -0
- package/samples/recipes/embedded-entity-form/package.json +0 -1
- package/samples/recipes/embedded-entity-form/src/web/accept-suggestion-drawer.tsx +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/record-detail-layout/package.json +0 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/user-profile/README.md +1 -1
- package/samples/recipes/wizard-form/package.json +0 -1
- package/samples/recipes/wizard-form/src/web/index.ts +2 -2
- package/samples/recipes/wizard-form/src/web/listing-review-section.tsx +2 -2
- package/samples/recipes/writeform-section/package.json +0 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import {
|
|
4
|
+
refuseTemplateEdit,
|
|
5
|
+
type TemplateEditPolicy,
|
|
6
|
+
templateEditAccess,
|
|
7
|
+
} from "../template-edit-policy.ts";
|
|
3
8
|
import { resolveTemplateVariables } from "../template-validation.ts";
|
|
4
9
|
import { upsertPayloadSchema } from "./shared.ts";
|
|
5
10
|
import { writeTemplateRow } from "./template-write.ts";
|
|
@@ -9,32 +14,36 @@ import { writeTemplateRow } from "./template-write.ts";
|
|
|
9
14
|
// via publish-Handler. SystemAdmin kann via tenantIdOverride für
|
|
10
15
|
// einen anderen Tenant schreiben (typisch: Plattform-Admin-UI das
|
|
11
16
|
// Tenant-Templates kuratiert).
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
event.payload.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
17
|
+
export function makeUpsertTenantWrite(policy: TemplateEditPolicy) {
|
|
18
|
+
return defineWriteHandler({
|
|
19
|
+
name: "upsert-tenant",
|
|
20
|
+
schema: upsertPayloadSchema.extend({
|
|
21
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
22
|
+
status: z.enum(["draft", "active"]).default("draft"),
|
|
23
|
+
}),
|
|
24
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
25
|
+
description:
|
|
26
|
+
"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; a TenantAdmin may write it only for templates the app lists in tenantEditable, otherwise SystemAdmin only.",
|
|
27
|
+
handler: async (event, ctx) => {
|
|
28
|
+
const refused = refuseTemplateEdit(policy, event.user, event.payload);
|
|
29
|
+
if (refused !== null) return refused;
|
|
30
|
+
// @cast-boundary engine-payload — override is a zod-validated string and
|
|
31
|
+
// event.user.tenantId is already TenantId-branded; forTenant rejects the
|
|
32
|
+
// override for non-SystemAdmins.
|
|
33
|
+
const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId) as TenantId;
|
|
34
|
+
return writeTemplateRow({
|
|
35
|
+
ctx,
|
|
36
|
+
user: event.user,
|
|
37
|
+
tenantId,
|
|
38
|
+
scope: "tenant",
|
|
39
|
+
status: event.payload.status,
|
|
40
|
+
variables: resolveTemplateVariables(
|
|
41
|
+
ctx.registry,
|
|
42
|
+
event.payload.slug,
|
|
43
|
+
event.payload.variableSchema,
|
|
44
|
+
),
|
|
45
|
+
payload: event.payload,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -47,17 +47,19 @@ type TemplateDraftInput = {
|
|
|
47
47
|
readonly scope: TemplateWriteScope;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
export async function
|
|
50
|
+
// Image keys the editor may reference: the system rows plus, for a tenant write, the caller's own.
|
|
51
|
+
export async function knownResourceKeysFor(
|
|
52
52
|
ctx: HandlerContext,
|
|
53
53
|
user: SessionUser,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
where: {
|
|
55
|
+
readonly slug: string;
|
|
56
|
+
readonly kind: string;
|
|
57
|
+
readonly scope: TemplateWriteScope;
|
|
58
|
+
readonly locale: string | undefined;
|
|
59
|
+
},
|
|
60
|
+
): Promise<string[]> {
|
|
61
|
+
const { slug, kind, scope, locale } = where;
|
|
62
|
+
return [
|
|
61
63
|
...(await knownResourceKeys(ctx.db, {
|
|
62
64
|
tenantId: SYSTEM_TENANT_ID,
|
|
63
65
|
slug,
|
|
@@ -69,6 +71,19 @@ export async function validateTemplateDraft(
|
|
|
69
71
|
? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
|
|
70
72
|
: []),
|
|
71
73
|
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
|
|
77
|
+
export async function validateTemplateDraft(
|
|
78
|
+
ctx: HandlerContext,
|
|
79
|
+
user: SessionUser,
|
|
80
|
+
payload: TemplateDraftInput,
|
|
81
|
+
) {
|
|
82
|
+
const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
|
|
83
|
+
const { slug, kind, locale } = payload;
|
|
84
|
+
// The dry run must not promise what the caller's write handlers would refuse.
|
|
85
|
+
const scope: TemplateWriteScope = isSystemAdmin(user) ? payload.scope : "tenant";
|
|
86
|
+
const known = await knownResourceKeysFor(ctx, user, { slug, kind, scope, locale });
|
|
72
87
|
const evaluation = evaluateTemplateWrite({
|
|
73
88
|
kind,
|
|
74
89
|
scope,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { isResourceImageContentType } from "../shared/index.ts";
|
|
1
2
|
export {
|
|
2
3
|
createTemplateResolverApi,
|
|
3
4
|
type ResolveRequest,
|
|
@@ -8,6 +9,7 @@ export {
|
|
|
8
9
|
} from "./api.ts";
|
|
9
10
|
export {
|
|
10
11
|
type CompileDocumentBodyOptions,
|
|
12
|
+
collectDocumentImageVariables,
|
|
11
13
|
collectDocumentResourceKeys,
|
|
12
14
|
compileDocumentBodyToHandlebars,
|
|
13
15
|
toResourceDataUri,
|
|
@@ -75,7 +77,7 @@ export {
|
|
|
75
77
|
isDocumentReferenceName,
|
|
76
78
|
isDocumentResourceKey,
|
|
77
79
|
} from "./document-schema.ts";
|
|
78
|
-
export { createTemplateResolverFeature } from "./feature.ts";
|
|
80
|
+
export { createTemplateResolverFeature, type TemplateResolverOptions } from "./feature.ts";
|
|
79
81
|
export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.ts";
|
|
80
82
|
export type {
|
|
81
83
|
OverviewKindGroup,
|
|
@@ -126,6 +128,11 @@ export {
|
|
|
126
128
|
templateResourcesTable,
|
|
127
129
|
} from "./table.ts";
|
|
128
130
|
export { type TemplateBundle, templateBundleSchema } from "./template-bundle.ts";
|
|
131
|
+
export {
|
|
132
|
+
type TemplateEditPolicy,
|
|
133
|
+
type TenantEditableTemplate,
|
|
134
|
+
templateEditAccess,
|
|
135
|
+
} from "./template-edit-policy.ts";
|
|
129
136
|
export {
|
|
130
137
|
isMailTemplateMeta,
|
|
131
138
|
type LayoutTemplateMeta,
|
|
@@ -20,6 +20,7 @@ export const TemplateResolverHandlers = {
|
|
|
20
20
|
resourceRemove: "template-resolver:write:resource-remove",
|
|
21
21
|
resourceLinkPdf: "template-resolver:write:resource-link-pdf",
|
|
22
22
|
sendTest: "template-resolver:write:send-test",
|
|
23
|
+
patchDraft: "template-resolver:write:patch-draft",
|
|
23
24
|
} as const;
|
|
24
25
|
|
|
25
26
|
export const TemplateResolverQueries = {
|
|
@@ -32,7 +33,9 @@ export const TemplateResolverQueries = {
|
|
|
32
33
|
diffToSystem: "template-resolver:query:diff-to-system",
|
|
33
34
|
validate: "template-resolver:query:validate",
|
|
34
35
|
preview: "template-resolver:query:preview",
|
|
36
|
+
documentBlocks: "template-resolver:query:document-blocks",
|
|
35
37
|
exportBundle: "template-resolver:query:export-bundle",
|
|
38
|
+
sendTestStatus: "template-resolver:query:send-test-status",
|
|
36
39
|
// Public pair — anonymous-reachable, kind pinned to text-block.
|
|
37
40
|
bySlug: "template-resolver:query:by-slug",
|
|
38
41
|
byTenant: "template-resolver:query:by-tenant",
|
|
@@ -61,6 +64,7 @@ export const TemplateResolverErrors = {
|
|
|
61
64
|
targetExists: "target_exists",
|
|
62
65
|
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
63
66
|
systemOnlyTemplate: "system_only_template",
|
|
67
|
+
templateNotTenantEditable: "template_not_tenant_editable",
|
|
64
68
|
bundleLocalesDiffer: "bundle_locales_differ",
|
|
65
69
|
resourceTooLarge: "template_resource_too_large",
|
|
66
70
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
@@ -78,6 +78,7 @@ export type SeedSystemTemplateOptions = {
|
|
|
78
78
|
* system admin uploaded through resource-upload. */
|
|
79
79
|
readonly linkedResources?: Record<string, string>;
|
|
80
80
|
readonly meta?: unknown;
|
|
81
|
+
readonly title?: string;
|
|
81
82
|
readonly parentTemplateId?: string | null;
|
|
82
83
|
readonly by?: SessionUser;
|
|
83
84
|
readonly ifExists?: SeedIfExists;
|
|
@@ -138,7 +139,7 @@ export async function seedSystemTemplate(
|
|
|
138
139
|
variableSchema,
|
|
139
140
|
...(linkedResources !== undefined && { linkedResources }),
|
|
140
141
|
meta,
|
|
141
|
-
title: null,
|
|
142
|
+
title: opts.title ?? null,
|
|
142
143
|
folder: null,
|
|
143
144
|
scope: "system" as const,
|
|
144
145
|
parentTemplateId,
|
|
@@ -261,6 +262,7 @@ export async function seedSystemTemplateBundle(
|
|
|
261
262
|
contentFormat: bundle.contentFormat,
|
|
262
263
|
variableSchema: bundle.variableSchema ?? {},
|
|
263
264
|
meta: entry.meta,
|
|
265
|
+
...(entry.title !== undefined ? { title: entry.title } : {}),
|
|
264
266
|
...(opts.by !== undefined ? { by: opts.by } : {}),
|
|
265
267
|
...(opts.ifExists !== undefined ? { ifExists: opts.ifExists } : {}),
|
|
266
268
|
});
|
|
@@ -40,7 +40,12 @@ const bundleResourceSchema = z
|
|
|
40
40
|
const pdfAttachmentLinkBodySchema = templatePdfAttachmentLinkSchema.omit({ kind: true });
|
|
41
41
|
|
|
42
42
|
const bundleLocaleEntrySchema = z
|
|
43
|
-
.object({
|
|
43
|
+
.object({
|
|
44
|
+
body: z.string().max(200_000),
|
|
45
|
+
meta: z.unknown().optional(),
|
|
46
|
+
// Shown in the template overview instead of the slug; one per locale, so it is translated.
|
|
47
|
+
title: z.string().min(1).max(200).optional(),
|
|
48
|
+
})
|
|
44
49
|
.strict();
|
|
45
50
|
|
|
46
51
|
export const templateBundleSchema = z
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
type HandlerContext,
|
|
4
|
+
hasAccess,
|
|
5
|
+
type RoleAccessRule,
|
|
6
|
+
type SessionUser,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import {
|
|
9
|
+
AccessDeniedError,
|
|
10
|
+
type WriteFailure,
|
|
11
|
+
writeFailure,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
13
|
+
import type { TemplateKind } from "./constants.ts";
|
|
14
|
+
import { TemplateResolverErrors } from "./qualified-names.ts";
|
|
15
|
+
import { type TemplateResourceRow, templateResourcesTable } from "./table.ts";
|
|
16
|
+
import { isSystemAdmin } from "./template-draft.ts";
|
|
17
|
+
|
|
18
|
+
export type TenantEditableTemplate = {
|
|
19
|
+
readonly kind: TemplateKind;
|
|
20
|
+
readonly slug: string;
|
|
21
|
+
readonly access: RoleAccessRule;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TemplateEditPolicy = {
|
|
25
|
+
readonly tenantEditable: readonly TenantEditableTemplate[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type EditTarget = { readonly slug: string; readonly kind: string };
|
|
29
|
+
type EditUser = Pick<SessionUser, "roles">;
|
|
30
|
+
|
|
31
|
+
const SYSTEM_ADMIN_ROLE = "SystemAdmin";
|
|
32
|
+
|
|
33
|
+
export function templateEditAccess(entries: readonly TenantEditableTemplate[]): RoleAccessRule {
|
|
34
|
+
const roles = new Set<string>([SYSTEM_ADMIN_ROLE]);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
for (const role of entry.access.roles) roles.add(role);
|
|
37
|
+
}
|
|
38
|
+
return { roles: [...roles] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mayEditTemplate(
|
|
42
|
+
policy: TemplateEditPolicy,
|
|
43
|
+
user: EditUser,
|
|
44
|
+
target: EditTarget,
|
|
45
|
+
): boolean {
|
|
46
|
+
if (isSystemAdmin(user)) return true;
|
|
47
|
+
const entry = policy.tenantEditable.find(
|
|
48
|
+
(candidate) => candidate.slug === target.slug && candidate.kind === target.kind,
|
|
49
|
+
);
|
|
50
|
+
return entry !== undefined && hasAccess(user, entry.access);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function refuseTemplateEdit(
|
|
54
|
+
policy: TemplateEditPolicy,
|
|
55
|
+
user: EditUser,
|
|
56
|
+
target: EditTarget,
|
|
57
|
+
): WriteFailure | null {
|
|
58
|
+
if (mayEditTemplate(policy, user, target)) return null;
|
|
59
|
+
return writeFailure(
|
|
60
|
+
new AccessDeniedError({
|
|
61
|
+
i18nKey: "templateResolver.errors.templateNotTenantEditable",
|
|
62
|
+
details: { reason: TemplateResolverErrors.templateNotTenantEditable },
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fail closed: a missing row gives a non-SystemAdmin the same refusal as a locked one,
|
|
68
|
+
// so the response never reveals whether an id exists. SystemAdmin keeps the 404.
|
|
69
|
+
export function refuseMissingTemplate(
|
|
70
|
+
policy: TemplateEditPolicy,
|
|
71
|
+
user: EditUser,
|
|
72
|
+
): WriteFailure | null {
|
|
73
|
+
return isSystemAdmin(user) ? null : refuseTemplateEdit(policy, user, { slug: "", kind: "" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function refuseTemplateEditById(
|
|
77
|
+
db: HandlerContext["db"],
|
|
78
|
+
policy: TemplateEditPolicy,
|
|
79
|
+
user: EditUser,
|
|
80
|
+
tenantId: string,
|
|
81
|
+
templateId: string,
|
|
82
|
+
): Promise<WriteFailure | null> {
|
|
83
|
+
if (isSystemAdmin(user)) return null;
|
|
84
|
+
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
85
|
+
tenantId,
|
|
86
|
+
id: templateId,
|
|
87
|
+
});
|
|
88
|
+
return row === undefined
|
|
89
|
+
? refuseMissingTemplate(policy, user)
|
|
90
|
+
: refuseTemplateEdit(policy, user, row);
|
|
91
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import
|
|
3
|
-
HandlerContext,
|
|
4
|
-
SessionUser,
|
|
5
|
-
TemplateListVariableDef,
|
|
6
|
-
TemplateScalarVariableDef,
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
type HandlerContext,
|
|
4
|
+
type SessionUser,
|
|
5
|
+
type TemplateListVariableDef,
|
|
6
|
+
type TemplateScalarVariableDef,
|
|
7
|
+
type TemplateVariableDef,
|
|
8
|
+
type TemplateVariables,
|
|
9
|
+
templateVariablesToZod,
|
|
8
10
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
11
|
import {
|
|
10
12
|
type KumikoError,
|
|
11
13
|
NotFoundError,
|
|
12
14
|
UnprocessableError,
|
|
13
15
|
ValidationError,
|
|
16
|
+
validationErrorFromZod,
|
|
14
17
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
15
18
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
16
19
|
import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
|
|
17
|
-
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
20
|
+
import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
|
|
21
|
+
import * as z from "zod";
|
|
18
22
|
import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.ts";
|
|
19
23
|
import { fileContextForTenant } from "../file-foundation/index.ts";
|
|
20
24
|
import {
|
|
@@ -117,6 +121,12 @@ function sampleList(def: TemplateListVariableDef, labelText: LabelText): Record<
|
|
|
117
121
|
return rows;
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
function sampleValueFor(name: string, def: TemplateVariableDef, labelText: LabelText): unknown {
|
|
125
|
+
// An images variable has no sample: file ids are record data, never preview content.
|
|
126
|
+
if (def.type === "images") return [];
|
|
127
|
+
return def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
// Declared examples win; the fallbacks are deterministic so a preview never differs between calls.
|
|
121
131
|
export function sampleValuesFor(
|
|
122
132
|
variables: TemplateVariables,
|
|
@@ -124,8 +134,7 @@ export function sampleValuesFor(
|
|
|
124
134
|
): Record<string, unknown> {
|
|
125
135
|
const values: Record<string, unknown> = {};
|
|
126
136
|
for (const [name, def] of Object.entries(variables)) {
|
|
127
|
-
values[name] =
|
|
128
|
-
def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
|
|
137
|
+
values[name] = sampleValueFor(name, def, labelText);
|
|
129
138
|
}
|
|
130
139
|
return values;
|
|
131
140
|
}
|
|
@@ -177,6 +186,8 @@ export type TemplatePreviewInput = {
|
|
|
177
186
|
readonly content?: string | undefined;
|
|
178
187
|
readonly meta?: unknown;
|
|
179
188
|
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
189
|
+
/** Replaces the declared example values for the named variables. */
|
|
190
|
+
readonly variables?: Readonly<Record<string, unknown>> | undefined;
|
|
180
191
|
};
|
|
181
192
|
|
|
182
193
|
type ValidationResponse = Awaited<ReturnType<typeof validateTemplateDraft>>["response"];
|
|
@@ -197,6 +208,7 @@ export type TemplatePreview = {
|
|
|
197
208
|
readonly scope: TemplateScope;
|
|
198
209
|
readonly version: number;
|
|
199
210
|
} | null;
|
|
211
|
+
/** The values the preview was actually rendered with: examples plus any overrides. */
|
|
200
212
|
readonly sampleValues: Readonly<Record<string, unknown>>;
|
|
201
213
|
readonly checks: readonly PreviewCheck[];
|
|
202
214
|
readonly validation: ValidationResponse;
|
|
@@ -689,7 +701,17 @@ export async function renderTemplatePreview(
|
|
|
689
701
|
|
|
690
702
|
const content = evaluation.content ?? "";
|
|
691
703
|
const meta = parseTemplateMeta(input.kind, parseStoredJson(evaluation.metaJson));
|
|
692
|
-
const
|
|
704
|
+
const exampleValues = sampleValuesFor(validation.variables, labelTextFor(ctx, input.locale));
|
|
705
|
+
let sampleValues = exampleValues;
|
|
706
|
+
if (input.variables !== undefined) {
|
|
707
|
+
const overrides = z.object({
|
|
708
|
+
variables: templateVariablesToZod(validation.variables).partial().strict(),
|
|
709
|
+
});
|
|
710
|
+
const checked = overrides.safeParse({ variables: input.variables });
|
|
711
|
+
if (!checked.success) return failed(validationErrorFromZod(checked.error));
|
|
712
|
+
// Original values, not the parse result: parsing would strip keys inside list rows.
|
|
713
|
+
sampleValues = { ...exampleValues, ...input.variables };
|
|
714
|
+
}
|
|
693
715
|
|
|
694
716
|
if (input.kind === "document-pdf") {
|
|
695
717
|
return renderDocumentPreview(ctx, user, {
|
|
@@ -65,6 +65,7 @@ const scalarVariableSchema = z.discriminatedUnion("type", [
|
|
|
65
65
|
|
|
66
66
|
const variableDefSchema = z.union([
|
|
67
67
|
scalarVariableSchema,
|
|
68
|
+
z.object({ ...variableCommon, type: z.literal("images") }),
|
|
68
69
|
z.object({
|
|
69
70
|
...variableCommon,
|
|
70
71
|
type: z.literal("list"),
|
|
@@ -76,7 +77,7 @@ const variableDefSchema = z.union([
|
|
|
76
77
|
// A stored variableSchema value is either a bare string (legacy "this is a text
|
|
77
78
|
// variable") or a TemplateVariableDef object; anything else is not declared and
|
|
78
79
|
// therefore shows up as an unknown placeholder instead of being guessed at.
|
|
79
|
-
function toVariableDef(raw: unknown): TemplateVariableDef | null {
|
|
80
|
+
export function toVariableDef(raw: unknown): TemplateVariableDef | null {
|
|
80
81
|
if (typeof raw === "string") return { type: "text" };
|
|
81
82
|
const parsed = variableDefSchema.safeParse(raw);
|
|
82
83
|
return parsed.success ? parsed.data : null;
|
|
@@ -126,17 +127,24 @@ function listItemFields(
|
|
|
126
127
|
return def?.type === "list" ? def.item : {};
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// An images variable holds file ids: as text it would leak them, so only an images block
|
|
131
|
+
// (and a condition on its presence) may name it.
|
|
129
132
|
function isKnownName(
|
|
130
133
|
name: string,
|
|
131
134
|
scope: PlaceholderScope,
|
|
132
135
|
itemFields: Readonly<Record<string, unknown>>,
|
|
136
|
+
allowImages = false,
|
|
133
137
|
): Resolution {
|
|
134
138
|
const { variables } = scope;
|
|
135
139
|
const root = rootName(name);
|
|
136
140
|
// Declared variables and item fields are scalars: a dotted path under one never renders a value.
|
|
137
141
|
if (Object.hasOwn(itemFields, name)) return { known: true, used: "" };
|
|
138
142
|
if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
|
|
139
|
-
if (Object.hasOwn(variables, name))
|
|
143
|
+
if (Object.hasOwn(variables, name)) {
|
|
144
|
+
return variables[name]?.type === "images" && !allowImages
|
|
145
|
+
? { known: false, used: "" }
|
|
146
|
+
: { known: true, used: name };
|
|
147
|
+
}
|
|
140
148
|
return { known: false, used: "" };
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -144,8 +152,9 @@ function recordName(
|
|
|
144
152
|
name: string,
|
|
145
153
|
itemFields: Readonly<Record<string, unknown>>,
|
|
146
154
|
scope: PlaceholderScope,
|
|
155
|
+
allowImages = false,
|
|
147
156
|
) {
|
|
148
|
-
const resolution = isKnownName(name, scope, itemFields);
|
|
157
|
+
const resolution = isKnownName(name, scope, itemFields, allowImages);
|
|
149
158
|
if (!resolution.known) scope.unknown.add(name);
|
|
150
159
|
else if (resolution.used !== "") scope.used.add(resolution.used);
|
|
151
160
|
}
|
|
@@ -154,10 +163,18 @@ function enclosingEachName(element: ScanElement): string | null {
|
|
|
154
163
|
return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
function nameAttributeOf(element: ScanElement): string | undefined {
|
|
167
|
+
const tag = element.tagName.toLowerCase();
|
|
168
|
+
if (tag === "span") return "data-variable";
|
|
169
|
+
if (tag !== "div") return undefined;
|
|
170
|
+
return element.hasAttribute("data-images") ? "data-images" : "data-steps";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function recordImagesName(name: string, scope: PlaceholderScope): void {
|
|
174
|
+
const def = Object.hasOwn(scope.variables, name) ? scope.variables[name] : undefined;
|
|
175
|
+
if (def?.type === "images") scope.used.add(name);
|
|
176
|
+
else scope.unknown.add(name);
|
|
177
|
+
}
|
|
161
178
|
|
|
162
179
|
function scanRepeatRows(root: ScanElement, scope: PlaceholderScope): void {
|
|
163
180
|
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
@@ -174,18 +191,22 @@ function scanConditions(root: ScanElement, scope: PlaceholderScope): void {
|
|
|
174
191
|
scope.variables,
|
|
175
192
|
enclosingEachName(conditionScopeElement(element)),
|
|
176
193
|
);
|
|
177
|
-
recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope);
|
|
194
|
+
recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope, true);
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
function scanReferences(root: ScanElement, scope: PlaceholderScope): void {
|
|
182
199
|
for (const element of Array.from(
|
|
183
|
-
root.querySelectorAll(
|
|
200
|
+
root.querySelectorAll(
|
|
201
|
+
"span[data-variable], div[data-steps], div[data-images], a[href], img[data-resource]",
|
|
202
|
+
),
|
|
184
203
|
)) {
|
|
185
204
|
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
186
205
|
const tag = element.tagName.toLowerCase();
|
|
187
|
-
const nameAttribute =
|
|
188
|
-
if (nameAttribute
|
|
206
|
+
const nameAttribute = nameAttributeOf(element);
|
|
207
|
+
if (nameAttribute === "data-images") {
|
|
208
|
+
recordImagesName(element.getAttribute(nameAttribute) ?? "", scope);
|
|
209
|
+
} else if (nameAttribute !== undefined) {
|
|
189
210
|
recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
|
|
190
211
|
} else if (tag === "a") {
|
|
191
212
|
const href = element.getAttribute("href") ?? "";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
1
|
+
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
2
|
// @runtime client
|
|
3
3
|
// Client feature factory for the text-block content tree. Apps mount it via
|
|
4
4
|
// createKumikoApp({ clientFeatures: [textBlocksClient()] }); it loads the
|
|
@@ -12,7 +12,6 @@ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
|
12
12
|
// (a hint that the slug exists but is empty).
|
|
13
13
|
|
|
14
14
|
import { useShellUser } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
15
|
-
import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
16
15
|
import type {
|
|
17
16
|
TargetRef,
|
|
18
17
|
TreeChildrenSubscribe,
|
|
@@ -28,11 +27,12 @@ import {
|
|
|
28
27
|
useQuery,
|
|
29
28
|
useTranslation,
|
|
30
29
|
} from "@cosmicdrift/kumiko-renderer";
|
|
30
|
+
import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-renderer/live";
|
|
31
31
|
import {
|
|
32
32
|
type ClientFeatureDefinition,
|
|
33
33
|
PlainContentEditor,
|
|
34
34
|
RichContentEditor,
|
|
35
|
-
} from "@cosmicdrift/kumiko-renderer
|
|
35
|
+
} from "@cosmicdrift/kumiko-renderer/web";
|
|
36
36
|
import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
|
|
37
37
|
import { type FormEvent, type ReactNode, useEffect, useId, useState } from "react";
|
|
38
38
|
import {
|
|
@@ -5,6 +5,7 @@ import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions
|
|
|
5
5
|
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
6
6
|
import {
|
|
7
7
|
DocumentButton,
|
|
8
|
+
DocumentImages,
|
|
8
9
|
DocumentPageBreak,
|
|
9
10
|
DocumentResource,
|
|
10
11
|
DocumentSteps,
|
|
@@ -23,6 +24,7 @@ export type DocumentEditorLabels = {
|
|
|
23
24
|
readonly pageBreak: string;
|
|
24
25
|
readonly button?: string;
|
|
25
26
|
readonly steps?: string;
|
|
27
|
+
readonly images?: string;
|
|
26
28
|
/** Canvas block label of an image, e.g. "Image · inline". */
|
|
27
29
|
readonly imageBlock?: string;
|
|
28
30
|
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
@@ -206,6 +208,31 @@ function createStepsView(labels: DocumentEditorLabels): NodeViewRenderer {
|
|
|
206
208
|
};
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
// A sketch only: the photos are record data, known at render time.
|
|
212
|
+
function createImagesView(labels: DocumentEditorLabels): NodeViewRenderer {
|
|
213
|
+
return ({ node }) => {
|
|
214
|
+
const dom = createElementWithClass(
|
|
215
|
+
"div",
|
|
216
|
+
"relative my-3 rounded border border-dashed border-input px-3 pb-3 pt-4",
|
|
217
|
+
);
|
|
218
|
+
dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
|
|
219
|
+
dom.contentEditable = "false";
|
|
220
|
+
const variable = String(node.attrs["variable"]);
|
|
221
|
+
const variableLabels = labels.variableLabels ?? {};
|
|
222
|
+
const variableLabel = Object.hasOwn(variableLabels, variable)
|
|
223
|
+
? variableLabels[variable]
|
|
224
|
+
: undefined;
|
|
225
|
+
dom.append(createBlockLabel(`${labels.images ?? "Photos"} · ${variableLabel ?? variable}`));
|
|
226
|
+
const grid = createElementWithClass("div", "grid grid-cols-2 gap-1.5");
|
|
227
|
+
grid.append(
|
|
228
|
+
createElementWithClass("div", "h-12 rounded bg-muted"),
|
|
229
|
+
createElementWithClass("div", "h-12 rounded bg-muted"),
|
|
230
|
+
);
|
|
231
|
+
dom.append(grid);
|
|
232
|
+
return { dom };
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
209
236
|
// Replaced by name, not appended: the schema must keep exactly one node per
|
|
210
237
|
// name, and only the editor view differs from the shared document schema.
|
|
211
238
|
export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Extensions {
|
|
@@ -224,6 +251,8 @@ export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Ex
|
|
|
224
251
|
: extension;
|
|
225
252
|
case DocumentSteps.name:
|
|
226
253
|
return DocumentSteps.extend({ addNodeView: () => createStepsView(labels) });
|
|
254
|
+
case DocumentImages.name:
|
|
255
|
+
return DocumentImages.extend({ addNodeView: () => createImagesView(labels) });
|
|
227
256
|
case DocumentPageBreak.name:
|
|
228
257
|
return DocumentPageBreak.extend({
|
|
229
258
|
addNodeView: () => createPageBreakView(labels.pageBreak),
|