@happyvertical/smrt-manufacturing 0.53.6 → 0.54.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.
@@ -1,69 +1,440 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * AssemblyForm — the create and edit form for an assembly, rendered by
4
- * smrt-fields `ObjectForm` for `@happyvertical/smrt-manufacturing:Assembly`.
3
+ * AssemblyForm — add an assembly, or edit one: name, description, category,
4
+ * part reference, price, estimated labour, default operation and tags.
5
+ * Presentational: it never persists. The host receives trimmed, checked
6
+ * values in `onsubmit` and saves them through the Assembly REST route or
7
+ * `AssemblyCollection`.
5
8
  *
6
- * The fields, their order, labels, help and visibility come from the
7
- * generated Assembly definition and the consumer's resolved field policy, so
8
- * a field the policy hides (price, for someone who may not see it) is not
9
- * rendered. Pass `fields` and `policy` directly, or omit both beneath an
10
- * `ObjectFormSourceProvider` that knows the Assembly definition.
9
+ * The host passes the operations to choose from (the active ones). An
10
+ * operation already set on the assembly that is retired, or no longer in the
11
+ * list, stays visible as the current value, marked, and can be cleared.
11
12
  *
12
- * Presentational: the host owns the record and persistence. `onsubmit`
13
- * should save through the Assembly REST route or `AssemblyCollection` and
14
- * resolve `true` once the record is stored.
13
+ * Field policy: a field named in `hiddenFields` is not rendered, and one in
14
+ * `readonlyFields` is shown but not editable (a price a viewer may see but
15
+ * not change). Neither is checked, and `onsubmit` carries its initial value
16
+ * unchanged, so a host should save only fields the reader may write.
15
17
  */
16
18
 
19
+ import { Button } from '@happyvertical/smrt-ui';
20
+ import { Form, Input, Select, Textarea } from '@happyvertical/smrt-ui/forms';
21
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
22
+ import { M } from '../i18n.js';
17
23
  import {
18
- ObjectForm,
19
- type ObjectFormProps,
20
- type ResolvedObjectFieldPolicy,
21
- } from '@happyvertical/smrt-fields/svelte';
22
- import type { Snippet } from 'svelte';
24
+ type AssemblyFormField,
25
+ type AssemblyFormInitial,
26
+ type AssemblyFormInvalidField,
27
+ type AssemblyFormValues,
28
+ assemblyFormDraft,
29
+ currencyExponent,
30
+ keepProtectedFields,
31
+ type OperationView,
32
+ validateAssemblyForm,
33
+ } from '../types.js';
23
34
 
24
- /** The canonical object reference ObjectForm and field policy use for Assembly. */
25
- const ASSEMBLY_OBJECT_REF = '@happyvertical/smrt-manufacturing:Assembly';
35
+ const { t } = useI18n();
26
36
 
27
37
  export interface AssemblyFormProps {
28
- /** Generated browser-safe Assembly field definitions; pass with `policy`. */
29
- fields?: ObjectFormProps['fields'];
30
- /** The resolved field policy for Assembly; pass with `fields`. */
31
- policy?: ResolvedObjectFieldPolicy;
32
- /** The record being created or edited. Bind it to keep values in the host. */
33
- value?: Record<string, unknown>;
34
- /** True for a new assembly (policy defaults may prefill), false for an edit. */
35
- isNewRecord?: boolean;
38
+ /** The assembly being edited; omit or pass null to add one. */
39
+ assembly?: AssemblyFormInitial | null;
40
+ /** Invoked with trimmed, checked values. The form never persists. */
41
+ onsubmit: (values: AssemblyFormValues) => void;
42
+ /** Invoked when the form is canceled; omit to hide the cancel button. */
43
+ oncancel?: () => void;
36
44
  /** Blocks input and submission while the host saves. */
37
- disabled?: boolean;
38
- /** Shows the basic/advanced field toggle. */
39
- showModeSwitch?: boolean;
40
- /** Controls rendered after the fields, inside the form (e.g. a cancel button). */
41
- actions?: Snippet;
42
- /** Saves the record; resolve `true` only once it is stored. */
43
- onsubmit?: ObjectFormProps['onsubmit'];
45
+ loading?: boolean;
46
+ /** Operations to choose a default from: the active ones, host-loaded. */
47
+ operations?: readonly OperationView[];
48
+ /** ISO 4217 currency of the price, named in its help text. */
49
+ currency?: string;
50
+ /** Fields the reader may not see: not rendered. */
51
+ hiddenFields?: readonly AssemblyFormField[];
52
+ /** Fields the reader may see but not change: rendered read-only. */
53
+ readonlyFields?: readonly AssemblyFormField[];
44
54
  }
