@globalbrain/sefirot 4.59.1 → 4.60.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/lib/blocks/lens/FieldData.ts +10 -0
- package/lib/blocks/lens/components/LensCatalog.vue +60 -23
- package/lib/blocks/lens/components/LensSheetField.vue +1 -1
- package/lib/blocks/lens/components/LensTable.vue +4 -3
- package/lib/blocks/lens/components/LensTableAvatarCell.vue +2 -1
- package/lib/blocks/lens/components/LensTableEditableCell.vue +2 -1
- package/lib/blocks/lens/fields/AvatarField.ts +0 -4
- package/lib/blocks/lens/fields/Field.ts +13 -2
- package/lib/blocks/lens/fields/FileUploadField.ts +0 -4
- package/lib/blocks/lens/fields/SelectField.ts +0 -4
- package/package.json +8 -8
|
@@ -61,6 +61,16 @@ export interface FieldDataBase {
|
|
|
61
61
|
showOnDetail?: boolean
|
|
62
62
|
showOnCreate?: boolean
|
|
63
63
|
showOnUpdate?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* The server-declared blank for the field's input, as a wire-format
|
|
66
|
+
* payload value — the server owns this entirely: types whose blank isn't
|
|
67
|
+
* null (a multiple select's `[]`) declare it here, and an entity may set
|
|
68
|
+
* its own (e.g. `'draft'` for a status select). Feeds `inputEmptyValue`:
|
|
69
|
+
* the create form seeds from it, and an editor opening on a null stored
|
|
70
|
+
* value falls back to it. Null or absent (backends predating the key)
|
|
71
|
+
* means a null blank.
|
|
72
|
+
*/
|
|
73
|
+
emptyValue?: any
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
export interface AvatarFieldData extends FieldDataBase {
|
|
@@ -74,6 +74,11 @@ export interface Props {
|
|
|
74
74
|
// normally. The value remains accessible via `record[indexField]` in
|
|
75
75
|
// `cell-clicked` events and through the `selected` model regardless.
|
|
76
76
|
//
|
|
77
|
+
// When omitted, the identifier defaults to `id`, and the `selected`
|
|
78
|
+
// model is keyed by it on any catalog — editable or not — whenever the
|
|
79
|
+
// loaded rows carry it (server-default selects typically do). Only rows
|
|
80
|
+
// without the identifier fall back to positional selection keys.
|
|
81
|
+
//
|
|
77
82
|
// If the caller's `select` is empty or `null` (i.e. "use server
|
|
78
83
|
// defaults"), the index field is **not** auto-appended — that would
|
|
79
84
|
// narrow the result to just that single column on backends that treat
|
|
@@ -584,33 +589,59 @@ const tableSelect = computed(() => {
|
|
|
584
589
|
const idField = computed(() => props.indexField ?? 'id')
|
|
585
590
|
|
|
586
591
|
// Whether the currently loaded rows actually carry the effective row identifier.
|
|
587
|
-
// Editing / opening a row needs it (`resolveId`),
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
+
// Editing / opening a row needs it (`resolveId`), and the table's selection key
|
|
593
|
+
// only switches onto it when present (`tableIndexField` below). Rows fetched
|
|
594
|
+
// while `editable` was still false — or before an `indexField` change settled —
|
|
595
|
+
// won't have it until the triggered refetch lands. Editing is gated on this (see
|
|
596
|
+
// the `editable` getter) so a quick save in that window can't post
|
|
597
|
+
// `id: undefined`. An empty result has nothing to edit or select, so it
|
|
598
|
+
// doesn't block.
|
|
592
599
|
const rowsCarryIndexField = computed(() => {
|
|
593
600
|
const rows = result.value?.data
|
|
594
601
|
if (!rows || rows.length === 0) { return true }
|
|
595
602
|
return idField.value in rows[0]
|
|
596
603
|
})
|
|
597
604
|
|
|
598
|
-
// The row-identifier the table uses as its selection key
|
|
599
|
-
// `
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
605
|
+
// The row-identifier the table uses as its selection key: the effective
|
|
606
|
+
// `idField`, on any catalog whose loaded rows carry it. Without this, a catalog
|
|
607
|
+
// relying on the default `id` would leave the table on positional selection
|
|
608
|
+
// keys, so anything that shifts rows — an optimistic create/delete, a page
|
|
609
|
+
// change, a re-sort, a refresh-banner apply — would leave `selected` pointing
|
|
610
|
+
// at the wrong records. Editable or not: a read-only catalog with
|
|
611
|
+
// `v-model:selected` (a picker dialog, say) needs identity keys just as much,
|
|
612
|
+
// and a server-default select typically already carries `id`. Mirrors
|
|
613
|
+
// `edit.indexField`.
|
|
603
614
|
//
|
|
604
|
-
// Gated on `rowsCarryIndexField`:
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
615
|
+
// Gated on `rowsCarryIndexField`: while the loaded rows don't carry the key —
|
|
616
|
+
// an explicit `select` that omits it, or an `indexField` change (on an editable
|
|
617
|
+
// catalog the triggered refetch below brings the new key in; a read-only
|
|
618
|
+
// catalog stays positional until a fetch happens to include it) — switching
|
|
619
|
+
// STable onto it would key every row's selection to `undefined`, wiping the
|
|
620
|
+
// current selection and making any row-select emit `[undefined]` (so parent
|
|
621
|
+
// bulk actions act on no/wrong records). Stay on the positional key until the
|
|
622
|
+
// loaded rows carry it.
|
|
611
623
|
const tableIndexField = computed(() => {
|
|
612
|
-
|
|
613
|
-
|
|
624
|
+
return rowsCarryIndexField.value ? idField.value : undefined
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
// `selected` keys live in `tableIndexField`'s domain — record identity vs row
|
|
628
|
+
// position. If that domain changes while rows are on screen (a view change
|
|
629
|
+
// dropping the identifier column, one re-adding it, a runtime `indexField`
|
|
630
|
+
// swap), the held keys mean nothing under the new domain — but STable's
|
|
631
|
+
// selection pruning keeps any that happen to *collide* with it (a numeric id
|
|
632
|
+
// surviving as a row position, or vice versa), silently re-pointing the
|
|
633
|
+
// selection at wrong rows for the parent's bulk actions. Clear it instead.
|
|
634
|
+
//
|
|
635
|
+
// A flip settling on a fetch that follows an empty table (`hadRows` false —
|
|
636
|
+
// first load, or a filter state with no matches) doesn't count: nothing was
|
|
637
|
+
// selectable under the previous domain, so anything already in `selected` is a
|
|
638
|
+
// parent-provided seed targeting the domain being settled, and must survive it.
|
|
639
|
+
let hadRows = false
|
|
640
|
+
watch([tableIndexField, () => result.value?.data], ([field], [prevField]) => {
|
|
641
|
+
if (field !== prevField && hadRows && selected.value?.length) {
|
|
642
|
+
selected.value = []
|
|
643
|
+
}
|
|
644
|
+
hadRows = (result.value?.data?.length ?? 0) > 0
|
|
614
645
|
})
|
|
615
646
|
|
|
616
647
|
// The `indexField` is appended to the request `select` so the server
|
|
@@ -622,10 +653,16 @@ const tableIndexField = computed(() => {
|
|
|
622
653
|
//
|
|
623
654
|
// Editable catalogs must carry a row identifier so updates / deletes can
|
|
624
655
|
// address each row; when no `indexField` is configured the identifier
|
|
625
|
-
// defaults to `id`.
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
656
|
+
// defaults to `id`. Read-only catalogs get no *default* identifier appended
|
|
657
|
+
// (an explicit `indexField` is always included, per its prop doc): the
|
|
658
|
+
// selection key (`tableIndexField` above) is opportunistic — it keys by the
|
|
659
|
+
// identifier when the rows already carry it (server defaults typically
|
|
660
|
+
// include `id`) and stays positional when an explicit `select` omits it,
|
|
661
|
+
// rather than growing every read-only request by an extra column. When the
|
|
662
|
+
// caller has no concrete select list, nothing is added either — leaving the
|
|
663
|
+
// request empty lets the server use its own defaults (which typically
|
|
664
|
+
// include `id`, so the read-only sheet still opens). See `indexField` prop
|
|
665
|
+
// docs above.
|
|
629
666
|
function withIndexField(fields: string[]): string[] {
|
|
630
667
|
if (fields.length === 0) { return [] }
|
|
631
668
|
const field = props.indexField ?? (props.editable ? 'id' : null)
|
|
@@ -95,7 +95,7 @@ const formEl = ref<HTMLElement | null>(null)
|
|
|
95
95
|
|
|
96
96
|
function start() {
|
|
97
97
|
const raw = props.record[props.fieldKey]
|
|
98
|
-
model.value = props.field.payloadToInput(raw
|
|
98
|
+
model.value = raw != null ? props.field.payloadToInput(raw) : props.field.inputEmptyValue()
|
|
99
99
|
reset()
|
|
100
100
|
editing.value = true
|
|
101
101
|
// Focus the input on open (matching the inline table editor): better UX, and
|
|
@@ -236,9 +236,10 @@ const table = useTable({
|
|
|
236
236
|
orders,
|
|
237
237
|
columns,
|
|
238
238
|
// A getter (not a snapshot) so the selection key stays reactive: `indexField`
|
|
239
|
-
// can change after mount — e.g.
|
|
240
|
-
//
|
|
241
|
-
//
|
|
239
|
+
// can change after mount — e.g. rows landing without the identifier flip the
|
|
240
|
+
// catalog from identity keys back to positional, and an `indexField` change
|
|
241
|
+
// settling re-keys it. STable reads this inside a computed, so the getter
|
|
242
|
+
// establishes the dependency and selection re-keys correctly.
|
|
242
243
|
get indexField() { return props.indexField },
|
|
243
244
|
borderless: true
|
|
244
245
|
})
|
|
@@ -193,7 +193,8 @@ function startNames() {
|
|
|
193
193
|
return
|
|
194
194
|
}
|
|
195
195
|
for (const entry of nameEntries.value) {
|
|
196
|
-
|
|
196
|
+
const raw = props.record[entry.key]
|
|
197
|
+
nameModel[entry.key] = raw != null ? entry.field.payloadToInput(raw) : entry.field.inputEmptyValue()
|
|
197
198
|
}
|
|
198
199
|
reset()
|
|
199
200
|
inline?.start(myKey.value)
|
|
@@ -156,7 +156,8 @@ function start() {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
inputComponent.value = props.field.formInputComponent()
|
|
159
|
-
|
|
159
|
+
const raw = props.value
|
|
160
|
+
model.value = raw != null ? props.field.payloadToInput(raw) : props.field.inputEmptyValue()
|
|
160
161
|
reset()
|
|
161
162
|
inline?.start(myKey.value)
|
|
162
163
|
nextTick(() => {
|
|
@@ -56,10 +56,6 @@ export class AvatarField extends Field<AvatarFieldData> {
|
|
|
56
56
|
return false
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
override inputEmptyValue(): any {
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
override formInputComponent(): any {
|
|
64
60
|
return this.defineFormInputComponent((props, { emit }) => {
|
|
65
61
|
return () => h(LensInputAvatar, {
|
|
@@ -255,10 +255,21 @@ export abstract class Field<T extends FieldData> {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
258
|
+
* The value an empty input starts from: the definition's `emptyValue`, a
|
|
259
|
+
* wire-format payload value run through `payloadToInput`. The server owns
|
|
260
|
+
* the blank entirely — field types with a non-null blank (a multiple
|
|
261
|
+
* select's `[]`, say) declare it in their definition, so there are no
|
|
262
|
+
* per-type overrides here. Only null/undefined fall back to a null blank,
|
|
263
|
+
* so `false` and `0` are real values.
|
|
264
|
+
*
|
|
265
|
+
* Feeds the create-form seeding and the editors' null fallback alike, so
|
|
266
|
+
* a non-blank `emptyValue` also pre-fills the editor of a record whose
|
|
267
|
+
* stored value is null.
|
|
259
268
|
*/
|
|
260
269
|
inputEmptyValue(): any {
|
|
261
|
-
return null
|
|
270
|
+
return this.data.emptyValue != null
|
|
271
|
+
? this.payloadToInput(this.data.emptyValue)
|
|
272
|
+
: null
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
/**
|
|
@@ -49,10 +49,6 @@ export class FileUploadField extends Field<FileUploadFieldData> {
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
override inputEmptyValue(): any {
|
|
53
|
-
return []
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
override formInputComponent(): any {
|
|
57
53
|
return this.defineFormInputComponent((props, { emit }) => {
|
|
58
54
|
return () => h(SInputFileUpload, {
|
|
@@ -150,10 +150,6 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
150
150
|
return this.optionsForValues(value).map((o) => this.labelForOption(o)).join(', ')
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
override inputEmptyValue(): any {
|
|
154
|
-
return this.data.multiple ? [] : null
|
|
155
|
-
}
|
|
156
|
-
|
|
157
153
|
override formInputComponent(): any {
|
|
158
154
|
switch (this.data.inputAs) {
|
|
159
155
|
case 'dropdown':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globalbrain/sefirot",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.60.0",
|
|
4
4
|
"description": "Vue Components for Global Brain Design System.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@iconify-json/ph": "^1.2.2",
|
|
60
60
|
"@iconify-json/ri": "^1.2.10",
|
|
61
61
|
"@popperjs/core": "^2.11.8",
|
|
62
|
-
"@sentry/browser": "^10.
|
|
63
|
-
"@sentry/vue": "^10.
|
|
62
|
+
"@sentry/browser": "^10.63.0",
|
|
63
|
+
"@sentry/vue": "^10.63.0",
|
|
64
64
|
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
65
65
|
"@tinyhttp/content-disposition": "^2.2.4",
|
|
66
66
|
"@tinyhttp/cookie": "^2.1.1",
|
|
@@ -84,11 +84,11 @@
|
|
|
84
84
|
"jsdom": "^29.1.1",
|
|
85
85
|
"lodash-es": "^4.18.1",
|
|
86
86
|
"magic-string": "^0.30.21",
|
|
87
|
-
"markdown-it": "^14.
|
|
87
|
+
"markdown-it": "^14.3.0",
|
|
88
88
|
"normalize.css": "^8.0.1",
|
|
89
89
|
"ofetch": "^1.5.1",
|
|
90
90
|
"pinia": "^3.0.4",
|
|
91
|
-
"postcss": "^8.5.
|
|
91
|
+
"postcss": "^8.5.16",
|
|
92
92
|
"postcss-nested": "^7.0.2",
|
|
93
93
|
"punycode": "^2.3.1",
|
|
94
94
|
"qs": "^6.15.3",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"@histoire/plugin-vue": "1.0.0-beta.1",
|
|
105
105
|
"@release-it/conventional-changelog": "^11.0.1",
|
|
106
106
|
"@types/jsdom": "^28.0.3",
|
|
107
|
-
"@types/node": "^26.0
|
|
108
|
-
"@typescript-eslint/rule-tester": "^8.62.
|
|
107
|
+
"@types/node": "^26.1.0",
|
|
108
|
+
"@typescript-eslint/rule-tester": "^8.62.1",
|
|
109
109
|
"@vitest/coverage-v8": "^4.1.9",
|
|
110
110
|
"@vue/test-utils": "^2.4.11",
|
|
111
111
|
"eslint": "^9.39.4",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"typescript": "~6.0.3",
|
|
116
116
|
"vitepress": "^2.0.0-alpha.17",
|
|
117
117
|
"vitest": "^4.1.9",
|
|
118
|
-
"vue-tsc": "^3.3.
|
|
118
|
+
"vue-tsc": "^3.3.6"
|
|
119
119
|
},
|
|
120
120
|
"packageManager": "pnpm@11.9.0"
|
|
121
121
|
}
|