frappe-ui 0.1.6 → 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 +31 -0
- package/src/components/ListView/ListHeaderItem.vue +22 -0
- package/src/components/ListView/ListRow.vue +66 -23
- package/src/components/ListView/ListRowItem.vue +42 -7
- package/src/components/ListView/ListRows.vue +14 -0
- package/src/components/ListView/ListSelectBanner.vue +82 -0
- package/src/components/ListView/ListView.vue +77 -100
- package/src/components/ListView/utils.js +22 -0
- package/src/components/ListView.story.md +123 -0
- package/src/components/ListView.story.vue +200 -0
- package/src/index.js +6 -1
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>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
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
|
+
}"
|
|
7
|
+
>
|
|
8
|
+
<Checkbox
|
|
9
|
+
v-if="options.selectable"
|
|
10
|
+
class="cursor-pointer duration-300"
|
|
11
|
+
:modelValue="allRowsSelected"
|
|
12
|
+
@click.stop="toggleAllRows"
|
|
13
|
+
/>
|
|
14
|
+
<slot>
|
|
15
|
+
<ListHeaderItem
|
|
16
|
+
v-for="column in columns"
|
|
17
|
+
:key="column.key"
|
|
18
|
+
:item="column"
|
|
19
|
+
/>
|
|
20
|
+
</slot>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import Checkbox from '../Checkbox.vue'
|
|
26
|
+
import ListHeaderItem from './ListHeaderItem.vue'
|
|
27
|
+
import { getGridTemplateColumns } from './utils'
|
|
28
|
+
import { inject } from 'vue'
|
|
29
|
+
|
|
30
|
+
const { columns, options, allRowsSelected, toggleAllRows } = inject('list')
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex items-center space-x-2 text-base text-gray-600"
|
|
4
|
+
:class="alignmentMap[item.align]"
|
|
5
|
+
>
|
|
6
|
+
<slot name="prefix" v-bind="{ item }" />
|
|
7
|
+
<div>
|
|
8
|
+
{{ item.label }}
|
|
9
|
+
</div>
|
|
10
|
+
<slot name="suffix" v-bind="{ item }" />
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import { alignmentMap } from './utils'
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
item: {
|
|
18
|
+
type: Object,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
@@ -1,29 +1,72 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { inject } from 'vue'
|
|
3
|
-
import Checkbox from '../Checkbox.vue'
|
|
4
|
-
|
|
5
|
-
const list = inject('list')
|
|
6
|
-
const props = defineProps({
|
|
7
|
-
as: { type: String, default: 'div' },
|
|
8
|
-
row: { type: Object, default: () => ({}), required: true },
|
|
9
|
-
})
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
1
|
<template>
|
|
13
2
|
<component
|
|
14
|
-
:is="
|
|
15
|
-
class="
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
?
|
|
19
|
-
:
|
|
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) }
|
|
20
9
|
"
|
|
21
10
|
>
|
|
22
|
-
<
|
|
23
|
-
:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
<component
|
|
12
|
+
:is="options.getRowRoute ? 'template' : 'button'"
|
|
13
|
+
class="[all:unset] hover:[all:unset]"
|
|
14
|
+
>
|
|
15
|
+
<div
|
|
16
|
+
class="grid items-center space-x-4 rounded px-2 py-2.5"
|
|
17
|
+
:class="
|
|
18
|
+
selections.has(row[rowKey])
|
|
19
|
+
? 'bg-gray-100 hover:bg-gray-200'
|
|
20
|
+
: 'hover:bg-gray-50'
|
|
21
|
+
"
|
|
22
|
+
:style="{
|
|
23
|
+
gridTemplateColumns: getGridTemplateColumns(
|
|
24
|
+
columns,
|
|
25
|
+
options.selectable
|
|
26
|
+
),
|
|
27
|
+
}"
|
|
28
|
+
>
|
|
29
|
+
<Checkbox
|
|
30
|
+
v-if="options.selectable"
|
|
31
|
+
:modelValue="selections.has(row[rowKey])"
|
|
32
|
+
@click.stop="toggleRow(row[rowKey])"
|
|
33
|
+
class="cursor-pointer duration-300"
|
|
34
|
+
/>
|
|
35
|
+
<div
|
|
36
|
+
v-for="column in columns"
|
|
37
|
+
:key="column.key"
|
|
38
|
+
:class="alignmentMap[column.align]"
|
|
39
|
+
>
|
|
40
|
+
<slot v-bind="{ column, item: row[column.key] }">
|
|
41
|
+
<ListRowItem
|
|
42
|
+
:item="row[column.key]"
|
|
43
|
+
:type="column.type"
|
|
44
|
+
:align="column.align"
|
|
45
|
+
/>
|
|
46
|
+
</slot>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div v-if="!isLastRow" class="mx-2 h-px border-t border-gray-200" />
|
|
50
|
+
</component>
|
|
28
51
|
</component>
|
|
29
52
|
</template>
|
|
53
|
+
|
|
54
|
+
<script setup>
|
|
55
|
+
import Checkbox from '../Checkbox.vue'
|
|
56
|
+
import ListRowItem from './ListRowItem.vue'
|
|
57
|
+
import { alignmentMap, getGridTemplateColumns } from './utils'
|
|
58
|
+
import { computed, inject } from 'vue'
|
|
59
|
+
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
row: {
|
|
62
|
+
type: Object,
|
|
63
|
+
required: true,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const isLastRow = computed(() => {
|
|
68
|
+
return rows[rows.length - 1][rowKey] === props.row[rowKey]
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const { rows, columns, rowKey, options, selections, toggleRow } = inject('list')
|
|
72
|
+
</script>
|
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
<script setup></script>
|
|
2
|
-
|
|
3
1
|
<template>
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
<component
|
|
3
|
+
:is="options.showTooltip ? Tooltip : 'div'"
|
|
4
|
+
v-bind="options.showTooltip ? { text: label } : {}"
|
|
5
|
+
class="flex items-center space-x-2"
|
|
6
|
+
:class="alignmentMap[align]"
|
|
7
7
|
>
|
|
8
|
-
<slot
|
|
9
|
-
|
|
8
|
+
<slot name="prefix" />
|
|
9
|
+
<slot v-bind="{ label }">
|
|
10
|
+
<div class="truncate text-base">
|
|
11
|
+
{{ label }}
|
|
12
|
+
</div>
|
|
13
|
+
</slot>
|
|
14
|
+
<slot name="suffix" />
|
|
15
|
+
</component>
|
|
10
16
|
</template>
|
|
17
|
+
<script setup>
|
|
18
|
+
import { alignmentMap } from './utils'
|
|
19
|
+
import Tooltip from '../Tooltip.vue'
|
|
20
|
+
import { computed, inject } from 'vue'
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
item: {
|
|
24
|
+
type: [String, Number, Object],
|
|
25
|
+
default: '',
|
|
26
|
+
},
|
|
27
|
+
align: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: 'left',
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const label = computed(() => {
|
|
34
|
+
return getValue(props.item).label || ''
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function getValue(value) {
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
return { label: value }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { options } = inject('list')
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-y-auto">
|
|
3
|
+
<slot>
|
|
4
|
+
<ListRow v-for="(row, i) in rows" :key="row[rowKey]" :row="row" />
|
|
5
|
+
</slot>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import ListRow from './ListRow.vue'
|
|
11
|
+
import { inject } from 'vue'
|
|
12
|
+
|
|
13
|
+
const { rows, rowKey } = inject('list')
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition
|
|
3
|
+
enter-active-class="duration-300 ease-out"
|
|
4
|
+
enter-from-class="transform opacity-0"
|
|
5
|
+
enter-to-class="opacity-100"
|
|
6
|
+
leave-active-class="duration-300 ease-in"
|
|
7
|
+
leave-from-class="opacity-100"
|
|
8
|
+
leave-to-class="transform opacity-0"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
v-if="selections.size"
|
|
12
|
+
class="absolute inset-x-0 bottom-6 mx-auto w-max text-base"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
class="flex min-w-[596px] items-center space-x-3 rounded-lg bg-white px-4 py-2 shadow-2xl"
|
|
16
|
+
:class="$attrs.class"
|
|
17
|
+
>
|
|
18
|
+
<slot
|
|
19
|
+
v-bind="{
|
|
20
|
+
selections,
|
|
21
|
+
allRowsSelected,
|
|
22
|
+
selectAll: () => toggleAllRows(true),
|
|
23
|
+
unselectAll: () => toggleAllRows(false),
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
class="flex flex-1 justify-between border-r border-gray-300 text-gray-900"
|
|
28
|
+
>
|
|
29
|
+
<div class="flex items-center space-x-3">
|
|
30
|
+
<Checkbox
|
|
31
|
+
:modelValue="true"
|
|
32
|
+
:disabled="true"
|
|
33
|
+
class="text-gray-900"
|
|
34
|
+
/>
|
|
35
|
+
<div>{{ selectedText }}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="mr-3">
|
|
38
|
+
<slot
|
|
39
|
+
name="actions"
|
|
40
|
+
v-bind="{
|
|
41
|
+
selections,
|
|
42
|
+
allRowsSelected,
|
|
43
|
+
selectAll: () => toggleAllRows(true),
|
|
44
|
+
unselectAll: () => toggleAllRows(false),
|
|
45
|
+
}"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="flex items-center space-x-1">
|
|
50
|
+
<Button
|
|
51
|
+
class="w- text-gray-700"
|
|
52
|
+
:disabled="allRowsSelected"
|
|
53
|
+
:class="allRowsSelected ? 'cursor-not-allowed' : ''"
|
|
54
|
+
variant="ghost"
|
|
55
|
+
@click="toggleAllRows(true)"
|
|
56
|
+
>
|
|
57
|
+
Select all
|
|
58
|
+
</Button>
|
|
59
|
+
<Button icon="x" variant="ghost" @click="toggleAllRows(false)" />
|
|
60
|
+
</div>
|
|
61
|
+
</slot>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</transition>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup>
|
|
68
|
+
import Checkbox from '../Checkbox.vue'
|
|
69
|
+
import Button from '../Button.vue'
|
|
70
|
+
import { computed, inject } from 'vue'
|
|
71
|
+
|
|
72
|
+
defineOptions({
|
|
73
|
+
inheritAttrs: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
let selectedText = computed(() => {
|
|
77
|
+
let title = selections.size === 1 ? 'Row' : 'Rows'
|
|
78
|
+
return `${selections.size} ${title} selected`
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const { selections, allRowsSelected, toggleAllRows } = inject('list')
|
|
82
|
+
</script>
|
|
@@ -1,116 +1,93 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/>
|
|
13
|
-
<div
|
|
14
|
-
v-for="column in props.columns"
|
|
15
|
-
:key="column"
|
|
16
|
-
class="flex-1 text-sm text-gray-600"
|
|
17
|
-
:class="column.class"
|
|
18
|
-
>
|
|
19
|
-
{{ column.label }}
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
<div id="list-rows" class="h-full overflow-y-auto">
|
|
23
|
-
<template v-for="row in props.rows" :key="row.name">
|
|
24
|
-
<slot name="list-row" :row="row">
|
|
25
|
-
<ListRow as="div" :row="row">
|
|
26
|
-
<ListRowItem v-for="column in props.columns" :key="column.name">
|
|
27
|
-
{{ row[column.name] }}
|
|
28
|
-
</ListRowItem>
|
|
29
|
-
</ListRow>
|
|
30
|
-
</slot>
|
|
31
|
-
</template>
|
|
32
|
-
</div>
|
|
33
|
-
<transition
|
|
34
|
-
enter-active-class="duration-300 ease-out"
|
|
35
|
-
enter-from-class="transform opacity-0"
|
|
36
|
-
enter-to-class="opacity-100"
|
|
37
|
-
leave-active-class="duration-300 ease-in"
|
|
38
|
-
leave-from-class="opacity-100"
|
|
39
|
-
leave-to-class="transform opacity-0"
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
v-if="state.selections.size"
|
|
43
|
-
class="fixed inset-x-0 bottom-6 mx-auto w-max text-base"
|
|
44
|
-
>
|
|
45
|
-
<div
|
|
46
|
-
class="flex w-[596px] items-center space-x-3 rounded-lg bg-white px-4 py-2 shadow-2xl"
|
|
47
|
-
>
|
|
48
|
-
<div
|
|
49
|
-
class="flex flex-1 items-center space-x-3 border-r border-gray-300 text-gray-900"
|
|
50
|
-
>
|
|
51
|
-
<Checkbox
|
|
52
|
-
:modelValue="true"
|
|
53
|
-
:disabled="true"
|
|
54
|
-
class="text-gray-900"
|
|
55
|
-
/>
|
|
56
|
-
<div>{{ state.selectedText }}</div>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="flex items-center space-x-1">
|
|
59
|
-
<Button
|
|
60
|
-
class="text-gray-700"
|
|
61
|
-
:disabled="state.allRowsSelected"
|
|
62
|
-
:class="state.allRowsSelected ? 'cursor-not-allowed' : ''"
|
|
63
|
-
variant="ghost"
|
|
64
|
-
@click="state.toggleAllRows(true)"
|
|
65
|
-
>
|
|
66
|
-
Select all
|
|
67
|
-
</Button>
|
|
68
|
-
<Button
|
|
69
|
-
icon="x"
|
|
70
|
-
variant="ghost"
|
|
71
|
-
@click="state.toggleAllRows(false)"
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</transition>
|
|
2
|
+
<div class="relative flex w-full flex-1 flex-col overflow-x-auto">
|
|
3
|
+
<div
|
|
4
|
+
class="flex w-max min-w-full flex-col overflow-y-hidden"
|
|
5
|
+
:class="$attrs.class"
|
|
6
|
+
>
|
|
7
|
+
<slot>
|
|
8
|
+
<ListHeader />
|
|
9
|
+
<ListRows />
|
|
10
|
+
<ListSelectBanner v-if="_options.selectable" />
|
|
11
|
+
</slot>
|
|
77
12
|
</div>
|
|
78
13
|
</div>
|
|
79
14
|
</template>
|
|
80
15
|
<script setup>
|
|
81
|
-
import
|
|
82
|
-
import
|
|
83
|
-
import
|
|
84
|
-
import
|
|
85
|
-
|
|
16
|
+
import ListHeader from './ListHeader.vue'
|
|
17
|
+
import ListRows from './ListRows.vue'
|
|
18
|
+
import ListSelectBanner from './ListSelectBanner.vue'
|
|
19
|
+
import { reactive, computed, provide } from 'vue'
|
|
20
|
+
|
|
21
|
+
defineOptions({
|
|
22
|
+
inheritAttrs: false,
|
|
23
|
+
})
|
|
86
24
|
|
|
87
25
|
const props = defineProps({
|
|
88
|
-
columns: {
|
|
89
|
-
|
|
26
|
+
columns: {
|
|
27
|
+
type: Array,
|
|
28
|
+
default: [],
|
|
29
|
+
},
|
|
30
|
+
rows: {
|
|
31
|
+
type: Array,
|
|
32
|
+
default: [],
|
|
33
|
+
},
|
|
34
|
+
rowKey: {
|
|
35
|
+
type: String,
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
options: {
|
|
39
|
+
type: Object,
|
|
40
|
+
default: {
|
|
41
|
+
getRowRoute: null,
|
|
42
|
+
onRowClick: null,
|
|
43
|
+
showTooltip: true,
|
|
44
|
+
selectable: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
90
47
|
})
|
|
91
48
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
let selections = reactive(new Set())
|
|
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
|
+
}
|
|
100
62
|
})
|
|
101
|
-
provide('list', state)
|
|
102
63
|
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
64
|
+
const allRowsSelected = computed(() => {
|
|
65
|
+
if (!props.rows.length) return false
|
|
66
|
+
return selections.size === props.rows.length
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function toggleRow(row) {
|
|
70
|
+
if (!selections.delete(row)) {
|
|
71
|
+
selections.add(row)
|
|
72
|
+
}
|
|
107
73
|
}
|
|
108
74
|
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
state.selections.add(name)
|
|
75
|
+
function toggleAllRows(select) {
|
|
76
|
+
if (!select || allRowsSelected.value) {
|
|
77
|
+
selections.clear()
|
|
78
|
+
return
|
|
114
79
|
}
|
|
80
|
+
props.rows.forEach((row) => selections.add(row[props.rowKey]))
|
|
115
81
|
}
|
|
82
|
+
|
|
83
|
+
provide('list', {
|
|
84
|
+
rowKey: props.rowKey,
|
|
85
|
+
rows: props.rows,
|
|
86
|
+
columns: props.columns,
|
|
87
|
+
options: _options.value,
|
|
88
|
+
selections,
|
|
89
|
+
allRowsSelected,
|
|
90
|
+
toggleRow,
|
|
91
|
+
toggleAllRows,
|
|
92
|
+
})
|
|
116
93
|
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const alignmentMap = {
|
|
16
|
+
left: 'justify-start',
|
|
17
|
+
start: 'justify-start',
|
|
18
|
+
center: 'justify-center',
|
|
19
|
+
middle: 'justify-center',
|
|
20
|
+
right: 'justify-end',
|
|
21
|
+
end: 'justify-end',
|
|
22
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
**Column:**
|
|
2
|
+
|
|
3
|
+
1. `label` & `key` is required in column object.
|
|
4
|
+
|
|
5
|
+
2. `width` is optional and it is used to set column width in list
|
|
6
|
+
|
|
7
|
+
1. If you need a column to be `3` times a default column then add `3`. if
|
|
8
|
+
width is not mentioned default will be `1`
|
|
9
|
+
2. You can also add custom width in px and rem e.g `300px` or `12rem`
|
|
10
|
+
3. Combination of both can also be used.
|
|
11
|
+
|
|
12
|
+
3. `align` is also optional. You can change the alignment of the content in the
|
|
13
|
+
column by setting it as.
|
|
14
|
+
|
|
15
|
+
1. `start` or `left` (default)
|
|
16
|
+
2. `center` or `middle`
|
|
17
|
+
3. `end` or `right`
|
|
18
|
+
|
|
19
|
+
4. You can add more attributes which can be used to render custom column header
|
|
20
|
+
items.
|
|
21
|
+
|
|
22
|
+
**Row**
|
|
23
|
+
|
|
24
|
+
1. The row object must contain a unique_key which was mentioned in ListView
|
|
25
|
+
`row-key`
|
|
26
|
+
2. You can either add all row fields in a separate `row` object or just add them
|
|
27
|
+
in directly if the fieldnames doesn't conflict with `route` or `onClick` E.g.
|
|
28
|
+
1
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
{
|
|
32
|
+
// unique_key 'id'
|
|
33
|
+
id: 1,
|
|
34
|
+
|
|
35
|
+
// row fields
|
|
36
|
+
name: 'John Doe',
|
|
37
|
+
age: 25,
|
|
38
|
+
email: 'john@doe.com',
|
|
39
|
+
|
|
40
|
+
// if you need to route
|
|
41
|
+
route: { label: 'User', { params: { userId: 1 } }
|
|
42
|
+
|
|
43
|
+
// if you need to perform action
|
|
44
|
+
onClick: () => console.log('John Doe was clicked')
|
|
45
|
+
|
|
46
|
+
// you can add more options after this which you can use to render custom row items
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
E.g. 2
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
{
|
|
54
|
+
// unique_key 'id'
|
|
55
|
+
id: 1,
|
|
56
|
+
|
|
57
|
+
// row fields in separate row object
|
|
58
|
+
row: {
|
|
59
|
+
name: 'John Doe',
|
|
60
|
+
age: 25,
|
|
61
|
+
email: 'john@doe.com',
|
|
62
|
+
route: '', // used separate row to avoid this conflict
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// if you need to route
|
|
66
|
+
route: { label: 'User', { params: { userId: 1 } }
|
|
67
|
+
|
|
68
|
+
// if you need to perform action
|
|
69
|
+
onClick: () => console.log('John Doe was clicked')
|
|
70
|
+
|
|
71
|
+
// you can add more options after this which you can use to render custom row items
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. You can also add an object for the field value but make sure it has a `label`
|
|
76
|
+
attribute which holds the actual value to be shown
|
|
77
|
+
```
|
|
78
|
+
row: {
|
|
79
|
+
name: {
|
|
80
|
+
label: 'John Doe',
|
|
81
|
+
image: '/johndoe.jpg',
|
|
82
|
+
},
|
|
83
|
+
age: 25,
|
|
84
|
+
status: {
|
|
85
|
+
label: 'Active',
|
|
86
|
+
color: 'green'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
4. Click action: Add route or onClick event in row object
|
|
91
|
+
1. If you want to route using router-link just add a
|
|
92
|
+
`route: { name: 'User', params: { userId: 2 } }`
|
|
93
|
+
2. if you need to do some action or open a dialog add a click event instead
|
|
94
|
+
of a route `onClick: () => console.log('John Doe was clicked')`
|
|
95
|
+
|
|
96
|
+
**Selection Banner:**
|
|
97
|
+
|
|
98
|
+
**Without custom action buttons:**
|
|
99
|
+
<img width="1213" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/36fafcf5-45c6-43f0-acde-f64afe38b550">
|
|
100
|
+
|
|
101
|
+
**With custom action buttons:**
|
|
102
|
+
<img width="1212" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/55e751b2-df66-4ff0-b852-af463014463f">
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
<ListSelectBanner>
|
|
106
|
+
<template #actions>
|
|
107
|
+
<div class="flex gap-2">
|
|
108
|
+
<Button variant="ghost" label="Delete" />
|
|
109
|
+
<Button variant="ghost" label="Edit" />
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
</ListSelectBanner>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also make your own custom selection banner
|
|
116
|
+
|
|
117
|
+
<img width="629" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/38dfa834-96a2-4ac5-ad4b-30b3e6871d3f">
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
<ListSelectBanner>
|
|
121
|
+
<div>Custom Banner</div>
|
|
122
|
+
</ListSelectBanner>
|
|
123
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ListView from './ListView/ListView.vue'
|
|
3
|
+
import ListHeader from './ListView/ListHeader.vue'
|
|
4
|
+
import ListHeaderItem from './ListView/ListHeaderItem.vue'
|
|
5
|
+
import ListRows from './ListView/ListRows.vue'
|
|
6
|
+
import ListRow from './ListView/ListRow.vue'
|
|
7
|
+
import ListRowItem from './ListView/ListRowItem.vue'
|
|
8
|
+
import ListSelectBanner from './ListView/ListSelectBanner.vue'
|
|
9
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
10
|
+
import Badge from './Badge.vue'
|
|
11
|
+
import Button from './Button.vue'
|
|
12
|
+
import Avatar from './Avatar.vue'
|
|
13
|
+
|
|
14
|
+
const simple_columns = [
|
|
15
|
+
{
|
|
16
|
+
label: 'Name',
|
|
17
|
+
key: 'name',
|
|
18
|
+
width: 3,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: 'Email',
|
|
22
|
+
key: 'email',
|
|
23
|
+
width: '200px',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'Role',
|
|
27
|
+
key: 'role',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'Status',
|
|
31
|
+
key: 'status',
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const simple_rows = [
|
|
36
|
+
{
|
|
37
|
+
id: 1,
|
|
38
|
+
row: {
|
|
39
|
+
name: 'John Doe',
|
|
40
|
+
email: 'john@doe.com',
|
|
41
|
+
status: 'Active',
|
|
42
|
+
role: 'Developer',
|
|
43
|
+
},
|
|
44
|
+
onClick: () => console.log('John Doe was clicked'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 2,
|
|
48
|
+
row: {
|
|
49
|
+
name: 'Jane Doe',
|
|
50
|
+
email: 'jane@doe.com',
|
|
51
|
+
status: 'Inactive',
|
|
52
|
+
role: 'HR',
|
|
53
|
+
},
|
|
54
|
+
route: { name: 'User', params: { userId: 2 } },
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const custom_columns = [
|
|
59
|
+
{
|
|
60
|
+
label: 'Name',
|
|
61
|
+
key: 'name',
|
|
62
|
+
width: 3,
|
|
63
|
+
icon: 'user',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Email',
|
|
67
|
+
key: 'email',
|
|
68
|
+
width: '200px',
|
|
69
|
+
icon: 'at-sign',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'Role',
|
|
73
|
+
key: 'role',
|
|
74
|
+
icon: 'users',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'Status',
|
|
78
|
+
key: 'status',
|
|
79
|
+
icon: 'check-circle',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const custom_rows = [
|
|
84
|
+
{
|
|
85
|
+
id: 1,
|
|
86
|
+
row: {
|
|
87
|
+
name: {
|
|
88
|
+
label: 'John Doe',
|
|
89
|
+
image: 'https://avatars.githubusercontent.com/u/499550',
|
|
90
|
+
},
|
|
91
|
+
email: 'john@doe.com',
|
|
92
|
+
status: {
|
|
93
|
+
label: 'Active',
|
|
94
|
+
bg_color: 'bg-green-600',
|
|
95
|
+
},
|
|
96
|
+
role: {
|
|
97
|
+
label: 'Developer',
|
|
98
|
+
color: 'green',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
onClick: () => console.log('John Doe was clicked'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 2,
|
|
105
|
+
row: {
|
|
106
|
+
name: {
|
|
107
|
+
label: 'Jane Doe',
|
|
108
|
+
image: 'https://avatars.githubusercontent.com/u/499120',
|
|
109
|
+
},
|
|
110
|
+
email: 'jane@doe.com',
|
|
111
|
+
status: {
|
|
112
|
+
label: 'Inactive',
|
|
113
|
+
bg_color: 'bg-red-600',
|
|
114
|
+
},
|
|
115
|
+
role: {
|
|
116
|
+
label: 'HR',
|
|
117
|
+
color: 'red',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
route: { name: 'User', params: { userId: 2 } },
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<template>
|
|
126
|
+
<Story :layout="{ type: 'grid', width: '95%' }">
|
|
127
|
+
<Variant title="Simple List">
|
|
128
|
+
<ListView
|
|
129
|
+
class="h-[250px]"
|
|
130
|
+
:columns="simple_columns"
|
|
131
|
+
:rows="simple_rows"
|
|
132
|
+
row-key="id"
|
|
133
|
+
/>
|
|
134
|
+
</Variant>
|
|
135
|
+
<Variant title="Custom List">
|
|
136
|
+
<ListView
|
|
137
|
+
class="h-[250px]"
|
|
138
|
+
:columns="custom_columns"
|
|
139
|
+
:rows="custom_rows"
|
|
140
|
+
row-key="id"
|
|
141
|
+
>
|
|
142
|
+
<ListHeader>
|
|
143
|
+
<ListHeaderItem
|
|
144
|
+
v-for="column in custom_columns"
|
|
145
|
+
:key="column.key"
|
|
146
|
+
:column="column"
|
|
147
|
+
>
|
|
148
|
+
<template #prefix="{ column }">
|
|
149
|
+
<FeatherIcon :name="column.icon" class="h-4 w-4" />
|
|
150
|
+
</template>
|
|
151
|
+
</ListHeaderItem>
|
|
152
|
+
</ListHeader>
|
|
153
|
+
<ListRows>
|
|
154
|
+
<ListRow
|
|
155
|
+
v-for="(row, i) in custom_rows"
|
|
156
|
+
:key="i"
|
|
157
|
+
v-slot="{ column, item }"
|
|
158
|
+
:row="row"
|
|
159
|
+
:idx="i"
|
|
160
|
+
>
|
|
161
|
+
<ListRowItem :item="item" :align="column.align">
|
|
162
|
+
<template #prefix>
|
|
163
|
+
<div
|
|
164
|
+
v-if="column.key == 'status'"
|
|
165
|
+
class="h-3 w-3 rounded-full"
|
|
166
|
+
:class="item.bg_color"
|
|
167
|
+
/>
|
|
168
|
+
<Avatar
|
|
169
|
+
v-if="column.key == 'name'"
|
|
170
|
+
:shape="'circle'"
|
|
171
|
+
:image="item.image"
|
|
172
|
+
size="sm"
|
|
173
|
+
/>
|
|
174
|
+
</template>
|
|
175
|
+
<Badge
|
|
176
|
+
v-if="column.key == 'role'"
|
|
177
|
+
variant="subtle"
|
|
178
|
+
:theme="item.color"
|
|
179
|
+
size="md"
|
|
180
|
+
:label="item.label"
|
|
181
|
+
/>
|
|
182
|
+
</ListRowItem>
|
|
183
|
+
</ListRow>
|
|
184
|
+
</ListRows>
|
|
185
|
+
<ListSelectBanner>
|
|
186
|
+
<template #actions="{ unselectAll }">
|
|
187
|
+
<div class="flex gap-2">
|
|
188
|
+
<Button variant="ghost" label="Delete" />
|
|
189
|
+
<Button
|
|
190
|
+
variant="ghost"
|
|
191
|
+
label="Unselect all"
|
|
192
|
+
@click="unselectAll"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
</ListSelectBanner>
|
|
197
|
+
</ListView>
|
|
198
|
+
</Variant>
|
|
199
|
+
</Story>
|
|
200
|
+
</template>
|
package/src/index.js
CHANGED
|
@@ -38,14 +38,19 @@ export {
|
|
|
38
38
|
TextEditorFloatingMenu,
|
|
39
39
|
TextEditorContent,
|
|
40
40
|
} from './components/TextEditor'
|
|
41
|
-
export { default as
|
|
41
|
+
export { default as ListView } from './components/ListView/ListView.vue'
|
|
42
|
+
export { default as ListHeader } from './components/ListView/ListHeader.vue'
|
|
43
|
+
export { default as ListHeaderItem } from './components/ListView/ListHeaderItem.vue'
|
|
44
|
+
export { default as ListRows } from './components/ListView/ListRows.vue'
|
|
42
45
|
export { default as ListRow } from './components/ListView/ListRow.vue'
|
|
43
46
|
export { default as ListRowItem } from './components/ListView/ListRowItem.vue'
|
|
47
|
+
export { default as ListSelectBanner } from './components/ListView/ListSelectBanner.vue'
|
|
44
48
|
export { default as Toast } from './components/Toast.vue'
|
|
45
49
|
export { toast, Toasts } from './components/toast.js'
|
|
46
50
|
export { default as Tooltip } from './components/Tooltip.vue'
|
|
47
51
|
export { default as CommandPalette } from './components/CommandPalette/CommandPalette.vue'
|
|
48
52
|
export { default as CommandPaletteItem } from './components/CommandPalette/CommandPaletteItem.vue'
|
|
53
|
+
export { default as ListFilter } from './components/ListFilter/ListFilter.vue'
|
|
49
54
|
|
|
50
55
|
// directives
|
|
51
56
|
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|