@globalbrain/sefirot 4.34.1 → 4.35.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.
Files changed (55) hide show
  1. package/config/nuxt.js +44 -1
  2. package/config/vite.js +2 -3
  3. package/lib/blocks/lens/FieldContext.ts +5 -0
  4. package/lib/blocks/lens/FieldData.ts +140 -0
  5. package/lib/blocks/lens/FieldRegistry.ts +23 -0
  6. package/lib/blocks/lens/FileDownloader.ts +1 -0
  7. package/lib/blocks/lens/FilterOperator.ts +33 -0
  8. package/lib/blocks/lens/LensQuery.ts +10 -0
  9. package/lib/blocks/lens/LensResult.ts +20 -0
  10. package/lib/blocks/lens/ResourceFetcher.ts +3 -0
  11. package/lib/blocks/lens/Rule.ts +12 -0
  12. package/lib/blocks/lens/components/LensCatalog.vue +490 -0
  13. package/lib/blocks/lens/components/LensCatalogControl.vue +220 -0
  14. package/lib/blocks/lens/components/LensCatalogFooter.vue +46 -0
  15. package/lib/blocks/lens/components/LensCatalogStateFilter.vue +171 -0
  16. package/lib/blocks/lens/components/LensCatalogStateFilterCondition.vue +86 -0
  17. package/lib/blocks/lens/components/LensCatalogStateFilterGroup.vue +102 -0
  18. package/lib/blocks/lens/components/LensCatalogStateSort.vue +159 -0
  19. package/lib/blocks/lens/components/LensFormFilter.vue +169 -0
  20. package/lib/blocks/lens/components/LensFormFilterCondition.vue +205 -0
  21. package/lib/blocks/lens/components/LensFormFilterGroup.vue +175 -0
  22. package/lib/blocks/lens/components/LensFormOverride.vue +45 -0
  23. package/lib/blocks/lens/components/LensFormOverrideBase.vue +204 -0
  24. package/lib/blocks/lens/components/LensFormView.vue +347 -0
  25. package/lib/blocks/lens/components/LensTable.vue +154 -0
  26. package/lib/blocks/lens/composables/FieldFactory.ts +27 -0
  27. package/lib/blocks/lens/composables/FieldRegistry.ts +16 -0
  28. package/lib/blocks/lens/composables/FileDownloader.ts +10 -0
  29. package/lib/blocks/lens/composables/ResourceFetcher.ts +30 -0
  30. package/lib/blocks/lens/composables/SetupLens.ts +55 -0
  31. package/lib/blocks/lens/fields/ContentField.ts +34 -0
  32. package/lib/blocks/lens/fields/DateField.ts +66 -0
  33. package/lib/blocks/lens/fields/DatetimeField.ts +35 -0
  34. package/lib/blocks/lens/fields/Field.ts +244 -0
  35. package/lib/blocks/lens/fields/FileUploadField.ts +63 -0
  36. package/lib/blocks/lens/fields/IdField.ts +34 -0
  37. package/lib/blocks/lens/fields/LinkField.ts +53 -0
  38. package/lib/blocks/lens/fields/NumberField.ts +32 -0
  39. package/lib/blocks/lens/fields/RelatedManyField.ts +62 -0
  40. package/lib/blocks/lens/fields/SelectField.ts +198 -0
  41. package/lib/blocks/lens/fields/SlackMessageField.ts +34 -0
  42. package/lib/blocks/lens/fields/TextField.ts +46 -0
  43. package/lib/blocks/lens/fields/TextareaField.ts +49 -0
  44. package/lib/blocks/lens/filter-inputs/FilterInput.ts +72 -0
  45. package/lib/blocks/lens/filter-inputs/NumberFilterInput.ts +26 -0
  46. package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +76 -0
  47. package/lib/blocks/lens/filter-inputs/TextFilterInput.ts +26 -0
  48. package/lib/blocks/lens/validation/RuleMapper.ts +22 -0
  49. package/lib/components/SInputTextarea.vue +28 -10
  50. package/lib/components/STable.vue +230 -61
  51. package/lib/components/STableCell.vue +2 -2
  52. package/lib/composables/TableAnimation.ts +180 -0
  53. package/lib/support/Scroll.ts +263 -0
  54. package/lib/support/Utils.ts +1 -1
  55. package/package.json +7 -15
