frappe-ui 0.1.7 → 0.1.8
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/package.json +1 -1
- package/src/components/Dialog.vue +1 -1
- package/src/components/ListFilter/FilterIcon.vue +31 -0
- package/src/components/ListFilter/ListFilter.vue +292 -0
- package/src/components/ListFilter/NestedPopover.vue +60 -0
- package/src/components/ListFilter/SearchComplete.vue +85 -0
- package/src/components/ListView/ListHeader.vue +7 -4
- package/src/components/ListView/ListHeaderItem.vue +5 -5
- package/src/components/ListView/ListRow.vue +23 -23
- package/src/components/ListView/ListRowItem.vue +7 -4
- package/src/components/ListView/ListRows.vue +1 -6
- package/src/components/ListView/ListSelectBanner.vue +1 -1
- package/src/components/ListView/ListView.vue +25 -2
- package/src/components/ListView/utils.js +12 -13
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
width="16"
|
|
4
|
+
height="17"
|
|
5
|
+
viewBox="0 0 16 17"
|
|
6
|
+
fill="none"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
>
|
|
9
|
+
<path
|
|
10
|
+
d="M2 4.5H14"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
stroke-miterlimit="10"
|
|
13
|
+
stroke-linecap="round"
|
|
14
|
+
stroke-linejoin="round"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M4 8.5H12"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
stroke-miterlimit="10"
|
|
20
|
+
stroke-linecap="round"
|
|
21
|
+
stroke-linejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="M6.5 12.5H9.5"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
stroke-miterlimit="10"
|
|
27
|
+
stroke-linecap="round"
|
|
28
|
+
stroke-linejoin="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NestedPopover>
|
|
3
|
+
<template #target>
|
|
4
|
+
<Button label="Filter">
|
|
5
|
+
<template #prefix><FilterIcon class="h-4" /></template>
|
|
6
|
+
<template v-if="filters.size" #suffix>
|
|
7
|
+
<div
|
|
8
|
+
class="flex h-5 w-5 items-center justify-center rounded bg-gray-900 pt-[1px] text-2xs font-medium text-white"
|
|
9
|
+
>
|
|
10
|
+
{{ filters.size }}
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
</Button>
|
|
14
|
+
</template>
|
|
15
|
+
<template #body="{ close }">
|
|
16
|
+
<div class="my-2 rounded-lg border border-gray-100 bg-white shadow-xl">
|
|
17
|
+
<div class="min-w-[400px] p-2">
|
|
18
|
+
<div
|
|
19
|
+
v-if="filters.length"
|
|
20
|
+
v-for="(filter, i) in filters"
|
|
21
|
+
:key="i"
|
|
22
|
+
id="filter-list"
|
|
23
|
+
class="mb-3 flex items-center justify-between gap-2"
|
|
24
|
+
>
|
|
25
|
+
<div class="flex flex-1 items-center gap-2">
|
|
26
|
+
<div
|
|
27
|
+
class="w-13 flex-shrink-0 pl-2 text-end text-base text-gray-600"
|
|
28
|
+
>
|
|
29
|
+
{{ i == 0 ? 'Where' : 'And' }}
|
|
30
|
+
</div>
|
|
31
|
+
<div id="fieldname" class="!min-w-[140px] flex-1">
|
|
32
|
+
<Autocomplete
|
|
33
|
+
:value="filter.fieldname"
|
|
34
|
+
:options="fields"
|
|
35
|
+
@change="filter.fieldname = $event.value"
|
|
36
|
+
placeholder="Filter by..."
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="operator" class="!min-w-[140px] flex-shrink-0">
|
|
40
|
+
<FormControl
|
|
41
|
+
type="select"
|
|
42
|
+
:modelValue="filter.operator"
|
|
43
|
+
@update:modelValue="filter.operator = $event.value"
|
|
44
|
+
:options="getOperators(filter.field.fieldtype)"
|
|
45
|
+
placeholder="Operator"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div id="value" class="!min-w-[140px] flex-1">
|
|
49
|
+
<SearchComplete
|
|
50
|
+
v-if="
|
|
51
|
+
typeLink.includes(filter.field.fieldtype) &&
|
|
52
|
+
['=', '!='].includes(filter.operator)
|
|
53
|
+
"
|
|
54
|
+
:doctype="filter.field.options"
|
|
55
|
+
:value="filter.value"
|
|
56
|
+
@change="filter.value = $event.value"
|
|
57
|
+
placeholder="Value"
|
|
58
|
+
/>
|
|
59
|
+
<component
|
|
60
|
+
v-else
|
|
61
|
+
:is="
|
|
62
|
+
getValueSelector(
|
|
63
|
+
filter.field.fieldtype,
|
|
64
|
+
filter.field.options
|
|
65
|
+
)
|
|
66
|
+
"
|
|
67
|
+
v-model="filter.value"
|
|
68
|
+
placeholder="Value"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="flex-shrink-0">
|
|
73
|
+
<Button variant="ghost" icon="x" @click="removeFilter(i)" />
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div
|
|
77
|
+
v-else
|
|
78
|
+
class="mb-3 flex h-7 items-center px-3 text-sm text-gray-600"
|
|
79
|
+
>
|
|
80
|
+
Empty - Choose a field to filter by
|
|
81
|
+
</div>
|
|
82
|
+
<div class="flex items-center justify-between gap-2">
|
|
83
|
+
<Autocomplete
|
|
84
|
+
value=""
|
|
85
|
+
:options="fields"
|
|
86
|
+
@change="(field) => addFilter(field.value)"
|
|
87
|
+
placeholder="Filter by..."
|
|
88
|
+
>
|
|
89
|
+
<template #target="{ togglePopover }">
|
|
90
|
+
<Button
|
|
91
|
+
class="!text-gray-600"
|
|
92
|
+
variant="ghost"
|
|
93
|
+
@click="togglePopover()"
|
|
94
|
+
label="Add filter"
|
|
95
|
+
>
|
|
96
|
+
<template #prefix>
|
|
97
|
+
<FeatherIcon name="plus" class="h-4" />
|
|
98
|
+
</template>
|
|
99
|
+
</Button>
|
|
100
|
+
</template>
|
|
101
|
+
</Autocomplete>
|
|
102
|
+
<Button
|
|
103
|
+
v-if="filters.length"
|
|
104
|
+
class="!text-gray-600"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
label="Clear all filter"
|
|
107
|
+
@click="filters = []"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</template>
|
|
113
|
+
</NestedPopover>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<script setup>
|
|
117
|
+
import { Autocomplete, FeatherIcon, FormControl } from 'frappe-ui'
|
|
118
|
+
import { computed, h } from 'vue'
|
|
119
|
+
import FilterIcon from './FilterIcon.vue'
|
|
120
|
+
import NestedPopover from './NestedPopover.vue'
|
|
121
|
+
import SearchComplete from './SearchComplete.vue'
|
|
122
|
+
|
|
123
|
+
const typeCheck = ['Check']
|
|
124
|
+
const typeLink = ['Link']
|
|
125
|
+
const typeNumber = ['Float', 'Int']
|
|
126
|
+
const typeSelect = ['Select']
|
|
127
|
+
const typeString = [
|
|
128
|
+
'Data',
|
|
129
|
+
'Long Text',
|
|
130
|
+
'Small Text',
|
|
131
|
+
'Text Editor',
|
|
132
|
+
'Text',
|
|
133
|
+
'JSON',
|
|
134
|
+
'Code',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const emits = defineEmits(['update:modelValue'])
|
|
138
|
+
const props = defineProps({
|
|
139
|
+
modelValue: {
|
|
140
|
+
type: Object,
|
|
141
|
+
default: () => ({}),
|
|
142
|
+
},
|
|
143
|
+
docfields: {
|
|
144
|
+
type: Array,
|
|
145
|
+
default: () => [],
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const fields = computed(() => {
|
|
150
|
+
const fields = props.docfields
|
|
151
|
+
.filter((field) => {
|
|
152
|
+
return (
|
|
153
|
+
!field.is_virtual &&
|
|
154
|
+
(typeCheck.includes(field.fieldtype) ||
|
|
155
|
+
typeLink.includes(field.fieldtype) ||
|
|
156
|
+
typeNumber.includes(field.fieldtype) ||
|
|
157
|
+
typeSelect.includes(field.fieldtype) ||
|
|
158
|
+
typeString.includes(field.fieldtype))
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
.map((field) => {
|
|
162
|
+
return {
|
|
163
|
+
label: field.label,
|
|
164
|
+
value: field.fieldname,
|
|
165
|
+
description: field.fieldtype,
|
|
166
|
+
...field,
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
return fields
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const filters = computed({
|
|
173
|
+
get: () => makeFiltersList(props.modelValue),
|
|
174
|
+
set: (value) => emits('update:modelValue', makeFiltersDict(value)),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
function makeFiltersList(filtersDict) {
|
|
178
|
+
return Object.entries(filtersDict).map(([fieldname, [operator, value]]) => {
|
|
179
|
+
const field = getField(fieldname)
|
|
180
|
+
return {
|
|
181
|
+
fieldname,
|
|
182
|
+
operator,
|
|
183
|
+
value,
|
|
184
|
+
field,
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getField(fieldname) {
|
|
190
|
+
return fields.value.find((f) => f.fieldname === fieldname)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function makeFiltersDict(filtersList) {
|
|
194
|
+
return filtersList.reduce((acc, filter) => {
|
|
195
|
+
const { fieldname, operator, value } = filter
|
|
196
|
+
acc[fieldname] = [operator, value]
|
|
197
|
+
return acc
|
|
198
|
+
}, {})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getOperators(fieldtype) {
|
|
202
|
+
let options = []
|
|
203
|
+
if (typeString.includes(fieldtype) || typeLink.includes(fieldtype)) {
|
|
204
|
+
options.push(
|
|
205
|
+
...[
|
|
206
|
+
{ label: 'Equals', value: '=' },
|
|
207
|
+
{ label: 'Not Equals', value: '!=' },
|
|
208
|
+
{ label: 'Like', value: 'like' },
|
|
209
|
+
{ label: 'Not Like', value: 'not like' },
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
if (typeNumber.includes(fieldtype)) {
|
|
214
|
+
options.push(
|
|
215
|
+
...[
|
|
216
|
+
{ label: '<', value: '<' },
|
|
217
|
+
{ label: '>', value: '>' },
|
|
218
|
+
{ label: '<=', value: '<=' },
|
|
219
|
+
{ label: '>=', value: '>=' },
|
|
220
|
+
{ label: 'Equals', value: '=' },
|
|
221
|
+
{ label: 'Not Equals', value: '!=' },
|
|
222
|
+
]
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
if (typeSelect.includes(fieldtype)) {
|
|
226
|
+
options.push(
|
|
227
|
+
...[
|
|
228
|
+
{ label: 'Equals', value: '=' },
|
|
229
|
+
{ label: 'Not Equals', value: '!=' },
|
|
230
|
+
]
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
if (typeCheck.includes(fieldtype)) {
|
|
234
|
+
options.push(...[{ label: 'Equals', value: '=' }])
|
|
235
|
+
}
|
|
236
|
+
return options
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getDefaultOperator(fieldtype) {
|
|
240
|
+
if (
|
|
241
|
+
typeSelect.includes(fieldtype) ||
|
|
242
|
+
typeLink.includes(fieldtype) ||
|
|
243
|
+
typeCheck.includes(fieldtype) ||
|
|
244
|
+
typeNumber.includes(fieldtype)
|
|
245
|
+
) {
|
|
246
|
+
return '='
|
|
247
|
+
}
|
|
248
|
+
return 'like'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getValueSelector(fieldtype, options) {
|
|
252
|
+
if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
|
|
253
|
+
const _options =
|
|
254
|
+
fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
|
|
255
|
+
return h(FormControl, {
|
|
256
|
+
type: 'select',
|
|
257
|
+
options: _options,
|
|
258
|
+
})
|
|
259
|
+
} else {
|
|
260
|
+
return h(FormControl, { type: 'text' })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getDefaultValue(field) {
|
|
265
|
+
if (typeSelect.includes(field.fieldtype)) {
|
|
266
|
+
return getSelectOptions(field.options)[0]
|
|
267
|
+
}
|
|
268
|
+
if (typeCheck.includes(field.fieldtype)) {
|
|
269
|
+
return 'Yes'
|
|
270
|
+
}
|
|
271
|
+
return ''
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getSelectOptions(options) {
|
|
275
|
+
return options.split('\n')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function addFilter(fieldname) {
|
|
279
|
+
const field = getField(fieldname)
|
|
280
|
+
const filter = {
|
|
281
|
+
fieldname,
|
|
282
|
+
operator: getDefaultOperator(field.fieldtype),
|
|
283
|
+
value: getDefaultValue(field),
|
|
284
|
+
field,
|
|
285
|
+
}
|
|
286
|
+
filters.value = [...filters.value, filter]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function removeFilter(index) {
|
|
290
|
+
filters.value = filters.value.filter((_, i) => i !== index)
|
|
291
|
+
}
|
|
292
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover v-slot="{ open }">
|
|
3
|
+
<PopoverButton
|
|
4
|
+
as="div"
|
|
5
|
+
ref="reference"
|
|
6
|
+
@click="updatePosition"
|
|
7
|
+
@focusin="updatePosition"
|
|
8
|
+
@keydown="updatePosition"
|
|
9
|
+
v-slot="{ open }"
|
|
10
|
+
>
|
|
11
|
+
<slot name="target" v-bind="{ open }" />
|
|
12
|
+
</PopoverButton>
|
|
13
|
+
<div v-show="open">
|
|
14
|
+
<PopoverPanel
|
|
15
|
+
v-slot="{ open, close }"
|
|
16
|
+
ref="popover"
|
|
17
|
+
static
|
|
18
|
+
class="z-[100]"
|
|
19
|
+
>
|
|
20
|
+
<slot name="body" v-bind="{ open, close }" />
|
|
21
|
+
</PopoverPanel>
|
|
22
|
+
</div>
|
|
23
|
+
</Popover>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
28
|
+
import { createPopper } from '@popperjs/core'
|
|
29
|
+
import { nextTick, ref, onBeforeUnmount } from 'vue'
|
|
30
|
+
|
|
31
|
+
const props = defineProps({
|
|
32
|
+
placement: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: 'bottom-start',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const reference = ref(null)
|
|
39
|
+
const popover = ref(null)
|
|
40
|
+
|
|
41
|
+
let popper = ref(null)
|
|
42
|
+
|
|
43
|
+
function setupPopper() {
|
|
44
|
+
if (!popper.value) {
|
|
45
|
+
popper.value = createPopper(reference.value.el, popover.value.el, {
|
|
46
|
+
placement: props.placement,
|
|
47
|
+
})
|
|
48
|
+
} else {
|
|
49
|
+
popper.value.update()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updatePosition() {
|
|
54
|
+
nextTick(() => setupPopper())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onBeforeUnmount(() => {
|
|
58
|
+
popper.value?.destroy()
|
|
59
|
+
})
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Autocomplete
|
|
3
|
+
placeholder="Select an option"
|
|
4
|
+
:options="options"
|
|
5
|
+
:value="selection"
|
|
6
|
+
@update:query="(q) => onUpdateQuery(q)"
|
|
7
|
+
@change="(v) => (selection = v)"
|
|
8
|
+
/>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import { Autocomplete, createListResource } from 'frappe-ui'
|
|
13
|
+
import { computed, ref, watch } from 'vue'
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
value: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: false,
|
|
19
|
+
default: '',
|
|
20
|
+
},
|
|
21
|
+
doctype: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
searchField: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
default: 'name',
|
|
29
|
+
},
|
|
30
|
+
labelField: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: false,
|
|
33
|
+
default: 'name',
|
|
34
|
+
},
|
|
35
|
+
valueField: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: false,
|
|
38
|
+
default: 'name',
|
|
39
|
+
},
|
|
40
|
+
pageLength: {
|
|
41
|
+
type: Number,
|
|
42
|
+
required: false,
|
|
43
|
+
default: 10,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
watch(
|
|
48
|
+
() => props.doctype,
|
|
49
|
+
(value) => {
|
|
50
|
+
r.doctype = value
|
|
51
|
+
r.reload()
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const r = createListResource({
|
|
56
|
+
doctype: props.doctype,
|
|
57
|
+
pageLength: props.pageLength,
|
|
58
|
+
cache: ['link_doctype', props.doctype],
|
|
59
|
+
auto: true,
|
|
60
|
+
fields: [props.labelField, props.searchField, props.valueField],
|
|
61
|
+
onSuccess: () => {
|
|
62
|
+
selection.value = props.value
|
|
63
|
+
? options.value.find((o) => o.value === props.value)
|
|
64
|
+
: null
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
const options = computed(
|
|
68
|
+
() =>
|
|
69
|
+
r.data?.map((result) => ({
|
|
70
|
+
label: result[props.labelField],
|
|
71
|
+
value: result[props.valueField],
|
|
72
|
+
})) || []
|
|
73
|
+
)
|
|
74
|
+
const selection = ref(null)
|
|
75
|
+
|
|
76
|
+
function onUpdateQuery(query) {
|
|
77
|
+
r.update({
|
|
78
|
+
filters: {
|
|
79
|
+
[props.searchField]: ['like', `%${query}%`],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
r.reload()
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="
|
|
4
|
-
:style="{
|
|
3
|
+
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
|
4
|
+
:style="{
|
|
5
|
+
gridTemplateColumns: getGridTemplateColumns(columns, options.selectable),
|
|
6
|
+
}"
|
|
5
7
|
>
|
|
6
8
|
<Checkbox
|
|
9
|
+
v-if="options.selectable"
|
|
7
10
|
class="cursor-pointer duration-300"
|
|
8
11
|
:modelValue="allRowsSelected"
|
|
9
12
|
@click.stop="toggleAllRows"
|
|
@@ -12,7 +15,7 @@
|
|
|
12
15
|
<ListHeaderItem
|
|
13
16
|
v-for="column in columns"
|
|
14
17
|
:key="column.key"
|
|
15
|
-
:
|
|
18
|
+
:item="column"
|
|
16
19
|
/>
|
|
17
20
|
</slot>
|
|
18
21
|
</div>
|
|
@@ -24,5 +27,5 @@ import ListHeaderItem from './ListHeaderItem.vue'
|
|
|
24
27
|
import { getGridTemplateColumns } from './utils'
|
|
25
28
|
import { inject } from 'vue'
|
|
26
29
|
|
|
27
|
-
const { columns, allRowsSelected, toggleAllRows } = inject('list')
|
|
30
|
+
const { columns, options, allRowsSelected, toggleAllRows } = inject('list')
|
|
28
31
|
</script>
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="flex items-center space-x-2 text-base text-gray-600"
|
|
4
|
-
:class="alignmentMap[
|
|
4
|
+
:class="alignmentMap[item.align]"
|
|
5
5
|
>
|
|
6
|
-
<slot name="prefix" v-bind="{
|
|
6
|
+
<slot name="prefix" v-bind="{ item }" />
|
|
7
7
|
<div>
|
|
8
|
-
{{
|
|
8
|
+
{{ item.label }}
|
|
9
9
|
</div>
|
|
10
|
-
<slot name="suffix" v-bind="{
|
|
10
|
+
<slot name="suffix" v-bind="{ item }" />
|
|
11
11
|
</div>
|
|
12
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup>
|
|
15
15
|
import { alignmentMap } from './utils'
|
|
16
16
|
const props = defineProps({
|
|
17
|
-
|
|
17
|
+
item: {
|
|
18
18
|
type: Object,
|
|
19
19
|
required: true,
|
|
20
20
|
},
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
|
-
:is="
|
|
4
|
-
class="
|
|
5
|
-
v-bind="
|
|
3
|
+
:is="options.getRowRoute ? 'router-link' : 'div'"
|
|
4
|
+
class="flex cursor-pointer flex-col transition-all duration-300 ease-in-out"
|
|
5
|
+
v-bind="
|
|
6
|
+
options.getRowRoute
|
|
7
|
+
? { to: options.getRowRoute(row) }
|
|
8
|
+
: { onClick: () => options.onRowClick(row) }
|
|
9
|
+
"
|
|
6
10
|
>
|
|
7
11
|
<component
|
|
8
|
-
:is="
|
|
12
|
+
:is="options.getRowRoute ? 'template' : 'button'"
|
|
9
13
|
class="[all:unset] hover:[all:unset]"
|
|
10
14
|
>
|
|
11
15
|
<div
|
|
@@ -15,9 +19,15 @@
|
|
|
15
19
|
? 'bg-gray-100 hover:bg-gray-200'
|
|
16
20
|
: 'hover:bg-gray-50'
|
|
17
21
|
"
|
|
18
|
-
:style="{
|
|
22
|
+
:style="{
|
|
23
|
+
gridTemplateColumns: getGridTemplateColumns(
|
|
24
|
+
columns,
|
|
25
|
+
options.selectable
|
|
26
|
+
),
|
|
27
|
+
}"
|
|
19
28
|
>
|
|
20
29
|
<Checkbox
|
|
30
|
+
v-if="options.selectable"
|
|
21
31
|
:modelValue="selections.has(row[rowKey])"
|
|
22
32
|
@click.stop="toggleRow(row[rowKey])"
|
|
23
33
|
class="cursor-pointer duration-300"
|
|
@@ -27,19 +37,16 @@
|
|
|
27
37
|
:key="column.key"
|
|
28
38
|
:class="alignmentMap[column.align]"
|
|
29
39
|
>
|
|
30
|
-
<slot v-bind="{ column, item:
|
|
40
|
+
<slot v-bind="{ column, item: row[column.key] }">
|
|
31
41
|
<ListRowItem
|
|
32
|
-
:item="
|
|
42
|
+
:item="row[column.key]"
|
|
33
43
|
:type="column.type"
|
|
34
44
|
:align="column.align"
|
|
35
45
|
/>
|
|
36
46
|
</slot>
|
|
37
47
|
</div>
|
|
38
48
|
</div>
|
|
39
|
-
<div
|
|
40
|
-
v-if="idx < rows.length - 1"
|
|
41
|
-
class="mx-2 h-px border-t border-gray-200"
|
|
42
|
-
/>
|
|
49
|
+
<div v-if="!isLastRow" class="mx-2 h-px border-t border-gray-200" />
|
|
43
50
|
</component>
|
|
44
51
|
</component>
|
|
45
52
|
</template>
|
|
@@ -48,25 +55,18 @@
|
|
|
48
55
|
import Checkbox from '../Checkbox.vue'
|
|
49
56
|
import ListRowItem from './ListRowItem.vue'
|
|
50
57
|
import { alignmentMap, getGridTemplateColumns } from './utils'
|
|
51
|
-
import { inject } from 'vue'
|
|
58
|
+
import { computed, inject } from 'vue'
|
|
52
59
|
|
|
53
60
|
const props = defineProps({
|
|
54
61
|
row: {
|
|
55
62
|
type: Object,
|
|
56
63
|
required: true,
|
|
57
64
|
},
|
|
58
|
-
idx: {
|
|
59
|
-
type: Number,
|
|
60
|
-
required: true,
|
|
61
|
-
},
|
|
62
65
|
})
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
return row
|
|
69
|
-
}
|
|
67
|
+
const isLastRow = computed(() => {
|
|
68
|
+
return rows[rows.length - 1][rowKey] === props.row[rowKey]
|
|
69
|
+
})
|
|
70
70
|
|
|
71
|
-
const { rows, columns, rowKey, selections, toggleRow } = inject('list')
|
|
71
|
+
const { rows, columns, rowKey, options, selections, toggleRow } = inject('list')
|
|
72
72
|
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
:
|
|
2
|
+
<component
|
|
3
|
+
:is="options.showTooltip ? Tooltip : 'div'"
|
|
4
|
+
v-bind="options.showTooltip ? { text: label } : {}"
|
|
4
5
|
class="flex items-center space-x-2"
|
|
5
6
|
:class="alignmentMap[align]"
|
|
6
7
|
>
|
|
@@ -11,12 +12,12 @@
|
|
|
11
12
|
</div>
|
|
12
13
|
</slot>
|
|
13
14
|
<slot name="suffix" />
|
|
14
|
-
</
|
|
15
|
+
</component>
|
|
15
16
|
</template>
|
|
16
17
|
<script setup>
|
|
17
18
|
import { alignmentMap } from './utils'
|
|
18
19
|
import Tooltip from '../Tooltip.vue'
|
|
19
|
-
import { computed } from 'vue'
|
|
20
|
+
import { computed, inject } from 'vue'
|
|
20
21
|
|
|
21
22
|
const props = defineProps({
|
|
22
23
|
item: {
|
|
@@ -39,4 +40,6 @@ function getValue(value) {
|
|
|
39
40
|
}
|
|
40
41
|
return { label: value }
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
const { options } = inject('list')
|
|
42
45
|
</script>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative flex w-full flex-1 flex-col overflow-x-auto">
|
|
3
3
|
<div
|
|
4
|
-
class="
|
|
4
|
+
class="flex w-max min-w-full flex-col overflow-y-hidden"
|
|
5
5
|
:class="$attrs.class"
|
|
6
6
|
>
|
|
7
7
|
<slot>
|
|
8
8
|
<ListHeader />
|
|
9
9
|
<ListRows />
|
|
10
|
-
<ListSelectBanner />
|
|
10
|
+
<ListSelectBanner v-if="_options.selectable" />
|
|
11
11
|
</slot>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
@@ -35,10 +35,32 @@ const props = defineProps({
|
|
|
35
35
|
type: String,
|
|
36
36
|
required: true,
|
|
37
37
|
},
|
|
38
|
+
options: {
|
|
39
|
+
type: Object,
|
|
40
|
+
default: {
|
|
41
|
+
getRowRoute: null,
|
|
42
|
+
onRowClick: null,
|
|
43
|
+
showTooltip: true,
|
|
44
|
+
selectable: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
38
47
|
})
|
|
39
48
|
|
|
40
49
|
let selections = reactive(new Set())
|
|
41
50
|
|
|
51
|
+
let _options = computed(() => {
|
|
52
|
+
function defaultTrue(value) {
|
|
53
|
+
return value === undefined ? true : value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
getRowRoute: props.options.getRowRoute || null,
|
|
58
|
+
onRowClick: props.options.onRowClick || null,
|
|
59
|
+
showTooltip: defaultTrue(props.options.showTooltip),
|
|
60
|
+
selectable: defaultTrue(props.options.selectable),
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
42
64
|
const allRowsSelected = computed(() => {
|
|
43
65
|
if (!props.rows.length) return false
|
|
44
66
|
return selections.size === props.rows.length
|
|
@@ -62,6 +84,7 @@ provide('list', {
|
|
|
62
84
|
rowKey: props.rowKey,
|
|
63
85
|
rows: props.rows,
|
|
64
86
|
columns: props.columns,
|
|
87
|
+
options: _options.value,
|
|
65
88
|
selections,
|
|
66
89
|
allRowsSelected,
|
|
67
90
|
toggleRow,
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
export function getGridTemplateColumns(columns) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
1
|
+
export function getGridTemplateColumns(columns, withCheckbox = true) {
|
|
2
|
+
let checkBoxWidth = withCheckbox ? '14px ' : ''
|
|
3
|
+
let columnsWidth = columns
|
|
4
|
+
.map((col) => {
|
|
5
|
+
let width = col.width || 1
|
|
6
|
+
if (typeof width === 'number') {
|
|
7
|
+
return width + 'fr'
|
|
8
|
+
}
|
|
9
|
+
return width
|
|
10
|
+
})
|
|
11
|
+
.join(' ')
|
|
12
|
+
return checkBoxWidth + columnsWidth
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export const alignmentMap = {
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ export { toast, Toasts } from './components/toast.js'
|
|
|
50
50
|
export { default as Tooltip } from './components/Tooltip.vue'
|
|
51
51
|
export { default as CommandPalette } from './components/CommandPalette/CommandPalette.vue'
|
|
52
52
|
export { default as CommandPaletteItem } from './components/CommandPalette/CommandPaletteItem.vue'
|
|
53
|
+
export { default as ListFilter } from './components/ListFilter/ListFilter.vue'
|
|
53
54
|
|
|
54
55
|
// directives
|
|
55
56
|
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|