frappe-ui 0.1.36 → 0.1.37
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/ListView/ListEmptyState.vue +24 -0
- package/src/components/ListView/ListGroupHeader.vue +35 -0
- package/src/components/ListView/ListGroupRows.vue +17 -0
- package/src/components/ListView/ListGroups.vue +22 -0
- package/src/components/ListView/ListRow.vue +15 -2
- package/src/components/ListView/ListRowItem.vue +21 -4
- package/src/components/ListView/ListView.vue +26 -5
- package/src/components/ListView.story.md +25 -0
- package/src/components/ListView.story.vue +220 -9
- package/src/components/Popover.vue +29 -7
- package/src/icons/DownSolid.vue +8 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex h-full w-full flex-col items-center justify-center text-base"
|
|
4
|
+
>
|
|
5
|
+
<slot>
|
|
6
|
+
<div class="text-xl font-medium">{{ list.options.emptyState.title }}</div>
|
|
7
|
+
<div class="mt-1 text-base text-gray-600">
|
|
8
|
+
{{ list.options.emptyState.description }}
|
|
9
|
+
</div>
|
|
10
|
+
<Button
|
|
11
|
+
v-if="list.options.emptyState.button"
|
|
12
|
+
v-bind="list.options.emptyState.button"
|
|
13
|
+
class="mt-4"
|
|
14
|
+
></Button>
|
|
15
|
+
</slot>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import { inject } from 'vue'
|
|
21
|
+
import Button from '../Button.vue'
|
|
22
|
+
|
|
23
|
+
const list = inject('list')
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center px-2 py-1.5">
|
|
3
|
+
<button @click="toggleGroup" class="-ml-[1px] rounded-sm hover:bg-gray-50">
|
|
4
|
+
<DownSolid
|
|
5
|
+
class="h-4 w-4 text-gray-900 transition-transform duration-200"
|
|
6
|
+
:class="[group.collapsed ? '-rotate-90' : '']"
|
|
7
|
+
/>
|
|
8
|
+
</button>
|
|
9
|
+
<div class="ml-[15px] w-full">
|
|
10
|
+
<slot>
|
|
11
|
+
<span class="text-base font-medium leading-6">
|
|
12
|
+
{{ group.group }}
|
|
13
|
+
</span>
|
|
14
|
+
</slot>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="mx-2 h-px border-t border-gray-200"></div>
|
|
18
|
+
</template>
|
|
19
|
+
<script setup>
|
|
20
|
+
import DownSolid from '../../icons/DownSolid.vue'
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
group: {
|
|
24
|
+
type: Object,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function toggleGroup() {
|
|
30
|
+
if (props.group.collapsed == null) {
|
|
31
|
+
props.group.collapsed = false
|
|
32
|
+
}
|
|
33
|
+
props.group.collapsed = !props.group.collapsed
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mb-5 mt-2" v-if="!group.collapsed">
|
|
3
|
+
<ListRow v-for="row in group.rows" :key="row[list.rowKey]" :row="row" />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
<script setup>
|
|
7
|
+
import ListRow from './ListRow.vue'
|
|
8
|
+
import { inject } from 'vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
group: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
const list = inject('list')
|
|
17
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-y-auto">
|
|
3
|
+
<div v-for="group in list.rows" :key="group.group">
|
|
4
|
+
<ListGroupHeader :group="group">
|
|
5
|
+
<slot
|
|
6
|
+
name="group-header"
|
|
7
|
+
v-if="$slots['group-header']"
|
|
8
|
+
v-bind="{ group }"
|
|
9
|
+
/>
|
|
10
|
+
</ListGroupHeader>
|
|
11
|
+
<ListGroupRows :group="group" />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import ListGroupHeader from './ListGroupHeader.vue'
|
|
18
|
+
import ListGroupRows from './ListGroupRows.vue'
|
|
19
|
+
import { inject } from 'vue'
|
|
20
|
+
|
|
21
|
+
const list = inject('list')
|
|
22
|
+
</script>
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
class="[all:unset] hover:[all:unset]"
|
|
15
15
|
>
|
|
16
16
|
<div
|
|
17
|
-
class="grid items-center space-x-4 rounded px-2
|
|
17
|
+
class="grid items-center space-x-4 rounded px-2"
|
|
18
18
|
:class="
|
|
19
19
|
list.selections.has(row[list.rowKey])
|
|
20
20
|
? 'bg-gray-100 hover:bg-gray-200'
|
|
21
21
|
: 'hover:bg-gray-50'
|
|
22
22
|
"
|
|
23
23
|
:style="{
|
|
24
|
+
height: rowHeight,
|
|
24
25
|
gridTemplateColumns: getGridTemplateColumns(
|
|
25
26
|
list.columns,
|
|
26
27
|
list.options.selectable
|
|
@@ -42,7 +43,12 @@
|
|
|
42
43
|
]"
|
|
43
44
|
>
|
|
44
45
|
<slot v-bind="{ idx: i, column, item: row[column.key] }">
|
|
45
|
-
<ListRowItem
|
|
46
|
+
<ListRowItem
|
|
47
|
+
:column="column"
|
|
48
|
+
:row="row"
|
|
49
|
+
:item="row[column.key]"
|
|
50
|
+
:align="column.align"
|
|
51
|
+
/>
|
|
46
52
|
</slot>
|
|
47
53
|
</div>
|
|
48
54
|
</div>
|
|
@@ -73,4 +79,11 @@ const isLastRow = computed(() => {
|
|
|
73
79
|
props.row[list.value.rowKey]
|
|
74
80
|
)
|
|
75
81
|
})
|
|
82
|
+
|
|
83
|
+
const rowHeight = computed(() => {
|
|
84
|
+
if (typeof list.value.options.rowHeight === 'number') {
|
|
85
|
+
return `${list.value.options.rowHeight}px`
|
|
86
|
+
}
|
|
87
|
+
return list.value.options.rowHeight
|
|
88
|
+
})
|
|
76
89
|
</script>
|
|
@@ -5,21 +5,38 @@
|
|
|
5
5
|
class="flex items-center space-x-2"
|
|
6
6
|
:class="alignmentMap[align]"
|
|
7
7
|
>
|
|
8
|
-
<slot name="prefix"
|
|
8
|
+
<slot name="prefix">
|
|
9
|
+
<component
|
|
10
|
+
v-if="column.prefix"
|
|
11
|
+
:is="
|
|
12
|
+
typeof column.prefix === 'function'
|
|
13
|
+
? column.prefix({ row })
|
|
14
|
+
: column.prefix
|
|
15
|
+
"
|
|
16
|
+
/>
|
|
17
|
+
</slot>
|
|
9
18
|
<slot v-bind="{ label }">
|
|
10
19
|
<div class="truncate text-base">
|
|
11
|
-
{{ label }}
|
|
20
|
+
{{ column?.getLabel ? column.getLabel({ row }) : label }}
|
|
12
21
|
</div>
|
|
13
22
|
</slot>
|
|
14
23
|
<slot name="suffix" />
|
|
15
24
|
</component>
|
|
16
25
|
</template>
|
|
17
26
|
<script setup>
|
|
18
|
-
import { alignmentMap } from './utils'
|
|
19
|
-
import Tooltip from '../Tooltip.vue'
|
|
20
27
|
import { computed, inject } from 'vue'
|
|
28
|
+
import Tooltip from '../Tooltip.vue'
|
|
29
|
+
import { alignmentMap } from './utils'
|
|
21
30
|
|
|
22
31
|
const props = defineProps({
|
|
32
|
+
column: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: {},
|
|
35
|
+
},
|
|
36
|
+
row: {
|
|
37
|
+
type: Object,
|
|
38
|
+
default: {},
|
|
39
|
+
},
|
|
23
40
|
item: {
|
|
24
41
|
type: [String, Number, Object],
|
|
25
42
|
default: '',
|
|
@@ -4,17 +4,23 @@
|
|
|
4
4
|
class="flex w-max min-w-full flex-col overflow-y-hidden"
|
|
5
5
|
:class="$attrs.class"
|
|
6
6
|
>
|
|
7
|
-
<slot>
|
|
7
|
+
<slot v-bind="{ showGroupedRows, selectable }">
|
|
8
8
|
<ListHeader />
|
|
9
|
-
<
|
|
10
|
-
|
|
9
|
+
<template v-if="props.rows.length">
|
|
10
|
+
<ListGroups v-if="showGroupedRows" />
|
|
11
|
+
<ListRows v-else />
|
|
12
|
+
</template>
|
|
13
|
+
<ListEmptyState v-else />
|
|
14
|
+
<ListSelectBanner v-if="selectable" />
|
|
11
15
|
</slot>
|
|
12
16
|
</div>
|
|
13
17
|
</div>
|
|
14
18
|
</template>
|
|
15
19
|
<script setup>
|
|
20
|
+
import ListEmptyState from './ListEmptyState.vue'
|
|
16
21
|
import ListHeader from './ListHeader.vue'
|
|
17
22
|
import ListRows from './ListRows.vue'
|
|
23
|
+
import ListGroups from './ListGroups.vue'
|
|
18
24
|
import ListSelectBanner from './ListSelectBanner.vue'
|
|
19
25
|
import { reactive, computed, provide, watch } from 'vue'
|
|
20
26
|
|
|
@@ -37,13 +43,18 @@ const props = defineProps({
|
|
|
37
43
|
},
|
|
38
44
|
options: {
|
|
39
45
|
type: Object,
|
|
40
|
-
default: {
|
|
46
|
+
default: () => ({
|
|
41
47
|
getRowRoute: null,
|
|
42
48
|
onRowClick: null,
|
|
43
49
|
showTooltip: true,
|
|
44
50
|
selectable: true,
|
|
45
51
|
resizeColumn: false,
|
|
46
|
-
|
|
52
|
+
rowHeight: 40,
|
|
53
|
+
emptyState: {
|
|
54
|
+
title: 'No Data',
|
|
55
|
+
description: 'No data available',
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
47
58
|
},
|
|
48
59
|
})
|
|
49
60
|
|
|
@@ -70,6 +81,8 @@ let _options = computed(() => {
|
|
|
70
81
|
showTooltip: defaultTrue(props.options.showTooltip),
|
|
71
82
|
selectable: defaultTrue(props.options.selectable),
|
|
72
83
|
resizeColumn: defaultFalse(props.options.resizeColumn),
|
|
84
|
+
rowHeight: props.options.rowHeight || 40,
|
|
85
|
+
emptyState: props.options.emptyState,
|
|
73
86
|
}
|
|
74
87
|
})
|
|
75
88
|
|
|
@@ -78,6 +91,14 @@ const allRowsSelected = computed(() => {
|
|
|
78
91
|
return selections.size === props.rows.length
|
|
79
92
|
})
|
|
80
93
|
|
|
94
|
+
const selectable = computed(() => {
|
|
95
|
+
return _options.value.selectable
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
let showGroupedRows = computed(() => {
|
|
99
|
+
return props.rows.every((row) => row.group)
|
|
100
|
+
})
|
|
101
|
+
|
|
81
102
|
function toggleRow(row) {
|
|
82
103
|
if (!selections.delete(row)) {
|
|
83
104
|
selections.add(row)
|
|
@@ -62,6 +62,31 @@ required to be passed in the `row` object.
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### Grouped Rows
|
|
66
|
+
|
|
67
|
+
To render grouped rows, you must provide `rows` in the following format:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
group: 'Group Title 1',
|
|
73
|
+
collapsed: false,
|
|
74
|
+
rows: [
|
|
75
|
+
{id: 1, key1: value1, key2: value2, ...},
|
|
76
|
+
{id: 2, key1: value1, key2: value2, ...},
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
group: 'Group Title 2',
|
|
81
|
+
collapsed: false,
|
|
82
|
+
rows: [
|
|
83
|
+
{id: 3, key1: value1, key2: value2, ...},
|
|
84
|
+
{id: 4, key1: value1, key2: value2, ...},
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
```
|
|
89
|
+
|
|
65
90
|
### Options
|
|
66
91
|
|
|
67
92
|
1. If you want to route using router-link just add a `getRowRoute` function
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import
|
|
2
|
+
import { reactive, h, ref } from 'vue'
|
|
3
|
+
import Avatar from './Avatar.vue'
|
|
4
|
+
import Badge from './Badge.vue'
|
|
5
|
+
import Button from './Button.vue'
|
|
6
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
3
7
|
import ListHeader from './ListView/ListHeader.vue'
|
|
4
8
|
import ListHeaderItem from './ListView/ListHeaderItem.vue'
|
|
5
|
-
import ListRows from './ListView/ListRows.vue'
|
|
6
9
|
import ListRow from './ListView/ListRow.vue'
|
|
7
10
|
import ListRowItem from './ListView/ListRowItem.vue'
|
|
11
|
+
import ListRows from './ListView/ListRows.vue'
|
|
12
|
+
import ListGroups from './ListView/ListGroups.vue'
|
|
8
13
|
import ListSelectBanner from './ListView/ListSelectBanner.vue'
|
|
9
|
-
import
|
|
10
|
-
import Badge from './Badge.vue'
|
|
11
|
-
import Button from './Button.vue'
|
|
12
|
-
import Avatar from './Avatar.vue'
|
|
13
|
-
import { reactive } from 'vue'
|
|
14
|
+
import ListView from './ListView/ListView.vue'
|
|
14
15
|
|
|
15
16
|
const state = reactive({
|
|
16
17
|
selectable: true,
|
|
17
18
|
showTooltip: true,
|
|
18
19
|
resizeColumn: true,
|
|
20
|
+
emptyState: {
|
|
21
|
+
title: 'No records found',
|
|
22
|
+
description: 'Create a new record to get started',
|
|
23
|
+
button: {
|
|
24
|
+
label: 'New Record',
|
|
25
|
+
variant: 'solid',
|
|
26
|
+
onClick: () => console.log('New Record'),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
19
29
|
})
|
|
20
30
|
|
|
21
31
|
const simple_columns = reactive([
|
|
@@ -23,6 +33,14 @@ const simple_columns = reactive([
|
|
|
23
33
|
label: 'Name',
|
|
24
34
|
key: 'name',
|
|
25
35
|
width: 3,
|
|
36
|
+
getLabel: ({ row }) => row.name,
|
|
37
|
+
prefix: ({ row }) => {
|
|
38
|
+
return h(Avatar, {
|
|
39
|
+
shape: 'circle',
|
|
40
|
+
image: row.user_image,
|
|
41
|
+
size: 'sm',
|
|
42
|
+
})
|
|
43
|
+
},
|
|
26
44
|
},
|
|
27
45
|
{
|
|
28
46
|
label: 'Email',
|
|
@@ -46,6 +64,7 @@ const simple_rows = [
|
|
|
46
64
|
email: 'john@doe.com',
|
|
47
65
|
status: 'Active',
|
|
48
66
|
role: 'Developer',
|
|
67
|
+
user_image: 'https://avatars.githubusercontent.com/u/499550',
|
|
49
68
|
},
|
|
50
69
|
{
|
|
51
70
|
id: 2,
|
|
@@ -53,9 +72,148 @@ const simple_rows = [
|
|
|
53
72
|
email: 'jane@doe.com',
|
|
54
73
|
status: 'Inactive',
|
|
55
74
|
role: 'HR',
|
|
75
|
+
user_image: 'https://avatars.githubusercontent.com/u/499120',
|
|
56
76
|
},
|
|
57
77
|
]
|
|
58
78
|
|
|
79
|
+
const group_columns = reactive([
|
|
80
|
+
{
|
|
81
|
+
label: 'Name',
|
|
82
|
+
key: 'name',
|
|
83
|
+
width: 3,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: 'Email',
|
|
87
|
+
key: 'email',
|
|
88
|
+
width: '200px',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label: 'Role',
|
|
92
|
+
key: 'role',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Status',
|
|
96
|
+
key: 'status',
|
|
97
|
+
},
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
const grouped_rows = ref([
|
|
101
|
+
{
|
|
102
|
+
group: 'Developer',
|
|
103
|
+
collapsed: false,
|
|
104
|
+
rows: [
|
|
105
|
+
{
|
|
106
|
+
id: 2,
|
|
107
|
+
name: 'Gary Fox',
|
|
108
|
+
email: 'gary@fox.com',
|
|
109
|
+
status: 'Inactive',
|
|
110
|
+
role: 'Developer',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 6,
|
|
114
|
+
name: 'Emily Davis',
|
|
115
|
+
email: 'emily@davis.com',
|
|
116
|
+
status: 'Active',
|
|
117
|
+
role: 'Developer',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 9,
|
|
121
|
+
name: 'David Lee',
|
|
122
|
+
email: 'david@lee.com',
|
|
123
|
+
status: 'Inactive',
|
|
124
|
+
role: 'Developer',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
group: 'Manager',
|
|
130
|
+
collapsed: false,
|
|
131
|
+
rows: [
|
|
132
|
+
{
|
|
133
|
+
id: 3,
|
|
134
|
+
name: 'John Doe',
|
|
135
|
+
email: 'john@doe.com',
|
|
136
|
+
status: 'Active',
|
|
137
|
+
role: 'Manager',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 8,
|
|
141
|
+
name: 'Sarah Wilson',
|
|
142
|
+
email: 'sarah@wilson.com',
|
|
143
|
+
status: 'Active',
|
|
144
|
+
role: 'Manager',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
group: 'Designer',
|
|
150
|
+
collapsed: false,
|
|
151
|
+
rows: [
|
|
152
|
+
{
|
|
153
|
+
id: 4,
|
|
154
|
+
name: 'Alice Smith',
|
|
155
|
+
email: 'alice@smith.com',
|
|
156
|
+
status: 'Active',
|
|
157
|
+
role: 'Designer',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 10,
|
|
161
|
+
name: 'Olivia Taylor',
|
|
162
|
+
email: 'olivia@taylor.com',
|
|
163
|
+
status: 'Active',
|
|
164
|
+
role: 'Designer',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
group: 'HR',
|
|
170
|
+
collapsed: false,
|
|
171
|
+
rows: [
|
|
172
|
+
{
|
|
173
|
+
id: 1,
|
|
174
|
+
name: 'Jane Mary',
|
|
175
|
+
email: 'jane@doe.com',
|
|
176
|
+
status: 'Inactive',
|
|
177
|
+
role: 'HR',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 7,
|
|
181
|
+
name: 'Michael Brown',
|
|
182
|
+
email: 'michael@brown.com',
|
|
183
|
+
status: 'Inactive',
|
|
184
|
+
role: 'HR',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 12,
|
|
188
|
+
name: 'Sophia Martinez',
|
|
189
|
+
email: 'sophia@martinez.com',
|
|
190
|
+
status: 'Active',
|
|
191
|
+
role: 'HR',
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
group: 'Tester',
|
|
197
|
+
collapsed: false,
|
|
198
|
+
rows: [
|
|
199
|
+
{
|
|
200
|
+
id: 5,
|
|
201
|
+
name: 'Bob Johnson',
|
|
202
|
+
email: 'bob@johnson.com',
|
|
203
|
+
status: 'Inactive',
|
|
204
|
+
role: 'Tester',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 11,
|
|
208
|
+
name: 'James Anderson',
|
|
209
|
+
email: 'james@anderson.com',
|
|
210
|
+
status: 'Inactive',
|
|
211
|
+
role: 'Tester',
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
])
|
|
216
|
+
|
|
59
217
|
const custom_columns = reactive([
|
|
60
218
|
{
|
|
61
219
|
label: 'Name',
|
|
@@ -121,7 +279,7 @@ const custom_rows = [
|
|
|
121
279
|
<Story :layout="{ type: 'grid', width: '95%' }">
|
|
122
280
|
<Variant title="Simple List">
|
|
123
281
|
<ListView
|
|
124
|
-
class="h-[
|
|
282
|
+
class="h-[150px]"
|
|
125
283
|
:columns="simple_columns"
|
|
126
284
|
:rows="simple_rows"
|
|
127
285
|
:options="{
|
|
@@ -135,7 +293,7 @@ const custom_rows = [
|
|
|
135
293
|
</Variant>
|
|
136
294
|
<Variant title="Custom List">
|
|
137
295
|
<ListView
|
|
138
|
-
class="h-[
|
|
296
|
+
class="h-[150px]"
|
|
139
297
|
:columns="custom_columns"
|
|
140
298
|
:rows="custom_rows"
|
|
141
299
|
:options="{
|
|
@@ -202,11 +360,64 @@ const custom_rows = [
|
|
|
202
360
|
</ListSelectBanner>
|
|
203
361
|
</ListView>
|
|
204
362
|
</Variant>
|
|
363
|
+
<Variant title="Grouped Rows">
|
|
364
|
+
<ListView
|
|
365
|
+
class="h-[250px]"
|
|
366
|
+
:columns="group_columns"
|
|
367
|
+
:rows="grouped_rows"
|
|
368
|
+
:options="{
|
|
369
|
+
getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }),
|
|
370
|
+
selectable: state.selectable,
|
|
371
|
+
showTooltip: state.showTooltip,
|
|
372
|
+
resizeColumn: state.resizeColumn,
|
|
373
|
+
}"
|
|
374
|
+
row-key="id"
|
|
375
|
+
v-slot="{ showGroupedRows, selectable }"
|
|
376
|
+
>
|
|
377
|
+
<ListHeader />
|
|
378
|
+
<ListGroups v-if="showGroupedRows">
|
|
379
|
+
<template #group-header="{ group }">
|
|
380
|
+
<span class="text-base font-medium leading-6 text-gray-900">
|
|
381
|
+
{{ group.group }} ({{ group.rows.length }})
|
|
382
|
+
</span>
|
|
383
|
+
</template>
|
|
384
|
+
</ListGroups>
|
|
385
|
+
<ListRows v-else />
|
|
386
|
+
<ListSelectBanner v-if="selectable" />
|
|
387
|
+
</ListView>
|
|
388
|
+
</Variant>
|
|
389
|
+
<Variant title="Empty List">
|
|
390
|
+
<div>
|
|
391
|
+
<ListView
|
|
392
|
+
class="h-[250px]"
|
|
393
|
+
:columns="simple_columns"
|
|
394
|
+
:rows="[]"
|
|
395
|
+
:options="{
|
|
396
|
+
selectable: state.selectable,
|
|
397
|
+
showTooltip: state.showTooltip,
|
|
398
|
+
resizeColumn: state.resizeColumn,
|
|
399
|
+
emptyState: state.emptyState,
|
|
400
|
+
}"
|
|
401
|
+
row-key="id"
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
</Variant>
|
|
205
405
|
|
|
206
406
|
<template #controls>
|
|
207
407
|
<HstCheckbox v-model="state.selectable" title="Selectable" />
|
|
208
408
|
<HstCheckbox v-model="state.showTooltip" title="Show tooltip" />
|
|
209
409
|
<HstCheckbox v-model="state.resizeColumn" title="Resize Column" />
|
|
410
|
+
<!-- empty state config -->
|
|
411
|
+
<HstText
|
|
412
|
+
v-model="state.emptyState.title"
|
|
413
|
+
title="Empty Title"
|
|
414
|
+
placeholder="No records found"
|
|
415
|
+
/>
|
|
416
|
+
<HstText
|
|
417
|
+
v-model="state.emptyState.description"
|
|
418
|
+
title="Empty Description"
|
|
419
|
+
placeholder="Create a new record to get started"
|
|
420
|
+
/>
|
|
210
421
|
</template>
|
|
211
422
|
</Story>
|
|
212
423
|
</template>
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
<teleport to="#frappeui-popper-root">
|
|
18
18
|
<div
|
|
19
19
|
ref="popover"
|
|
20
|
-
|
|
21
|
-
class="
|
|
20
|
+
class="relative z-[100]"
|
|
21
|
+
:class="[popoverContainerClass, popoverClass]"
|
|
22
22
|
:style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
|
|
23
23
|
@mouseover="pointerOverTargetOrPopup = true"
|
|
24
24
|
@mouseleave="onMouseleave"
|
|
@@ -87,6 +87,7 @@ export default {
|
|
|
87
87
|
expose: ['open', 'close'],
|
|
88
88
|
data() {
|
|
89
89
|
return {
|
|
90
|
+
popoverContainerClass: 'body-container',
|
|
90
91
|
showPopup: false,
|
|
91
92
|
targetWidth: null,
|
|
92
93
|
pointerOverTargetOrPopup: false,
|
|
@@ -111,14 +112,35 @@ export default {
|
|
|
111
112
|
},
|
|
112
113
|
mounted() {
|
|
113
114
|
this.listener = (e) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
const clickedElement = e.target
|
|
116
|
+
const reference = this.$refs.reference
|
|
117
|
+
const popoverBody = this.$refs.popover
|
|
118
|
+
const insideClick =
|
|
119
|
+
clickedElement === reference ||
|
|
120
|
+
clickedElement === popoverBody ||
|
|
121
|
+
reference?.contains(clickedElement) ||
|
|
122
|
+
popoverBody?.contains(clickedElement)
|
|
118
123
|
if (insideClick) {
|
|
119
124
|
return
|
|
120
125
|
}
|
|
121
|
-
|
|
126
|
+
|
|
127
|
+
const root = document.getElementById('frappeui-popper-root')
|
|
128
|
+
const insidePopoverRoot = root.contains(clickedElement)
|
|
129
|
+
if (!insidePopoverRoot) {
|
|
130
|
+
return this.close()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bodyClass = `.${this.popoverContainerClass}`
|
|
134
|
+
const clickedElementBody = clickedElement?.closest(bodyClass)
|
|
135
|
+
const currentPopoverBody = reference?.closest(bodyClass)
|
|
136
|
+
const isSiblingClicked =
|
|
137
|
+
clickedElementBody &&
|
|
138
|
+
currentPopoverBody &&
|
|
139
|
+
clickedElementBody === currentPopoverBody
|
|
140
|
+
|
|
141
|
+
if (isSiblingClicked) {
|
|
142
|
+
this.close()
|
|
143
|
+
}
|
|
122
144
|
}
|
|
123
145
|
if (this.hideOnBlur) {
|
|
124
146
|
document.addEventListener('click', this.listener)
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export {
|
|
|
41
41
|
export { default as ListView } from './components/ListView/ListView.vue'
|
|
42
42
|
export { default as ListHeader } from './components/ListView/ListHeader.vue'
|
|
43
43
|
export { default as ListHeaderItem } from './components/ListView/ListHeaderItem.vue'
|
|
44
|
+
export { default as ListEmptyState } from './components/ListView/ListEmptyState.vue'
|
|
44
45
|
export { default as ListRows } from './components/ListView/ListRows.vue'
|
|
45
46
|
export { default as ListRow } from './components/ListView/ListRow.vue'
|
|
46
47
|
export { default as ListRowItem } from './components/ListView/ListRowItem.vue'
|