@cosmicdrift/kumiko-bundled-features 0.182.0 → 0.183.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.183.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -121,12 +121,12 @@
|
|
|
121
121
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
125
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
126
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
127
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
128
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
129
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
124
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.183.0",
|
|
125
|
+
"@cosmicdrift/kumiko-framework": "0.183.0",
|
|
126
|
+
"@cosmicdrift/kumiko-headless": "0.183.0",
|
|
127
|
+
"@cosmicdrift/kumiko-renderer": "0.183.0",
|
|
128
|
+
"@cosmicdrift/kumiko-renderer-web": "0.183.0",
|
|
129
|
+
"@cosmicdrift/kumiko-types": "0.183.0",
|
|
130
130
|
"@mollie/api-client": "^4.5.0",
|
|
131
131
|
"imapflow": "^1.3.3",
|
|
132
132
|
"mailparser": "^3.9.8",
|
|
@@ -42,7 +42,14 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
|
|
|
42
42
|
const hasUserOwned = collections.some((c) => c.ownership === "user");
|
|
43
43
|
return defineFeature("template-resolver", (r) => {
|
|
44
44
|
r.describe(
|
|
45
|
-
|
|
45
|
+
[
|
|
46
|
+
"Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail-html`, `document-pdf`, `ai-prompt`, `text-block`, `image-snapshot`).",
|
|
47
|
+
"Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes.",
|
|
48
|
+
"Text an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.",
|
|
49
|
+
"How a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links). Both offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format and wins over the built-in one.",
|
|
50
|
+
'A collection is tenant-wide by default. With `ownership: "user"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.',
|
|
51
|
+
"Replaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
|
|
52
|
+
].join("\n\n"),
|
|
46
53
|
);
|
|
47
54
|
r.uiHints({
|
|
48
55
|
displayLabel: "Template Resolver",
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
20
|
import {
|
|
21
21
|
CONTENT_EDITOR_ELEMENT_ID,
|
|
22
|
+
ContentPreview,
|
|
22
23
|
type FeatureSchema,
|
|
23
24
|
useAppFeatures,
|
|
24
25
|
useContentEditor,
|
|
@@ -27,7 +28,11 @@ import {
|
|
|
27
28
|
useQuery,
|
|
28
29
|
useTranslation,
|
|
29
30
|
} from "@cosmicdrift/kumiko-renderer";
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type ClientFeatureDefinition,
|
|
33
|
+
PlainContentEditor,
|
|
34
|
+
RichContentEditor,
|
|
35
|
+
} from "@cosmicdrift/kumiko-renderer-web";
|
|
31
36
|
import { type FormEvent, type ReactNode, useEffect, useState } from "react";
|
|
32
37
|
import {
|
|
33
38
|
collectionHandlerName,
|
|
@@ -224,12 +229,21 @@ function findVariableSchema(
|
|
|
224
229
|
features: readonly FeatureSchema[],
|
|
225
230
|
collectionId?: string,
|
|
226
231
|
): readonly string[] {
|
|
227
|
-
|
|
232
|
+
return Object.keys(findVariableExamples(features, collectionId));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Same lookup, for the example values the Preview substitutes in for
|
|
236
|
+
// `{{name}}` — the values half of the same variableSchema map.
|
|
237
|
+
function findVariableExamples(
|
|
238
|
+
features: readonly FeatureSchema[],
|
|
239
|
+
collectionId?: string,
|
|
240
|
+
): Readonly<Record<string, string>> {
|
|
241
|
+
if (collectionId === undefined) return {};
|
|
228
242
|
for (const feature of features) {
|
|
229
243
|
const match = feature.contentCollections?.find((c) => c.id === collectionId);
|
|
230
|
-
if (match !== undefined) return
|
|
244
|
+
if (match !== undefined) return match.variableSchema ?? {};
|
|
231
245
|
}
|
|
232
|
-
return
|
|
246
|
+
return {};
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
// Edit form: loads the current values via by-slug, lets TenantAdmin and
|
|
@@ -278,6 +292,7 @@ function TextBlockEditor({
|
|
|
278
292
|
const features = useAppFeatures();
|
|
279
293
|
const contentFormat = findContentFormat(features, collectionId);
|
|
280
294
|
const variables = findVariableSchema(features, collectionId);
|
|
295
|
+
const variableExamples = findVariableExamples(features, collectionId);
|
|
281
296
|
const ContentEditor = useContentEditor(contentFormat);
|
|
282
297
|
const canWrite =
|
|
283
298
|
user?.roles.includes("TenantAdmin") === true || user?.roles.includes("SystemAdmin") === true;
|
|
@@ -302,6 +317,7 @@ function TextBlockEditor({
|
|
|
302
317
|
|
|
303
318
|
const [title, setTitle] = useState("");
|
|
304
319
|
const [content, setContent] = useState("");
|
|
320
|
+
const [previewing, setPreviewing] = useState(false);
|
|
305
321
|
const [submitting, setSubmitting] = useState(false);
|
|
306
322
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
307
323
|
const [savedMsg, setSavedMsg] = useState<string | null>(null);
|
|
@@ -390,13 +406,34 @@ function TextBlockEditor({
|
|
|
390
406
|
required
|
|
391
407
|
/>
|
|
392
408
|
</Field>
|
|
393
|
-
<Field
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
409
|
+
<Field
|
|
410
|
+
id={CONTENT_EDITOR_ELEMENT_ID}
|
|
411
|
+
label={t("template-resolver.editor.contentLabel")}
|
|
412
|
+
labelAppendix={
|
|
413
|
+
<Button
|
|
414
|
+
type="button"
|
|
415
|
+
variant="secondary"
|
|
416
|
+
size="sm"
|
|
417
|
+
onClick={() => setPreviewing((p) => !p)}
|
|
418
|
+
>
|
|
419
|
+
{previewing ? t("kumiko.contentEditor.editMode") : t("kumiko.contentEditor.preview")}
|
|
420
|
+
</Button>
|
|
421
|
+
}
|
|
422
|
+
>
|
|
423
|
+
{previewing ? (
|
|
424
|
+
<ContentPreview
|
|
425
|
+
content={content}
|
|
426
|
+
variables={variableExamples}
|
|
427
|
+
contentFormat={contentFormat}
|
|
428
|
+
/>
|
|
429
|
+
) : (
|
|
430
|
+
<ContentEditor
|
|
431
|
+
value={content}
|
|
432
|
+
onChange={setContent}
|
|
433
|
+
variables={variables}
|
|
434
|
+
readOnly={disabled}
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
400
437
|
</Field>
|
|
401
438
|
{saveError !== null && <Banner variant="error">{saveError}</Banner>}
|
|
402
439
|
{savedMsg !== null && <Banner variant="info">{savedMsg}</Banner>}
|
|
@@ -447,6 +484,7 @@ export function textBlocksClient(opts?: {
|
|
|
447
484
|
// another clientFeature still wins, last-wins same as columnRenderers.
|
|
448
485
|
contentEditors: {
|
|
449
486
|
plain: PlainContentEditor,
|
|
487
|
+
rich: RichContentEditor,
|
|
450
488
|
// ponytail: same textarea+chips as "plain" — markdown is stored as
|
|
451
489
|
// text either way. Shares PlainContentEditor's fixed-DOM-id ceiling
|
|
452
490
|
// (see its own file comment); swap for a live-preview widget or a
|