@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.
- package/config/nuxt.js +44 -1
- package/config/vite.js +2 -3
- 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,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>
|