@kubex/zinc 1.1.95 → 1.1.96
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/custom-elements.json +193 -428
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +3 -3
- package/dist/zn.d.ts +113 -224
- package/dist/zn.min.js +331 -377
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +21 -92
- package/package.json +1 -1
- package/src/components/flow-builder/flow-builder.component.ts +16 -1
- package/src/components/flow-builder/flow-builder.test.ts +206 -1
- package/src/components/flow-builder/flow-geometry.test.ts +102 -0
- package/src/components/flow-builder/flow.types.ts +82 -15
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
- package/src/components/page-builder/page-builder.component.ts +229 -429
- package/src/components/page-builder/page-builder.scss +10 -64
- package/src/components/page-builder/page-builder.test.ts +162 -614
- package/src/components/page-builder/page.types.ts +7 -75
- package/src/components/remarkd-editor/remarkd-editor.component.ts +37 -11
- package/src/components/remarkd-editor/remarkd-editor.test.ts +79 -0
- package/src/components/page-builder/page-tree.test.ts +0 -483
- package/src/components/page-builder/page-tree.ts +0 -329
|
@@ -8,25 +8,10 @@ export interface PageSection {
|
|
|
8
8
|
label?: string;
|
|
9
9
|
/** Section content, keyed by field name (the inspector's `name` attributes). */
|
|
10
10
|
data: Record<string, unknown>;
|
|
11
|
-
/**
|
|
12
|
-
layout?: PageContainerLayout;
|
|
13
|
-
/** Container instances only: one ordered stack per cell, row-major. */
|
|
14
|
-
cells?: PageSection[][];
|
|
15
|
-
/**
|
|
16
|
-
* @deprecated The pre-cells fixed-slot shape. Read on load and migrated to
|
|
17
|
-
* `cells`; never written back.
|
|
18
|
-
*/
|
|
11
|
+
/** Slot contents for container sections, sized to the type's `slots`. Empty slots are null. */
|
|
19
12
|
children?: (PageSection | null)[];
|
|
20
13
|
}
|
|
21
14
|
|
|
22
|
-
/** A container instance's editor-chosen layout. */
|
|
23
|
-
export interface PageContainerLayout {
|
|
24
|
-
/** One weight per column; the array length IS the column count. */
|
|
25
|
-
widths: number[];
|
|
26
|
-
/** When true the builder offers a trailing empty row instead of a pinned cell count. */
|
|
27
|
-
grow: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
15
|
/** The complete serialisable state of a page. Order = render order. */
|
|
31
16
|
export interface PageState {
|
|
32
17
|
sections: PageSection[];
|
|
@@ -53,65 +38,18 @@ export interface PageSectionType {
|
|
|
53
38
|
configTemplate?: HTMLTemplateElement;
|
|
54
39
|
/** Programmatic inspector body — takes precedence over `configTemplate`. */
|
|
55
40
|
renderConfig?: (section: PageSection, update: (data: Record<string, unknown>) => void) => TemplateResult;
|
|
56
|
-
/** Marks this type a container: its card renders a grid of cells on the canvas. */
|
|
57
|
-
container?: boolean;
|
|
58
41
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*/
|
|
62
|
-
defaultWidths?: number[];
|
|
63
|
-
/** Whether a new instance of this container starts growable. */
|
|
64
|
-
defaultGrow?: boolean;
|
|
65
|
-
/**
|
|
66
|
-
* @deprecated Use `container` with `columns`/`widths`. Read as a container of
|
|
67
|
-
* `DEFAULT_WIDTHS` columns with the cell count pinned to this number.
|
|
42
|
+
* Number of child slots this section offers on the canvas (a container tile);
|
|
43
|
+
* rendered as a 3-column grid. Containers cannot be placed inside other containers.
|
|
68
44
|
*/
|
|
69
45
|
slots?: number;
|
|
70
|
-
/**
|
|
71
|
-
* Section type keys allowed in this container's cells. On a `container` type,
|
|
72
|
-
* omitting it allows any type, subject to the nesting cap — that is how nesting
|
|
73
|
-
* is reachable without enumerating types. The deprecated `slots=` alias keeps the
|
|
74
|
-
* old rule instead: omitting it there allows any non-container type.
|
|
75
|
-
*/
|
|
46
|
+
/** Section type keys allowed in this container's slots. Omit to allow any non-container type. */
|
|
76
47
|
accepts?: string[];
|
|
77
48
|
}
|
|
78
49
|
|
|
79
|
-
/**
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
export const MAX_WIDTH = 12;
|
|
83
|
-
/** Container nesting levels: a top-level container is 1, one inside a cell is 2. */
|
|
84
|
-
export const MAX_CONTAINER_LEVELS = 2;
|
|
85
|
-
/** Global section budget; also the sanity clamp on an incoming `cells` length. */
|
|
86
|
-
export const MAX_SECTIONS = 500;
|
|
87
|
-
/** What a bare `container` declaration seeds. */
|
|
88
|
-
export const DEFAULT_WIDTHS: readonly number[] = [1, 1, 1];
|
|
89
|
-
|
|
90
|
-
/** Container-ness is a property of the registered type, never of the instance. */
|
|
91
|
-
export function isContainer(type: PageSectionType | undefined): boolean {
|
|
92
|
-
return Boolean(type?.container ?? (type?.slots !== undefined && type.slots > 0));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Coerces a weights list into a usable one: each entry a whole number from 1 to
|
|
97
|
-
* MAX_WIDTH (anything unusable becomes 1), at most MAX_COLUMNS entries, falling
|
|
98
|
-
* back to DEFAULT_WIDTHS only when nothing usable remains.
|
|
99
|
-
*/
|
|
100
|
-
export function sanitiseWidths(raw: unknown): number[] {
|
|
101
|
-
const list = Array.isArray(raw) ? raw.slice(0, MAX_COLUMNS) : [];
|
|
102
|
-
const clean = list.map(entry => {
|
|
103
|
-
const n = typeof entry === 'number' && Number.isFinite(entry) ? Math.floor(entry) : 1;
|
|
104
|
-
return Math.min(Math.max(n, 1), MAX_WIDTH);
|
|
105
|
-
});
|
|
106
|
-
return clean.length ? clean : [...DEFAULT_WIDTHS];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** The layout a newly placed instance of a container type starts with. */
|
|
110
|
-
export function defaultLayout(type: PageSectionType | undefined): PageContainerLayout {
|
|
111
|
-
return {
|
|
112
|
-
widths: sanitiseWidths(type?.defaultWidths ?? [...DEFAULT_WIDTHS]),
|
|
113
|
-
grow: Boolean(type?.defaultGrow),
|
|
114
|
-
};
|
|
50
|
+
/** A container section's slot contents, padded/truncated to the type's slot count. */
|
|
51
|
+
export function sectionChildren(section: PageSection, type: PageSectionType): (PageSection | null)[] {
|
|
52
|
+
return Array.from({length: type.slots ?? 0}, (_, i) => section.children?.[i] ?? null);
|
|
115
53
|
}
|
|
116
54
|
|
|
117
55
|
/** Drag-and-drop MIME carrying a section type id from the palette to the canvas. */
|
|
@@ -147,12 +85,6 @@ function optionLabel(type: PageSectionType | undefined, name: string, value: str
|
|
|
147
85
|
* so a tile reads as its linked item however its other fields were filled in.
|
|
148
86
|
*/
|
|
149
87
|
export function sectionSummary(section: PageSection, type?: PageSectionType): string {
|
|
150
|
-
if (isContainer(type)) {
|
|
151
|
-
const columns = sanitiseWidths(section.layout?.widths ?? type?.defaultWidths).length;
|
|
152
|
-
const count = (section.cells ?? []).reduce((n, cell) => n + cell.length, 0);
|
|
153
|
-
const col = `${columns} column${columns === 1 ? '' : 's'}`;
|
|
154
|
-
return `${col} · ${count} section${count === 1 ? '' : 's'}`;
|
|
155
|
-
}
|
|
156
88
|
const strings = Object.entries(section.data)
|
|
157
89
|
.filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim() !== '');
|
|
158
90
|
for (const [name, value] of strings) {
|
|
@@ -4,12 +4,10 @@ import {defaultValue} from "../../internal/default-value";
|
|
|
4
4
|
import {FormControlController} from "../../internal/form";
|
|
5
5
|
import {property, query, state} from 'lit/decorators.js';
|
|
6
6
|
import {parse as remarkdParse} from "remarkd-js";
|
|
7
|
-
import {SlashMenuController} from "../slash-menu/slash-menu-controller";
|
|
8
7
|
import {unsafeHTML} from "lit/directives/unsafe-html.js";
|
|
9
8
|
import {watch} from "../../internal/watch";
|
|
10
9
|
import ZincElement from '../../internal/zinc-element';
|
|
11
|
-
import ZnSlashMenu from "../slash-menu";
|
|
12
|
-
import type {SlashMenuItem} from "../slash-menu/slash-menu-items";
|
|
10
|
+
import ZnSlashMenu, {SlashMenuController, type SlashMenuItem} from "../slash-menu";
|
|
13
11
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
14
12
|
import type ZnFile from "../file";
|
|
15
13
|
|
|
@@ -159,6 +157,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
159
157
|
@state() private editShell = '';
|
|
160
158
|
@state() private rawMode = false;
|
|
161
159
|
@state() private includeOptions: IncludeOption[] | null = null;
|
|
160
|
+
@state() private includeLoadFailed = false;
|
|
162
161
|
@state() private includePickerIndex: number | null = null;
|
|
163
162
|
@state() private includeQuery = '';
|
|
164
163
|
|
|
@@ -274,6 +273,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
274
273
|
handleIncludeUrlChange() {
|
|
275
274
|
this.includeRequest = null;
|
|
276
275
|
this.includeOptions = null;
|
|
276
|
+
this.includeLoadFailed = false;
|
|
277
277
|
if (this.hasIncludeBlock()) void this.loadIncludeOptions();
|
|
278
278
|
}
|
|
279
279
|
|
|
@@ -827,6 +827,20 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
827
827
|
return data.uploadPath;
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Resolves an app-relative path the way the console's pagelet handler does:
|
|
832
|
+
* an app fragment's URLs sit under the app base, and the console puts the app's
|
|
833
|
+
* `gaid` on the host element. Links rendered here live in the shadow root,
|
|
834
|
+
* where a click retargets to the host, so that handler never sees them — the
|
|
835
|
+
* href has to carry the base itself.
|
|
836
|
+
*/
|
|
837
|
+
private appPath(path: string): string {
|
|
838
|
+
const gaid = this.getAttribute('gaid') ?? '';
|
|
839
|
+
const trimmed = path.replace(/^\/+/, '');
|
|
840
|
+
if (!gaid || trimmed.startsWith(gaid)) return path;
|
|
841
|
+
return `/${gaid}/${trimmed}`;
|
|
842
|
+
}
|
|
843
|
+
|
|
830
844
|
private hasIncludeBlock(): boolean {
|
|
831
845
|
return this.blocks.some(block => this.parseIncludeBlock(block) !== null);
|
|
832
846
|
}
|
|
@@ -836,15 +850,25 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
836
850
|
if (!this.includeUrl) return Promise.resolve([]);
|
|
837
851
|
if (!this.includeRequest) {
|
|
838
852
|
this.includeRequest = fetch(this.includeUrl, {headers: {Accept: 'application/json'}})
|
|
839
|
-
.then(res =>
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
console.error('[zn-remarkd-editor] include list failed', error);
|
|
843
|
-
return [] as IncludeOption[];
|
|
853
|
+
.then(res => {
|
|
854
|
+
if (!res.ok) throw new Error(`include list request failed: ${res.status}`);
|
|
855
|
+
return res.json() as Promise<{items?: IncludeOption[]}>;
|
|
844
856
|
})
|
|
845
|
-
.then(
|
|
857
|
+
.then(data => {
|
|
858
|
+
const items = data.items ?? [];
|
|
846
859
|
this.includeOptions = items;
|
|
860
|
+
this.includeLoadFailed = false;
|
|
847
861
|
return items;
|
|
862
|
+
})
|
|
863
|
+
.catch(error => {
|
|
864
|
+
// A list that never arrived is not evidence an embed is broken: leave
|
|
865
|
+
// the options unresolved so chips keep their marker labels, and let
|
|
866
|
+
// the next picker open try again.
|
|
867
|
+
console.error('[zn-remarkd-editor] include list failed', error);
|
|
868
|
+
this.includeOptions = null;
|
|
869
|
+
this.includeLoadFailed = true;
|
|
870
|
+
this.includeRequest = null;
|
|
871
|
+
return [] as IncludeOption[];
|
|
848
872
|
});
|
|
849
873
|
}
|
|
850
874
|
return this.includeRequest;
|
|
@@ -1009,7 +1033,8 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1009
1033
|
? `Include not found · ${data.target}`
|
|
1010
1034
|
: !option && filePath ? 'File include' : 'Included content'}</span>
|
|
1011
1035
|
${option?.url ? html`
|
|
1012
|
-
<a class="remarkd-editor__include-link" href=${option.url}
|
|
1036
|
+
<a class="remarkd-editor__include-link" href=${this.appPath(option.url)}
|
|
1037
|
+
target="_blank" rel="noreferrer"
|
|
1013
1038
|
@click=${(e: MouseEvent) => e.stopPropagation()}>Open</a>` : ''}
|
|
1014
1039
|
</div>`;
|
|
1015
1040
|
}
|
|
@@ -1158,7 +1183,8 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1158
1183
|
tooltip="Cancel" @click=${this.closeIncludePicker}></zn-button>
|
|
1159
1184
|
</div>
|
|
1160
1185
|
${this.includeOptions === null
|
|
1161
|
-
? html`<div class="remarkd-editor__include-picker-empty"
|
|
1186
|
+
? html`<div class="remarkd-editor__include-picker-empty">${
|
|
1187
|
+
this.includeLoadFailed ? 'Could not load Includes' : 'Loading…'}</div>`
|
|
1162
1188
|
: items.length
|
|
1163
1189
|
? items.map(item => html`
|
|
1164
1190
|
<button type="button" class="remarkd-editor__include-option"
|
|
@@ -461,4 +461,83 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
|
|
|
461
461
|
.map(button => button.getAttribute('tooltip'));
|
|
462
462
|
expect(tooltips).to.not.contain('Include');
|
|
463
463
|
});
|
|
464
|
+
// A list that never arrived is not evidence an embed is broken: flagging every
|
|
465
|
+
// chip "not found" on a failed request sends people hunting for the wrong bug.
|
|
466
|
+
it('should not flag includes when the list request fails', async () => {
|
|
467
|
+
const original = window.fetch;
|
|
468
|
+
window.fetch = () => Promise.resolve(new Response('nope', {status: 404}));
|
|
469
|
+
try {
|
|
470
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
471
|
+
<zn-remarkd-editor include-url="/options"
|
|
472
|
+
value="include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
|
|
473
|
+
await el.updateComplete;
|
|
474
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include'),
|
|
475
|
+
'the chip never rendered');
|
|
476
|
+
|
|
477
|
+
const chip = el.shadowRoot!.querySelector('.remarkd-editor__include')!;
|
|
478
|
+
expect(chip.classList.contains('remarkd-editor__include--missing')).to.be.false;
|
|
479
|
+
expect(chip.querySelector('.remarkd-editor__include-title')!.textContent).to.contain('Payment Terms');
|
|
480
|
+
} finally {
|
|
481
|
+
window.fetch = original;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should say so in the picker when the list request fails', async () => {
|
|
486
|
+
const original = window.fetch;
|
|
487
|
+
window.fetch = () => Promise.resolve(new Response('nope', {status: 500}));
|
|
488
|
+
try {
|
|
489
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
490
|
+
<zn-remarkd-editor include-url="/options" value="# Title"></zn-remarkd-editor>`);
|
|
491
|
+
const buttons = el.shadowRoot!.querySelectorAll<HTMLElement>('.remarkd-editor__toolbar zn-button');
|
|
492
|
+
buttons[buttons.length - 1].dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
493
|
+
|
|
494
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-picker-empty')?.textContent
|
|
495
|
+
?.includes('Could not load'), 'the picker never reported the failure');
|
|
496
|
+
} finally {
|
|
497
|
+
window.fetch = original;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
// An app fragment's URLs are relative to its app base, and the console puts the
|
|
501
|
+
// app's gaid on the host element. A link built client-side from JSON is inside
|
|
502
|
+
// the shadow root, where a click retargets to the host, so the pagelet handler
|
|
503
|
+
// never sees it — the href has to carry the base itself.
|
|
504
|
+
it('should resolve an include link against the app base from gaid', async () => {
|
|
505
|
+
stubIncludeList([{id: 'inc-1', title: 'Payment Terms', scope: 'Global',
|
|
506
|
+
languages: 'English', url: '/global/includes/inc-1'}]);
|
|
507
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
508
|
+
<zn-remarkd-editor include-url="/ch/kb/kb/kb1/documents/doc1/includes/options"
|
|
509
|
+
gaid="ch/kb"
|
|
510
|
+
value="include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
|
|
511
|
+
|
|
512
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-link'),
|
|
513
|
+
'the include list never resolved');
|
|
514
|
+
expect(el.shadowRoot!.querySelector<HTMLAnchorElement>('.remarkd-editor__include-link')!
|
|
515
|
+
.getAttribute('href')).to.equal('/ch/kb/global/includes/inc-1');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should leave an include link alone when it already carries the app base', async () => {
|
|
519
|
+
stubIncludeList([{id: 'inc-1', title: 'Payment Terms', languages: 'English',
|
|
520
|
+
url: '/ch/kb/global/includes/inc-1'}]);
|
|
521
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
522
|
+
<zn-remarkd-editor include-url="/options" gaid="ch/kb"
|
|
523
|
+
value="include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
|
|
524
|
+
|
|
525
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-link'),
|
|
526
|
+
'the include list never resolved');
|
|
527
|
+
expect(el.shadowRoot!.querySelector<HTMLAnchorElement>('.remarkd-editor__include-link')!
|
|
528
|
+
.getAttribute('href')).to.equal('/ch/kb/global/includes/inc-1');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should leave an include link alone without a gaid', async () => {
|
|
532
|
+
stubIncludeList([{id: 'inc-1', title: 'Payment Terms', languages: 'English',
|
|
533
|
+
url: '/global/includes/inc-1'}]);
|
|
534
|
+
const el = await fixture<ZnRemarkdEditor>(html`
|
|
535
|
+
<zn-remarkd-editor include-url="/options"
|
|
536
|
+
value="include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
|
|
537
|
+
|
|
538
|
+
await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-link'),
|
|
539
|
+
'the include list never resolved');
|
|
540
|
+
expect(el.shadowRoot!.querySelector<HTMLAnchorElement>('.remarkd-editor__include-link')!
|
|
541
|
+
.getAttribute('href')).to.equal('/global/includes/inc-1');
|
|
542
|
+
});
|
|
464
543
|
});
|