45
55
 
46
- let {
47
- fields,
48
- policy,
49
- value = $bindable<Record<string, unknown>>({}),
50
- isNewRecord = true,
51
- disabled = false,
52
- showModeSwitch = true,
53
- actions,
56
+ const {
57
+ assembly = null,
54
58
  onsubmit,
59
+ oncancel,
60
+ loading = false,
61
+ operations = [],
62
+ currency = 'USD',
63
+ hiddenFields = [],
64
+ readonlyFields = [],
55
65
  }: AssemblyFormProps = $props();
66
+
67
+ const uid = $props.id();
68
+ const exponent = $derived(currencyExponent(currency));
69
+ const isNew = $derived(assembly === null);
70
+
71
+ function getInitialFormState() {
72
+ const initialExponent = currencyExponent(currency);
73
+ return {
74
+ assembly,
75
+ exponent: initialExponent,
76
+ draft: assemblyFormDraft(assembly, initialExponent),
77
+ };
78
+ }
79
+
80
+ const initialFormState = getInitialFormState();
81
+ let name = $state(initialFormState.draft.name);
82
+ let description = $state(initialFormState.draft.description);
83
+ let category = $state(initialFormState.draft.category);
84
+ let partReference = $state(initialFormState.draft.partReference);
85
+ let price = $state(initialFormState.draft.price);
86
+ let estimatedLabourMinutes = $state(
87
+ initialFormState.draft.estimatedLabourMinutes,
88
+ );
89
+ let defaultOperationId = $state(initialFormState.draft.defaultOperationId);
90
+ let tags = $state(initialFormState.draft.tags);
91
+ let invalid = $state<AssemblyFormInvalidField[]>([]);
92
+ let appliedAssembly: AssemblyFormInitial | null = initialFormState.assembly;
93
+ let appliedExponent = initialFormState.exponent;
94
+
95
+ // Follow the host when it swaps the assembly being edited.
96
+ // Follow a change of currency: show the stored price in the new minor unit.
97
+ $effect(() => {
98
+ if (appliedExponent === exponent) return;
99
+ appliedExponent = exponent;
100
+ price = assemblyFormDraft(assembly, exponent).price;
101
+ });
102
+
103
+ $effect(() => {
104
+ if (appliedAssembly === assembly) return;
105
+ appliedAssembly = assembly;
106
+ const draft = assemblyFormDraft(assembly, exponent);
107
+ name = draft.name;
108
+ description = draft.description;
109
+ category = draft.category;
110
+ partReference = draft.partReference;
111
+ price = draft.price;
112
+ estimatedLabourMinutes = draft.estimatedLabourMinutes;
113
+ defaultOperationId = draft.defaultOperationId;
114
+ tags = draft.tags;
115
+ invalid = [];
116
+ });
117
+
118
+ const shown = (field: AssemblyFormField) => !hiddenFields.includes(field);
119
+ const locked = (field: AssemblyFormField) =>
120
+ loading || readonlyFields.includes(field);
121
+
122
+ // The operation set when the form opened, kept in the list even if retired.
123
+ const originalOperationId = $derived(assembly?.defaultOperationId ?? '');
124
+
125
+ function operationLabel(operation: OperationView): string {
126
+ const label = operation.name
127
+ ? `${operation.code} - ${operation.name}`
128
+ : operation.code;
129
+ return operation.isActive
130
+ ? label
131
+ : t(M['manufacturing.assembly_form.operation_retired'], { label });
132
+ }
133
+
134
+ const operationChoices = $derived.by(() => {
135
+ const choices: { id: string; label: string }[] = operations
136
+ .filter(
137
+ (operation) => operation.isActive || operation.id === originalOperationId,
138
+ )
139
+ .map((operation) => ({
140
+ id: operation.id,
141
+ label: operationLabel(operation),
142
+ }));
143
+ if (
144
+ originalOperationId &&
145
+ !choices.some((c) => c.id === originalOperationId)
146
+ ) {
147
+ choices.push({
148
+ id: originalOperationId,
149
+ label: t(M['manufacturing.assembly_form.operation_unknown'], {
150
+ id: originalOperationId,
151
+ }),
152
+ });
153
+ }
154
+ return choices;
155
+ });
156
+
157
+ function errorMessage(field: AssemblyFormInvalidField): string {
158
+ switch (field) {
159
+ case 'name':
160
+ return t(M['manufacturing.assembly_form.error_name']);
161
+ case 'price':
162
+ return t(M['manufacturing.assembly_form.error_price']);
163
+ default:
164
+ return t(M['manufacturing.assembly_form.error_labour']);
165
+ }
166
+ }
167
+
168
+ const errors = $derived(
169
+ invalid.map((field) => ({ field, message: errorMessage(field) })),
170
+ );
171
+
172
+ function errorId(field: AssemblyFormInvalidField): string {
173
+ return `${uid}-error-${field}`;
174
+ }
175
+
176
+ function describedBy(
177
+ field: AssemblyFormInvalidField | null,
178
+ hintId?: string,
179
+ ): string | undefined {
180
+ const ids = [
181
+ hintId,
182
+ field && invalid.includes(field) ? errorId(field) : undefined,
183
+ ];
184
+ return ids.filter(Boolean).join(' ') || undefined;
185
+ }
186
+
187
+ const policyProtected = $derived([...hiddenFields, ...readonlyFields]);
188
+
189
+ function handleSubmit() {
190
+ const draft = {
191
+ name,
192
+ description,
193
+ category,
194
+ partReference,
195
+ price,
196
+ estimatedLabourMinutes,
197
+ defaultOperationId,
198
+ tags,
199
+ };
200
+ // A field the reader left as it was keeps its stored value exactly (a tag
201
+ // with a comma, a price this form cannot express); the name is always checked.
202
+ const initialDraft = assemblyFormDraft(assembly, exponent);
203
+ const untouched = (Object.keys(draft) as AssemblyFormField[]).filter(
204
+ (field) => field !== 'name' && draft[field] === initialDraft[field],
205
+ );
206
+ const protectedFields = [...policyProtected, ...untouched];
207
+ const result = validateAssemblyForm(draft, policyProtected, exponent);
208
+ const checked =
209
+ result.ok || untouched.length === 0
210
+ ? result
211
+ : validateAssemblyForm(draft, protectedFields, exponent);
212
+ if (!checked.ok) {
213
+ invalid = checked.invalid;
214
+ return;
215
+ }
216
+ invalid = [];
217
+ onsubmit(keepProtectedFields(checked.values, assembly, protectedFields));
218
+ }
56
219
  </script>
57
220
 
58
- <ObjectForm
59
- objectRef={ASSEMBLY_OBJECT_REF}
60
- {fields}
61
- {policy}
62
- bind:value
63
- {isNewRecord}
64
- {disabled}
65
- {showModeSwitch}
66
- {actions}
67
- {onsubmit}
68
- class="assembly-form"
69
- />
221
+ <div class="assembly-form-shell">
222
+ <Form class="assembly-form" onsubmit={handleSubmit}>
223
+ {#if shown('name')}
224
+ <div class="field">
225
+ <label for="{uid}-name">
226
+ {t(M['manufacturing.assembly_form.name'])}<span class="required" aria-hidden="true">*</span>
227
+ </label>
228
+ <Input
229
+ id="{uid}-name"
230
+ name="name"
231
+ type="text"
232
+ bind:value={name}
233
+ required
234
+ readonly={readonlyFields.includes('name')}
235
+ disabled={loading}
236
+ aria-invalid={invalid.includes('name') ? 'true' : undefined}
237
+ aria-describedby={describedBy('name')}
238
+ />
239
+ </div>
240
+ {/if}
241
+
242
+ {#if shown('description')}
243
+ <div class="field">
244
+ <label for="{uid}-description">{t(M['manufacturing.assembly_form.description'])}</label>
245
+ <Textarea
246
+ id="{uid}-description"
247
+ name="description"
248
+ bind:value={description}
249
+ readonly={readonlyFields.includes('description')}
250
+ disabled={loading}
251
+ />
252
+ </div>
253
+ {/if}
254
+
255
+ {#if shown('partReference')}
256
+ <div class="field">
257
+ <label for="{uid}-part-reference">{t(M['manufacturing.assembly_form.part_reference'])}</label>
258
+ <Input
259
+ id="{uid}-part-reference"
260
+ name="partReference"
261
+ type="text"
262
+ bind:value={partReference}
263
+ readonly={readonlyFields.includes('partReference')}
264
+ disabled={loading}
265
+ aria-describedby="{uid}-part-reference-hint"
266
+ />
267
+ <span class="hint" id="{uid}-part-reference-hint">
268
+ {t(M['manufacturing.assembly_form.part_reference_help'])}
269
+ </span>
270
+ </div>
271
+ {/if}
272
+
273
+ {#if shown('category')}
274
+ <div class="field">
275
+ <label for="{uid}-category">{t(M['manufacturing.assembly_form.category'])}</label>
276
+ <Input
277
+ id="{uid}-category"
278
+ name="category"
279
+ type="text"
280
+ bind:value={category}
281
+ readonly={readonlyFields.includes('category')}
282
+ disabled={loading}
283
+ />
284
+ </div>
285
+ {/if}
286
+
287
+ {#if shown('price')}
288
+ <div class="field">
289
+ <label for="{uid}-price">{t(M['manufacturing.assembly_form.price'])}</label>
290
+ <Input
291
+ id="{uid}-price"
292
+ name="price"
293
+ type="text"
294
+ inputmode="decimal"
295
+ autocomplete="off"
296
+ bind:value={price}
297
+ readonly={readonlyFields.includes('price')}
298
+ disabled={loading}
299
+ aria-invalid={invalid.includes('price') ? 'true' : undefined}
300
+ aria-describedby={describedBy('price', `${uid}-price-hint`)}
301
+ />
302
+ <span class="hint" id="{uid}-price-hint">
303
+ {t(M['manufacturing.assembly_form.price_help'], { currency })}
304
+ </span>
305
+ </div>
306
+ {/if}
307
+
308
+ {#if shown('estimatedLabourMinutes')}
309
+ <div class="field">
310
+ <label for="{uid}-labour">{t(M['manufacturing.assembly_form.labour'])}</label>
311
+ <Input
312
+ id="{uid}-labour"
313
+ name="estimatedLabourMinutes"
314
+ type="text"
315
+ inputmode="numeric"
316
+ autocomplete="off"
317
+ bind:value={estimatedLabourMinutes}
318
+ readonly={readonlyFields.includes('estimatedLabourMinutes')}
319
+ disabled={loading}
320
+ aria-invalid={invalid.includes('estimatedLabourMinutes') ? 'true' : undefined}
321
+ aria-describedby={describedBy('estimatedLabourMinutes', `${uid}-labour-hint`)}
322
+ />
323
+ <span class="hint" id="{uid}-labour-hint">
324
+ {t(M['manufacturing.assembly_form.labour_help'])}
325
+ </span>
326
+ </div>
327
+ {/if}
328
+
329
+ {#if shown('defaultOperationId')}
330
+ <div class="field">
331
+ <label for="{uid}-operation">{t(M['manufacturing.assembly_form.operation'])}</label>
332
+ <Select
333
+ id="{uid}-operation"
334
+ name="defaultOperationId"
335
+ bind:value={defaultOperationId}
336
+ disabled={locked('defaultOperationId')}
337
+ aria-describedby="{uid}-operation-hint"
338
+ >
339
+ <option value="">{t(M['manufacturing.assembly_form.operation_none'])}</option>
340
+ {#each operationChoices as choice (choice.id)}
341
+ <option value={choice.id}>{choice.label}</option>
342
+ {/each}
343
+ </Select>
344
+ <span class="hint" id="{uid}-operation-hint">
345
+ {t(M['manufacturing.assembly_form.operation_help'])}
346
+ </span>
347
+ </div>
348
+ {/if}
349
+
350
+ {#if shown('tags')}
351
+ <div class="field">
352
+ <label for="{uid}-tags">{t(M['manufacturing.assembly_form.tags'])}</label>
353
+ <Input
354
+ id="{uid}-tags"
355
+ name="tags"
356
+ type="text"
357
+ autocomplete="off"
358
+ bind:value={tags}
359
+ readonly={readonlyFields.includes('tags')}
360
+ disabled={loading}
361
+ aria-describedby="{uid}-tags-hint"
362
+ />
363
+ <span class="hint" id="{uid}-tags-hint">
364
+ {t(M['manufacturing.assembly_form.tags_help'])}
365
+ </span>
366
+ </div>
367
+ {/if}
368
+
369
+ {#if errors.length > 0}
370
+ <div class="errors" role="alert">
371
+ <ul>
372
+ {#each errors as error (error.field)}
373
+ <li id={errorId(error.field)}>{error.message}</li>
374
+ {/each}
375
+ </ul>
376
+ </div>
377
+ {/if}
378
+
379
+ <div class="actions">
380
+ {#if oncancel}
381
+ <Button variant="secondary" type="button" onclick={oncancel} disabled={loading}>
382
+ {t(M['manufacturing.assembly_form.cancel'])}
383
+ </Button>
384
+ {/if}
385
+ <Button variant="primary" type="submit" disabled={loading}>
386
+ {#if loading}
387
+ {t(M['manufacturing.assembly_form.saving'])}
388
+ {:else if isNew}
389
+ {t(M['manufacturing.assembly_form.submit_add'])}
390
+ {:else}
391
+ {t(M['manufacturing.assembly_form.submit_edit'])}
392
+ {/if}
393
+ </Button>
394
+ </div>
395
+ </Form>
396
+ </div>
397
+
398
+ <style>
399
+ .assembly-form-shell :global(.assembly-form) {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: var(--smrt-spacing-md, 1rem);
403
+ }
404
+
405
+ .field {
406
+ display: flex;
407
+ flex-direction: column;
408
+ gap: 0.25rem;
409
+ }
410
+
411
+ label {
412
+ font: var(--smrt-typography-body-medium-font, 500 0.875rem / 1.25 sans-serif);
413
+ color: var(--smrt-color-on-surface-variant, inherit);
414
+ }
415
+
416
+ .required {
417
+ color: var(--smrt-color-error, inherit);
418
+ }
419
+
420
+ .hint {
421
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
422
+ color: var(--smrt-color-on-surface-variant, inherit);
423
+ }
424
+
425
+ .errors {
426
+ color: var(--smrt-color-error, inherit);
427
+ }
428
+
429
+ .errors ul {
430
+ margin: 0;
431
+ padding-left: 1.25rem;
432
+ }
433
+
434
+ .actions {
435
+ display: flex;
436
+ justify-content: flex-end;
437
+ gap: var(--smrt-spacing-sm, 0.5rem);
438
+ margin-top: var(--smrt-spacing-sm, 0.5rem);
439
+ }
440
+ </style>
@@ -1,38 +1,23 @@
1
- /**
2
- * AssemblyForm — the create and edit form for an assembly, rendered by
3
- * smrt-fields `ObjectForm` for `@happyvertical/smrt-manufacturing:Assembly`.
4
- *
5
- * The fields, their order, labels, help and visibility come from the
6
- * generated Assembly definition and the consumer's resolved field policy, so
7
- * a field the policy hides (price, for someone who may not see it) is not
8
- * rendered. Pass `fields` and `policy` directly, or omit both beneath an
9
- * `ObjectFormSourceProvider` that knows the Assembly definition.
10
- *
11
- * Presentational: the host owns the record and persistence. `onsubmit`
12
- * should save through the Assembly REST route or `AssemblyCollection` and
13
- * resolve `true` once the record is stored.
14
- */
15
- import { type ObjectFormProps, type ResolvedObjectFieldPolicy } from '@happyvertical/smrt-fields/svelte';
16
- import type { Snippet } from 'svelte';
1
+ import { type AssemblyFormField, type AssemblyFormInitial, type AssemblyFormValues, type OperationView } from '../types.js';
17
2
  export interface AssemblyFormProps {
18
- /** Generated browser-safe Assembly field definitions; pass with `policy`. */
19
- fields?: ObjectFormProps['fields'];
20
- /** The resolved field policy for Assembly; pass with `fields`. */
21
- policy?: ResolvedObjectFieldPolicy;
22
- /** The record being created or edited. Bind it to keep values in the host. */
23
- value?: Record<string, unknown>;
24
- /** True for a new assembly (policy defaults may prefill), false for an edit. */
25
- isNewRecord?: boolean;
3
+ /** The assembly being edited; omit or pass null to add one. */
4
+ assembly?: AssemblyFormInitial | null;
5
+ /** Invoked with trimmed, checked values. The form never persists. */
6
+ onsubmit: (values: AssemblyFormValues) => void;
7
+ /** Invoked when the form is canceled; omit to hide the cancel button. */
8
+ oncancel?: () => void;
26
9
  /** Blocks input and submission while the host saves. */
27
- disabled?: boolean;
28
- /** Shows the basic/advanced field toggle. */
29
- showModeSwitch?: boolean;
30
- /** Controls rendered after the fields, inside the form (e.g. a cancel button). */
31
- actions?: Snippet;
32
- /** Saves the record; resolve `true` only once it is stored. */
33
- onsubmit?: ObjectFormProps['onsubmit'];
10
+ loading?: boolean;
11
+ /** Operations to choose a default from: the active ones, host-loaded. */
12
+ operations?: readonly OperationView[];
13
+ /** ISO 4217 currency of the price, named in its help text. */
14
+ currency?: string;
15
+ /** Fields the reader may not see: not rendered. */
16
+ hiddenFields?: readonly AssemblyFormField[];
17
+ /** Fields the reader may see but not change: rendered read-only. */
18
+ readonlyFields?: readonly AssemblyFormField[];
34
19
  }
35
- declare const AssemblyForm: import("svelte").Component<AssemblyFormProps, {}, "value">;
20
+ declare const AssemblyForm: import("svelte").Component<AssemblyFormProps, {}, "">;
36
21
  type AssemblyForm = ReturnType<typeof AssemblyForm>;
37
22
  export default AssemblyForm;
38
23
  //# sourceMappingURL=AssemblyForm.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssemblyForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyForm.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC/B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,kEAAkE;IAClE,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,gFAAgF;IAChF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;CACxC;AA2BD,QAAA,MAAM,YAAY,4DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"AssemblyForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyForm.svelte.ts"],"names":[],"mappings":"AAuBA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAExB,KAAK,kBAAkB,EAIvB,KAAK,aAAa,EAEnB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,UAAU,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,oEAAoE;IACpE,cAAc,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C;AAmSD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -5,19 +5,19 @@
5
5
  * the host loads assemblies, their SKUs and active bills, and adapts each row
6
6
  * with `toAssemblyView`.
7
7
  *
8
- * Pass the resolved field `policy` for `@happyvertical/smrt-manufacturing:Assembly`
9
- * to apply the consumer's field policy: a field the policy hides (price, for
8
+ * Pass the resolved field `policy` for Assembly (any object whose `fields`
9
+ * name each field's `visibility`) to apply the consumer's field policy: a field the policy hides (price, for
10
10
  * a viewer who may not see it) drops its column.
11
11
  */
12
12
 
13
- import {
14
- policyToVisibleColumnIds,
15
- type ResolvedObjectFieldPolicy,
16
- } from '@happyvertical/smrt-fields/svelte';
17
13
  import { Button, CurrencyDisplay } from '@happyvertical/smrt-ui';
18
14
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
19
15
  import { M } from '../i18n.js';
20
- import { type AssemblyView, splitLabourMinutes } from '../types.js';
16
+ import {
17
+ type AssemblyFieldPolicy,
18
+ type AssemblyView,
19
+ splitLabourMinutes,
20
+ } from '../types.js';
21
21
 
22
22
  const { t } = useI18n();
23
23
 
@@ -25,7 +25,7 @@ export interface AssemblyListProps {
25
25
  /** Assemblies to show, in the host's order. */
26
26
  assemblies: AssemblyView[];
27
27
  /** The resolved field policy for Assembly; hidden fields drop their column. */
28
- policy?: ResolvedObjectFieldPolicy;
28
+ policy?: AssemblyFieldPolicy;
29
29
  /** ISO 4217 currency of the prices. */
30
30
  currency?: string;
31
31
  /** Invoked with the assembly id when a name is activated. */
@@ -53,12 +53,18 @@ const POLICY_COLUMNS = [
53
53
  { id: 'labour' },
54
54
  { id: 'bill' },
55
55
  ];
56
- const FIELD_BY_COLUMN = { labour: 'estimatedLabourMinutes' };
56
+ const FIELD_BY_COLUMN: Record<string, string> = {
57
+ labour: 'estimatedLabourMinutes',
58
+ };
57
59
 
58
60
  const visible = $derived(
59
- policy
60
- ? policyToVisibleColumnIds(policy, POLICY_COLUMNS, FIELD_BY_COLUMN)
61
- : new Set(POLICY_COLUMNS.map((column) => column.id)),
61
+ new Set(
62
+ POLICY_COLUMNS.filter(
63
+ (column) =>
64
+ policy?.fields[FIELD_BY_COLUMN[column.id] ?? column.id]?.visibility !==
65
+ 'hidden',
66
+ ).map((column) => column.id),
67
+ ),
62
68
  );
63
69
 
64
70
  /** The row header: the name, or a visible identifier when the name is hidden. */
@@ -1,20 +1,9 @@
1
- /**
2
- * AssemblyList — a table of assemblies: name, part reference, SKU codes,
3
- * price, labour estimate and the version of the active bill. Presentational:
4
- * the host loads assemblies, their SKUs and active bills, and adapts each row
5
- * with `toAssemblyView`.
6
- *
7
- * Pass the resolved field `policy` for `@happyvertical/smrt-manufacturing:Assembly`
8
- * to apply the consumer's field policy: a field the policy hides (price, for
9
- * a viewer who may not see it) drops its column.
10
- */
11
- import { type ResolvedObjectFieldPolicy } from '@happyvertical/smrt-fields/svelte';
12
- import { type AssemblyView } from '../types.js';
1
+ import { type AssemblyFieldPolicy, type AssemblyView } from '../types.js';
13
2
  export interface AssemblyListProps {
14
3
  /** Assemblies to show, in the host's order. */
15
4
  assemblies: AssemblyView[];
16
5
  /** The resolved field policy for Assembly; hidden fields drop their column. */
17
- policy?: ResolvedObjectFieldPolicy;
6
+ policy?: AssemblyFieldPolicy;
18
7
  /** ISO 4217 currency of the prices. */
19
8
  currency?: string;
20
9
  /** Invoked with the assembly id when a name is activated. */
@@ -1 +1 @@
1
- {"version":3,"file":"AssemblyList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyList.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,mCAAmC,CAAC;AAI3C,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,aAAa,CAAC;AAGpE,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAuJD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"AssemblyList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyList.svelte.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA4JD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}