@globalbrain/sefirot 4.34.0 → 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.
- package/config/nuxt.js +53 -2
- package/config/vite.js +43 -5
- package/lib/blocks/lens/FieldContext.ts +5 -0
- package/lib/blocks/lens/FieldData.ts +140 -0
- package/lib/blocks/lens/FieldRegistry.ts +23 -0
- package/lib/blocks/lens/FileDownloader.ts +1 -0
- package/lib/blocks/lens/FilterOperator.ts +33 -0
- package/lib/blocks/lens/LensQuery.ts +10 -0
- package/lib/blocks/lens/LensResult.ts +20 -0
- package/lib/blocks/lens/ResourceFetcher.ts +3 -0
- package/lib/blocks/lens/Rule.ts +12 -0
- package/lib/blocks/lens/components/LensCatalog.vue +490 -0
- package/lib/blocks/lens/components/LensCatalogControl.vue +220 -0
- package/lib/blocks/lens/components/LensCatalogFooter.vue +46 -0
- package/lib/blocks/lens/components/LensCatalogStateFilter.vue +171 -0
- package/lib/blocks/lens/components/LensCatalogStateFilterCondition.vue +86 -0
- package/lib/blocks/lens/components/LensCatalogStateFilterGroup.vue +102 -0
- package/lib/blocks/lens/components/LensCatalogStateSort.vue +159 -0
- package/lib/blocks/lens/components/LensFormFilter.vue +169 -0
- package/lib/blocks/lens/components/LensFormFilterCondition.vue +205 -0
- package/lib/blocks/lens/components/LensFormFilterGroup.vue +175 -0
- package/lib/blocks/lens/components/LensFormOverride.vue +45 -0
- package/lib/blocks/lens/components/LensFormOverrideBase.vue +204 -0
- package/lib/blocks/lens/components/LensFormView.vue +347 -0
- package/lib/blocks/lens/components/LensTable.vue +154 -0
- package/lib/blocks/lens/composables/FieldFactory.ts +27 -0
- package/lib/blocks/lens/composables/FieldRegistry.ts +16 -0
- package/lib/blocks/lens/composables/FileDownloader.ts +10 -0
- package/lib/blocks/lens/composables/ResourceFetcher.ts +30 -0
- package/lib/blocks/lens/composables/SetupLens.ts +55 -0
- package/lib/blocks/lens/fields/ContentField.ts +34 -0
- package/lib/blocks/lens/fields/DateField.ts +66 -0
- package/lib/blocks/lens/fields/DatetimeField.ts +35 -0
- package/lib/blocks/lens/fields/Field.ts +244 -0
- package/lib/blocks/lens/fields/FileUploadField.ts +63 -0
- package/lib/blocks/lens/fields/IdField.ts +34 -0
- package/lib/blocks/lens/fields/LinkField.ts +53 -0
- package/lib/blocks/lens/fields/NumberField.ts +32 -0
- package/lib/blocks/lens/fields/RelatedManyField.ts +62 -0
- package/lib/blocks/lens/fields/SelectField.ts +198 -0
- package/lib/blocks/lens/fields/SlackMessageField.ts +34 -0
- package/lib/blocks/lens/fields/TextField.ts +46 -0
- package/lib/blocks/lens/fields/TextareaField.ts +49 -0
- package/lib/blocks/lens/filter-inputs/FilterInput.ts +72 -0
- package/lib/blocks/lens/filter-inputs/NumberFilterInput.ts +26 -0
- package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +76 -0
- package/lib/blocks/lens/filter-inputs/TextFilterInput.ts +26 -0
- package/lib/blocks/lens/validation/RuleMapper.ts +22 -0
- package/lib/components/SInputTextarea.vue +28 -10
- package/lib/components/STable.vue +230 -61
- package/lib/components/STableCell.vue +2 -2
- package/lib/composables/TableAnimation.ts +180 -0
- package/lib/support/Scroll.ts +263 -0
- package/lib/support/Utils.ts +1 -1
- package/package.json +7 -15
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useDebounceFn, useElementSize } from '@vueuse/core'
|
|
3
|
+
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import { useQuery } from '../../../composables/Api'
|
|
5
|
+
import { usePower } from '../../../composables/Power'
|
|
6
|
+
import { type FieldData } from '../FieldData'
|
|
7
|
+
import { type LensQuery, type LensQuerySort } from '../LensQuery'
|
|
8
|
+
import { type LensResult } from '../LensResult'
|
|
9
|
+
import LensCatalogControl, { type FilterPresets } from './LensCatalogControl.vue'
|
|
10
|
+
import LensCatalogFooter from './LensCatalogFooter.vue'
|
|
11
|
+
import LensCatalogStateFilter from './LensCatalogStateFilter.vue'
|
|
12
|
+
import LensCatalogStateSort from './LensCatalogStateSort.vue'
|
|
13
|
+
import LensFormFilter from './LensFormFilter.vue'
|
|
14
|
+
import LensFormView from './LensFormView.vue'
|
|
15
|
+
import LensTable from './LensTable.vue'
|
|
16
|
+
|
|
17
|
+
export interface Props {
|
|
18
|
+
// API endpoint that returns lens result.
|
|
19
|
+
endpoint: string
|
|
20
|
+
|
|
21
|
+
// Search target of the entity.
|
|
22
|
+
entity?: string
|
|
23
|
+
|
|
24
|
+
// Fields to be selected initially.
|
|
25
|
+
select?: string[]
|
|
26
|
+
|
|
27
|
+
// Fields that can be selected by the users. If not provided,
|
|
28
|
+
// all fields in `select` prop will be considered selectable.
|
|
29
|
+
selectable?: string[]
|
|
30
|
+
|
|
31
|
+
// Keys to be searched when users input search query.
|
|
32
|
+
queryKeys?: string[]
|
|
33
|
+
|
|
34
|
+
// Placeholder text for the query search input.
|
|
35
|
+
queryPh?: string
|
|
36
|
+
|
|
37
|
+
// Initial filters to be applied on load. This filters are only applied
|
|
38
|
+
// on the initial load and users can modify them later. Thus, note making
|
|
39
|
+
// this prop reactive will not make sense.
|
|
40
|
+
filters?: any[]
|
|
41
|
+
|
|
42
|
+
// Fixed filters that are always applied and cannot be modified by users.
|
|
43
|
+
fixedFilters?: any[]
|
|
44
|
+
|
|
45
|
+
// Preset of filters shown to the users as "Quick filters".
|
|
46
|
+
filterPresets?: FilterPresets[]
|
|
47
|
+
|
|
48
|
+
// Initial sort to be applied on load.
|
|
49
|
+
sort?: LensQuerySort[]
|
|
50
|
+
|
|
51
|
+
// Override settings for fields.
|
|
52
|
+
overrides?: Record<string, Partial<FieldData>>
|
|
53
|
+
|
|
54
|
+
// Whether to show advanced filters.
|
|
55
|
+
canFilter?: boolean
|
|
56
|
+
|
|
57
|
+
// Whether to show advanced sorting options.
|
|
58
|
+
canSort?: boolean
|
|
59
|
+
|
|
60
|
+
// Whether to hide the condition blocks.
|
|
61
|
+
hideConditions?: boolean
|
|
62
|
+
|
|
63
|
+
// Field name to be used as index field for selection.
|
|
64
|
+
indexField?: string
|
|
65
|
+
|
|
66
|
+
// Fields that are clickable to emit `cell-clicked` event when clicked.
|
|
67
|
+
clickableFields?: string[]
|
|
68
|
+
|
|
69
|
+
// Whether to show border around the catalog.
|
|
70
|
+
border?: boolean
|
|
71
|
+
|
|
72
|
+
// Padding to add around various blocks in the catalog. This padding only
|
|
73
|
+
// applies to the X axis (left and right). The default is `16px`.
|
|
74
|
+
padding?: string
|
|
75
|
+
|
|
76
|
+
// Height of the catalog. When set, it will set max height of the table
|
|
77
|
+
// thereby enabling vertical scroll within the table.
|
|
78
|
+
//
|
|
79
|
+
// Available values:
|
|
80
|
+
// - `fill`: Fill the entire visible page.
|
|
81
|
+
// - Any valid CSS height value (e.g. `480px`, `calc(100vh - 48px)`, etc.)
|
|
82
|
+
height?: string
|
|
83
|
+
|
|
84
|
+
// Height offset to be subtracted when `height` is set to `fill`. This is
|
|
85
|
+
// useful when you have other fixed height elements on the page such as
|
|
86
|
+
// extra control headers, nav bars, etc. on top of the lens catalog.
|
|
87
|
+
heightOffset?: string
|
|
88
|
+
|
|
89
|
+
// Whether to show empty state when there is no data. When this is enabled,
|
|
90
|
+
// the catalog will hide entire catalog component and renders the given
|
|
91
|
+
// `#empty-state` slot instead.
|
|
92
|
+
showEmptyState?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
96
|
+
canFilter: true,
|
|
97
|
+
canSort: true
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const selected = defineModel<number[]>('selected')
|
|
101
|
+
const hideConditions = defineModel<boolean>('hideConditions', { default: false })
|
|
102
|
+
|
|
103
|
+
const emit = defineEmits<{
|
|
104
|
+
'select-updated': [select: string[]]
|
|
105
|
+
'selectable-updated': [selectable: string[]]
|
|
106
|
+
'filters-updated': [filters: any[]]
|
|
107
|
+
'sort-updated': [sort: LensQuerySort[]]
|
|
108
|
+
'overrides-updated': [overrides: Record<string, Partial<FieldData>>]
|
|
109
|
+
'cell-clicked': [value: any, record: any]
|
|
110
|
+
}>()
|
|
111
|
+
|
|
112
|
+
const filterDialog = usePower()
|
|
113
|
+
const viewDialog = usePower()
|
|
114
|
+
|
|
115
|
+
const conditionBlocksEl = ref<HTMLElement | null>(null)
|
|
116
|
+
|
|
117
|
+
// Whether the catalog has loaded initial results at least once. This
|
|
118
|
+
// determines whether to show empty state or not. Because empty state
|
|
119
|
+
// should only be shown when there is no data from the beginning, not
|
|
120
|
+
// after some data has been loaded once and then resulted in no data
|
|
121
|
+
// due to user filtering.
|
|
122
|
+
const hasInitialResults = ref(false)
|
|
123
|
+
|
|
124
|
+
let prevFetchInput: LensQuery | null = null
|
|
125
|
+
let prevFetchResult: LensResult | null = null
|
|
126
|
+
|
|
127
|
+
const _select = ref(props.select ?? [])
|
|
128
|
+
const _selectable = ref(props.selectable ?? props.select ?? [])
|
|
129
|
+
|
|
130
|
+
const query = ref<string | null>(null)
|
|
131
|
+
|
|
132
|
+
const queryFilter = computed(() => {
|
|
133
|
+
if (!props.queryKeys || props.queryKeys.length === 0 || !query.value) {
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
return ['$or', props.queryKeys.map((key) => {
|
|
137
|
+
return [key, 'like', `%${query.value}%`]
|
|
138
|
+
})]
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const _filters = ref(props.filters ?? [])
|
|
142
|
+
|
|
143
|
+
const _sort = ref<LensQuerySort[]>([])
|
|
144
|
+
const defaultSort = ref(props.sort ?? [])
|
|
145
|
+
|
|
146
|
+
const _overrides = ref(props.overrides ?? {})
|
|
147
|
+
|
|
148
|
+
const page = ref(1)
|
|
149
|
+
const perPage = ref(100)
|
|
150
|
+
|
|
151
|
+
const { data: result, execute: refresh, loading } = useQuery(async (http) => {
|
|
152
|
+
const input = {
|
|
153
|
+
entity: props.entity ?? '__no_entity__',
|
|
154
|
+
select: _select.value,
|
|
155
|
+
filters: createInputFilters(queryFilter.value, _filters.value),
|
|
156
|
+
sort: _sort.value.length > 0 ? _sort.value : defaultSort.value ?? [],
|
|
157
|
+
page: page.value,
|
|
158
|
+
perPage: perPage.value
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (prevFetchInput && JSON.stringify(prevFetchInput) === JSON.stringify(input)) {
|
|
162
|
+
return prevFetchResult!
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const res = await http.post<LensResult>(props.endpoint, input)
|
|
166
|
+
|
|
167
|
+
prevFetchInput = input
|
|
168
|
+
prevFetchResult = res
|
|
169
|
+
|
|
170
|
+
return res
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const doRefresh = useDebounceFn(refresh, 50)
|
|
174
|
+
|
|
175
|
+
const _showEmptyState = computed(() => {
|
|
176
|
+
return props.showEmptyState && !hasInitialResults.value && result.value?.data.length === 0
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const hasConditions = computed(() => {
|
|
180
|
+
return _filters.value.length > 0 || _sort.value.length > 0
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const borderWidth = computed(() => {
|
|
184
|
+
return props.border ? 'var(--lens-catalog-border-width, 1px)' : '0'
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const paddings = computed(() => ({
|
|
188
|
+
'--lens-catalog-control-padding': `0 ${props.padding ?? '16px'}`,
|
|
189
|
+
'--lens-catalog-filters-block-padding': `12px ${props.padding ?? '16px'}`,
|
|
190
|
+
'--lens-catalog-sorts-block-padding': `12px ${props.padding ?? '16px'}`,
|
|
191
|
+
'--lens-catalog-footer-padding': `0 ${props.padding ?? '16px'}`,
|
|
192
|
+
'--table-padding-left': `calc(${props.padding ?? '16px'} - 16px)`
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
const conditionBlocksSize = useElementSize(conditionBlocksEl)
|
|
196
|
+
|
|
197
|
+
const headerHeight = 'var(--lens-catalog-global-height-offset)'
|
|
198
|
+
const controlHeight = '48px - 1px'
|
|
199
|
+
const conditionBlocksHeight = computed(() => conditionBlocksSize.height.value > 0 ? `${conditionBlocksSize.height.value}px - 1px` : '0px')
|
|
200
|
+
const columnsHeight = '40px - 1px'
|
|
201
|
+
const footerHeight = '56px - 1px'
|
|
202
|
+
|
|
203
|
+
const containerMinHeight = computed(() => {
|
|
204
|
+
if (!props.height) {
|
|
205
|
+
return undefined
|
|
206
|
+
}
|
|
207
|
+
if (props.height === 'fill') {
|
|
208
|
+
return { minHeight: `calc(100vh - ${headerHeight} - ${props.heightOffset ?? '0px'})` }
|
|
209
|
+
}
|
|
210
|
+
return { height: props.height, minHeight: props.height, maxHeight: props.height }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const containerStyles = computed(() => {
|
|
214
|
+
return {
|
|
215
|
+
borderWidth: borderWidth.value,
|
|
216
|
+
...paddings.value,
|
|
217
|
+
...(containerMinHeight.value ?? {})
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const tableMaxHeight = computed(() => {
|
|
222
|
+
if (!props.height) {
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
if (props.height === 'fill') {
|
|
226
|
+
return `--table-max-height: calc(100vh - ${headerHeight} - ${controlHeight} - ${conditionBlocksHeight.value} - ${columnsHeight} - ${footerHeight} - ${props.heightOffset ?? '0px'})`
|
|
227
|
+
}
|
|
228
|
+
return `--table-max-height: calc(${props.height} - ${controlHeight} - ${conditionBlocksHeight.value} - ${columnsHeight} - ${footerHeight})`
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Initial setup when the result is loaded for the first time.
|
|
232
|
+
watch(result, (res) => {
|
|
233
|
+
if (!hasInitialResults.value && res!.data.length > 0) {
|
|
234
|
+
hasInitialResults.value = true
|
|
235
|
+
}
|
|
236
|
+
if (_select.value.length === 0) {
|
|
237
|
+
_select.value = res!.query.select
|
|
238
|
+
}
|
|
239
|
+
if (_selectable.value.length === 0) {
|
|
240
|
+
_selectable.value = res!.query.select
|
|
241
|
+
}
|
|
242
|
+
}, { once: true })
|
|
243
|
+
|
|
244
|
+
// Create lens filters option by combining query (free search) filters,
|
|
245
|
+
// user selected filters, and fixed filters.
|
|
246
|
+
function createInputFilters(queryFilters: any[], filters: any[]) {
|
|
247
|
+
return [
|
|
248
|
+
...(props.fixedFilters ?? []),
|
|
249
|
+
queryFilters,
|
|
250
|
+
...filters
|
|
251
|
+
].filter((f) => f?.length > 0)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onQuery(value: string | null) {
|
|
255
|
+
query.value = value
|
|
256
|
+
page.value = 1
|
|
257
|
+
doRefresh()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function onFiltersUpdated(filters: any[]) {
|
|
261
|
+
_filters.value = filters
|
|
262
|
+
page.value = 1
|
|
263
|
+
doRefresh()
|
|
264
|
+
filterDialog.off()
|
|
265
|
+
emit('filters-updated', _filters.value)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onInlineFilterUpdated(filter: any[]) {
|
|
269
|
+
const sameFilterIndex = _filters.value.findIndex((f) => f[0] === filter[0])
|
|
270
|
+
sameFilterIndex === -1 ? applyNewFilter(filter) : replaceFilter(sameFilterIndex, filter)
|
|
271
|
+
page.value = 1
|
|
272
|
+
doRefresh()
|
|
273
|
+
emit('filters-updated', _filters.value)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function applyNewFilter(filter: any[]) {
|
|
277
|
+
if (filter[2].length > 0) {
|
|
278
|
+
_filters.value.push(filter)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function replaceFilter(index: number, filter: any[]) {
|
|
283
|
+
if (filter[2].length === 0) {
|
|
284
|
+
_filters.value.splice(index, 1)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
_filters.value[index] = filter
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function onResetFilters() {
|
|
291
|
+
_filters.value = []
|
|
292
|
+
query.value = null
|
|
293
|
+
page.value = 1
|
|
294
|
+
doRefresh()
|
|
295
|
+
emit('filters-updated', _filters.value)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function onSortUpdated(sort: LensQuerySort) {
|
|
299
|
+
_sort.value = [sort]
|
|
300
|
+
page.value = 1
|
|
301
|
+
doRefresh()
|
|
302
|
+
emit('sort-updated', _sort.value)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function onResetSorts() {
|
|
306
|
+
_sort.value = []
|
|
307
|
+
page.value = 1
|
|
308
|
+
doRefresh()
|
|
309
|
+
emit('sort-updated', _sort.value)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function onViewUpdated(newSelect: string[], newSelectable: string[], overrides: Record<string, Partial<FieldData>>) {
|
|
313
|
+
_select.value = newSelect
|
|
314
|
+
_selectable.value = newSelectable
|
|
315
|
+
_overrides.value = overrides
|
|
316
|
+
doRefresh()
|
|
317
|
+
viewDialog.off()
|
|
318
|
+
emit('select-updated', _select.value)
|
|
319
|
+
emit('selectable-updated', _selectable.value)
|
|
320
|
+
emit('overrides-updated', _overrides.value)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function onUpdateSelected(value: number[]) {
|
|
324
|
+
selected.value = value
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function onResetSelection() {
|
|
328
|
+
selected.value = []
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function onPrev() {
|
|
332
|
+
page.value--
|
|
333
|
+
doRefresh()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function onNext() {
|
|
337
|
+
page.value++
|
|
338
|
+
doRefresh()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
defineExpose({
|
|
342
|
+
/**
|
|
343
|
+
* Mutates the fetched records directly. This exposed method can be used to
|
|
344
|
+
* do in-place updates of records from the parent component. However, it's
|
|
345
|
+
* very hacky and should be avoided if possible. We are doing it like this
|
|
346
|
+
* because we couldn't come up with better alternative at the moment.
|
|
347
|
+
*/
|
|
348
|
+
updateRecords(fn: (records: Record<string, any>[]) => void): void {
|
|
349
|
+
if (result.value?.data) {
|
|
350
|
+
fn(result.value.data)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
</script>
|
|
355
|
+
|
|
356
|
+
<template>
|
|
357
|
+
<SCard class="LensCatalog" :class="{ 'show-empty-state': _showEmptyState }" :style="containerStyles">
|
|
358
|
+
<template v-if="_showEmptyState">
|
|
359
|
+
<slot name="empty-state" />
|
|
360
|
+
</template>
|
|
361
|
+
|
|
362
|
+
<div v-else class="container">
|
|
363
|
+
<LensCatalogControl
|
|
364
|
+
v-if="result"
|
|
365
|
+
:query
|
|
366
|
+
:query-ph
|
|
367
|
+
:filter-presets
|
|
368
|
+
:selected
|
|
369
|
+
:show-query="!!(queryKeys && queryKeys.length > 0)"
|
|
370
|
+
:show-filters="canFilter"
|
|
371
|
+
:show-sort="canSort"
|
|
372
|
+
:is-condition-active="!hideConditions"
|
|
373
|
+
:is-condition-disabled="!hasConditions"
|
|
374
|
+
@search="onQuery"
|
|
375
|
+
@filter="filterDialog.on()"
|
|
376
|
+
@filter-preset-selected="onFiltersUpdated"
|
|
377
|
+
@view="viewDialog.on()"
|
|
378
|
+
@reset-selection="onResetSelection"
|
|
379
|
+
@toggle-conditions="hideConditions = !hideConditions"
|
|
380
|
+
>
|
|
381
|
+
<template v-if="$slots['controls-sub-left']" #sub-left>
|
|
382
|
+
<slot name="controls-sub-left" />
|
|
383
|
+
</template>
|
|
384
|
+
<template v-if="$slots['controls-sub-right']" #sub-right>
|
|
385
|
+
<slot name="controls-sub-right" />
|
|
386
|
+
</template>
|
|
387
|
+
</LensCatalogControl>
|
|
388
|
+
<div v-else class="control-skeleton" />
|
|
389
|
+
<div v-if="!hideConditions && result && (_filters.length > 0 || _sort.length > 0)" ref="conditionBlocksEl" class="condition-blocks">
|
|
390
|
+
<LensCatalogStateFilter
|
|
391
|
+
v-if="_filters.length > 0"
|
|
392
|
+
:filters="_filters"
|
|
393
|
+
:fields="result.fields"
|
|
394
|
+
@reset="onResetFilters"
|
|
395
|
+
/>
|
|
396
|
+
<LensCatalogStateSort
|
|
397
|
+
v-if="_sort.length > 0"
|
|
398
|
+
:fields="result.fields"
|
|
399
|
+
:sorts="_sort"
|
|
400
|
+
@reset="onResetSorts"
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
<SCardBlock class="list" :style="tableMaxHeight">
|
|
404
|
+
<LensTable
|
|
405
|
+
:result
|
|
406
|
+
:loading
|
|
407
|
+
:overrides="_overrides"
|
|
408
|
+
:index-field
|
|
409
|
+
:selected
|
|
410
|
+
:clickable-fields
|
|
411
|
+
@filter-updated="onInlineFilterUpdated"
|
|
412
|
+
@sort-updated="onSortUpdated"
|
|
413
|
+
@update:selected="onUpdateSelected"
|
|
414
|
+
@cell-clicked="(v, r) => emit('cell-clicked', v, r)"
|
|
415
|
+
/>
|
|
416
|
+
<LensCatalogFooter
|
|
417
|
+
v-if="result && result?.pagination.total > 0"
|
|
418
|
+
:result
|
|
419
|
+
:loading
|
|
420
|
+
@prev="onPrev"
|
|
421
|
+
@next="onNext"
|
|
422
|
+
/>
|
|
423
|
+
</SCardBlock>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<SModal :open="filterDialog.state.value" @close="filterDialog.off">
|
|
427
|
+
<LensFormFilter
|
|
428
|
+
v-if="result?.fields"
|
|
429
|
+
:fields="result.fields"
|
|
430
|
+
:filters="_filters"
|
|
431
|
+
:filterable="_selectable"
|
|
432
|
+
@cancel="filterDialog.off"
|
|
433
|
+
@apply="onFiltersUpdated"
|
|
434
|
+
/>
|
|
435
|
+
</SModal>
|
|
436
|
+
|
|
437
|
+
<SModal :open="viewDialog.state.value" @close="viewDialog.off">
|
|
438
|
+
<LensFormView
|
|
439
|
+
v-if="result?.fields"
|
|
440
|
+
:select="_select"
|
|
441
|
+
:selectable="_selectable"
|
|
442
|
+
:fields="result.fields"
|
|
443
|
+
:overrides="_overrides"
|
|
444
|
+
@cancel="viewDialog.off"
|
|
445
|
+
@apply="onViewUpdated"
|
|
446
|
+
/>
|
|
447
|
+
</SModal>
|
|
448
|
+
</SCard>
|
|
449
|
+
</template>
|
|
450
|
+
|
|
451
|
+
<style scoped lang="postcss">
|
|
452
|
+
.LensCatalog {
|
|
453
|
+
--c-bg-elv-2: var(--c-bg-1);
|
|
454
|
+
--c-bg-elv-3: var(--c-bg-1);
|
|
455
|
+
--c-bg-elv-4: var(--c-bg-2);
|
|
456
|
+
|
|
457
|
+
flex-grow: 1;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.LensCatalog.show-empty-state {
|
|
461
|
+
border-style: dashed;
|
|
462
|
+
background-color: var(--c-bg-1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.container {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: 1px;
|
|
469
|
+
min-height: 100%;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.list {
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
flex-grow: 1;
|
|
476
|
+
border-radius: 0 0 5px 5px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.control-skeleton {
|
|
480
|
+
height: 48px;
|
|
481
|
+
background-color: var(--c-bg-1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.condition-blocks {
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
gap: 1px;
|
|
488
|
+
background-color: var(--c-gutter);
|
|
489
|
+
}
|
|
490
|
+
</style>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import IconBook from '~icons/ph/book'
|
|
3
|
+
import IconBookOpenText from '~icons/ph/book-open-text'
|
|
4
|
+
import IconFunnelSimple from '~icons/ph/funnel-simple'
|
|
5
|
+
import IconLightning from '~icons/ph/lightning'
|
|
6
|
+
import IconMagnifyingGlass from '~icons/ph/magnifying-glass'
|
|
7
|
+
import IconSlidersHorizontal from '~icons/ph/sliders-horizontal'
|
|
8
|
+
import IconX from '~icons/ph/x'
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
import { type ActionList } from '../../../components/SActionList.vue'
|
|
11
|
+
import SActionMenu from '../../../components/SActionMenu.vue'
|
|
12
|
+
import SButton from '../../../components/SButton.vue'
|
|
13
|
+
import SInputText from '../../../components/SInputText.vue'
|
|
14
|
+
import { type DropdownSectionMenu } from '../../../composables/Dropdown'
|
|
15
|
+
import { useTrans } from '../../../composables/Lang'
|
|
16
|
+
|
|
17
|
+
export interface Props {
|
|
18
|
+
queryPh?: string
|
|
19
|
+
filterPresets?: FilterPresets[]
|
|
20
|
+
selected?: number[]
|
|
21
|
+
showQuery?: boolean
|
|
22
|
+
showFilters?: boolean
|
|
23
|
+
isConditionActive?: boolean
|
|
24
|
+
isConditionDisabled?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FilterPresets {
|
|
28
|
+
label: string
|
|
29
|
+
filters: any[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = defineProps<Props>()
|
|
33
|
+
|
|
34
|
+
const query = defineModel<string | null>('query', { required: true })
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits<{
|
|
37
|
+
'search': [query: string | null]
|
|
38
|
+
'filter': []
|
|
39
|
+
'filter-preset-selected': [filters: any[]]
|
|
40
|
+
'view': []
|
|
41
|
+
'reset-selection': []
|
|
42
|
+
'toggle-conditions': []
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const { t } = useTrans({
|
|
46
|
+
en: {
|
|
47
|
+
i_query_ph: 'Search records',
|
|
48
|
+
a_filter_preset: 'Quick filters',
|
|
49
|
+
a_filter: 'Filters',
|
|
50
|
+
a_view: 'View',
|
|
51
|
+
a_conditions: 'View current conditions',
|
|
52
|
+
selected_text: (c: number) => `${c} selected`
|
|
53
|
+
},
|
|
54
|
+
ja: {
|
|
55
|
+
i_query_ph: 'レコードを検索',
|
|
56
|
+
a_filter_preset: 'クイックフィルター',
|
|
57
|
+
a_conditions: '現在の検索条件を表示',
|
|
58
|
+
a_filter: '詳細検索',
|
|
59
|
+
a_view: '表示',
|
|
60
|
+
selected_text: (c: number) => `${c}件選択中`
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const presetOptions = computed<DropdownSectionMenu[]>(() => {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
type: 'menu',
|
|
68
|
+
options: createFilterPresetOptions()
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function createFilterPresetOptions(): ActionList {
|
|
74
|
+
return props.filterPresets?.map((preset) => {
|
|
75
|
+
return {
|
|
76
|
+
label: preset.label,
|
|
77
|
+
onClick: () => { emit('filter-preset-selected', preset.filters) }
|
|
78
|
+
}
|
|
79
|
+
}) || []
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<SCardBlock class="LensCatalogControl">
|
|
85
|
+
<template v-if="!selected || selected.length === 0">
|
|
86
|
+
<div class="main">
|
|
87
|
+
<SInputText
|
|
88
|
+
v-if="showQuery"
|
|
89
|
+
class="s-w-320"
|
|
90
|
+
size="sm"
|
|
91
|
+
:unit-before="IconMagnifyingGlass"
|
|
92
|
+
:placeholder="queryPh ?? t.i_query_ph"
|
|
93
|
+
:model-value="query"
|
|
94
|
+
@enter="$emit('search', $event)"
|
|
95
|
+
/>
|
|
96
|
+
<SActionMenu
|
|
97
|
+
v-if="filterPresets?.length"
|
|
98
|
+
type="outline"
|
|
99
|
+
size="sm"
|
|
100
|
+
:icon="IconLightning"
|
|
101
|
+
:label="t.a_filter_preset"
|
|
102
|
+
:options="presetOptions"
|
|
103
|
+
/>
|
|
104
|
+
<SButton
|
|
105
|
+
v-if="showFilters"
|
|
106
|
+
type="outline"
|
|
107
|
+
size="sm"
|
|
108
|
+
:icon="IconFunnelSimple"
|
|
109
|
+
:label="t.a_filter"
|
|
110
|
+
@click="$emit('filter')"
|
|
111
|
+
/>
|
|
112
|
+
<SButton
|
|
113
|
+
type="outline"
|
|
114
|
+
size="sm"
|
|
115
|
+
:icon="IconSlidersHorizontal"
|
|
116
|
+
:label="t.a_view"
|
|
117
|
+
@click="$emit('view')"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="sub">
|
|
121
|
+
<slot name="sub-left" />
|
|
122
|
+
<SButton
|
|
123
|
+
type="outline"
|
|
124
|
+
size="sm"
|
|
125
|
+
:icon="isConditionActive ? IconBookOpenText : IconBook"
|
|
126
|
+
:disabled="isConditionDisabled"
|
|
127
|
+
:tooltip="t.a_conditions"
|
|
128
|
+
@click="$emit('toggle-conditions')"
|
|
129
|
+
/>
|
|
130
|
+
<slot name="sub-right" />
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
<template v-else>
|
|
134
|
+
<div class="selected">
|
|
135
|
+
<div class="selected-box">
|
|
136
|
+
<div class="selected-text">
|
|
137
|
+
{{ t.selected_text(selected.length) }}
|
|
138
|
+
</div>
|
|
139
|
+
<button class="selected-reset" @click="$emit('reset-selection')">
|
|
140
|
+
<IconX class="selected-reset-icon" />
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
145
|
+
</SCardBlock>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<style scoped lang="postcss">
|
|
149
|
+
.LensCatalogControl {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
border-radius: 5px 5px 0 0;
|
|
153
|
+
padding: var(--lens-catalog-control-padding, 0 12px);
|
|
154
|
+
height: 48px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.main {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
flex-grow: 1;
|
|
161
|
+
gap: 8px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.selected {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.selected-box {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
border: 1px dashed var(--c-divider);
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
height: 32px;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.selected-text {
|
|
180
|
+
border-right: 1px dashed var(--c-divider);
|
|
181
|
+
padding: 0 10px;
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-feature-settings: "tnum";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.selected-reset {
|
|
187
|
+
display: flex;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
align-items: center;
|
|
190
|
+
width: 32px;
|
|
191
|
+
height: 32px;
|
|
192
|
+
transition: background-color 0.25s;
|
|
193
|
+
|
|
194
|
+
&:hover {
|
|
195
|
+
background-color: var(--c-bg-2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
&:active {
|
|
199
|
+
background-color: var(--c-bg-3);
|
|
200
|
+
transition: background-color 0.1s;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.selected-reset-icon {
|
|
205
|
+
width: 14px;
|
|
206
|
+
height: 14px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.selected-divider {
|
|
210
|
+
width: 1px;
|
|
211
|
+
height: 24px;
|
|
212
|
+
background-color: var(--c-divider);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.sub {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
}
|
|
220
|
+
</style>
|