@@ -0,0 +1,204 @@
1
+ <script setup lang="ts">
2
+ import SInputCheckbox from '../../../components/SInputCheckbox.vue'
3
+ import SInputNumber from '../../../components/SInputNumber.vue'
4
+ import SInputText from '../../../components/SInputText.vue'
5
+ import { useTrans } from '../../../composables/Lang'
6
+ import { useValidation } from '../../../composables/Validation'
7
+ import { maxLength, maxValue } from '../../../validation/rules'
8
+ import { type FieldData } from '../FieldData'
9
+
10
+ const props = defineProps<{
11
+ name: string
12
+ field: FieldData
13
+ }>()
14
+
15
+ const labelEn = defineModel<string | null>('labelEn', { required: true })
16
+ const labelJa = defineModel<string | null>('labelJa', { required: true })
17
+ const width = defineModel<number | null>('width', { required: true })
18
+ const freeze = defineModel<boolean>('freeze', { required: true })
19
+
20
+ const emit = defineEmits<{
21
+ cancel: []
22
+ saved: [field: any]
23
+ }>()
24
+
25
+ const { t } = useTrans({
26
+ en: {
27
+ title: 'Configure field',
28
+ d_key: 'Field key',
29
+ d_type: 'Field type',
30
+ i_label_en: 'Label (EN)',
31
+ i_label_en_ph: props.field.labelEn,
32
+ i_label_ja: 'Label (JA)',
33
+ i_label_ja_ph: props.field.labelJa,
34
+ i_width: 'Column width',
35
+ i_width_ph: '128',
36
+ i_freeze: 'Freeze column',
37
+ a_cancel: 'Cancel',
38
+ a_apply: 'Finish editing'
39
+ },
40
+ ja: {
41
+ title: '項目情報を設定',
42
+ d_key: '項目キー',
43
+ d_type: '項目タイプ',
44
+ i_label_en: 'ラベル (EN)',
45
+ i_label_en_ph: props.field.labelEn,
46
+ i_label_ja: 'ラベル (JA)',
47
+ i_label_ja_ph: props.field.labelJa,
48
+ i_width: '列の幅',
49
+ i_width_ph: '128',
50
+ i_freeze: '列をフリーズ',
51
+ a_cancel: 'キャンセル',
52
+ a_apply: '編集を完了'
53
+ }
54
+ })
55
+
56
+ const { validation, validateAndNotify } = useValidation(() => ({
57
+ labelEn: labelEn.value,
58
+ labelJa: labelJa.value,
59
+ width: width.value,
60
+ freeze: freeze.value
61
+ }), {
62
+ labelEn: { maxLength: maxLength(255) },
63
+ labelJa: { maxLength: maxLength(255) },
64
+ width: { maxValue: maxValue(10000) }
65
+ })
66
+
67
+ function isNotNullOrSame<T>(f: T | null, o: T) {
68
+ return (f !== null && f !== o)
69
+ }
70
+
71
+ async function onSave() {
72
+ if (!(await validateAndNotify())) {
73
+ return
74
+ }
75
+ const data: Partial<FieldData> = {}
76
+ if (isNotNullOrSame(labelEn.value, props.field.labelEn)) {
77
+ data.labelEn = labelEn.value!
78
+ }
79
+ if (isNotNullOrSame(labelJa.value, props.field.labelJa)) {
80
+ data.labelJa = labelJa.value!
81
+ }
82
+ if (isNotNullOrSame(width.value, props.field.width)) {
83
+ data.width = width.value!
84
+ }
85
+ if (isNotNullOrSame(freeze.value, props.field.freeze)) {
86
+ data.freeze = freeze.value!
87
+ }
88
+ emit('saved', data)
89
+ }
90
+ </script>
91
+
92
+ <template>
93
+ <SCard class="LensFormOverrideBase" size="large">
94
+ <SCardBlock class="s-p-32">
95
+ <SDoc>
96
+ <SContent>
97
+ <h2>{{ t.title }}</h2>
98
+ </SContent>
99
+ <div class="list">
100
+ <div class="item">
101
+ <div class="key">{{ t.d_key }}</div>
102
+ <div class="value subtle">{{ name }}</div>
103
+ </div>
104
+ <div class="item">
105
+ <div class="key">{{ t.d_type }}</div>
106
+ <div class="value subtle">{{ field.type }}</div>
107
+ </div>
108
+ <div class="item">
109
+ <div class="key">{{ t.i_label_en }}</div>
110
+ <div class="value">
111
+ <SInputText
112
+ v-model="labelEn"
113
+ size="mini"
114
+ :placeholder="t.i_label_en_ph"
115
+ :validation="validation.labelEn"
116
+ />
117
+ </div>
118
+ </div>
119
+ <div class="item">
120
+ <div class="key">{{ t.i_label_ja }}</div>
121
+ <div class="value">
122
+ <SInputText
123
+ v-model="labelJa"
124
+ size="mini"
125
+ :placeholder="t.i_label_ja_ph"
126
+ :validation="validation.labelJa"
127
+ />
128
+ </div>
129
+ </div>
130
+ <div class="item">
131
+ <div class="key">{{ t.i_width }}</div>
132
+ <div class="value">
133
+ <SInputNumber
134
+ v-model="width"
135
+ size="mini"
136
+ :placeholder="t.i_width_ph"
137
+ :validation="validation.width"
138
+ />
139
+ </div>
140
+ </div>
141
+ <div class="item">
142
+ <div class="key">{{ t.i_freeze }}</div>
143
+ <div class="value">
144
+ <SInputCheckbox v-model="freeze" />
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </SDoc>
149
+ </SCardBlock>
150
+ <SCardBlock class="s-py-16 s-px-32">
151
+ <SControl size="md">
152
+ <SControlRight>
153
+ <SControlButton
154
+ :label="t.a_cancel"
155
+ @click="$emit('cancel')"
156
+ />
157
+ <SControlButton
158
+ mode="info"
159
+ :label="t.a_apply"
160
+ @click="onSave"
161
+ />
162
+ </SControlRight>
163
+ </SControl>
164
+ </SCardBlock>
165
+ </SCard>
166
+ </template>
167
+
168
+ <style scoped lang="postcss">
169
+ .LensFormOverrideBase {
170
+ --c-bg-elv-2: var(--c-bg-1);
171
+ --c-bg-elv-3: var(--c-bg-1);
172
+ }
173
+
174
+ .fieldset {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 24px;
178
+ }
179
+
180
+ .list {
181
+ display: flex;
182
+ flex-direction: column;
183
+ }
184
+
185
+ :deep(.item) {
186
+ display: grid;
187
+ grid-template-columns: 168px 1fr;
188
+ align-items: center;
189
+ min-height: 48px;
190
+ }
191
+
192
+ :deep(.key) {
193
+ font-size: 14px;
194
+ color: var(--c-text-2);
195
+ }
196
+
197
+ :deep(.value) {
198
+ font-size: 14px;
199
+
200
+ &.subtle {
201
+ color: var(--c-text-2);
202
+ }
203
+ }
204
+ </style>
@@ -0,0 +1,347 @@
1
+ <script setup lang="ts">
2
+ import IconCheck from '~icons/ph/check'
3
+ import IconDotsSixVertical from '~icons/ph/dots-six-vertical'
4
+ import IconGear from '~icons/ph/gear'
5
+ import IconMinus from '~icons/ph/minus'
6
+ import IconTrash from '~icons/ph/trash'
7
+ import { cloneDeep } from 'lodash-es'
8
+ import { computed, ref } from 'vue'
9
+ import { useDraggable } from 'vue-draggable-plus'
10
+ import SButton from '../../../components/SButton.vue'
11
+ import SInputCheckbox from '../../../components/SInputCheckbox.vue'
12
+ import { useLang, useTrans } from '../../../composables/Lang'
13
+ import { usePower } from '../../../composables/Power'
14
+ import { type FieldData } from '../FieldData'
15
+ import LensFormOverride from './LensFormOverride.vue'
16
+
17
+ interface Props {
18
+ select: string[]
19
+ selectable: string[]
20
+ fields: Record<string, FieldData>
21
+ overrides: Record<string, Partial<FieldData>>
22
+ }
23
+
24
+ interface SelectOption {
25
+ uid: number
26
+ key: string
27
+ value: boolean
28
+ isEmpty: boolean
29
+ field: FieldData | null
30
+ override: Partial<FieldData>
31
+ }
32
+
33
+ const props = defineProps<Props>()
34
+
35
+ const emit = defineEmits<{
36
+ cancel: []
37
+ apply: [select: string[], selectable: string[], overrides: Record<string, Partial<FieldData>>]
38
+ }>()
39
+
40
+ const lang = useLang()
41
+
42
+ const { t } = useTrans({
43
+ en: {
44
+ title: 'Manage table view',
45
+ a_select_all: 'Select all',
46
+ a_clear_all: 'Clear all',
47
+ a_cancel: 'Cancel',
48
+ a_apply: 'Apply changes'
49
+ },
50
+ ja: {
51
+ title: 'テーブルの表示を更新する',
52
+ a_select_all: 'すべて選択',
53
+ a_clear_all: 'すべて解除',
54
+ a_cancel: 'キャンセル',
55
+ a_apply: '変更を適用'
56
+ }
57
+ })
58
+
59
+ const editDialog = usePower()
60
+
61
+ let _uid = 0
62
+
63
+ const el = ref<HTMLElement | null>(null)
64
+
65
+ const _select = ref(cloneDeep(props.select))
66
+ const _selectable = ref(cloneDeep(props.selectable))
67
+
68
+ const _selectDict = computed(() => {
69
+ return _select.value.reduce((acc, s) => {
70
+ acc[s] = true
71
+ return acc
72
+ }, {} as Record<string, boolean>)
73
+ })
74
+
75
+ const _overrides = cloneDeep(props.overrides) ?? {}
76
+
77
+ const selectOptions = ref(createSelectOptions())
78
+
79
+ const selectedOption = ref<SelectOption | null>(null)
80
+
81
+ useDraggable(el, selectOptions, {
82
+ handle: '.handle'
83
+ })
84
+
85
+ function createSelectOptions(): SelectOption[] {
86
+ return _selectable.value.map((s) => {
87
+ return {
88
+ uid: _uid++,
89
+ key: s,
90
+ value: _selectDict.value[s],
91
+ isEmpty: s === '__empty__',
92
+ field: props.fields[s],
93
+ override: _overrides[s] || {}
94
+ }
95
+ })
96
+ }
97
+
98
+ function getName(s: SelectOption): string {
99
+ return lang === 'ja'
100
+ ? s.override.labelJa || s.field?.labelJa || ''
101
+ : s.override.labelEn || s.field?.labelEn || ''
102
+ }
103
+
104
+ function onEdit(option: SelectOption) {
105
+ selectedOption.value = option
106
+ editDialog.on()
107
+ }
108
+
109
+ function onEdited(field: Partial<FieldData>) {
110
+ selectedOption.value!.override = field
111
+ editDialog.off()
112
+ selectedOption.value = null
113
+ }
114
+
115
+ function onSelectAll() {
116
+ selectOptions.value.forEach((s) => {
117
+ s.value = true
118
+ })
119
+ }
120
+
121
+ function onClearAll() {
122
+ selectOptions.value.forEach((s) => {
123
+ s.value = false
124
+ })
125
+ }
126
+
127
+ function onRemove(uid: number) {
128
+ const index = selectOptions.value.findIndex((s) => s.uid === uid)
129
+ if (index !== -1) {
130
+ selectOptions.value.splice(index, 1)
131
+ }
132
+ }
133
+
134
+ async function onApply() {
135
+ const select = selectOptions.value.filter((s) => s.value).map((s) => s.key)
136
+
137
+ const selectable = selectOptions.value.map((s) => s.key)
138
+
139
+ const overrides = selectOptions.value.reduce<Record<string, Partial<FieldData>>>((acc, s) => {
140
+ if (s.override && Object.keys(s.override).length > 0) {
141
+ acc[s.key] = s.override
142
+ }
143
+ return acc
144
+ }, {} as Record<string, Partial<FieldData>>)
145
+
146
+ emit('apply', select, selectable, overrides)
147
+ }
148
+ </script>
149
+
150
+ <template>
151
+ <SCard class="LensFormView" size="medium">
152
+ <SCardBlock class="s-p-32">
153
+ <SDoc>
154
+ <SContent>
155
+ <h2>{{ t.title }}</h2>
156
+ </SContent>
157
+ <div class="main">
158
+ <div class="actions">
159
+ <SButton
160
+ type="outline"
161
+ size="small"
162
+ mode="mute"
163
+ :icon="IconCheck"
164
+ :label="t.a_select_all"
165
+ @click="onSelectAll"
166
+ />
167
+ <SButton
168
+ type="outline"
169
+ size="small"
170
+ mode="mute"
171
+ :icon="IconMinus"
172
+ :label="t.a_clear_all"
173
+ @click="onClearAll"
174
+ />
175
+ </div>
176
+ <div ref="el" class="list">
177
+ <div
178
+ v-for="s, i in selectOptions"
179
+ :key="s.uid"
180
+ class="item"
181
+ :class="{ selected: s.value, empty: s.isEmpty }"
182
+ >
183
+ <div class="input" role="button" @click="selectOptions[i].value = !s.value">
184
+ <div class="handle">
185
+ <IconDotsSixVertical class="handle-svg" />
186
+ </div>
187
+ <div class="body">
188
+ <SInputCheckbox
189
+ :model-value="s.value"
190
+ />
191
+ <div class="key">
192
+ {{ getName(s) }}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ <div class="remove">
197
+ <SButton
198
+ v-if="!s.isEmpty"
199
+ type="text"
200
+ size="small"
201
+ mode="mute"
202
+ :icon="IconGear"
203
+ @click="onEdit(s)"
204
+ />
205
+ <SButton
206
+ v-if="s.isEmpty"
207
+ type="text"
208
+ size="small"
209
+ mode="mute"
210
+ :icon="IconTrash"
211
+ @click="onRemove(s.uid)"
212
+ />
213
+ </div>
214
+ </div>
215
+ </div>
216
+ <div class="actions" />
217
+ </div>
218
+ </SDoc>
219
+ </SCardBlock>
220
+ <SCardBlock class="s-py-16 s-px-32">
221
+ <SControl size="md">
222
+ <SControlRight>
223
+ <SControlButton
224
+ :label="t.a_cancel"
225
+ @click="$emit('cancel')"
226
+ />
227
+ <SControlButton
228
+ mode="info"
229
+ :label="t.a_apply"
230
+ @click="onApply"
231
+ />
232
+ </SControlRight>
233
+ </SControl>
234
+ </SCardBlock>
235
+
236
+ <SModal :open="editDialog.state.value" @close="editDialog.off()">
237
+ <LensFormOverride
238
+ v-if="selectedOption?.field && selectedOption?.override"
239
+ :name="selectedOption.key"
240
+ :field="selectedOption.field"
241
+ :override="selectedOption.override"
242
+ @cancel="editDialog.off()"
243
+ @saved="onEdited"
244
+ />
245
+ </SModal>
246
+ </SCard>
247
+ </template>
248
+
249
+ <style scoped lang="postcss">
250
+ .LensFormView {
251
+ --c-bg-elv-2: var(--c-bg-1);
252
+ --c-bg-elv-3: var(--c-bg-1);
253
+ }
254
+
255
+ .main {
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 8px;
259
+ }
260
+
261
+ .list {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 8px;
265
+ }
266
+
267
+ .item {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ border-radius: 6px;
272
+
273
+ &.selected .key {
274
+ color: var(--c-text-1);
275
+ }
276
+
277
+ &.selected.empty .key {
278
+ color: var(--c-text-2);
279
+ }
280
+
281
+ &.empty .key {
282
+ color: var(--c-text-3);
283
+ }
284
+ }
285
+
286
+ .input {
287
+ display: flex;
288
+ flex-grow: 1;
289
+ gap: 1px;
290
+ border: 1px dashed var(--c-divider);
291
+ border-radius: 6px;
292
+ background-color: var(--c-gutter);
293
+ overflow: hidden;
294
+ transition: border-color 0.25s;
295
+
296
+ &:hover {
297
+ border-color: var(--c-border-mute-2);
298
+ }
299
+ }
300
+
301
+ .handle {
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ width: 32px;
306
+ height: 32px;
307
+ background-color: var(--c-bg-1);
308
+ cursor: grab;
309
+ }
310
+
311
+ .handle-svg {
312
+ width: 16px;
313
+ height: 16px;
314
+ color: var(--c-text-2);
315
+ }
316
+
317
+ .body {
318
+ display: flex;
319
+ align-items: center;
320
+ flex-grow: 1;
321
+ padding: 0 8px;
322
+ background-color: var(--c-bg-1);
323
+ }
324
+
325
+ .key {
326
+ flex-grow: 1;
327
+ padding: 0 8px;
328
+ line-height: 32px;
329
+ font-size: 14px;
330
+ color: var(--c-text-3);
331
+ transition: color 0.25s;
332
+ }
333
+
334
+ .remove {
335
+ display: flex;
336
+ align-items: center;
337
+ flex-shrink: 0;
338
+ width: 32px;
339
+ }
340
+
341
+ .actions {
342
+ display: flex;
343
+ align-items: center;
344
+ flex-shrink: 0;
345
+ gap: 8px;
346
+ }
347
+ </style>
@@ -0,0 +1,154 @@
1
+ <script setup lang="ts">
2
+ import { computedAsync } from '@vueuse/core'
3
+ import { cloneDeep } from 'lodash-es'
4
+ import { computed } from 'vue'
5
+ import STable from '../../../components/STable.vue'
6
+ import { type DropdownSection } from '../../../composables/Dropdown'
7
+ import { type TableColumns, useTable } from '../../../composables/Table'
8
+ import { type FieldData } from '../FieldData'
9
+ import { type LensQuerySort } from '../LensQuery'
10
+ import { type LensResult } from '../LensResult'
11
+ import { useFieldFactory } from '../composables/FieldFactory'
12
+
13
+ const props = defineProps<{
14
+ result?: LensResult
15
+ overrides?: Record<string, Partial<FieldData>>
16
+ loading: boolean
17
+ selected?: number[]
18
+ }>()
19
+
20
+ const emit = defineEmits<{
21
+ 'update:selected': [value: number[]]
22
+ 'filter-updated': [filter: any[]]
23
+ 'sort-updated': [sort: LensQuerySort]
24
+ 'cell-clicked': [value: any, record: any]
25
+ }>()
26
+
27
+ const fieldFactory = useFieldFactory()
28
+
29
+ const records = computed(() => props.result?.data ?? [])
30
+
31
+ const orders = computed(() => [
32
+ ...(props.result?.query.select ?? []),
33
+ '__last_empty__'
34
+ ])
35
+
36
+ const columns = computedAsync(async () => {
37
+ const r = props.result
38
+
39
+ if (!r) {
40
+ return {}
41
+ }
42
+
43
+ // Prepare base columns that has `__last_empty__` to fill the end space.
44
+ const columns: TableColumns<any, any, any> = {
45
+ __last_empty__: {
46
+ cell: { type: 'empty' }
47
+ }
48
+ }
49
+
50
+ // Build the lest of columns based on selected fields.
51
+ for (const i in r.query.select) {
52
+ const key = r.query.select[i]
53
+
54
+ const _fieldData = cloneDeep(r.fields[key])
55
+
56
+ const overriddenFieldData = Object.assign(
57
+ _fieldData,
58
+ props.overrides?.[key] ?? {}
59
+ )
60
+
61
+ const field = fieldFactory.make(overriddenFieldData)
62
+
63
+ columns[key] = field.tableColumn()
64
+
65
+ const dropdown: DropdownSection[] = []
66
+
67
+ const sortMenu = field.tableSortMenu(onSortUpdated)
68
+ const filterMenu = await field.tableFilterMenu(r.query.filters, onFilterUpdated)
69
+
70
+ if (sortMenu) {
71
+ dropdown.push(sortMenu)
72
+ }
73
+
74
+ if (filterMenu) {
75
+ dropdown.push(filterMenu)
76
+ }
77
+
78
+ if (dropdown.length > 0) {
79
+ columns[key].dropdown = dropdown
80
+ }
81
+ }
82
+
83
+ return columns
84
+ }, {})
85
+
86
+ const table = useTable({
87
+ records,
88
+ orders,
89
+ columns,
90
+ borderless: true
91
+ })
92
+
93
+ function onSelect(value?: number[]) {
94
+ emit('update:selected', value ?? [])
95
+ }
96
+
97
+ function onFilterUpdated(filter: any[]) {
98
+ emit('filter-updated', filter)
99
+ }
100
+
101
+ function onSortUpdated(sort: LensQuerySort) {
102
+ emit('sort-updated', sort)
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <div class="LensTable" :class="{ 'is-loading': loading, 'is-empty': (result?.data.length ?? 0) === 0 }">
108
+ <STable
109
+ v-if="Object.keys(columns).length > 0"
110
+ class="table"
111
+ :options="table"
112
+ :selected
113
+ @update:selected="onSelect"
114
+ />
115
+ </div>
116
+ </template>
117
+
118
+ <style scoped lang="postcss">
119
+ .LensTable {
120
+ display: flex;
121
+ flex-direction: column;
122
+ flex-grow: 1;
123
+ height: 100%;
124
+ background-color: var(--c-bg-1);
125
+
126
+ /**
127
+ * Set all numbers to use tabular numbers. This is a current hack to apply
128
+ * mono-spaced numbers to fields like Opportunity ID ("OPP-0017263").
129
+ * In ideal scenario, we should be able to set this option per field base,
130
+ * but it is quite tricky to scope the desired field in CSS at the moment.
131
+ */
132
+ font-feature-settings: "tnum";
133
+
134
+ --c-bg-elv-3: var(--c-bg-1);
135
+ }
136
+
137
+ .LensTable.is-loading,
138
+ .LensTable.is-empty {
139
+ .table {
140
+ border-bottom: 1px solid transparent;
141
+ }
142
+ }
143
+
144
+ .LensTable :deep(.col-__select) {
145
+ --table-col-position: sticky;
146
+ --table-col-z-index: 50;
147
+ }
148
+
149
+ .LensTable :deep(.col-__last_empty__) { --table-col-width: 560px; }
150
+
151
+ .table {
152
+ border-bottom: 1px solid var(--c-gutter);
153
+ }
154
+ </style>