@globalbrain/sefirot 4.49.0 → 4.51.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 +1 -1
- package/lib/blocks/lens/components/LensCatalog.vue +62 -15
- package/lib/blocks/lens/components/LensSheet.vue +19 -2
- package/lib/blocks/lens/components/LensSheetField.vue +9 -1
- package/lib/blocks/lens/components/LensTable.vue +2 -2
- package/lib/blocks/lens/components/LensTableEditableCell.vue +117 -13
- package/lib/blocks/lens/composables/LensEdit.ts +6 -0
- package/lib/blocks/lens/fields/SelectField.ts +28 -0
- package/package.json +1 -1
|
@@ -146,7 +146,7 @@ export interface DecimalFieldData extends FieldDataBase {
|
|
|
146
146
|
|
|
147
147
|
export interface SelectFieldData extends FieldDataBase {
|
|
148
148
|
type: 'select'
|
|
149
|
-
displayAs: 'text' | 'state'
|
|
149
|
+
displayAs: 'text' | 'state' | 'pills'
|
|
150
150
|
inputAs: 'dropdown' | 'radio'
|
|
151
151
|
placeholderEn: string | null
|
|
152
152
|
placeholderJa: string | null
|
|
@@ -147,24 +147,47 @@ export interface Props {
|
|
|
147
147
|
// *custom* `indexField`, however, must be listed in `select` explicitly (an
|
|
148
148
|
// empty/default `select` drops it from the rendered columns, leaving the rows
|
|
149
149
|
// with no opener).
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
150
|
+
//
|
|
151
|
+
// Turning this on also enables `creatable`, `deletable`, and `inlineEditable`
|
|
152
|
+
// by default; pass `false` to any of them to opt out.
|
|
153
|
+
//
|
|
154
|
+
// Pass a predicate `(record) => boolean` instead of `true` to allow editing
|
|
155
|
+
// only some rows (e.g. from a per-record policy): the inline affordance and
|
|
156
|
+
// the sheet's per-field edit are hidden for the rows it rejects.
|
|
157
|
+
editable?: boolean | ((record: Record<string, any>) => boolean)
|
|
158
|
+
|
|
159
|
+
// Whether records can be deleted from the sheet. Defaults to enabled for an
|
|
160
|
+
// editable catalog. Pass `false` to hide the delete button entirely, or a
|
|
161
|
+
// predicate `(record) => boolean` to allow deleting only some rows (e.g. from
|
|
162
|
+
// a per-record policy). Delete is reachable only through the sheet, so this
|
|
163
|
+
// has no effect unless the catalog is `editable`.
|
|
164
|
+
deletable?: boolean | ((record: Record<string, any>) => boolean)
|
|
165
|
+
|
|
166
|
+
// Whether new records can be created (enables create mode in the sheet and
|
|
167
|
+
// the exposed `openCreate()` method). Defaults to enabled when the catalog is
|
|
168
|
+
// `editable`; pass `false` to disable.
|
|
154
169
|
creatable?: boolean
|
|
155
170
|
|
|
156
171
|
// Width of the record sheet (any valid CSS width). Defaults to `480px`.
|
|
157
172
|
sheetWidth?: string
|
|
158
173
|
|
|
159
|
-
// Enable inline editing directly in the table: cells for `showOnUpdate`
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
174
|
+
// Enable inline editing directly in the table: cells for `showOnUpdate` fields
|
|
175
|
+
// gain a hover edit affordance that opens an inline editor (reusing the sheet's
|
|
176
|
+
// CRUD edit context). Defaults to enabled when the catalog is `editable`; pass
|
|
177
|
+
// `false` to restrict editing to the record sheet.
|
|
178
|
+
inlineEditable?: boolean
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
const props = withDefaults(defineProps<Props>(), {
|
|
166
182
|
canFilter: true,
|
|
167
|
-
canSort: true
|
|
183
|
+
canSort: true,
|
|
184
|
+
// Default these on so an editable catalog gets create / delete / inline edit
|
|
185
|
+
// without opting in to each. A boolean prop can't tell "omitted" from `false`
|
|
186
|
+
// (Vue casts an absent boolean to `false`), so they default on here and the
|
|
187
|
+
// resolvers below gate them on `editable`; pass the prop `false` to opt out.
|
|
188
|
+
creatable: true,
|
|
189
|
+
deletable: true,
|
|
190
|
+
inlineEditable: true
|
|
168
191
|
})
|
|
169
192
|
|
|
170
193
|
const selected = defineModel<any[]>('selected')
|
|
@@ -1121,9 +1144,10 @@ async function openSheet(record: Record<string, any>): Promise<void> {
|
|
|
1121
1144
|
}
|
|
1122
1145
|
|
|
1123
1146
|
function openCreate(): void {
|
|
1124
|
-
//
|
|
1125
|
-
// this method is exposed (and provided to
|
|
1126
|
-
|
|
1147
|
+
// Creation is enabled by `creatable`, which defaults to on for an editable
|
|
1148
|
+
// catalog. Honor it here even though this method is exposed (and provided to
|
|
1149
|
+
// children).
|
|
1150
|
+
if (!props.editable || !props.creatable) {
|
|
1127
1151
|
return
|
|
1128
1152
|
}
|
|
1129
1153
|
// Supersede any in-flight openSheet `/show` so it can't open over the create
|
|
@@ -1136,6 +1160,27 @@ function openCreate(): void {
|
|
|
1136
1160
|
sheet.on()
|
|
1137
1161
|
}
|
|
1138
1162
|
|
|
1163
|
+
// Catalog-level editing gate: `editable` is set (boolean `true` or a predicate)
|
|
1164
|
+
// and the rows carry the index field needed to resolve a record's id.
|
|
1165
|
+
function editEnabled(): boolean {
|
|
1166
|
+
return !!props.editable && rowsCarryIndexField.value
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Per-record refinement: a predicate `editable`/`deletable` decides each row, a
|
|
1170
|
+
// boolean applies to all.
|
|
1171
|
+
function canEdit(record: Record<string, any>): boolean {
|
|
1172
|
+
return editEnabled() && (typeof props.editable === 'function' ? props.editable(record) : true)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Delete is a stronger action than edit and rides the same editable sheet, so a
|
|
1176
|
+
// row must be editable before it can be deleted — building on `canEdit` makes a
|
|
1177
|
+
// per-record `editable` predicate gate delete too (a row it rejects is never
|
|
1178
|
+
// deletable). `deletable` then refines further: on unless explicitly `false`, or
|
|
1179
|
+
// its own per-record predicate.
|
|
1180
|
+
function canDelete(record: Record<string, any>): boolean {
|
|
1181
|
+
return canEdit(record) && (typeof props.deletable === 'function' ? props.deletable(record) : props.deletable)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1139
1184
|
provideLensEdit({
|
|
1140
1185
|
// Getters so the injected context tracks prop changes after mount (e.g.
|
|
1141
1186
|
// permissions resolving async, or a flag toggling `editable` off): LensTable
|
|
@@ -1145,8 +1190,8 @@ provideLensEdit({
|
|
|
1145
1190
|
// trigger a refetch to pull in the identifier, but it lands asynchronously —
|
|
1146
1191
|
// keep editing off until the rows actually carry it, so a save in that window
|
|
1147
1192
|
// can't `resolveId()` to `undefined`.
|
|
1148
|
-
get editable() { return
|
|
1149
|
-
get creatable() { return !!props.creatable },
|
|
1193
|
+
get editable() { return editEnabled() },
|
|
1194
|
+
get creatable() { return !!props.editable && props.creatable },
|
|
1150
1195
|
// Use the same `__no_entity__` fallback as the search / CRUD requests so slot
|
|
1151
1196
|
// side-channel saves (which read this) target the same entity, not an empty one.
|
|
1152
1197
|
get entity() { return entityName.value },
|
|
@@ -1155,6 +1200,8 @@ provideLensEdit({
|
|
|
1155
1200
|
// resolves (or changes) after mount — otherwise the new identifier column stays
|
|
1156
1201
|
// non-clickable while the old field is still treated as the id.
|
|
1157
1202
|
get indexField() { return props.indexField ?? 'id' },
|
|
1203
|
+
canEdit,
|
|
1204
|
+
canDelete,
|
|
1158
1205
|
resolveId,
|
|
1159
1206
|
save,
|
|
1160
1207
|
create,
|
|
@@ -1288,7 +1335,7 @@ defineExpose({
|
|
|
1288
1335
|
:index-field="tableIndexField"
|
|
1289
1336
|
:selected
|
|
1290
1337
|
:clickable-fields
|
|
1291
|
-
:inline-
|
|
1338
|
+
:inline-editable
|
|
1292
1339
|
@filter-updated="onInlineFilterUpdated"
|
|
1293
1340
|
@sort-updated="onSortUpdated"
|
|
1294
1341
|
@update:selected="onUpdateSelected"
|
|
@@ -126,6 +126,11 @@ const saving = ref(false)
|
|
|
126
126
|
// sheet opened; `onCreate` re-checks it too.
|
|
127
127
|
const creatable = computed(() => !!edit?.creatable)
|
|
128
128
|
|
|
129
|
+
// Whether the open record may be deleted. Reactive (a getter on the edit
|
|
130
|
+
// context) so the delete button hides if a per-record `deletable` predicate, or
|
|
131
|
+
// a permission change, rejects it after the sheet has opened.
|
|
132
|
+
const deletable = computed(() => !!props.record && !!edit?.canDelete(props.record))
|
|
133
|
+
|
|
129
134
|
// Backend-only validation errors (e.g. `unique`) returned by a rejected
|
|
130
135
|
// create, fed to Vuelidate via `$externalResults` so they surface on the
|
|
131
136
|
// offending field. The create form's keys are the bare field keys, matching
|
|
@@ -255,7 +260,11 @@ function saveRecord(values: Record<string, any>): Promise<void> {
|
|
|
255
260
|
// but saving against the partial row mid-`/show` (or after it failed) could
|
|
256
261
|
// overwrite a not-yet-loaded detail field with an empty value — the built-in
|
|
257
262
|
// fields avoid this by not rendering until the record is ready.
|
|
258
|
-
|
|
263
|
+
//
|
|
264
|
+
// Also honor the per-record edit gate: a custom slot editor funnels through
|
|
265
|
+
// here, so a row a `editable` predicate rejects must not be saved through the
|
|
266
|
+
// slot any more than through the built-in cell / field editors.
|
|
267
|
+
if (props.loading || props.error || !props.record || !edit || !edit.canEdit(props.record)) {
|
|
259
268
|
return Promise.resolve()
|
|
260
269
|
}
|
|
261
270
|
edit.save(props.record, values)
|
|
@@ -270,6 +279,10 @@ const slotProps = computed(() => ({
|
|
|
270
279
|
// record has loaded; `save` also hard-refuses while loading/error as a guard.
|
|
271
280
|
loading: props.loading ?? false,
|
|
272
281
|
error: props.error ?? false,
|
|
282
|
+
// Whether a per-record `editable` predicate allows editing this row, so a slot
|
|
283
|
+
// editor can disable its own controls for a rejected row; `save` enforces it
|
|
284
|
+
// regardless, but otherwise the refusal is only visible as a silent no-op.
|
|
285
|
+
canEdit: !!props.record && !!edit?.canEdit(props.record),
|
|
273
286
|
save: saveRecord
|
|
274
287
|
}))
|
|
275
288
|
</script>
|
|
@@ -338,7 +351,7 @@ const slotProps = computed(() => ({
|
|
|
338
351
|
@click="onCreate"
|
|
339
352
|
/>
|
|
340
353
|
</template>
|
|
341
|
-
<template v-else-if="record && !loading && !error">
|
|
354
|
+
<template v-else-if="record && !loading && !error && deletable">
|
|
342
355
|
<template v-if="confirmingDelete.state.value">
|
|
343
356
|
<span class="confirm-text">{{ t.confirm_delete }}</span>
|
|
344
357
|
<SButton size="medium" :label="t.cancel" @click="confirmingDelete.off" />
|
|
@@ -457,6 +470,10 @@ const slotProps = computed(() => ({
|
|
|
457
470
|
border-top: 1px solid var(--c-divider);
|
|
458
471
|
}
|
|
459
472
|
|
|
473
|
+
.footer:empty {
|
|
474
|
+
display: none;
|
|
475
|
+
}
|
|
476
|
+
|
|
460
477
|
.confirm-text {
|
|
461
478
|
margin-right: auto;
|
|
462
479
|
font-size: 13px;
|
|
@@ -22,7 +22,7 @@ const { t } = useTrans({
|
|
|
22
22
|
|
|
23
23
|
const edit = useLensEdit()
|
|
24
24
|
|
|
25
|
-
const editable = computed(() => !!edit && (props.field as any).data?.showOnUpdate === true)
|
|
25
|
+
const editable = computed(() => !!edit?.canEdit(props.record) && (props.field as any).data?.showOnUpdate === true)
|
|
26
26
|
|
|
27
27
|
// Some field types do not implement a detail renderer or an editable input
|
|
28
28
|
// (they `throw new Error('Not implemented.')`). Resolve them defensively so a
|
|
@@ -121,6 +121,14 @@ async function apply() {
|
|
|
121
121
|
return
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// A per-record `editable` predicate can flip to reject this row while the editor
|
|
125
|
+
// is open (e.g. a refresh marks it locked). Re-check before persisting so an
|
|
126
|
+
// already-open editor can't save a row the policy now rejects.
|
|
127
|
+
if (!canEdit.value) {
|
|
128
|
+
editing.value = false
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
// Optimistic: patch + persist in the background, then close immediately.
|
|
125
133
|
edit!.save(props.record, {
|
|
126
134
|
[props.fieldKey]: props.field.inputToPayload(model.value)
|
|
@@ -27,7 +27,7 @@ const props = defineProps<{
|
|
|
27
27
|
indexField?: string
|
|
28
28
|
// When set (and editing is enabled), cells for `showOnUpdate` fields
|
|
29
29
|
// render a hover edit affordance that opens an inline editor.
|
|
30
|
-
|
|
30
|
+
inlineEditable?: boolean
|
|
31
31
|
}>()
|
|
32
32
|
|
|
33
33
|
const emit = defineEmits<{
|
|
@@ -140,7 +140,7 @@ const columns = computedAsync(async () => {
|
|
|
140
140
|
} as TableCell<any, any>
|
|
141
141
|
}
|
|
142
142
|
} else if (
|
|
143
|
-
props.
|
|
143
|
+
props.inlineEditable
|
|
144
144
|
&& edit?.editable
|
|
145
145
|
&& key !== edit.indexField
|
|
146
146
|
&& overriddenFieldData.showOnUpdate === true
|
|
@@ -3,9 +3,13 @@ import IconPencilSimple from '~icons/ph/pencil-simple'
|
|
|
3
3
|
import { useElementBounding } from '@vueuse/core'
|
|
4
4
|
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from 'vue'
|
|
5
5
|
import SButton from '../../../components/SButton.vue'
|
|
6
|
+
import SLink from '../../../components/SLink.vue'
|
|
7
|
+
import SPill, { type Mode as PillMode } from '../../../components/SPill.vue'
|
|
8
|
+
import SState, { type Mode as StateMode } from '../../../components/SState.vue'
|
|
6
9
|
import { useManualDropdownPosition } from '../../../composables/Dropdown'
|
|
7
10
|
import { useTrans } from '../../../composables/Lang'
|
|
8
11
|
import { useValidation } from '../../../composables/Validation'
|
|
12
|
+
import { day } from '../../../support/Day'
|
|
9
13
|
import { type FieldData } from '../FieldData'
|
|
10
14
|
import { useLensEdit } from '../composables/LensEdit'
|
|
11
15
|
import { useLensInlineEdit } from '../composables/LensInlineEdit'
|
|
@@ -29,6 +33,10 @@ const inline = useLensInlineEdit()
|
|
|
29
33
|
const myKey = computed(() => `${edit?.resolveId(props.record)}:${props.fieldKey}`)
|
|
30
34
|
const editing = computed(() => inline?.activeKey.value === myKey.value)
|
|
31
35
|
|
|
36
|
+
// A predicate `editable` on the catalog can disable editing per row; hide the
|
|
37
|
+
// affordance and refuse to open the editor when this record is rejected.
|
|
38
|
+
const canEdit = computed(() => !!edit?.canEdit(props.record))
|
|
39
|
+
|
|
32
40
|
// If the backing value is replaced while this editor is open — a refresh banner
|
|
33
41
|
// apply, a parent `refresh()`, or a requery that keeps this row visible — the
|
|
34
42
|
// `model` captured back in `start()` is now stale, and saving it would overwrite
|
|
@@ -54,21 +62,57 @@ onUnmounted(() => {
|
|
|
54
62
|
|
|
55
63
|
// Reuse the field's own table-cell rendering for the display value so the
|
|
56
64
|
// label mapping (e.g. select option labels) matches the read-only columns.
|
|
57
|
-
|
|
58
|
-
const displayValue = computed(() => {
|
|
65
|
+
const resolvedCell = computed<any>(() => {
|
|
59
66
|
try {
|
|
60
67
|
const cell = props.field.tableColumn().cell
|
|
61
|
-
|
|
62
|
-
if (resolved && (resolved.type === 'text' || resolved.type === 'number')) {
|
|
63
|
-
return resolved.value ?? ''
|
|
64
|
-
}
|
|
65
|
-
// `state` cells (e.g. a select with displayAs: 'state') carry the localized
|
|
66
|
-
// label, not the raw payload — show that rather than falling through.
|
|
67
|
-
if (resolved && resolved.type === 'state') {
|
|
68
|
-
return resolved.label ?? ''
|
|
69
|
-
}
|
|
68
|
+
return typeof cell === 'function' ? cell(props.value, props.record) : cell
|
|
70
69
|
} catch {
|
|
71
|
-
// Field types without a
|
|
70
|
+
// Field types without a usable cell fall through to the generic display.
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// A `pills` cell (e.g. a multi-select with displayAs: 'pills') renders as pills
|
|
76
|
+
// rather than text, mirroring the read-only `STableCellPills` column.
|
|
77
|
+
const displayPills = computed<{ label: string; color?: PillMode }[] | null>(() => {
|
|
78
|
+
const cell = resolvedCell.value
|
|
79
|
+
return cell && cell.type === 'pills' ? cell.pills : null
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// A `state` cell (e.g. a select with displayAs: 'state') renders as a status
|
|
83
|
+
// badge rather than its bare label, mirroring the read-only `STableCellState`
|
|
84
|
+
// column.
|
|
85
|
+
const displayState = computed<{ label: string; mode?: StateMode } | null>(() => {
|
|
86
|
+
const cell = resolvedCell.value
|
|
87
|
+
return cell && cell.type === 'state' ? { label: cell.label, mode: cell.mode } : null
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// A text cell carrying a `link` (or `onClick`) — e.g. a LinkField — renders as a
|
|
91
|
+
// link rather than bare text, mirroring the read-only STableCellText so a column
|
|
92
|
+
// that was clickable before inline editing was enabled stays clickable instead of
|
|
93
|
+
// degrading to plain text. The text itself comes from `displayValue` below.
|
|
94
|
+
const displayLink = computed<{ link: string | null; onClick?: (v: any, r: any) => void } | null>(() => {
|
|
95
|
+
const cell = resolvedCell.value
|
|
96
|
+
if (cell && cell.type === 'text' && (cell.link != null || typeof cell.onClick === 'function')) {
|
|
97
|
+
return { link: cell.link ?? null, onClick: cell.onClick }
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Falls back to a plain representation for non-text displays (pills and state
|
|
103
|
+
// cells are rendered as their own components in the template above).
|
|
104
|
+
const displayValue = computed(() => {
|
|
105
|
+
const resolved: any = resolvedCell.value
|
|
106
|
+
if (resolved && (resolved.type === 'text' || resolved.type === 'number')) {
|
|
107
|
+
return resolved.value ?? ''
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A `day` cell (e.g. a DateField) carries a Day value plus a format; render the
|
|
111
|
+
// formatted day — mirroring the read-only STableCellDay — so an inline-editable
|
|
112
|
+
// date column shows e.g. `YYYY-MM-DD` rather than the raw ISO/timestamp string
|
|
113
|
+
// the generic fallback below would otherwise surface.
|
|
114
|
+
if (resolved && resolved.type === 'day') {
|
|
115
|
+
return resolved.value ? day(resolved.value).format(resolved.format ?? 'YYYY-MM-DD HH:mm:ss') : ''
|
|
72
116
|
}
|
|
73
117
|
|
|
74
118
|
const v = props.value
|
|
@@ -105,6 +149,10 @@ const editorStyle = computed(() => ({
|
|
|
105
149
|
}))
|
|
106
150
|
|
|
107
151
|
function start() {
|
|
152
|
+
if (!canEdit.value) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
108
156
|
inputComponent.value = props.field.formInputComponent()
|
|
109
157
|
model.value = props.field.payloadToInput(props.value ?? props.field.inputEmptyValue())
|
|
110
158
|
reset()
|
|
@@ -143,6 +191,19 @@ async function apply() {
|
|
|
143
191
|
return
|
|
144
192
|
}
|
|
145
193
|
|
|
194
|
+
// A per-record `editable` predicate can flip to reject this row while the editor
|
|
195
|
+
// is open (e.g. a refresh marks it locked) — `start()` only gates opening, so
|
|
196
|
+
// re-check before persisting. Without this an already-open editor could save a
|
|
197
|
+
// row the policy now rejects. Close only if this cell is still the active one
|
|
198
|
+
// (mirrors the post-save close below) so bailing can't wipe an editor the user
|
|
199
|
+
// opened on another cell during the `await validate()`.
|
|
200
|
+
if (!canEdit.value) {
|
|
201
|
+
if (inline?.activeKey.value === myKey.value) {
|
|
202
|
+
inline.stop()
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
146
207
|
// Optimistic: patch + persist in the background, then close immediately.
|
|
147
208
|
edit!.save(props.record, {
|
|
148
209
|
[props.fieldKey]: props.field.inputToPayload(model.value)
|
|
@@ -184,8 +245,33 @@ function isTextLikeInput(target: EventTarget | null): boolean {
|
|
|
184
245
|
|
|
185
246
|
<template>
|
|
186
247
|
<div ref="anchor" class="LensTableEditableCell" :class="{ editing }">
|
|
187
|
-
<
|
|
248
|
+
<div v-if="displayPills" class="pills">
|
|
249
|
+
<SPill
|
|
250
|
+
v-for="(pill, i) in displayPills"
|
|
251
|
+
:key="i"
|
|
252
|
+
size="mini"
|
|
253
|
+
:mode="pill.color"
|
|
254
|
+
:label="pill.label"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
<SState
|
|
258
|
+
v-else-if="displayState"
|
|
259
|
+
size="mini"
|
|
260
|
+
:mode="displayState.mode"
|
|
261
|
+
:label="displayState.label"
|
|
262
|
+
/>
|
|
263
|
+
<SLink
|
|
264
|
+
v-else-if="displayLink"
|
|
265
|
+
class="value link"
|
|
266
|
+
:href="displayLink.link"
|
|
267
|
+
:role="displayLink.onClick ? 'button' : null"
|
|
268
|
+
@click="() => displayLink?.onClick?.(value, record)"
|
|
269
|
+
>
|
|
270
|
+
{{ displayValue }}
|
|
271
|
+
</SLink>
|
|
272
|
+
<span v-else class="value">{{ displayValue }}</span>
|
|
188
273
|
<button
|
|
274
|
+
v-if="canEdit"
|
|
189
275
|
class="edit"
|
|
190
276
|
type="button"
|
|
191
277
|
:aria-label="`${t.edit} ${field.label()}`"
|
|
@@ -233,6 +319,24 @@ function isTextLikeInput(target: EventTarget | null): boolean {
|
|
|
233
319
|
text-overflow: ellipsis;
|
|
234
320
|
}
|
|
235
321
|
|
|
322
|
+
.value.link {
|
|
323
|
+
color: var(--c-text-info-1);
|
|
324
|
+
transition: color 0.1s;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.value.link:hover {
|
|
328
|
+
color: var(--c-text-info-2);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.pills {
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-wrap: nowrap;
|
|
334
|
+
align-items: center;
|
|
335
|
+
gap: 4px;
|
|
336
|
+
min-width: 0;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
}
|
|
339
|
+
|
|
236
340
|
.edit {
|
|
237
341
|
position: absolute;
|
|
238
342
|
top: 50%;
|
|
@@ -13,6 +13,12 @@ export interface LensEditContext {
|
|
|
13
13
|
/** Whether new records may be created. */
|
|
14
14
|
creatable: boolean
|
|
15
15
|
|
|
16
|
+
/** Whether the given record may be edited, inline or in the sheet. */
|
|
17
|
+
canEdit: (record: Record<string, any>) => boolean
|
|
18
|
+
|
|
19
|
+
/** Whether the given record may be deleted from the sheet. */
|
|
20
|
+
canDelete: (record: Record<string, any>) => boolean
|
|
21
|
+
|
|
16
22
|
/** The entity key being edited (e.g. `topic`, `user`). */
|
|
17
23
|
entity: string
|
|
18
24
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { xor } from 'lodash-es'
|
|
2
2
|
import { h } from 'vue'
|
|
3
|
+
import SDescPill from '../../../components/SDescPill.vue'
|
|
3
4
|
import SInputCheckboxes from '../../../components/SInputCheckboxes.vue'
|
|
4
5
|
import SInputDropdown from '../../../components/SInputDropdown.vue'
|
|
5
6
|
import SInputRadios from '../../../components/SInputRadios.vue'
|
|
@@ -38,6 +39,8 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
38
39
|
switch (this.data.displayAs) {
|
|
39
40
|
case 'state':
|
|
40
41
|
return this.tableCellState(v, _r)
|
|
42
|
+
case 'pills':
|
|
43
|
+
return this.tableCellPills(v, _r)
|
|
41
44
|
case 'text':
|
|
42
45
|
return this.tableCellText(v, _r)
|
|
43
46
|
default:
|
|
@@ -45,6 +48,18 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
protected tableCellPills(v: any, _r: any): TableCell {
|
|
52
|
+
v = Array.isArray(v) ? v : [v]
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
type: 'pills',
|
|
56
|
+
pills: this.optionsForValues(v).map((o) => ({
|
|
57
|
+
label: this.labelForOption(o),
|
|
58
|
+
color: o.mode
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
protected tableCellText(v: any, _r: any): TableCell {
|
|
49
64
|
v = Array.isArray(v) ? v : [v]
|
|
50
65
|
|
|
@@ -95,6 +110,8 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
95
110
|
switch (this.data.displayAs) {
|
|
96
111
|
case 'state':
|
|
97
112
|
return this.renderDataListItemValueForState(value)
|
|
113
|
+
case 'pills':
|
|
114
|
+
return this.renderDataListItemValueForPills(value)
|
|
98
115
|
case 'text':
|
|
99
116
|
return this.renderDataListItemValueForText(value)
|
|
100
117
|
default:
|
|
@@ -103,6 +120,17 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
103
120
|
})
|
|
104
121
|
}
|
|
105
122
|
|
|
123
|
+
protected renderDataListItemValueForPills(value: any): any {
|
|
124
|
+
value = Array.isArray(value) ? value : [value]
|
|
125
|
+
|
|
126
|
+
return h(SDescPill, {
|
|
127
|
+
pill: this.optionsForValues(value).map((o) => ({
|
|
128
|
+
label: this.labelForOption(o),
|
|
129
|
+
mode: o.mode
|
|
130
|
+
}))
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
protected renderDataListItemValueForState(value: any): any {
|
|
107
135
|
if (this.data.multiple) {
|
|
108
136
|
throw new Error('Displaying select field as state with multiple option is not supported.')
|