@happyvertical/smrt-svelte 0.55.23 → 0.55.24
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/AGENTS.md +2 -2
- package/README.md +2 -2
- package/agents/overview-surfaces.md +108 -4
- package/agents/screens.md +171 -5
- package/dist/components/overview/index.d.ts +4 -1
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +3 -0
- package/dist/components/overview/record.d.ts +51 -0
- package/dist/components/overview/record.d.ts.map +1 -0
- package/dist/components/overview/record.js +146 -0
- package/dist/components/overview/server.d.ts +4 -0
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +4 -0
- package/dist/components/overview/types.d.ts +12 -0
- package/dist/components/overview/types.d.ts.map +1 -1
- package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/RecordTable.svelte +167 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +13 -2
- package/dist/components/overview/widgets/core.d.ts.map +1 -1
- package/dist/components/overview/widgets/core.js +43 -2
- package/dist/components/overview/widgets/data.d.ts +97 -8
- package/dist/components/overview/widgets/data.d.ts.map +1 -1
- package/dist/components/overview/widgets/line-totals.d.ts +48 -0
- package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
- package/dist/components/overview/widgets/line-totals.js +91 -0
- package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
- package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-loaders.js +150 -0
- package/dist/components/overview/widgets/record-table.d.ts +45 -0
- package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-table.js +201 -0
- package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
- package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-widgets.js +100 -0
- package/dist/components/screens/DetailScreen.svelte +105 -6
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
- package/dist/components/screens/EditForm.svelte +39 -3
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecipeScreens.svelte +18 -18
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecordHeader.svelte +238 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecordScreen.svelte +511 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/columns.d.ts +29 -0
- package/dist/components/screens/columns.d.ts.map +1 -0
- package/dist/components/screens/columns.js +115 -0
- package/dist/components/screens/fields.d.ts +12 -2
- package/dist/components/screens/fields.d.ts.map +1 -1
- package/dist/components/screens/fields.js +37 -5
- package/dist/components/screens/index.d.ts +7 -2
- package/dist/components/screens/index.d.ts.map +1 -1
- package/dist/components/screens/index.js +6 -1
- package/dist/components/screens/record-header.d.ts +51 -0
- package/dist/components/screens/record-header.d.ts.map +1 -0
- package/dist/components/screens/record-header.js +118 -0
- package/dist/components/screens/relations.d.ts +49 -0
- package/dist/components/screens/relations.d.ts.map +1 -0
- package/dist/components/screens/relations.js +98 -0
- package/dist/components/screens/roles.d.ts +56 -0
- package/dist/components/screens/roles.d.ts.map +1 -0
- package/dist/components/screens/roles.js +174 -0
- package/dist/components/screens/types.d.ts +182 -1
- package/dist/components/screens/types.d.ts.map +1 -1
- package/dist/components/screens/values.d.ts +23 -1
- package/dist/components/screens/values.d.ts.map +1 -1
- package/dist/components/screens/values.js +80 -4
- package/dist/i18n/strings.overview.d.ts +25 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +25 -0
- package/dist/i18n/strings.screens.d.ts +8 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -1
- package/dist/i18n/strings.screens.js +8 -0
- package/package.json +16 -16
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The totals block of a document's line items (#3718 record page), pure and
|
|
3
|
+
* Node-tested. The record's own subtotal / discount / tax / total are
|
|
4
|
+
* authoritative and shown as they are; a missing one is computed from the
|
|
5
|
+
* lines (subtotal = sum of line amounts, total = subtotal - discount + tax).
|
|
6
|
+
* Lines that add up to the recorded total are tax-inclusive and agree.
|
|
7
|
+
* When the record's figures disagree with the lines, the block flags it
|
|
8
|
+
* ("Lines add up to $X") instead of silently overriding either side.
|
|
9
|
+
*
|
|
10
|
+
* Money is integer minor units throughout.
|
|
11
|
+
*/
|
|
12
|
+
function money(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isSafeInteger(value)
|
|
14
|
+
? value
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
/** Sum of integer minor-unit amounts; `null` on a non-integer or past the safe range. */
|
|
18
|
+
export function sumLineAmounts(amounts) {
|
|
19
|
+
let total = 0;
|
|
20
|
+
for (const amount of amounts) {
|
|
21
|
+
if (amount === null || amount === undefined || amount === '')
|
|
22
|
+
continue;
|
|
23
|
+
const value = money(amount);
|
|
24
|
+
if (value === null)
|
|
25
|
+
return null;
|
|
26
|
+
total += value;
|
|
27
|
+
if (!Number.isSafeInteger(total))
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The totals block for `record` totals over lines summing to `lineSum`
|
|
34
|
+
* (`null` when the lines are a partial page and the host sent no sum).
|
|
35
|
+
*/
|
|
36
|
+
export function lineItemTotals(record, lineSum) {
|
|
37
|
+
const subtotalRecorded = money(record?.subtotal);
|
|
38
|
+
const discount = money(record?.discount);
|
|
39
|
+
const tax = money(record?.tax);
|
|
40
|
+
const totalRecorded = money(record?.total);
|
|
41
|
+
const lines = [];
|
|
42
|
+
// Line amounts may be tax-inclusive (smrt-commerce's are: qty x price -
|
|
43
|
+
// discount + tax), so lines adding up to the recorded total agree with the
|
|
44
|
+
// record, and a missing subtotal is backed out of that total, not the sum.
|
|
45
|
+
const inclusive = lineSum !== null && totalRecorded !== null && lineSum === totalRecorded;
|
|
46
|
+
const subtotal = subtotalRecorded ??
|
|
47
|
+
(inclusive ? totalRecorded + (discount ?? 0) - (tax ?? 0) : lineSum);
|
|
48
|
+
if (subtotal !== null) {
|
|
49
|
+
lines.push({
|
|
50
|
+
key: 'subtotal',
|
|
51
|
+
value: subtotal,
|
|
52
|
+
computed: subtotalRecorded === null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (discount !== null && discount !== 0) {
|
|
56
|
+
lines.push({ key: 'discount', value: discount, computed: false });
|
|
57
|
+
}
|
|
58
|
+
if (tax !== null && tax !== 0) {
|
|
59
|
+
lines.push({ key: 'tax', value: tax, computed: false });
|
|
60
|
+
}
|
|
61
|
+
const computedTotal = subtotal === null ? null : subtotal - (discount ?? 0) + (tax ?? 0);
|
|
62
|
+
const total = totalRecorded ?? computedTotal;
|
|
63
|
+
if (total !== null) {
|
|
64
|
+
lines.push({
|
|
65
|
+
key: 'total',
|
|
66
|
+
value: total,
|
|
67
|
+
computed: totalRecorded === null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
let mismatch = null;
|
|
71
|
+
if (lineSum !== null && !inclusive) {
|
|
72
|
+
if (subtotalRecorded !== null && subtotalRecorded !== lineSum) {
|
|
73
|
+
mismatch = {
|
|
74
|
+
field: 'subtotal',
|
|
75
|
+
recorded: subtotalRecorded,
|
|
76
|
+
lines: lineSum,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
else if (subtotalRecorded === null && totalRecorded !== null) {
|
|
80
|
+
const fromLines = lineSum - (discount ?? 0) + (tax ?? 0);
|
|
81
|
+
if (fromLines !== totalRecorded) {
|
|
82
|
+
mismatch = {
|
|
83
|
+
field: 'total',
|
|
84
|
+
recorded: totalRecorded,
|
|
85
|
+
lines: fromLines,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { lines, lineSum, mismatch };
|
|
91
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loaders of the record-page widgets (#3718 record page), Svelte-free (on
|
|
3
|
+
* both entries); the definitions are in `record-widgets.ts`. The widgets: `related` (one tab per
|
|
4
|
+
* `children` relation, each a records table with a totals footer) and, with
|
|
5
|
+
* `line-items`, the document's lines and totals block. They are built from
|
|
6
|
+
* the record's relations, so they register per record view
|
|
7
|
+
* (`registerRecordWidgets`), not with the core widgets.
|
|
8
|
+
*
|
|
9
|
+
* Loader contract: `related` needs no loader of its own. Each tab is ONE
|
|
10
|
+
* call of the host's core `records` loader with the parent-filter
|
|
11
|
+
* vocabulary — `{ model, field, filter: 'parent', sort: 'createdAt',
|
|
12
|
+
* direction: 'desc', limit, columns }` — so a host that implements the
|
|
13
|
+
* `records` widget's `filter: 'parent'` already implements `related`. The
|
|
14
|
+
* `columns` option tells the loader which `cells` to fill; `summaries` in
|
|
15
|
+
* its data (totals over every matching record) fill the footer when the tab
|
|
16
|
+
* shows a page of a longer list.
|
|
17
|
+
*/
|
|
18
|
+
import type { ScreenRelation } from '../../screens/relations.js';
|
|
19
|
+
import type { OverviewOptions, WidgetLoad } from '../types.js';
|
|
20
|
+
import { type LineItemsWidgetData, type RecordListWidgetData, type RelatedWidgetData } from './data.js';
|
|
21
|
+
import type { LineItemsTotals } from './line-totals.js';
|
|
22
|
+
/** Rows per tab by default. */
|
|
23
|
+
export declare const RELATED_WIDGET_LIMIT = 5;
|
|
24
|
+
/** At most this many relations (tabs) one `related` widget lists. */
|
|
25
|
+
export declare const RELATED_WIDGET_MAX_TABS = 20;
|
|
26
|
+
export interface RecordWidgetsOptions {
|
|
27
|
+
/** The record's relations (`deriveScreenRelations`). */
|
|
28
|
+
relations: readonly ScreenRelation[];
|
|
29
|
+
/** The host's core `records` loader; every tab is one parent-filtered load. */
|
|
30
|
+
records: WidgetLoad<RecordListWidgetData>;
|
|
31
|
+
/** A tab's "View all" link; else the loaded data's `href`. */
|
|
32
|
+
viewAllHref?: (relation: ScreenRelation) => string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* `line-items`: the parent record's own subtotal / discount / tax / total
|
|
35
|
+
* (read when the widget loads; `RecordScreen` reads them by role).
|
|
36
|
+
*/
|
|
37
|
+
totals?: () => LineItemsTotals | undefined;
|
|
38
|
+
/** `line-items`: the currency of the parent's amounts. */
|
|
39
|
+
currency?: () => string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Lines a `line-items` widget loads by default (and at most). */
|
|
42
|
+
export declare const LINE_ITEMS_LIMIT = 50;
|
|
43
|
+
/** The relations a `related` widget lists: `children` (never `items`), or the named ones. */
|
|
44
|
+
export declare function relatedRelations(relations: readonly ScreenRelation[], keys: readonly string[]): ScreenRelation[];
|
|
45
|
+
/** The `records` loader options one tab loads with. */
|
|
46
|
+
export declare function relatedTabOptions(relation: ScreenRelation, limit: number): OverviewOptions;
|
|
47
|
+
/**
|
|
48
|
+
* The `related` widget's load: one `records` load per listed relation, in
|
|
49
|
+
* parallel. A tab whose load fails is left out; if every tab fails, the
|
|
50
|
+
* first error is the widget's.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createRelatedLoader(options: RecordWidgetsOptions): WidgetLoad<RelatedWidgetData>;
|
|
53
|
+
/** The `items` relation a `line-items` widget shows: the named one, else the first. */
|
|
54
|
+
export declare function lineItemsRelation(relations: readonly ScreenRelation[], key: unknown): ScreenRelation | undefined;
|
|
55
|
+
/** The `records` loader options the line items load with. */
|
|
56
|
+
export declare function lineItemsOptions(relation: ScreenRelation, limit: number): OverviewOptions;
|
|
57
|
+
/**
|
|
58
|
+
* The `line-items` widget's load: one `records` load of the `items`
|
|
59
|
+
* relation (`filter: 'parent'`, in line order, up to {@link LINE_ITEMS_LIMIT}),
|
|
60
|
+
* plus the parent's totals. No `items` relation is an empty result.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createLineItemsLoader(options: RecordWidgetsOptions): WidgetLoad<LineItemsWidgetData>;
|
|
63
|
+
//# sourceMappingURL=record-loaders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-loaders.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/record-loaders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EAEX,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAQxD,+BAA+B;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IACrC,+EAA+E;IAC/E,OAAO,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAC1C,8DAA8D;IAC9D,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,cAAc,EAAE,CAMlB;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AA2CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,iBAAiB,CAAC,CAkC/B;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,GAAG,EAAE,OAAO,GACX,cAAc,GAAG,SAAS,CAM5B;AAED,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,eAAe,CAWjB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CAAC,mBAAmB,CAAC,CA6BjC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { relationKey } from '../../screens/relations.js';
|
|
2
|
+
import { isRecord, } from './data.js';
|
|
3
|
+
import { formatColumnsOption, parseColumnsOption, readRecordRows, resolveRecordColumns, } from './record-table.js';
|
|
4
|
+
/** Rows per tab by default. */
|
|
5
|
+
export const RELATED_WIDGET_LIMIT = 5;
|
|
6
|
+
/** At most this many relations (tabs) one `related` widget lists. */
|
|
7
|
+
export const RELATED_WIDGET_MAX_TABS = 20;
|
|
8
|
+
/** Lines a `line-items` widget loads by default (and at most). */
|
|
9
|
+
export const LINE_ITEMS_LIMIT = 50;
|
|
10
|
+
/** The relations a `related` widget lists: `children` (never `items`), or the named ones. */
|
|
11
|
+
export function relatedRelations(relations, keys) {
|
|
12
|
+
const children = relations.filter((r) => r.kind === 'children');
|
|
13
|
+
if (keys.length === 0)
|
|
14
|
+
return children.slice(0, RELATED_WIDGET_MAX_TABS);
|
|
15
|
+
return keys
|
|
16
|
+
.map((key) => children.find((r) => relationKey(r) === key))
|
|
17
|
+
.filter((r) => r !== undefined);
|
|
18
|
+
}
|
|
19
|
+
/** The `records` loader options one tab loads with. */
|
|
20
|
+
export function relatedTabOptions(relation, limit) {
|
|
21
|
+
return {
|
|
22
|
+
title: relation.label,
|
|
23
|
+
model: relation.model,
|
|
24
|
+
field: relation.field,
|
|
25
|
+
filter: 'parent',
|
|
26
|
+
sort: 'createdAt',
|
|
27
|
+
direction: 'desc',
|
|
28
|
+
limit,
|
|
29
|
+
columns: formatColumnsOption((relation.columns ?? []).map((c) => c.key)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function toTab(relation, data, href) {
|
|
33
|
+
const record = isRecord(data) ? data : {};
|
|
34
|
+
const rows = readRecordRows(record);
|
|
35
|
+
const keys = (relation.columns ?? []).map((c) => c.key);
|
|
36
|
+
const declared = [
|
|
37
|
+
...(Array.isArray(record.columns) ? record.columns : []),
|
|
38
|
+
...(relation.columns ?? []),
|
|
39
|
+
];
|
|
40
|
+
const summaries = {};
|
|
41
|
+
if (isRecord(record.summaries)) {
|
|
42
|
+
for (const [key, value] of Object.entries(record.summaries)) {
|
|
43
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
44
|
+
summaries[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const total = typeof record.total === 'number' && Number.isFinite(record.total)
|
|
48
|
+
? record.total
|
|
49
|
+
: rows.length;
|
|
50
|
+
const dataHref = typeof record.href === 'string' ? record.href : undefined;
|
|
51
|
+
return {
|
|
52
|
+
key: relationKey(relation),
|
|
53
|
+
label: relation.label,
|
|
54
|
+
rows,
|
|
55
|
+
total,
|
|
56
|
+
columns: resolveRecordColumns(keys, declared),
|
|
57
|
+
...((href ?? dataHref) ? { href: href ?? dataHref } : {}),
|
|
58
|
+
...(typeof record.currency === 'string'
|
|
59
|
+
? { currency: record.currency }
|
|
60
|
+
: {}),
|
|
61
|
+
...(typeof record.timeZone === 'string'
|
|
62
|
+
? { timeZone: record.timeZone }
|
|
63
|
+
: {}),
|
|
64
|
+
...(Object.keys(summaries).length > 0 ? { summaries } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `related` widget's load: one `records` load per listed relation, in
|
|
69
|
+
* parallel. A tab whose load fails is left out; if every tab fails, the
|
|
70
|
+
* first error is the widget's.
|
|
71
|
+
*/
|
|
72
|
+
export function createRelatedLoader(options) {
|
|
73
|
+
return async (widgetOptions, ctx) => {
|
|
74
|
+
const keys = parseColumnsOption(widgetOptions.relations, RELATED_WIDGET_MAX_TABS);
|
|
75
|
+
const limit = typeof widgetOptions.limit === 'number'
|
|
76
|
+
? widgetOptions.limit
|
|
77
|
+
: RELATED_WIDGET_LIMIT;
|
|
78
|
+
const selected = relatedRelations(options.relations, keys);
|
|
79
|
+
const settled = await Promise.allSettled(
|
|
80
|
+
// A loader that throws synchronously fails its tab, not the widget.
|
|
81
|
+
selected.map((relation) => new Promise((resolve) => resolve(options.records(relatedTabOptions(relation, limit), ctx)))));
|
|
82
|
+
const tabs = [];
|
|
83
|
+
let failure;
|
|
84
|
+
settled.forEach((result, index) => {
|
|
85
|
+
const relation = selected[index];
|
|
86
|
+
if (!relation)
|
|
87
|
+
return;
|
|
88
|
+
if (result.status === 'rejected') {
|
|
89
|
+
failure ??= result.reason;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
tabs.push(toTab(relation, result.value, options.viewAllHref?.(relation)));
|
|
93
|
+
});
|
|
94
|
+
if (tabs.length === 0 && failure !== undefined)
|
|
95
|
+
throw failure;
|
|
96
|
+
return { tabs };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** The `items` relation a `line-items` widget shows: the named one, else the first. */
|
|
100
|
+
export function lineItemsRelation(relations, key) {
|
|
101
|
+
const items = relations.filter((r) => r.kind === 'items');
|
|
102
|
+
if (typeof key === 'string' && key !== '') {
|
|
103
|
+
return items.find((r) => relationKey(r) === key);
|
|
104
|
+
}
|
|
105
|
+
return items[0];
|
|
106
|
+
}
|
|
107
|
+
/** The `records` loader options the line items load with. */
|
|
108
|
+
export function lineItemsOptions(relation, limit) {
|
|
109
|
+
return {
|
|
110
|
+
title: relation.label,
|
|
111
|
+
model: relation.model,
|
|
112
|
+
field: relation.field,
|
|
113
|
+
filter: 'parent',
|
|
114
|
+
sort: relation.sort ?? 'createdAt',
|
|
115
|
+
direction: 'asc',
|
|
116
|
+
limit,
|
|
117
|
+
columns: formatColumnsOption((relation.columns ?? []).map((c) => c.key)),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The `line-items` widget's load: one `records` load of the `items`
|
|
122
|
+
* relation (`filter: 'parent'`, in line order, up to {@link LINE_ITEMS_LIMIT}),
|
|
123
|
+
* plus the parent's totals. No `items` relation is an empty result.
|
|
124
|
+
*/
|
|
125
|
+
export function createLineItemsLoader(options) {
|
|
126
|
+
return async (widgetOptions, ctx) => {
|
|
127
|
+
const relation = lineItemsRelation(options.relations, widgetOptions.relation);
|
|
128
|
+
const totals = options.totals?.();
|
|
129
|
+
const currency = options.currency?.();
|
|
130
|
+
if (!relation) {
|
|
131
|
+
return { rows: [], columns: [], ...(totals ? { totals } : {}) };
|
|
132
|
+
}
|
|
133
|
+
const limit = typeof widgetOptions.limit === 'number'
|
|
134
|
+
? widgetOptions.limit
|
|
135
|
+
: LINE_ITEMS_LIMIT;
|
|
136
|
+
const data = await options.records(lineItemsOptions(relation, limit), ctx);
|
|
137
|
+
const tab = toTab(relation, data, undefined);
|
|
138
|
+
return {
|
|
139
|
+
rows: tab.rows,
|
|
140
|
+
columns: tab.columns,
|
|
141
|
+
...(tab.total !== undefined ? { total: tab.total } : {}),
|
|
142
|
+
...(totals ? { totals } : {}),
|
|
143
|
+
...((tab.currency ?? currency)
|
|
144
|
+
? { currency: tab.currency ?? currency }
|
|
145
|
+
: {}),
|
|
146
|
+
...(tab.summaries ? { summaries: tab.summaries } : {}),
|
|
147
|
+
...(tab.href ? { href: tab.href } : {}),
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FieldRole } from '../../screens/roles.js';
|
|
2
|
+
import { type RecordListColumn, type RecordListRow, type RecordStatusTone } from './data.js';
|
|
3
|
+
export { lineItemColumns, relatedColumns } from '../../screens/columns.js';
|
|
4
|
+
/** At most this many columns render (the widget is compact). */
|
|
5
|
+
export declare const RECORD_TABLE_MAX_COLUMNS = 6;
|
|
6
|
+
/** At most this many rows render. */
|
|
7
|
+
export declare const RECORD_TABLE_MAX_ROWS = 50;
|
|
8
|
+
/**
|
|
9
|
+
* The `columns` widget option: a comma-separated list of field names
|
|
10
|
+
* (`"number,status,issuedAt,totalAmount"`). Options are flat JSON
|
|
11
|
+
* primitives, so a list travels as text; invalid or repeated keys are
|
|
12
|
+
* dropped and the list is capped at {@link RECORD_TABLE_MAX_COLUMNS}.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseColumnsOption(value: unknown, max?: number): string[];
|
|
15
|
+
/** The inverse of {@link parseColumnsOption}. */
|
|
16
|
+
export declare function formatColumnsOption(columns: readonly string[]): string;
|
|
17
|
+
/** Common status values and their badge tone; anything else is neutral. */
|
|
18
|
+
export declare const DEFAULT_STATUS_TONES: Readonly<Record<string, RecordStatusTone>>;
|
|
19
|
+
/** Badge tone of a status value: `tones`, then the defaults, else neutral. */
|
|
20
|
+
export declare function statusTone(value: unknown, tones?: Readonly<Record<string, RecordStatusTone>>): RecordStatusTone;
|
|
21
|
+
/** `in_progress` -> `In progress`: a status value as a badge label. */
|
|
22
|
+
export declare function statusLabel(value: unknown): string;
|
|
23
|
+
export interface RecordCellContext {
|
|
24
|
+
currency?: string;
|
|
25
|
+
locale?: string;
|
|
26
|
+
timeZone?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A cell's text, by its column role. Empty values are `''`. */
|
|
29
|
+
export declare function formatRecordCell(column: Pick<RecordListColumn, 'role'>, value: unknown, context?: RecordCellContext): string;
|
|
30
|
+
/** True for the roles a footer sums. */
|
|
31
|
+
export declare function isSummedRole(role: FieldRole | undefined): boolean;
|
|
32
|
+
/** True when the footer sums this column (`summarize`, else by role). */
|
|
33
|
+
export declare function isSummedColumn(column: Pick<RecordListColumn, 'role' | 'summarize'>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Rows of untrusted widget data, validated: a row needs a string `title`
|
|
36
|
+
* (or any cells), ids default to the position, `cells` must be an object.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readRecordRows(data: unknown): RecordListRow[];
|
|
39
|
+
/**
|
|
40
|
+
* The columns to render: `keys` (the `columns` option) when given, else the
|
|
41
|
+
* data's own `columns`; a key's label and role come from the data's column
|
|
42
|
+
* list, else a humanized label and no role.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveRecordColumns(keys: readonly string[], dataColumns: unknown): RecordListColumn[];
|
|
45
|
+
//# sourceMappingURL=record-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-table.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/record-table.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACtB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI3E,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,qCAAqC;AACrC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,GAAG,GAAE,MAAiC,GACrC,MAAM,EAAE,CAUV;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEtE;AAED,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA+BzE,CAAC;AAEJ,8EAA8E;AAC9E,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,EACd,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GACjD,gBAAgB,CASlB;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGlD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,EACtC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,iBAAsB,GAC9B,MAAM,CA8BR;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAEjE;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,GACnD,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,CAiB7D;AAgBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,WAAW,EAAE,OAAO,GACnB,gBAAgB,EAAE,CA6BpB"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the records tables (#3718 record page): the `records`
|
|
3
|
+
* widget with columns, each tab of `related`, and `lineItems`. Columns
|
|
4
|
+
* come by role; cells format by role; the footer (smrt-ui `DataTable`
|
|
5
|
+
* `summaries`) counts the rows and sums the money and quantity columns.
|
|
6
|
+
*
|
|
7
|
+
* Svelte-free and Node-tested. No date is formatted in the viewer's zone:
|
|
8
|
+
* a date cell shows in `timeZone` (default UTC), so the server and the
|
|
9
|
+
* browser render the same text.
|
|
10
|
+
*/
|
|
11
|
+
import { humanizeFieldName } from '../../screens/fields.js';
|
|
12
|
+
import { formatScreenValue } from '../../screens/values.js';
|
|
13
|
+
import { isRecord, } from './data.js';
|
|
14
|
+
export { lineItemColumns, relatedColumns } from '../../screens/columns.js';
|
|
15
|
+
/** Column keys: `[A-Za-z_][A-Za-z0-9_.-]{0,63}`, the overview identifier shape. */
|
|
16
|
+
const COLUMN_KEY = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
|
|
17
|
+
/** At most this many columns render (the widget is compact). */
|
|
18
|
+
export const RECORD_TABLE_MAX_COLUMNS = 6;
|
|
19
|
+
/** At most this many rows render. */
|
|
20
|
+
export const RECORD_TABLE_MAX_ROWS = 50;
|
|
21
|
+
/**
|
|
22
|
+
* The `columns` widget option: a comma-separated list of field names
|
|
23
|
+
* (`"number,status,issuedAt,totalAmount"`). Options are flat JSON
|
|
24
|
+
* primitives, so a list travels as text; invalid or repeated keys are
|
|
25
|
+
* dropped and the list is capped at {@link RECORD_TABLE_MAX_COLUMNS}.
|
|
26
|
+
*/
|
|
27
|
+
export function parseColumnsOption(value, max = RECORD_TABLE_MAX_COLUMNS) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return [];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const part of value.split(',')) {
|
|
32
|
+
const key = part.trim();
|
|
33
|
+
if (!COLUMN_KEY.test(key) || out.includes(key))
|
|
34
|
+
continue;
|
|
35
|
+
out.push(key);
|
|
36
|
+
if (out.length === max)
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** The inverse of {@link parseColumnsOption}. */
|
|
42
|
+
export function formatColumnsOption(columns) {
|
|
43
|
+
return columns.join(',');
|
|
44
|
+
}
|
|
45
|
+
/** Common status values and their badge tone; anything else is neutral. */
|
|
46
|
+
export const DEFAULT_STATUS_TONES = {
|
|
47
|
+
active: 'success',
|
|
48
|
+
accepted: 'success',
|
|
49
|
+
approved: 'success',
|
|
50
|
+
completed: 'success',
|
|
51
|
+
confirmed: 'success',
|
|
52
|
+
delivered: 'success',
|
|
53
|
+
fulfilled: 'success',
|
|
54
|
+
paid: 'success',
|
|
55
|
+
settled: 'success',
|
|
56
|
+
shipped: 'success',
|
|
57
|
+
open: 'info',
|
|
58
|
+
pending: 'info',
|
|
59
|
+
processing: 'info',
|
|
60
|
+
sent: 'info',
|
|
61
|
+
viewed: 'info',
|
|
62
|
+
partial: 'warning',
|
|
63
|
+
overdue: 'warning',
|
|
64
|
+
on_hold: 'warning',
|
|
65
|
+
suspended: 'warning',
|
|
66
|
+
declined: 'error',
|
|
67
|
+
failed: 'error',
|
|
68
|
+
rejected: 'error',
|
|
69
|
+
refunded: 'default',
|
|
70
|
+
cancelled: 'default',
|
|
71
|
+
canceled: 'default',
|
|
72
|
+
draft: 'default',
|
|
73
|
+
inactive: 'default',
|
|
74
|
+
void: 'default',
|
|
75
|
+
written_off: 'default',
|
|
76
|
+
};
|
|
77
|
+
/** Badge tone of a status value: `tones`, then the defaults, else neutral. */
|
|
78
|
+
export function statusTone(value, tones) {
|
|
79
|
+
if (typeof value !== 'string')
|
|
80
|
+
return 'default';
|
|
81
|
+
const key = value
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[\s-]+/g, '_');
|
|
85
|
+
return (tones?.[value] ?? tones?.[key] ?? DEFAULT_STATUS_TONES[key] ?? 'default');
|
|
86
|
+
}
|
|
87
|
+
/** `in_progress` -> `In progress`: a status value as a badge label. */
|
|
88
|
+
export function statusLabel(value) {
|
|
89
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
90
|
+
return '';
|
|
91
|
+
return humanizeFieldName(value.trim());
|
|
92
|
+
}
|
|
93
|
+
/** A cell's text, by its column role. Empty values are `''`. */
|
|
94
|
+
export function formatRecordCell(column, value, context = {}) {
|
|
95
|
+
if (value === null || value === undefined || value === '')
|
|
96
|
+
return '';
|
|
97
|
+
switch (column.role) {
|
|
98
|
+
case 'money':
|
|
99
|
+
return formatScreenValue({ kind: 'money' }, value, {
|
|
100
|
+
currency: context.currency,
|
|
101
|
+
locale: context.locale,
|
|
102
|
+
});
|
|
103
|
+
case 'date':
|
|
104
|
+
if (typeof value === 'string' && !/^\d{4}-\d{2}-\d{2}/.test(value))
|
|
105
|
+
return value;
|
|
106
|
+
return formatScreenValue({ kind: 'datetime', definition: { format: 'date' } }, value, { locale: context.locale, timeZone: context.timeZone ?? 'UTC' });
|
|
107
|
+
case 'quantity':
|
|
108
|
+
return formatScreenValue({ kind: 'decimal' }, value, {
|
|
109
|
+
locale: context.locale,
|
|
110
|
+
});
|
|
111
|
+
case 'status':
|
|
112
|
+
return statusLabel(value);
|
|
113
|
+
default:
|
|
114
|
+
if (typeof value === 'number')
|
|
115
|
+
return formatScreenValue({ kind: 'decimal' }, value, {
|
|
116
|
+
locale: context.locale,
|
|
117
|
+
});
|
|
118
|
+
if (typeof value === 'boolean')
|
|
119
|
+
return value ? '✓' : '';
|
|
120
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** True for the roles a footer sums. */
|
|
124
|
+
export function isSummedRole(role) {
|
|
125
|
+
return role === 'money' || role === 'quantity';
|
|
126
|
+
}
|
|
127
|
+
/** True when the footer sums this column (`summarize`, else by role). */
|
|
128
|
+
export function isSummedColumn(column) {
|
|
129
|
+
return column.summarize ?? isSummedRole(column.role);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Rows of untrusted widget data, validated: a row needs a string `title`
|
|
133
|
+
* (or any cells), ids default to the position, `cells` must be an object.
|
|
134
|
+
*/
|
|
135
|
+
export function readRecordRows(data) {
|
|
136
|
+
if (!isRecord(data) || !Array.isArray(data.rows))
|
|
137
|
+
return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const row of data.rows.slice(0, RECORD_TABLE_MAX_ROWS)) {
|
|
140
|
+
if (!isRecord(row))
|
|
141
|
+
continue;
|
|
142
|
+
const cells = isRecord(row.cells) ? row.cells : undefined;
|
|
143
|
+
if (typeof row.title !== 'string' && !cells)
|
|
144
|
+
continue;
|
|
145
|
+
out.push({
|
|
146
|
+
id: typeof row.id === 'string' ? row.id : String(out.length),
|
|
147
|
+
title: typeof row.title === 'string' ? row.title : '',
|
|
148
|
+
...(typeof row.subtitle === 'string' ? { subtitle: row.subtitle } : {}),
|
|
149
|
+
...(typeof row.meta === 'string' ? { meta: row.meta } : {}),
|
|
150
|
+
...(typeof row.href === 'string' ? { href: row.href } : {}),
|
|
151
|
+
...(cells ? { cells } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
const ROLES = new Set([
|
|
157
|
+
'title',
|
|
158
|
+
'identifier',
|
|
159
|
+
'status',
|
|
160
|
+
'money',
|
|
161
|
+
'quantity',
|
|
162
|
+
'date',
|
|
163
|
+
'party',
|
|
164
|
+
'subtotal',
|
|
165
|
+
'tax',
|
|
166
|
+
'discount',
|
|
167
|
+
'total',
|
|
168
|
+
]);
|
|
169
|
+
/**
|
|
170
|
+
* The columns to render: `keys` (the `columns` option) when given, else the
|
|
171
|
+
* data's own `columns`; a key's label and role come from the data's column
|
|
172
|
+
* list, else a humanized label and no role.
|
|
173
|
+
*/
|
|
174
|
+
export function resolveRecordColumns(keys, dataColumns) {
|
|
175
|
+
const declared = [];
|
|
176
|
+
if (Array.isArray(dataColumns)) {
|
|
177
|
+
for (const entry of dataColumns) {
|
|
178
|
+
if (!isRecord(entry) || typeof entry.key !== 'string')
|
|
179
|
+
continue;
|
|
180
|
+
if (!COLUMN_KEY.test(entry.key))
|
|
181
|
+
continue;
|
|
182
|
+
declared.push({
|
|
183
|
+
key: entry.key,
|
|
184
|
+
label: typeof entry.label === 'string' && entry.label !== ''
|
|
185
|
+
? entry.label
|
|
186
|
+
: humanizeFieldName(entry.key),
|
|
187
|
+
...(typeof entry.role === 'string' && ROLES.has(entry.role)
|
|
188
|
+
? { role: entry.role }
|
|
189
|
+
: {}),
|
|
190
|
+
...(typeof entry.summarize === 'boolean'
|
|
191
|
+
? { summarize: entry.summarize }
|
|
192
|
+
: {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const order = keys.length > 0 ? keys : declared.map((c) => c.key);
|
|
197
|
+
return order.slice(0, RECORD_TABLE_MAX_COLUMNS).map((key) => declared.find((c) => c.key === key) ?? {
|
|
198
|
+
key,
|
|
199
|
+
label: humanizeFieldName(key),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record-page widget definitions (#3718 record page): `related` and
|
|
3
|
+
* `line-items` (the contract's `lineItems`), registered per record view by `registerRecordWidgets`
|
|
4
|
+
* because they are built from the record's relations. Loaders and the
|
|
5
|
+
* loader contract are in `record-loaders.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { type WidgetRegistry } from '../registry.js';
|
|
8
|
+
import type { WidgetDefinition, WidgetLoad } from '../types.js';
|
|
9
|
+
import type { LineItemsWidgetData, RelatedWidgetData } from './data.js';
|
|
10
|
+
import { type RecordWidgetsOptions } from './record-loaders.js';
|
|
11
|
+
/** `related`: tabbed related records. Options: `title`, `relations`, `limit`. */
|
|
12
|
+
export declare function createRelatedWidget(load: WidgetLoad<RelatedWidgetData>): WidgetDefinition<RelatedWidgetData>;
|
|
13
|
+
/**
|
|
14
|
+
* `line-items`: a document's lines and totals block. Options: `title`,
|
|
15
|
+
* `relation` (an `items` relation key; empty = the first), `limit`. The
|
|
16
|
+
* type id is kebab case (`line-items`): widget type ids are lowercase kebab.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLineItemsWidget(load: WidgetLoad<LineItemsWidgetData>): WidgetDefinition<LineItemsWidgetData>;
|
|
19
|
+
/**
|
|
20
|
+
* Register the record-page widgets for one record view's relations,
|
|
21
|
+
* `related` and `line-items`, both loading through the host's `records`
|
|
22
|
+
* loader. Returns a disposer.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerRecordWidgets(registry: WidgetRegistry | undefined, options: RecordWidgetsOptions, registerOptions?: {
|
|
25
|
+
replace?: boolean;
|
|
26
|
+
}): () => void;
|
|
27
|
+
//# sourceMappingURL=record-widgets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-widgets.d.ts","sourceRoot":"","sources":["../../../../src/components/overview/widgets/record-widgets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGxE,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,iFAAiF;AACjF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,CAAC,iBAAiB,CAAC,GAClC,gBAAgB,CAAC,iBAAiB,CAAC,CAkCrC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,CAAC,mBAAmB,CAAC,GACpC,gBAAgB,CAAC,mBAAmB,CAAC,CAiCvC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,cAAc,YAAwB,EAChD,OAAO,EAAE,oBAAoB,EAC7B,eAAe,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,MAAM,IAAI,CAcZ"}
|