@happyvertical/smrt-svelte 0.55.23 → 0.56.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/AGENTS.md +2 -2
- package/README.md +2 -2
- package/agents/overview-surfaces.md +117 -6
- package/agents/screens.md +171 -5
- package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
- package/dist/components/overview/OverviewGrid.svelte +8 -4
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
- package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
- package/dist/components/overview/format.d.ts +3 -12
- package/dist/components/overview/format.d.ts.map +1 -1
- package/dist/components/overview/format.js +2 -45
- package/dist/components/overview/grid-types.d.ts +6 -11
- package/dist/components/overview/grid-types.d.ts.map +1 -1
- package/dist/components/overview/index.d.ts +5 -1
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +3 -0
- package/dist/components/overview/recipe-widgets.d.ts +6 -3
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
- package/dist/components/overview/recipe-widgets.js +9 -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/registry.d.ts +3 -0
- package/dist/components/overview/registry.d.ts.map +1 -1
- package/dist/components/overview/registry.js +25 -0
- package/dist/components/overview/server.d.ts +5 -1
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +5 -1
- package/dist/components/overview/types.d.ts +29 -0
- package/dist/components/overview/types.d.ts.map +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte.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/components/workspace/admin-shell/AdminShell.svelte +4 -4
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
- 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,100 @@
|
|
|
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 { defaultWidgetRegistry } from '../registry.js';
|
|
8
|
+
import LineItemsWidget from './LineItemsWidget.svelte';
|
|
9
|
+
import RelatedWidget from './RelatedWidget.svelte';
|
|
10
|
+
import { createLineItemsLoader, createRelatedLoader, LINE_ITEMS_LIMIT, RELATED_WIDGET_LIMIT, } from './record-loaders.js';
|
|
11
|
+
/** `related`: tabbed related records. Options: `title`, `relations`, `limit`. */
|
|
12
|
+
export function createRelatedWidget(load) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'related',
|
|
15
|
+
title: 'ui.overview.widget.related.title',
|
|
16
|
+
description: 'ui.overview.widget.related.description',
|
|
17
|
+
icon: 'layers',
|
|
18
|
+
options: [
|
|
19
|
+
{
|
|
20
|
+
key: 'title',
|
|
21
|
+
type: 'text',
|
|
22
|
+
label: 'ui.overview.opt.title',
|
|
23
|
+
help: 'ui.overview.opt.title_help',
|
|
24
|
+
maxLength: 80,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'relations',
|
|
28
|
+
type: 'text',
|
|
29
|
+
label: 'ui.overview.opt.relations',
|
|
30
|
+
help: 'ui.overview.opt.relations_help',
|
|
31
|
+
maxLength: 1000,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'limit',
|
|
35
|
+
type: 'integer',
|
|
36
|
+
label: 'ui.overview.opt.limit',
|
|
37
|
+
default: RELATED_WIDGET_LIMIT,
|
|
38
|
+
min: 1,
|
|
39
|
+
max: 20,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
component: RelatedWidget,
|
|
43
|
+
load,
|
|
44
|
+
defaultSpan: 4,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `line-items`: a document's lines and totals block. Options: `title`,
|
|
49
|
+
* `relation` (an `items` relation key; empty = the first), `limit`. The
|
|
50
|
+
* type id is kebab case (`line-items`): widget type ids are lowercase kebab.
|
|
51
|
+
*/
|
|
52
|
+
export function createLineItemsWidget(load) {
|
|
53
|
+
return {
|
|
54
|
+
type: 'line-items',
|
|
55
|
+
title: 'ui.overview.widget.line_items.title',
|
|
56
|
+
description: 'ui.overview.widget.line_items.description',
|
|
57
|
+
icon: 'fileText',
|
|
58
|
+
options: [
|
|
59
|
+
{
|
|
60
|
+
key: 'title',
|
|
61
|
+
type: 'text',
|
|
62
|
+
label: 'ui.overview.opt.title',
|
|
63
|
+
help: 'ui.overview.opt.title_help',
|
|
64
|
+
maxLength: 80,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: 'relation',
|
|
68
|
+
type: 'identifier',
|
|
69
|
+
label: 'ui.overview.opt.relation',
|
|
70
|
+
help: 'ui.overview.opt.relation_help',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'limit',
|
|
74
|
+
type: 'integer',
|
|
75
|
+
label: 'ui.overview.opt.limit',
|
|
76
|
+
default: LINE_ITEMS_LIMIT,
|
|
77
|
+
min: 1,
|
|
78
|
+
max: LINE_ITEMS_LIMIT,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
component: LineItemsWidget,
|
|
82
|
+
load,
|
|
83
|
+
defaultSpan: 4,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Register the record-page widgets for one record view's relations,
|
|
88
|
+
* `related` and `line-items`, both loading through the host's `records`
|
|
89
|
+
* loader. Returns a disposer.
|
|
90
|
+
*/
|
|
91
|
+
export function registerRecordWidgets(registry = defaultWidgetRegistry, options, registerOptions = {}) {
|
|
92
|
+
const disposers = [
|
|
93
|
+
registry.register(createRelatedWidget(createRelatedLoader(options)), registerOptions),
|
|
94
|
+
registry.register(createLineItemsWidget(createLineItemsLoader(options)), registerOptions),
|
|
95
|
+
];
|
|
96
|
+
return () => {
|
|
97
|
+
for (const dispose of disposers)
|
|
98
|
+
dispose();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* behind a "More details" disclosure. Hidden fields and fields outside the
|
|
9
9
|
* policy never render, so a sensitive field the policy omits cannot leak
|
|
10
10
|
* through this screen.
|
|
11
|
+
*
|
|
12
|
+
* Empty values (an empty JSON object or array included) show the "Not set"
|
|
13
|
+
* placeholder; a JSON object shows as a compact key/value list. A reference
|
|
14
|
+
* shows its target's label from `labels` (a link when `hrefFor` gives one);
|
|
15
|
+
* without a label it falls back to a shortened id in `<code>`, the full id in
|
|
16
|
+
* its tooltip.
|
|
11
17
|
*/
|
|
12
18
|
import { Alert, ConfirmDialog } from '@happyvertical/smrt-ui/feedback';
|
|
13
19
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
@@ -17,6 +23,7 @@ import { M } from '../../i18n/strings.screens.js';
|
|
|
17
23
|
import {
|
|
18
24
|
deriveScreenFields,
|
|
19
25
|
groupScreenFields,
|
|
26
|
+
humanizeFieldName,
|
|
20
27
|
screenSourceError,
|
|
21
28
|
screenTitles,
|
|
22
29
|
} from './fields.js';
|
|
@@ -27,7 +34,11 @@ import type {
|
|
|
27
34
|
ScreenPolicy,
|
|
28
35
|
ScreenRecord,
|
|
29
36
|
} from './types.js';
|
|
30
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
formatScreenValue,
|
|
39
|
+
jsonScreenEntries,
|
|
40
|
+
shortScreenId,
|
|
41
|
+
} from './values.js';
|
|
31
42
|
|
|
32
43
|
let {
|
|
33
44
|
definition,
|
|
@@ -45,6 +56,12 @@ let {
|
|
|
45
56
|
ondelete,
|
|
46
57
|
extraActions,
|
|
47
58
|
children,
|
|
59
|
+
relations = [],
|
|
60
|
+
hrefFor,
|
|
61
|
+
labels,
|
|
62
|
+
exclude,
|
|
63
|
+
hideEmpty = false,
|
|
64
|
+
variant = 'plain',
|
|
48
65
|
}: DetailScreenProps = $props();
|
|
49
66
|
|
|
50
67
|
const { t } = useI18n();
|
|
@@ -57,8 +74,17 @@ const allFields = $derived(
|
|
|
57
74
|
? []
|
|
58
75
|
: deriveScreenFields(definition, policy, { mode: 'view', include }),
|
|
59
76
|
);
|
|
60
|
-
const
|
|
61
|
-
|
|
77
|
+
const listed = $derived(
|
|
78
|
+
exclude ? allFields.filter((f) => !exclude.includes(f.name)) : allFields,
|
|
79
|
+
);
|
|
80
|
+
const filled = $derived(
|
|
81
|
+
hideEmpty ? listed.filter((f) => display(f) !== '') : listed,
|
|
82
|
+
);
|
|
83
|
+
const empty = $derived(
|
|
84
|
+
hideEmpty ? listed.filter((f) => display(f) === '') : [],
|
|
85
|
+
);
|
|
86
|
+
const primary = $derived(filled.filter((f) => f.tier === 'basic'));
|
|
87
|
+
const secondary = $derived(filled.filter((f) => f.tier === 'advanced'));
|
|
62
88
|
|
|
63
89
|
const heading = $derived.by(() => {
|
|
64
90
|
if (title) return title;
|
|
@@ -87,6 +113,19 @@ function safeHref(field: ScreenField, text: string): string | null {
|
|
|
87
113
|
return null;
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
/** A reference field's target model, from the `reference` relations. */
|
|
117
|
+
function targetModel(field: ScreenField): string | undefined {
|
|
118
|
+
return relations.find((r) => r.kind === 'reference' && r.field === field.name)
|
|
119
|
+
?.model;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The referenced record's label, or `undefined` when nothing names it. */
|
|
123
|
+
function referenceLabel(field: ScreenField, id: string): string | undefined {
|
|
124
|
+
const model = targetModel(field);
|
|
125
|
+
const label = model ? labels?.[model]?.(id) : undefined;
|
|
126
|
+
return label && label.trim() !== '' ? label : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
let confirmOpen = $state(false);
|
|
91
130
|
let deleteError = $state(false);
|
|
92
131
|
|
|
@@ -102,6 +141,8 @@ async function confirmDelete(): Promise<void> {
|
|
|
102
141
|
}
|
|
103
142
|
</script>
|
|
104
143
|
|
|
144
|
+
{#snippet shortId(id: string)}<code title={id}>{shortScreenId(id)}</code>{/snippet}
|
|
145
|
+
|
|
105
146
|
{#snippet fieldList(list: ScreenField[])}
|
|
106
147
|
{#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
|
|
107
148
|
<section class="smrt-detail-screen__group">
|
|
@@ -114,12 +155,34 @@ async function confirmDelete(): Promise<void> {
|
|
|
114
155
|
<dd>
|
|
115
156
|
{#if text === ''}
|
|
116
157
|
<span class="smrt-detail-screen__empty">{t(M['ui.screens.not_set'])}</span>
|
|
117
|
-
{:else if field.kind === 'json'
|
|
158
|
+
{:else if field.kind === 'json'}
|
|
159
|
+
{@const entries = jsonScreenEntries(record[field.name])}
|
|
160
|
+
{#if entries}
|
|
161
|
+
<dl class="smrt-detail-screen__pairs">
|
|
162
|
+
{#each entries as entry (entry.key)}
|
|
163
|
+
<div>
|
|
164
|
+
<dt>{humanizeFieldName(entry.key)}</dt>
|
|
165
|
+
<dd>{entry.value}</dd>
|
|
166
|
+
</div>
|
|
167
|
+
{/each}
|
|
168
|
+
</dl>
|
|
169
|
+
{:else}
|
|
170
|
+
<pre class="smrt-detail-screen__block">{text}</pre>
|
|
171
|
+
{/if}
|
|
172
|
+
{:else if field.kind === 'textarea'}
|
|
118
173
|
<pre class="smrt-detail-screen__block">{text}</pre>
|
|
119
174
|
{:else if safeHref(field, text)}
|
|
120
175
|
<a href={safeHref(field, text)}>{text}</a>
|
|
121
176
|
{:else if field.kind === 'reference'}
|
|
122
|
-
|
|
177
|
+
{@const href = hrefFor?.(field, text)}
|
|
178
|
+
{@const label = referenceLabel(field, text)}
|
|
179
|
+
{#if href}
|
|
180
|
+
<a href={href} class="smrt-detail-screen__reference">{#if label}{label}{:else}{@render shortId(text)}{/if}</a>
|
|
181
|
+
{:else if label}
|
|
182
|
+
<span class="smrt-detail-screen__reference">{label}</span>
|
|
183
|
+
{:else}
|
|
184
|
+
{@render shortId(text)}
|
|
185
|
+
{/if}
|
|
123
186
|
{:else}
|
|
124
187
|
{text}
|
|
125
188
|
{/if}
|
|
@@ -152,7 +215,12 @@ async function confirmDelete(): Promise<void> {
|
|
|
152
215
|
</PageHeader>
|
|
153
216
|
{/if}
|
|
154
217
|
|
|
155
|
-
<article
|
|
218
|
+
<article
|
|
219
|
+
class="smrt-detail-screen"
|
|
220
|
+
class:smrt-detail-screen--card={variant === 'card'}
|
|
221
|
+
aria-label={variant === 'card' ? t(M['ui.screens.details']) : heading}
|
|
222
|
+
data-smrt-component="DetailScreen"
|
|
223
|
+
>
|
|
156
224
|
{#if sourceError}
|
|
157
225
|
<Alert variant="error">{sourceError}</Alert>
|
|
158
226
|
{:else}
|
|
@@ -167,6 +235,11 @@ async function confirmDelete(): Promise<void> {
|
|
|
167
235
|
{@render fieldList(secondary)}
|
|
168
236
|
</Disclosure>
|
|
169
237
|
{/if}
|
|
238
|
+
{#if empty.length > 0}
|
|
239
|
+
<Disclosure title={t(M['ui.screens.show_empty'], { count: empty.length })}>
|
|
240
|
+
{@render fieldList(empty)}
|
|
241
|
+
</Disclosure>
|
|
242
|
+
{/if}
|
|
170
243
|
{@render children?.()}
|
|
171
244
|
{/if}
|
|
172
245
|
</article>
|
|
@@ -193,6 +266,15 @@ async function confirmDelete(): Promise<void> {
|
|
|
193
266
|
flex-direction: column;
|
|
194
267
|
gap: var(--smrt-spacing-lg, 1.5rem);
|
|
195
268
|
}
|
|
269
|
+
.smrt-detail-screen--card {
|
|
270
|
+
padding: var(--smrt-spacing-md, 1rem);
|
|
271
|
+
border: var(--smrt-hairline);
|
|
272
|
+
border-radius: var(--smrt-radius-lg);
|
|
273
|
+
background: var(--smrt-color-surface);
|
|
274
|
+
}
|
|
275
|
+
.smrt-detail-screen--card .smrt-detail-screen__fields {
|
|
276
|
+
grid-template-columns: repeat(auto-fill, minmax(max(14rem, 40%), 1fr));
|
|
277
|
+
}
|
|
196
278
|
.smrt-detail-screen__group-title {
|
|
197
279
|
margin: 0 0 var(--smrt-spacing-sm, 0.5rem);
|
|
198
280
|
font-size: var(--smrt-typography-title-medium-size, 1rem);
|
|
@@ -214,6 +296,23 @@ async function confirmDelete(): Promise<void> {
|
|
|
214
296
|
.smrt-detail-screen__empty {
|
|
215
297
|
color: var(--smrt-color-on-surface-variant);
|
|
216
298
|
}
|
|
299
|
+
.smrt-detail-screen__pairs {
|
|
300
|
+
display: grid;
|
|
301
|
+
gap: var(--smrt-spacing-1, 0.25rem);
|
|
302
|
+
margin: 0;
|
|
303
|
+
}
|
|
304
|
+
.smrt-detail-screen__pairs div {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-wrap: wrap;
|
|
307
|
+
gap: 0 var(--smrt-spacing-sm, 0.5rem);
|
|
308
|
+
}
|
|
309
|
+
.smrt-detail-screen__pairs dt {
|
|
310
|
+
color: var(--smrt-color-on-surface-variant);
|
|
311
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
312
|
+
}
|
|
313
|
+
.smrt-detail-screen__pairs dd {
|
|
314
|
+
margin: 0;
|
|
315
|
+
}
|
|
217
316
|
.smrt-detail-screen__block {
|
|
218
317
|
margin: 0;
|
|
219
318
|
white-space: pre-wrap;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DetailScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/DetailScreen.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DetailScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/DetailScreen.svelte.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAiOpB,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
FormActionBar,
|
|
26
26
|
FormGroup,
|
|
27
27
|
Input,
|
|
28
|
+
Select,
|
|
28
29
|
Textarea,
|
|
29
30
|
} from '@happyvertical/smrt-ui/forms';
|
|
30
31
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
@@ -32,6 +33,7 @@ import { PageHeader } from '@happyvertical/smrt-ui/layout';
|
|
|
32
33
|
import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
|
|
33
34
|
import { untrack } from 'svelte';
|
|
34
35
|
import { M } from '../../i18n/strings.screens.js';
|
|
36
|
+
import { statusLabel } from '../overview/widgets/record-table.js';
|
|
35
37
|
import {
|
|
36
38
|
deriveScreenFields,
|
|
37
39
|
groupScreenFields,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
DEFAULT_SCREEN_CURRENCY,
|
|
52
54
|
draftForCreate,
|
|
53
55
|
draftFromRecord,
|
|
56
|
+
formatScreenValue,
|
|
54
57
|
parseDraft,
|
|
55
58
|
} from './values.js';
|
|
56
59
|
|
|
@@ -67,6 +70,7 @@ let {
|
|
|
67
70
|
error = null,
|
|
68
71
|
onsubmit,
|
|
69
72
|
oncancel,
|
|
73
|
+
derivedFields = [],
|
|
70
74
|
}: EditFormProps = $props();
|
|
71
75
|
|
|
72
76
|
const { t } = useI18n();
|
|
@@ -86,6 +90,9 @@ const allFields = $derived(
|
|
|
86
90
|
? []
|
|
87
91
|
: deriveScreenFields(definition, policy, { mode: 'edit', include }),
|
|
88
92
|
);
|
|
93
|
+
/** Derived fields (a total computed from line items) show but never submit. */
|
|
94
|
+
const isDerived = (field: ScreenField): boolean =>
|
|
95
|
+
derivedFields.includes(field.name);
|
|
89
96
|
const basicFields = $derived(allFields.filter((f) => f.tier === 'basic'));
|
|
90
97
|
const advancedFields = $derived(allFields.filter((f) => f.tier === 'advanced'));
|
|
91
98
|
|
|
@@ -153,7 +160,11 @@ async function submit(): Promise<void> {
|
|
|
153
160
|
if (working || submitting) return;
|
|
154
161
|
failed = false;
|
|
155
162
|
hostErrors = {};
|
|
156
|
-
const parsed = parseDraft(
|
|
163
|
+
const parsed = parseDraft(
|
|
164
|
+
allFields.filter((f) => !isDerived(f)),
|
|
165
|
+
draft,
|
|
166
|
+
{ currency, record },
|
|
167
|
+
);
|
|
157
168
|
localErrors = parsed.errors;
|
|
158
169
|
if (Object.keys(parsed.errors).length > 0) {
|
|
159
170
|
if (advancedFields.some((f) => parsed.errors[f.name])) advancedOpen = true;
|
|
@@ -182,7 +193,15 @@ const formError = $derived(
|
|
|
182
193
|
</script>
|
|
183
194
|
|
|
184
195
|
{#snippet control(field: ScreenField)}
|
|
185
|
-
{#if field
|
|
196
|
+
{#if isDerived(field)}
|
|
197
|
+
<FormGroup label={label(field)} hint={t(M['ui.screens.derived_hint'])}>
|
|
198
|
+
<Input
|
|
199
|
+
name={field.name}
|
|
200
|
+
readonly
|
|
201
|
+
value={formatScreenValue(field, record?.[field.name], { currency })}
|
|
202
|
+
/>
|
|
203
|
+
</FormGroup>
|
|
204
|
+
{:else if field.kind === 'boolean'}
|
|
186
205
|
<div class="smrt-edit-form__check">
|
|
187
206
|
<Checkbox
|
|
188
207
|
name={field.name}
|
|
@@ -204,7 +223,24 @@ const formError = $derived(
|
|
|
204
223
|
error={messageFor(field)}
|
|
205
224
|
required={field.required}
|
|
206
225
|
>
|
|
207
|
-
{#if field.kind === '
|
|
226
|
+
{#if field.kind === 'select'}
|
|
227
|
+
<Select
|
|
228
|
+
name={field.name}
|
|
229
|
+
disabled={busy}
|
|
230
|
+
required={field.required}
|
|
231
|
+
bind:value={() => text(field.name), (v) => setText(field.name, v)}
|
|
232
|
+
>
|
|
233
|
+
{#if !field.required || text(field.name) === ''}
|
|
234
|
+
<option value="">{t(M['ui.screens.select_none'])}</option>
|
|
235
|
+
{/if}
|
|
236
|
+
{#each field.options ?? [] as option (option)}
|
|
237
|
+
<option value={option}>{statusLabel(option)}</option>
|
|
238
|
+
{/each}
|
|
239
|
+
{#if text(field.name) !== '' && !(field.options ?? []).includes(text(field.name))}
|
|
240
|
+
<option value={text(field.name)}>{text(field.name)}</option>
|
|
241
|
+
{/if}
|
|
242
|
+
</Select>
|
|
243
|
+
{:else if field.kind === 'textarea' || field.kind === 'json'}
|
|
208
244
|
<Textarea
|
|
209
245
|
name={field.name}
|
|
210
246
|
rows={field.kind === 'json' ? 8 : 4}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/EditForm.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EditForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/EditForm.svelte.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,YAAY,CAAC;AA6PpB,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* screen set for a package that ships models but no consumer UI: tags, facts,
|
|
6
6
|
* secrets, profiles, reports, prompts and the like.
|
|
7
7
|
*
|
|
8
|
-
* It composes `ListScreen`, `
|
|
8
|
+
* It composes `ListScreen`, `RecordScreen` (view and edit, the record view
|
|
9
|
+
* with its related-records overview) and `EditForm` (create) over a
|
|
9
10
|
* transport-neutral `source` (list / get / create / update / delete). The
|
|
10
11
|
* `view` and `recordId` props are bindable so an app can mirror them into its
|
|
11
12
|
* router (`onnavigate` fires on every user navigation); left unbound, the
|
|
@@ -18,10 +19,11 @@ import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
|
18
19
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
19
20
|
import { untrack } from 'svelte';
|
|
20
21
|
import { M } from '../../i18n/strings.screens.js';
|
|
21
|
-
import DetailScreen from './DetailScreen.svelte';
|
|
22
22
|
import EditForm from './EditForm.svelte';
|
|
23
23
|
import { screenTitles } from './fields.js';
|
|
24
24
|
import ListScreen from './ListScreen.svelte';
|
|
25
|
+
import RecordScreen from './RecordScreen.svelte';
|
|
26
|
+
import { deriveScreenRelations } from './relations.js';
|
|
25
27
|
import type {
|
|
26
28
|
RecipeScreensProps,
|
|
27
29
|
RecipeScreensSource,
|
|
@@ -44,6 +46,8 @@ let {
|
|
|
44
46
|
locale,
|
|
45
47
|
can = {},
|
|
46
48
|
onnavigate,
|
|
49
|
+
definitions,
|
|
50
|
+
record: recordProps = {},
|
|
47
51
|
}: RecipeScreensProps = $props();
|
|
48
52
|
|
|
49
53
|
const { t } = useI18n();
|
|
@@ -53,6 +57,9 @@ const idField = $derived(definition.idField ?? 'id');
|
|
|
53
57
|
const allowCreate = $derived(can.create !== false && !!source.create);
|
|
54
58
|
const allowUpdate = $derived(can.update !== false && !!source.update);
|
|
55
59
|
const allowDelete = $derived(can.delete !== false && !!source.delete);
|
|
60
|
+
const relations = $derived(
|
|
61
|
+
definitions ? deriveScreenRelations(definitions, definition.objectRef) : [],
|
|
62
|
+
);
|
|
56
63
|
|
|
57
64
|
let rows = $state<readonly ScreenRecord[]>([]);
|
|
58
65
|
let loading = $state(true);
|
|
@@ -182,33 +189,26 @@ async function remove(): Promise<void> {
|
|
|
182
189
|
/>
|
|
183
190
|
{:else if (view === 'view' || view === 'edit') && recordId !== null}
|
|
184
191
|
{#if current}
|
|
185
|
-
{#
|
|
186
|
-
|
|
187
|
-
<EditForm
|
|
188
|
-
{definition}
|
|
189
|
-
{policy}
|
|
190
|
-
{currency}
|
|
191
|
-
{fields}
|
|
192
|
-
record={current}
|
|
193
|
-
submitting={saving}
|
|
194
|
-
onsubmit={update}
|
|
195
|
-
oncancel={() => navigate('view', recordId)}
|
|
196
|
-
/>
|
|
197
|
-
{/key}
|
|
198
|
-
{:else}
|
|
199
|
-
<DetailScreen
|
|
192
|
+
{#key definition.objectRef}
|
|
193
|
+
<RecordScreen
|
|
200
194
|
{definition}
|
|
201
195
|
{policy}
|
|
202
196
|
{currency}
|
|
203
197
|
{locale}
|
|
204
198
|
{fields}
|
|
199
|
+
{relations}
|
|
205
200
|
record={current}
|
|
201
|
+
mode={view === 'edit' && allowUpdate ? 'edit' : 'view'}
|
|
206
202
|
{deleting}
|
|
203
|
+
submitting={saving}
|
|
207
204
|
onback={() => navigate('list')}
|
|
208
205
|
onedit={allowUpdate ? () => navigate('edit', recordId) : undefined}
|
|
206
|
+
oncancel={() => navigate('view', recordId)}
|
|
207
|
+
onsave={allowUpdate ? update : undefined}
|
|
209
208
|
ondelete={allowDelete ? remove : undefined}
|
|
209
|
+
{...recordProps}
|
|
210
210
|
/>
|
|
211
|
-
{/
|
|
211
|
+
{/key}
|
|
212
212
|
{:else if !resolving}
|
|
213
213
|
<Alert variant="warning">{t(M['ui.screens.not_found'])}</Alert>
|
|
214
214
|
<Button variant="ghost" onclick={() => navigate('list')}>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecipeScreens.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecipeScreens.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RecipeScreens.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/RecipeScreens.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,YAAY,CAAC;AAuLpB,QAAA,MAAM,aAAa,yEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|