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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -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 py-2.5"
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 :item="row[column.key]" :align="column.align" />
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
- <ListRows />
10
- <ListSelectBanner v-if="_options.selectable" />
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 ListView from './ListView/ListView.vue'
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 FeatherIcon from './FeatherIcon.vue'
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-[250px]"
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-[250px]"
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
- :class="popoverClass"
21
- class="popover-container relative z-[100]"
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
- let $els = [this.$refs.reference, this.$refs.popover]
115
- let insideClick = $els.some(
116
- ($el) => $el && (e.target === $el || $el.contains(e.target))
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
- this.close()
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)
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
3
+ <path
4
+ fill="currentColor"
5
+ d="M4.293 5.28h7.413a.5.5 0 0 1 .41.787l-3.707 5.295a.5.5 0 0 1-.82 0L3.884 6.067a.5.5 0 0 1 .41-.787Z"
6
+ />
7
+ </svg>
8
+ </template>
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'