@globalbrain/sefirot 4.58.0 → 4.59.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/components/LensCatalog.vue +55 -32
- package/lib/blocks/lens/components/LensTable.vue +9 -6
- package/lib/blocks/lens/components/LensTableAvatarCell.vue +2 -2
- package/lib/blocks/lens/components/LensTableEditableCell.vue +3 -3
- package/lib/blocks/lens/composables/LensEdit.ts +9 -0
- package/lib/components/SInputRadios.vue +6 -0
- package/package.json +1 -1
|
@@ -63,9 +63,6 @@ export interface Props {
|
|
|
63
63
|
// Whether to show advanced filters.
|
|
64
64
|
canFilter?: boolean
|
|
65
65
|
|
|
66
|
-
// Whether to show advanced sorting options.
|
|
67
|
-
canSort?: boolean
|
|
68
|
-
|
|
69
66
|
// Whether to hide the condition blocks.
|
|
70
67
|
hideConditions?: boolean
|
|
71
68
|
|
|
@@ -129,12 +126,16 @@ export interface Props {
|
|
|
129
126
|
// setup, so toggling it afterwards has no effect.
|
|
130
127
|
urlSync?: boolean
|
|
131
128
|
|
|
132
|
-
// Enable CRUD editing for the catalog.
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
129
|
+
// Enable CRUD editing for the catalog.
|
|
130
|
+
//
|
|
131
|
+
// Every catalog already renders a **read-only detail sheet**: clicking a
|
|
132
|
+
// row's index-field cell opens the record for viewing (each `showOnDetail`
|
|
133
|
+
// field, read-only). `editable` builds on that same sheet. When set, fields
|
|
134
|
+
// the backend marks `showOnUpdate` become inline-editable in the table, the
|
|
135
|
+
// sheet gains per-field editing and a delete button, and the `openCreate()`
|
|
136
|
+
// method / sheet create mode become available. The CRUD endpoints are derived
|
|
137
|
+
// from `endpoint` by replacing the trailing `/search` with `/update`,
|
|
138
|
+
// `/create`, and `/delete`.
|
|
138
139
|
//
|
|
139
140
|
// The row identifier (`indexField`, defaulting to `id`) is never
|
|
140
141
|
// inline- or sheet-editable on an existing row: optimistically changing
|
|
@@ -142,27 +143,28 @@ export interface Props {
|
|
|
142
143
|
// follow-up save/delete would address the not-yet-synced new id. It can
|
|
143
144
|
// still be set on creation (via a `showOnCreate` field).
|
|
144
145
|
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// empty/default `select` drops it
|
|
151
|
-
// with no opener).
|
|
146
|
+
// The sheet opener — and therefore the only way to view, edit, or delete a
|
|
147
|
+
// record — is the index-field cell, so that field must be a rendered column.
|
|
148
|
+
// The default `id` identifier is kept when `select` is empty (server
|
|
149
|
+
// defaults), so that case works as-is; a *custom* `indexField`, or `id`
|
|
150
|
+
// alongside an explicit `select`, must be listed in `select` explicitly (an
|
|
151
|
+
// empty/default `select` otherwise drops it, leaving the rows with no opener).
|
|
152
152
|
//
|
|
153
153
|
// Turning this on also enables `creatable`, `deletable`, and `inlineEditable`
|
|
154
154
|
// by default; pass `false` to any of them to opt out.
|
|
155
155
|
//
|
|
156
156
|
// Pass a predicate `(record) => boolean` instead of `true` to allow editing
|
|
157
157
|
// only some rows (e.g. from a per-record policy): the inline affordance and
|
|
158
|
-
// the sheet's per-field edit are hidden for the rows it rejects
|
|
158
|
+
// the sheet's per-field edit are hidden for the rows it rejects (the sheet
|
|
159
|
+
// still opens read-only).
|
|
159
160
|
editable?: boolean | ((record: Record<string, any>) => boolean)
|
|
160
161
|
|
|
161
162
|
// Whether records can be deleted from the sheet. Defaults to enabled for an
|
|
162
163
|
// editable catalog. Pass `false` to hide the delete button entirely, or a
|
|
163
164
|
// predicate `(record) => boolean` to allow deleting only some rows (e.g. from
|
|
164
|
-
// a per-record policy).
|
|
165
|
-
//
|
|
165
|
+
// a per-record policy). The delete button lives in the record sheet — which
|
|
166
|
+
// opens for read-only viewing on any catalog — but stays gated behind edit
|
|
167
|
+
// permission, so this has no effect unless the catalog is `editable`.
|
|
166
168
|
deletable?: boolean | ((record: Record<string, any>) => boolean)
|
|
167
169
|
|
|
168
170
|
// Whether new records can be created (enables create mode in the sheet and
|
|
@@ -182,7 +184,6 @@ export interface Props {
|
|
|
182
184
|
|
|
183
185
|
const props = withDefaults(defineProps<Props>(), {
|
|
184
186
|
canFilter: true,
|
|
185
|
-
canSort: true,
|
|
186
187
|
// Default these on so an editable catalog gets create / delete / inline edit
|
|
187
188
|
// without opting in to each. A boolean prop can't tell "omitted" from `false`
|
|
188
189
|
// (Vue casts an absent boolean to `false`), so they default on here and the
|
|
@@ -623,7 +624,8 @@ const tableIndexField = computed(() => {
|
|
|
623
624
|
// address each row; when no `indexField` is configured the identifier
|
|
624
625
|
// defaults to `id`. When the caller has no concrete select list, nothing
|
|
625
626
|
// is added either — leaving the request empty lets the server use its own
|
|
626
|
-
// defaults
|
|
627
|
+
// defaults (which include `id`, so the read-only sheet still opens). See
|
|
628
|
+
// `indexField` prop docs above.
|
|
627
629
|
function withIndexField(fields: string[]): string[] {
|
|
628
630
|
if (fields.length === 0) { return [] }
|
|
629
631
|
const field = props.indexField ?? (props.editable ? 'id' : null)
|
|
@@ -1029,6 +1031,19 @@ async function refreshCatalog(): Promise<void> {
|
|
|
1029
1031
|
await forceRefresh()
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
1034
|
+
// Close an open *view* sheet when the effective identifier itself changes (an
|
|
1035
|
+
// async `indexField` resolving from the `id` default to `slug`, say): the open
|
|
1036
|
+
// record was keyed under the old field and can't be re-matched to the new one
|
|
1037
|
+
// (its row lacks the new key, so `resolveId` would be `undefined`), which would
|
|
1038
|
+
// let a delete/save act on the wrong — or no — id. Watched on `idField` alone,
|
|
1039
|
+
// not the editable-gated source below: that source can't tell an identifier
|
|
1040
|
+
// change from `editable` merely switching on when both happen in the same flush,
|
|
1041
|
+
// and `editable` flipping on with the identifier unchanged must leave an open
|
|
1042
|
+
// read-only sheet alone (viewing is the default; its record is still valid).
|
|
1043
|
+
watch(idField, () => {
|
|
1044
|
+
if (sheet.state.value && sheetMode.value === 'view') { sheet.off() }
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1032
1047
|
// When the effective row identifier appears or changes while editing is enabled,
|
|
1033
1048
|
// the loaded rows won't carry it until a refetch runs — so refetch here. Two
|
|
1034
1049
|
// cases: `editable` resolving true after mount (async permissions), so
|
|
@@ -1037,16 +1052,10 @@ async function refreshCatalog(): Promise<void> {
|
|
|
1037
1052
|
// identifier, `rowsCarryIndexField` keeps editing gated off, and selection keys
|
|
1038
1053
|
// go `undefined` until an unrelated query change. A no-op when the identifier was
|
|
1039
1054
|
// already present (same request input → cached result reused).
|
|
1040
|
-
//
|
|
1041
|
-
// Close an open record sheet first: it was identified under the old field and
|
|
1042
|
-
// can't be reliably re-matched to the new one (its row lacks the new key, so
|
|
1043
|
-
// `resolveId` would be `undefined`), which would let a delete/save act on the
|
|
1044
|
-
// wrong — or no — id during and after the refetch.
|
|
1045
1055
|
watch(
|
|
1046
1056
|
() => (props.editable ? idField.value : null),
|
|
1047
1057
|
(field, prev) => {
|
|
1048
1058
|
if (!field || field === prev) { return }
|
|
1049
|
-
if (sheet.state.value && sheetMode.value === 'view') { sheet.off() }
|
|
1050
1059
|
// Supersede any search still in flight under the previous identifier state: it
|
|
1051
1060
|
// was issued before the new id/index field, so its rows won't carry the new
|
|
1052
1061
|
// key. If it resolved after this refetch it would assign those id-less rows
|
|
@@ -1468,14 +1477,24 @@ function editEnabled(): boolean {
|
|
|
1468
1477
|
return !!props.editable && rowsCarryIndexField.value
|
|
1469
1478
|
}
|
|
1470
1479
|
|
|
1480
|
+
// Catalog-level sheet gate: the read-only detail sheet is reachable on any
|
|
1481
|
+
// catalog — viewing is the default — as soon as the rows carry the index field
|
|
1482
|
+
// the opener resolves an id from. Editing, create, and delete stay gated on
|
|
1483
|
+
// `editable` (via `canEdit` / `canDelete` / the `creatable` getter), so a
|
|
1484
|
+
// non-editable catalog opens the same sheet read-only.
|
|
1485
|
+
function viewEnabled(): boolean {
|
|
1486
|
+
return rowsCarryIndexField.value
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1471
1489
|
// Per-record refinement: a predicate `editable`/`deletable` decides each row, a
|
|
1472
1490
|
// boolean applies to all.
|
|
1473
1491
|
function canEdit(record: Record<string, any>): boolean {
|
|
1474
1492
|
return editEnabled() && (typeof props.editable === 'function' ? props.editable(record) : true)
|
|
1475
1493
|
}
|
|
1476
1494
|
|
|
1477
|
-
// Delete is a stronger action than edit and
|
|
1478
|
-
//
|
|
1495
|
+
// Delete is a stronger action than edit and lives in the record sheet (whose
|
|
1496
|
+
// delete button only renders when editable), so a row must be editable before it
|
|
1497
|
+
// can be deleted — building on `canEdit` makes a
|
|
1479
1498
|
// per-record `editable` predicate gate delete too (a row it rejects is never
|
|
1480
1499
|
// deletable). `deletable` then refines further: on unless explicitly `false`, or
|
|
1481
1500
|
// its own per-record predicate.
|
|
@@ -1486,13 +1505,18 @@ function canDelete(record: Record<string, any>): boolean {
|
|
|
1486
1505
|
provideLensEdit({
|
|
1487
1506
|
// Getters so the injected context tracks prop changes after mount (e.g.
|
|
1488
1507
|
// permissions resolving async, or a flag toggling `editable` off): LensTable
|
|
1489
|
-
// gates inline editing and the id-cell sheet on
|
|
1508
|
+
// gates inline editing on `edit.editable` and the id-cell sheet opener on the
|
|
1509
|
+
// broader `edit.viewable`.
|
|
1490
1510
|
//
|
|
1491
1511
|
// Also gated on `rowsCarryIndexField`: when `editable` flips true after mount we
|
|
1492
1512
|
// trigger a refetch to pull in the identifier, but it lands asynchronously —
|
|
1493
1513
|
// keep editing off until the rows actually carry it, so a save in that window
|
|
1494
1514
|
// can't `resolveId()` to `undefined`.
|
|
1495
1515
|
get editable() { return editEnabled() },
|
|
1516
|
+
// `LensTable` gates the id-cell sheet opener on this: the read-only sheet is
|
|
1517
|
+
// reachable on any catalog once the rows carry the index field, independent of
|
|
1518
|
+
// `editable`.
|
|
1519
|
+
get viewable() { return viewEnabled() },
|
|
1496
1520
|
get creatable() { return !!props.editable && props.creatable },
|
|
1497
1521
|
// Use the same `__no_entity__` fallback as the search / CRUD requests so slot
|
|
1498
1522
|
// side-channel saves (which read this) target the same entity, not an empty one.
|
|
@@ -1575,7 +1599,6 @@ defineExpose({
|
|
|
1575
1599
|
:selected
|
|
1576
1600
|
:show-query="!!(queryKeys && queryKeys.length > 0)"
|
|
1577
1601
|
:show-filters="canFilter"
|
|
1578
|
-
:show-sort="canSort"
|
|
1579
1602
|
:is-condition-active="!hideConditions"
|
|
1580
1603
|
:is-condition-disabled="!hasConditions"
|
|
1581
1604
|
@search="onQuery"
|
|
@@ -1696,7 +1719,7 @@ defineExpose({
|
|
|
1696
1719
|
</SModal>
|
|
1697
1720
|
|
|
1698
1721
|
<LensSheet
|
|
1699
|
-
v-if="editable && result?.fields"
|
|
1722
|
+
v-if="(editable || rowsCarryIndexField) && result?.fields"
|
|
1700
1723
|
:open="sheet.state.value"
|
|
1701
1724
|
:mode="sheetMode"
|
|
1702
1725
|
:entity="entityName"
|
|
@@ -117,11 +117,14 @@ const columns = computedAsync(async () => {
|
|
|
117
117
|
|
|
118
118
|
const column = field.tableColumn()
|
|
119
119
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// opens the sheet
|
|
124
|
-
//
|
|
120
|
+
// The detail sheet is reachable on any catalog once the rows carry the index
|
|
121
|
+
// field (`edit.viewable`) — viewing is the default; `editable` only adds the
|
|
122
|
+
// per-field edit / delete affordances inside it. So clicking the row-identifier
|
|
123
|
+
// cell opens the record sheet (read-only, or editable when the catalog is)
|
|
124
|
+
// instead of following a link. Keyed off the configured index field for any
|
|
125
|
+
// field type — a slug/code identifier opens the sheet just like an `id` — so
|
|
126
|
+
// other id-type columns (e.g. a `company_id` reference link) keep their own
|
|
127
|
+
// navigation.
|
|
125
128
|
//
|
|
126
129
|
// Exception: an `id` field whose server value carries a `path` renders that
|
|
127
130
|
// path as a link; respect it so those rows navigate to the details page instead
|
|
@@ -130,7 +133,7 @@ const columns = computedAsync(async () => {
|
|
|
130
133
|
// a `link` / `slack_message` identifier) is still turned into the sheet opener,
|
|
131
134
|
// as is a column whose `cell` is undefined (falls straight through to the opener).
|
|
132
135
|
if (
|
|
133
|
-
edit?.
|
|
136
|
+
edit?.viewable
|
|
134
137
|
&& key === edit.indexField
|
|
135
138
|
) {
|
|
136
139
|
const original = column.cell
|
|
@@ -424,9 +424,9 @@ function onEditorKeydown(event: KeyboardEvent) {
|
|
|
424
424
|
display: flex;
|
|
425
425
|
flex-direction: column;
|
|
426
426
|
gap: 8px;
|
|
427
|
-
padding:
|
|
427
|
+
padding: 10px;
|
|
428
428
|
border: 1px solid var(--c-border);
|
|
429
|
-
border-radius:
|
|
429
|
+
border-radius: 6px;
|
|
430
430
|
background-color: var(--c-bg-1);
|
|
431
431
|
box-shadow: var(--shadow-depth-3);
|
|
432
432
|
}
|
|
@@ -147,7 +147,7 @@ const { validation, validate, reset } = useValidation(
|
|
|
147
147
|
|
|
148
148
|
const editorStyle = computed(() => ({
|
|
149
149
|
...inset.value,
|
|
150
|
-
width: `${Math.max(bounds.width.value,
|
|
150
|
+
width: `${Math.max(bounds.width.value, 224)}px`
|
|
151
151
|
}))
|
|
152
152
|
|
|
153
153
|
function start() {
|
|
@@ -352,9 +352,9 @@ function onEditorKeydown(event: KeyboardEvent) {
|
|
|
352
352
|
display: flex;
|
|
353
353
|
flex-direction: column;
|
|
354
354
|
gap: 8px;
|
|
355
|
-
padding:
|
|
355
|
+
padding: 10px;
|
|
356
356
|
border: 1px solid var(--c-border);
|
|
357
|
-
border-radius:
|
|
357
|
+
border-radius: 6px;
|
|
358
358
|
background-color: var(--c-bg-1);
|
|
359
359
|
box-shadow: var(--shadow-depth-3);
|
|
360
360
|
}
|
|
@@ -10,6 +10,15 @@ export interface LensEditContext {
|
|
|
10
10
|
/** Whether the catalog has editing enabled at all. */
|
|
11
11
|
editable: boolean
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Whether the detail sheet is reachable. Viewing is the default: true on any
|
|
15
|
+
* catalog once the rows carry the index field the opener resolves an id from,
|
|
16
|
+
* independent of `editable`. The index-field cell opens the sheet on this gate,
|
|
17
|
+
* while the mutation affordances inside stay gated on `canEdit` / `canDelete` /
|
|
18
|
+
* `creatable`, so a non-editable catalog opens a read-only sheet.
|
|
19
|
+
*/
|
|
20
|
+
viewable: boolean
|
|
21
|
+
|
|
13
22
|
/** Whether new records may be created. */
|
|
14
23
|
creatable: boolean
|
|
15
24
|
|