frappe-ui 0.0.4 → 0.0.5
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 +7 -1
- package/readme.md +6 -1
- package/src/components/Button.vue +43 -19
- package/src/components/Dialog.vue +166 -40
- package/src/components/Dropdown.vue +90 -197
- package/src/index.js +0 -1
- package/src/utils/call.js +15 -1
- package/src/utils/resources.js +8 -1
- package/src/components/NewDialog.vue +0 -183
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"test": "npx prettier --check ./src",
|
|
7
8
|
"prettier": "npx prettier -w ./src"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"src"
|
|
11
12
|
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/frappe/frappe-ui.git"
|
|
16
|
+
},
|
|
12
17
|
"author": "Frappe Technologies Pvt. Ltd.",
|
|
13
18
|
"license": "MIT",
|
|
14
19
|
"dependencies": {
|
|
20
|
+
"@headlessui/vue": "^1.4.3",
|
|
15
21
|
"@popperjs/core": "^2.11.2",
|
|
16
22
|
"@tailwindcss/forms": "^0.4.0",
|
|
17
23
|
"@tailwindcss/typography": "^0.5.0",
|
package/readme.md
CHANGED
|
@@ -5,6 +5,11 @@ A set of components and utilities for rapid UI development.
|
|
|
5
5
|
Frappe UI components are built using Vue 3 and Tailwind. Along with components,
|
|
6
6
|
it also ships with directives and utilities that make UI development easier.
|
|
7
7
|
|
|
8
|
+
<details>
|
|
9
|
+
<summary>Show components</summary>
|
|
10
|
+
<img src="https://user-images.githubusercontent.com/9355208/152111395-a62e1599-112c-450f-a32a-5debc791eb2e.png" width="80%" />
|
|
11
|
+
</details>
|
|
12
|
+
|
|
8
13
|
## Installation
|
|
9
14
|
```sh
|
|
10
15
|
npm install frappe-ui
|
|
@@ -310,4 +315,4 @@ export default {
|
|
|
310
315
|
```
|
|
311
316
|
|
|
312
317
|
## License
|
|
313
|
-
MIT
|
|
318
|
+
MIT
|
|
@@ -8,26 +8,38 @@
|
|
|
8
8
|
<LoadingIndicator
|
|
9
9
|
v-if="loading"
|
|
10
10
|
:class="{
|
|
11
|
-
'text-white':
|
|
12
|
-
'text-gray-600':
|
|
13
|
-
'text-red-200':
|
|
11
|
+
'text-white': appearance == 'primary',
|
|
12
|
+
'text-gray-600': appearance == 'secondary',
|
|
13
|
+
'text-red-200': appearance == 'danger',
|
|
14
14
|
}"
|
|
15
15
|
/>
|
|
16
|
-
<FeatherIcon
|
|
16
|
+
<FeatherIcon
|
|
17
|
+
v-else-if="iconLeft"
|
|
18
|
+
:name="iconLeft"
|
|
19
|
+
class="w-4 h-4 mr-1.5"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
/>
|
|
17
22
|
<template v-if="loading && loadingText">{{ loadingText }}</template>
|
|
18
23
|
<template v-else-if="icon">
|
|
19
|
-
<FeatherIcon :name="icon" class="w-4 h-4" />
|
|
24
|
+
<FeatherIcon :name="icon" class="w-4 h-4" :aria-label="label" />
|
|
20
25
|
</template>
|
|
21
26
|
<span :class="icon ? 'sr-only' : ''">
|
|
22
27
|
<slot></slot>
|
|
23
28
|
</span>
|
|
24
|
-
<FeatherIcon
|
|
29
|
+
<FeatherIcon
|
|
30
|
+
v-if="iconRight"
|
|
31
|
+
:name="iconRight"
|
|
32
|
+
class="w-4 h-4 ml-2"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
/>
|
|
25
35
|
</button>
|
|
26
36
|
</template>
|
|
27
37
|
<script>
|
|
28
38
|
import FeatherIcon from './FeatherIcon.vue'
|
|
29
39
|
import LoadingIndicator from './LoadingIndicator.vue'
|
|
30
40
|
|
|
41
|
+
const ValidAppearances = ['primary', 'secondary', 'danger', 'white', 'minimal']
|
|
42
|
+
|
|
31
43
|
export default {
|
|
32
44
|
name: 'Button',
|
|
33
45
|
components: {
|
|
@@ -35,9 +47,16 @@ export default {
|
|
|
35
47
|
LoadingIndicator,
|
|
36
48
|
},
|
|
37
49
|
props: {
|
|
38
|
-
|
|
50
|
+
label: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: null,
|
|
53
|
+
},
|
|
54
|
+
appearance: {
|
|
39
55
|
type: String,
|
|
40
56
|
default: 'secondary',
|
|
57
|
+
validator: (value) => {
|
|
58
|
+
return ValidAppearances.includes(value)
|
|
59
|
+
},
|
|
41
60
|
},
|
|
42
61
|
disabled: {
|
|
43
62
|
type: Boolean,
|
|
@@ -71,20 +90,25 @@ export default {
|
|
|
71
90
|
},
|
|
72
91
|
computed: {
|
|
73
92
|
buttonClasses() {
|
|
93
|
+
let appearanceClasses = {
|
|
94
|
+
primary:
|
|
95
|
+
'bg-blue-500 hover:bg-blue-600 text-white focus:ring-2 focus:ring-offset-2 focus:ring-blue-500',
|
|
96
|
+
secondary:
|
|
97
|
+
'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500',
|
|
98
|
+
danger:
|
|
99
|
+
'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500',
|
|
100
|
+
white:
|
|
101
|
+
'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400',
|
|
102
|
+
minimal:
|
|
103
|
+
'bg-transparent hover:bg-gray-200 active:bg-gray-200 text-gray-900',
|
|
104
|
+
}
|
|
74
105
|
return [
|
|
75
|
-
'inline-flex items-center justify-center text-base leading-5 rounded-md focus:outline-none',
|
|
106
|
+
'inline-flex items-center justify-center text-base leading-5 rounded-md transition-colors focus:outline-none',
|
|
76
107
|
this.icon ? 'p-1.5' : 'px-3 py-1',
|
|
77
|
-
|
|
78
|
-
'opacity-50 cursor-not-allowed pointer-events-none'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500':
|
|
82
|
-
this.type === 'secondary',
|
|
83
|
-
'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500':
|
|
84
|
-
this.type === 'danger',
|
|
85
|
-
'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400':
|
|
86
|
-
this.type === 'white',
|
|
87
|
-
},
|
|
108
|
+
this.isDisabled
|
|
109
|
+
? 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
110
|
+
: '',
|
|
111
|
+
appearanceClasses[this.appearance],
|
|
88
112
|
]
|
|
89
113
|
},
|
|
90
114
|
isDisabled() {
|
|
@@ -1,58 +1,184 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
<TransitionRoot as="template" :show="open">
|
|
3
|
+
<HDialog
|
|
4
|
+
as="div"
|
|
5
|
+
class="fixed inset-0 z-10 overflow-y-auto"
|
|
6
|
+
@close="open = false"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0"
|
|
10
|
+
>
|
|
11
|
+
<TransitionChild
|
|
12
|
+
as="template"
|
|
13
|
+
enter="ease-out duration-300"
|
|
14
|
+
enter-from="opacity-0"
|
|
15
|
+
enter-to="opacity-100"
|
|
16
|
+
leave="ease-in duration-200"
|
|
17
|
+
leave-from="opacity-100"
|
|
18
|
+
leave-to="opacity-0"
|
|
19
|
+
>
|
|
20
|
+
<DialogOverlay
|
|
21
|
+
class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
|
|
22
|
+
/>
|
|
23
|
+
</TransitionChild>
|
|
24
|
+
|
|
25
|
+
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
26
|
+
<span
|
|
27
|
+
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
​
|
|
31
|
+
</span>
|
|
32
|
+
<TransitionChild
|
|
33
|
+
as="template"
|
|
34
|
+
enter="ease-out duration-300"
|
|
35
|
+
enter-from="opacity-0 translate-y-4 sm:-translate-y-12 sm:scale-95"
|
|
36
|
+
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
|
37
|
+
leave="ease-in duration-200"
|
|
38
|
+
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
|
39
|
+
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
|
20
43
|
>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
<slot name="body">
|
|
45
|
+
<slot name="body-main">
|
|
46
|
+
<div class="px-4 py-5 bg-white sm:p-6">
|
|
47
|
+
<div>
|
|
48
|
+
<div
|
|
49
|
+
v-if="icon"
|
|
50
|
+
class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto mb-3 rounded-full sm:mx-0 sm:h-10 sm:w-10 sm:mb-0 sm:mr-4"
|
|
51
|
+
:class="{
|
|
52
|
+
'bg-yellow-100': icon.type === 'warning',
|
|
53
|
+
'bg-blue-100': icon.type === 'info',
|
|
54
|
+
'bg-red-100': icon.type === 'danger',
|
|
55
|
+
'bg-green-100': icon.type === 'success',
|
|
56
|
+
}"
|
|
57
|
+
>
|
|
58
|
+
<FeatherIcon
|
|
59
|
+
:name="icon.name"
|
|
60
|
+
class="w-6 h-6 text-red-600"
|
|
61
|
+
:class="{
|
|
62
|
+
'text-yellow-600': icon.type === 'warning',
|
|
63
|
+
'text-blue-600': icon.type === 'info',
|
|
64
|
+
'text-red-600': icon.type === 'danger',
|
|
65
|
+
'text-green-600': icon.type === 'success',
|
|
66
|
+
}"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="text-center sm:text-left">
|
|
71
|
+
<DialogTitle as="header">
|
|
72
|
+
<slot name="body-title">
|
|
73
|
+
<h3
|
|
74
|
+
class="mb-2 text-lg font-medium leading-6 text-gray-900"
|
|
75
|
+
>
|
|
76
|
+
{{ options.title || 'Untitled' }}
|
|
77
|
+
</h3>
|
|
78
|
+
</slot>
|
|
79
|
+
</DialogTitle>
|
|
80
|
+
|
|
81
|
+
<slot name="body-content">
|
|
82
|
+
<p class="text-sm text-gray-500" v-if="options.message">
|
|
83
|
+
{{ options.message }}
|
|
84
|
+
</p>
|
|
85
|
+
</slot>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</slot>
|
|
90
|
+
<div
|
|
91
|
+
class="px-4 py-3 space-y-2 sm:space-x-reverse sm:space-x-3 sm:space-y-0 bg-gray-50 sm:px-6 sm:flex sm:flex-row-reverse"
|
|
92
|
+
v-if="options?.actions || $slots.actions"
|
|
93
|
+
>
|
|
94
|
+
<slot name="actions" v-bind="{ close: () => (open = false) }">
|
|
95
|
+
<Button
|
|
96
|
+
v-for="action in options.actions"
|
|
97
|
+
:key="action.label"
|
|
98
|
+
:appearance="action.appearance"
|
|
99
|
+
@click="
|
|
100
|
+
() => {
|
|
101
|
+
if (action.handler && action.handler === 'cancel') {
|
|
102
|
+
open = false
|
|
103
|
+
} else {
|
|
104
|
+
action.handler()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
"
|
|
108
|
+
>
|
|
109
|
+
{{ action.label }}
|
|
110
|
+
</Button>
|
|
111
|
+
</slot>
|
|
112
|
+
</div>
|
|
113
|
+
</slot>
|
|
25
114
|
</div>
|
|
26
|
-
</
|
|
115
|
+
</TransitionChild>
|
|
27
116
|
</div>
|
|
28
|
-
</
|
|
29
|
-
|
|
30
|
-
class="sticky bottom-0 flex items-center justify-end p-4 bg-white sm:px-6 sm:py-4"
|
|
31
|
-
v-if="$slots.actions"
|
|
32
|
-
>
|
|
33
|
-
<slot name="actions"></slot>
|
|
34
|
-
</div>
|
|
35
|
-
</Modal>
|
|
117
|
+
</HDialog>
|
|
118
|
+
</TransitionRoot>
|
|
36
119
|
</template>
|
|
37
120
|
|
|
38
121
|
<script>
|
|
39
|
-
import
|
|
40
|
-
import
|
|
122
|
+
import { computed } from 'vue'
|
|
123
|
+
import {
|
|
124
|
+
Dialog as HDialog,
|
|
125
|
+
DialogOverlay,
|
|
126
|
+
DialogTitle,
|
|
127
|
+
TransitionChild,
|
|
128
|
+
TransitionRoot,
|
|
129
|
+
} from '@headlessui/vue'
|
|
130
|
+
import { Button, FeatherIcon } from 'frappe-ui'
|
|
41
131
|
|
|
42
132
|
export default {
|
|
43
133
|
name: 'Dialog',
|
|
44
|
-
props:
|
|
134
|
+
props: {
|
|
135
|
+
modelValue: {
|
|
136
|
+
type: Boolean,
|
|
137
|
+
required: true,
|
|
138
|
+
},
|
|
139
|
+
options: {
|
|
140
|
+
type: Object,
|
|
141
|
+
default() {
|
|
142
|
+
return {}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
45
146
|
emits: ['update:modelValue', 'close'],
|
|
46
147
|
components: {
|
|
47
|
-
|
|
148
|
+
HDialog,
|
|
149
|
+
DialogOverlay,
|
|
150
|
+
DialogTitle,
|
|
151
|
+
TransitionChild,
|
|
152
|
+
TransitionRoot,
|
|
153
|
+
Button,
|
|
48
154
|
FeatherIcon,
|
|
49
155
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
setup(props, { emit }) {
|
|
157
|
+
let open = computed({
|
|
158
|
+
get: () => props.modelValue,
|
|
159
|
+
set: (val) => {
|
|
160
|
+
emit('update:modelValue', val)
|
|
161
|
+
if (!val) {
|
|
162
|
+
emit('close')
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
return {
|
|
167
|
+
open,
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
computed: {
|
|
171
|
+
icon() {
|
|
172
|
+
if (!this.options?.icon) return null
|
|
173
|
+
|
|
174
|
+
let icon = this.options.icon
|
|
175
|
+
if (typeof icon === 'string') {
|
|
176
|
+
icon = {
|
|
177
|
+
name: icon,
|
|
178
|
+
type: 'info',
|
|
179
|
+
}
|
|
55
180
|
}
|
|
181
|
+
return icon
|
|
56
182
|
},
|
|
57
183
|
},
|
|
58
184
|
}
|
|
@@ -1,220 +1,113 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
:style="{ width: dropdownWidthFull ? targetWidth + 'px' : undefined }"
|
|
2
|
+
<Menu as="div" class="relative inline-block text-left">
|
|
3
|
+
<MenuButton>
|
|
4
|
+
<slot v-if="$slots.default"></slot>
|
|
5
|
+
<Button v-else v-bind="button">
|
|
6
|
+
{{ button ? button?.label || null : 'Options' }}
|
|
7
|
+
</Button>
|
|
8
|
+
</MenuButton>
|
|
9
|
+
|
|
10
|
+
<transition
|
|
11
|
+
enter-active-class="transition duration-100 ease-out"
|
|
12
|
+
enter-from-class="transform scale-95 opacity-0"
|
|
13
|
+
enter-to-class="transform scale-100 opacity-100"
|
|
14
|
+
leave-active-class="transition duration-75 ease-in"
|
|
15
|
+
leave-from-class="transform scale-100 opacity-100"
|
|
16
|
+
leave-to-class="transform scale-95 opacity-0"
|
|
17
|
+
>
|
|
18
|
+
<MenuItems
|
|
19
|
+
class="absolute z-10 mt-2 bg-white divide-y divide-gray-100 rounded-md shadow-lg min-w-40 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
|
20
|
+
:class="
|
|
21
|
+
placement === 'left'
|
|
22
|
+
? 'left-0 origin-top-left'
|
|
23
|
+
: 'right-0 origin-top-right'
|
|
24
|
+
"
|
|
26
25
|
>
|
|
27
|
-
<div class="
|
|
28
|
-
<div
|
|
29
|
-
|
|
26
|
+
<div v-for="group in groups" :key="group.key" class="px-1 py-1">
|
|
27
|
+
<div
|
|
28
|
+
v-if="group.group && !group.hideLabel"
|
|
29
|
+
class="px-2 py-1 text-xs font-semibold tracking-wider text-gray-500 uppercase"
|
|
30
|
+
>
|
|
31
|
+
{{ group.group }}
|
|
30
32
|
</div>
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
<MenuItem
|
|
34
|
+
v-for="item in group.items"
|
|
35
|
+
:key="item.label"
|
|
36
|
+
v-slot="{ active }"
|
|
37
|
+
>
|
|
38
|
+
<button
|
|
39
|
+
:class="[
|
|
40
|
+
active ? 'bg-gray-100' : 'text-gray-900',
|
|
41
|
+
'group flex rounded-md items-center w-full px-2 py-2 text-sm',
|
|
42
|
+
]"
|
|
43
|
+
:onClick="item.onClick"
|
|
44
|
+
>
|
|
45
|
+
<FeatherIcon
|
|
46
|
+
v-if="item.icon"
|
|
47
|
+
:name="item.icon"
|
|
48
|
+
class="flex-shrink-0 w-4 h-4 mr-2 text-gray-500"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
<span class="whitespace-nowrap">
|
|
52
|
+
{{ item.label }}
|
|
53
|
+
</span>
|
|
54
|
+
</button>
|
|
55
|
+
</MenuItem>
|
|
53
56
|
</div>
|
|
54
|
-
</
|
|
55
|
-
</
|
|
56
|
-
</
|
|
57
|
+
</MenuItems>
|
|
58
|
+
</transition>
|
|
59
|
+
</Menu>
|
|
57
60
|
</template>
|
|
58
61
|
|
|
59
62
|
<script>
|
|
60
|
-
import
|
|
61
|
-
import
|
|
63
|
+
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
|
64
|
+
import { FeatherIcon } from 'frappe-ui'
|
|
62
65
|
|
|
63
66
|
export default {
|
|
64
|
-
name: '
|
|
65
|
-
|
|
66
|
-
onOutsideClick,
|
|
67
|
-
},
|
|
68
|
-
props: {
|
|
69
|
-
items: {
|
|
70
|
-
type: Array,
|
|
71
|
-
default: () => [],
|
|
72
|
-
},
|
|
73
|
-
groups: {
|
|
74
|
-
type: Array,
|
|
75
|
-
default: null,
|
|
76
|
-
},
|
|
77
|
-
right: {
|
|
78
|
-
type: Boolean,
|
|
79
|
-
default: false,
|
|
80
|
-
},
|
|
81
|
-
isLoading: {
|
|
82
|
-
type: Boolean,
|
|
83
|
-
default: false,
|
|
84
|
-
},
|
|
85
|
-
dropdownWidthFull: {
|
|
86
|
-
type: Boolean,
|
|
87
|
-
default: false,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
67
|
+
name: 'NewDropdown',
|
|
68
|
+
props: ['button', 'options', 'placement'],
|
|
90
69
|
components: {
|
|
91
|
-
|
|
70
|
+
Menu,
|
|
71
|
+
MenuButton,
|
|
72
|
+
MenuItems,
|
|
73
|
+
MenuItem,
|
|
74
|
+
FeatherIcon,
|
|
92
75
|
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
highlightedIndex: -1,
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
computed: {
|
|
102
|
-
sortedGroups() {
|
|
103
|
-
if (Array.isArray(this.groups)) {
|
|
104
|
-
return this.groups
|
|
76
|
+
methods: {
|
|
77
|
+
normalizeDropdownItem(option) {
|
|
78
|
+
let onClick = option.handler || null
|
|
79
|
+
if (!onClick && option.route && this.$router) {
|
|
80
|
+
onClick = () => this.$router.push(option.route)
|
|
105
81
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.sort()
|
|
112
|
-
),
|
|
113
|
-
]
|
|
114
|
-
if (groupNames.length > 0) {
|
|
115
|
-
return groupNames
|
|
82
|
+
return {
|
|
83
|
+
label: option.label,
|
|
84
|
+
icon: option.icon,
|
|
85
|
+
group: option.group,
|
|
86
|
+
onClick,
|
|
116
87
|
}
|
|
117
|
-
return null
|
|
118
88
|
},
|
|
89
|
+
},
|
|
90
|
+
computed: {
|
|
119
91
|
dropdownItems() {
|
|
120
|
-
|
|
92
|
+
return (this.options || [])
|
|
121
93
|
.filter(Boolean)
|
|
122
|
-
.filter((
|
|
123
|
-
|
|
124
|
-
if (this.sortedGroups) {
|
|
125
|
-
let itemsByGroup = {}
|
|
126
|
-
|
|
127
|
-
for (let item of items) {
|
|
128
|
-
let group = item.group || ''
|
|
129
|
-
itemsByGroup[group] = itemsByGroup[group] || []
|
|
130
|
-
itemsByGroup[group].push(item)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let items = []
|
|
134
|
-
let i = 0
|
|
135
|
-
for (let group of this.sortedGroups) {
|
|
136
|
-
let groupItems = itemsByGroup[group]
|
|
137
|
-
groupItems = groupItems.map((d) => {
|
|
138
|
-
d.index = i
|
|
139
|
-
i++
|
|
140
|
-
return d
|
|
141
|
-
})
|
|
142
|
-
items = items.concat(
|
|
143
|
-
{
|
|
144
|
-
label: group,
|
|
145
|
-
isGroup: true,
|
|
146
|
-
},
|
|
147
|
-
groupItems
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return items
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return items.filter(Boolean).map((d, i) => {
|
|
155
|
-
d.index = i
|
|
156
|
-
return d
|
|
157
|
-
})
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
methods: {
|
|
161
|
-
selectItem(d) {
|
|
162
|
-
if (d.action) {
|
|
163
|
-
d.action()
|
|
164
|
-
}
|
|
94
|
+
.filter((option) => (option.condition ? option.condition() : true))
|
|
95
|
+
.map((option) => this.normalizeDropdownItem(option))
|
|
165
96
|
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.isShown = Boolean(flag)
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
selectHighlightedItem() {
|
|
174
|
-
if (![-1, this.items.length].includes(this.highlightedIndex)) {
|
|
175
|
-
// valid selection
|
|
176
|
-
let item = this.items[this.highlightedIndex]
|
|
177
|
-
this.selectItem(item)
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
highlightItemUp() {
|
|
181
|
-
this.highlightedIndex -= 1
|
|
182
|
-
if (this.highlightedIndex < 0) {
|
|
183
|
-
this.highlightedIndex = 0
|
|
184
|
-
}
|
|
185
|
-
this.$nextTick(() => {
|
|
186
|
-
let index = this.highlightedIndex
|
|
187
|
-
if (index !== 0) {
|
|
188
|
-
index -= 1
|
|
189
|
-
}
|
|
190
|
-
this.scrollToHighlighted()
|
|
191
|
-
})
|
|
192
|
-
},
|
|
193
|
-
highlightItemDown() {
|
|
194
|
-
this.highlightedIndex += 1
|
|
195
|
-
if (this.highlightedIndex > this.items.length) {
|
|
196
|
-
this.highlightedIndex = this.items.length
|
|
197
|
-
}
|
|
97
|
+
groups() {
|
|
98
|
+
let groups = this.options[0]?.group
|
|
99
|
+
? this.options
|
|
100
|
+
: [{ group: '', items: this.options }]
|
|
198
101
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
highlightedElement.scrollIntoView({ block: 'nearest' })
|
|
207
|
-
},
|
|
208
|
-
updateTargetWidth() {
|
|
209
|
-
this.$nextTick(() => {
|
|
210
|
-
this.targetWidth = this.$refs.target.clientWidth
|
|
102
|
+
return groups.map((group, i) => {
|
|
103
|
+
return {
|
|
104
|
+
key: i,
|
|
105
|
+
group: group.group,
|
|
106
|
+
hideLabel: group.hideLabel || false,
|
|
107
|
+
items: group.items.map((item) => this.normalizeDropdownItem(item)),
|
|
108
|
+
}
|
|
211
109
|
})
|
|
212
110
|
},
|
|
213
|
-
setItemRef(el) {
|
|
214
|
-
if (el) {
|
|
215
|
-
this.itemRefs.push(el)
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
111
|
},
|
|
219
112
|
}
|
|
220
113
|
</script>
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,6 @@ export { default as Badge } from './components/Badge.vue'
|
|
|
5
5
|
export { default as Button } from './components/Button.vue'
|
|
6
6
|
export { default as Card } from './components/Card.vue'
|
|
7
7
|
export { default as Dialog } from './components/Dialog.vue'
|
|
8
|
-
export { default as NewDialog } from './components/NewDialog.vue'
|
|
9
8
|
export { default as Dropdown } from './components/Dropdown.vue'
|
|
10
9
|
export { default as ErrorMessage } from './components/ErrorMessage.vue'
|
|
11
10
|
export { default as FeatherIcon } from './components/FeatherIcon.vue'
|
package/src/utils/call.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default async function call(method, args, options={}) {
|
|
1
|
+
export default async function call(method, args, options = {}) {
|
|
2
2
|
if (!args) {
|
|
3
3
|
args = {}
|
|
4
4
|
}
|
|
@@ -27,6 +27,20 @@ export default async function call(method, args, options={}) {
|
|
|
27
27
|
if (data.docs || method === 'login') {
|
|
28
28
|
return data
|
|
29
29
|
}
|
|
30
|
+
if (data.exc) {
|
|
31
|
+
try {
|
|
32
|
+
console.groupCollapsed(method)
|
|
33
|
+
console.log(`method: ${method}`)
|
|
34
|
+
console.log(`params:`, args)
|
|
35
|
+
let warning = JSON.parse(data.exc)
|
|
36
|
+
for (let text of warning) {
|
|
37
|
+
console.log(text)
|
|
38
|
+
}
|
|
39
|
+
console.groupEnd()
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn('Error printing debug messages', e)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
30
44
|
return data.message
|
|
31
45
|
} else {
|
|
32
46
|
let response = await res.text()
|
package/src/utils/resources.js
CHANGED
|
@@ -46,8 +46,13 @@ function createResource(options, vm, getResource) {
|
|
|
46
46
|
params = null
|
|
47
47
|
}
|
|
48
48
|
out.params = params || options.params
|
|
49
|
+
out.previousData = out.data ? JSON.parse(JSON.stringify(out.data)) : null
|
|
49
50
|
out.loading = true
|
|
50
51
|
|
|
52
|
+
if (options.onFetch) {
|
|
53
|
+
options.onFetch.call(vm, out.params)
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
if (options.validate) {
|
|
52
57
|
let invalidMessage
|
|
53
58
|
try {
|
|
@@ -67,7 +72,6 @@ function createResource(options, vm, getResource) {
|
|
|
67
72
|
|
|
68
73
|
try {
|
|
69
74
|
let data = await resourceFetcher(options.method, params || options.params)
|
|
70
|
-
out.previousData = out.data || null
|
|
71
75
|
out.data = data
|
|
72
76
|
out.fetched = true
|
|
73
77
|
if (options.onSuccess) {
|
|
@@ -93,6 +97,9 @@ function createResource(options, vm, getResource) {
|
|
|
93
97
|
|
|
94
98
|
function handleError(error) {
|
|
95
99
|
console.error(error)
|
|
100
|
+
if (out.previousData) {
|
|
101
|
+
out.data = out.previousData
|
|
102
|
+
}
|
|
96
103
|
out.error = error
|
|
97
104
|
if (options.onError) {
|
|
98
105
|
options.onError.call(vm, error)
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<TransitionRoot as="template" :show="open">
|
|
3
|
-
<Dialog
|
|
4
|
-
as="div"
|
|
5
|
-
class="fixed inset-0 z-10 overflow-y-auto"
|
|
6
|
-
@close="open = false"
|
|
7
|
-
>
|
|
8
|
-
<div
|
|
9
|
-
class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0"
|
|
10
|
-
>
|
|
11
|
-
<TransitionChild
|
|
12
|
-
as="template"
|
|
13
|
-
enter="ease-out duration-300"
|
|
14
|
-
enter-from="opacity-0"
|
|
15
|
-
enter-to="opacity-100"
|
|
16
|
-
leave="ease-in duration-200"
|
|
17
|
-
leave-from="opacity-100"
|
|
18
|
-
leave-to="opacity-0"
|
|
19
|
-
>
|
|
20
|
-
<DialogOverlay
|
|
21
|
-
class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
|
|
22
|
-
/>
|
|
23
|
-
</TransitionChild>
|
|
24
|
-
|
|
25
|
-
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
26
|
-
<span
|
|
27
|
-
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
28
|
-
aria-hidden="true"
|
|
29
|
-
>
|
|
30
|
-
​
|
|
31
|
-
</span>
|
|
32
|
-
<TransitionChild
|
|
33
|
-
as="template"
|
|
34
|
-
enter="ease-out duration-300"
|
|
35
|
-
enter-from="opacity-0 translate-y-4 sm:-translate-y-12 sm:scale-95"
|
|
36
|
-
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
|
37
|
-
leave="ease-in duration-200"
|
|
38
|
-
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
|
39
|
-
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
|
43
|
-
>
|
|
44
|
-
<slot name="dialog-body">
|
|
45
|
-
<slot name="dialog-main">
|
|
46
|
-
<div class="px-4 py-5 bg-white sm:p-6">
|
|
47
|
-
<div>
|
|
48
|
-
<div
|
|
49
|
-
v-if="icon"
|
|
50
|
-
class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto mb-3 rounded-full sm:mx-0 sm:h-10 sm:w-10 sm:mb-0 sm:mr-4"
|
|
51
|
-
:class="{
|
|
52
|
-
'bg-yellow-100': icon.type === 'warning',
|
|
53
|
-
'bg-blue-100': icon.type === 'info',
|
|
54
|
-
'bg-red-100': icon.type === 'danger',
|
|
55
|
-
'bg-green-100': icon.type === 'success',
|
|
56
|
-
}"
|
|
57
|
-
>
|
|
58
|
-
<FeatherIcon
|
|
59
|
-
:name="icon.name"
|
|
60
|
-
class="w-6 h-6 text-red-600"
|
|
61
|
-
:class="{
|
|
62
|
-
'text-yellow-600': icon.type === 'warning',
|
|
63
|
-
'text-blue-600': icon.type === 'info',
|
|
64
|
-
'text-red-600': icon.type === 'danger',
|
|
65
|
-
'text-green-600': icon.type === 'success',
|
|
66
|
-
}"
|
|
67
|
-
aria-hidden="true"
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
<div class="text-center sm:text-left">
|
|
71
|
-
<DialogTitle
|
|
72
|
-
as="h3"
|
|
73
|
-
class="text-lg font-medium leading-6 text-gray-900"
|
|
74
|
-
>
|
|
75
|
-
{{ options.title || 'Untitled' }}
|
|
76
|
-
</DialogTitle>
|
|
77
|
-
<div class="mt-2">
|
|
78
|
-
<slot name="dialog-content">
|
|
79
|
-
<p
|
|
80
|
-
class="text-sm text-gray-500"
|
|
81
|
-
v-if="options.message"
|
|
82
|
-
>
|
|
83
|
-
{{ options.message }}
|
|
84
|
-
</p>
|
|
85
|
-
</slot>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</slot>
|
|
91
|
-
<div
|
|
92
|
-
class="px-4 py-3 space-y-2 sm:space-x-reverse sm:space-x-3 sm:space-y-0 bg-gray-50 sm:px-6 sm:flex sm:flex-row-reverse"
|
|
93
|
-
v-if="options?.actions || $slots['dialog-actions']"
|
|
94
|
-
>
|
|
95
|
-
<slot
|
|
96
|
-
name="dialog-actions"
|
|
97
|
-
v-bind="{ close: () => (open = false) }"
|
|
98
|
-
>
|
|
99
|
-
<button
|
|
100
|
-
v-for="action in options.actions"
|
|
101
|
-
:key="action.label"
|
|
102
|
-
type="button"
|
|
103
|
-
class="inline-flex justify-center w-full px-4 py-2 text-base font-medium border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:w-auto sm:text-sm"
|
|
104
|
-
:class="{
|
|
105
|
-
'text-gray-700 bg-white border-gray-300 hover:bg-gray-50 focus:ring-primary-500':
|
|
106
|
-
!action.type || action.type === 'default',
|
|
107
|
-
'text-white bg-yellow-600 border-transparent hover:bg-yellow-700 focus:ring-yellow-500':
|
|
108
|
-
action.type === 'warning',
|
|
109
|
-
'text-white bg-blue-600 border-transparent hover:bg-blue-700 focus:ring-blue-500':
|
|
110
|
-
action.type === 'info',
|
|
111
|
-
'text-white bg-red-600 border-transparent hover:bg-red-700 focus:ring-red-500':
|
|
112
|
-
action.type === 'danger',
|
|
113
|
-
'text-white bg-green-600 border-transparent hover:bg-green-700 focus:ring-green-500':
|
|
114
|
-
action.type === 'success',
|
|
115
|
-
}"
|
|
116
|
-
@click="
|
|
117
|
-
() => {
|
|
118
|
-
if (action.action && action.action === 'cancel') {
|
|
119
|
-
open = false
|
|
120
|
-
} else {
|
|
121
|
-
action.action()
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
"
|
|
125
|
-
>
|
|
126
|
-
{{ action.label }}
|
|
127
|
-
</button>
|
|
128
|
-
</slot>
|
|
129
|
-
</div>
|
|
130
|
-
</slot>
|
|
131
|
-
</div>
|
|
132
|
-
</TransitionChild>
|
|
133
|
-
</div>
|
|
134
|
-
</Dialog>
|
|
135
|
-
</TransitionRoot>
|
|
136
|
-
</template>
|
|
137
|
-
|
|
138
|
-
<script>
|
|
139
|
-
import { ref, computed } from 'vue'
|
|
140
|
-
import {
|
|
141
|
-
Dialog,
|
|
142
|
-
DialogOverlay,
|
|
143
|
-
DialogTitle,
|
|
144
|
-
TransitionChild,
|
|
145
|
-
TransitionRoot,
|
|
146
|
-
} from '@headlessui/vue'
|
|
147
|
-
|
|
148
|
-
export default {
|
|
149
|
-
name: 'NewDialog',
|
|
150
|
-
props: ['modelValue', 'options'],
|
|
151
|
-
emits: ['update:modelValue'],
|
|
152
|
-
components: {
|
|
153
|
-
Dialog,
|
|
154
|
-
DialogOverlay,
|
|
155
|
-
DialogTitle,
|
|
156
|
-
TransitionChild,
|
|
157
|
-
TransitionRoot,
|
|
158
|
-
},
|
|
159
|
-
setup(props, { emit }) {
|
|
160
|
-
let open = computed({
|
|
161
|
-
get: () => props.modelValue,
|
|
162
|
-
set: (val) => emit('update:modelValue', val),
|
|
163
|
-
})
|
|
164
|
-
return {
|
|
165
|
-
open,
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
computed: {
|
|
169
|
-
icon() {
|
|
170
|
-
if (!this.options?.icon) return null
|
|
171
|
-
|
|
172
|
-
let icon = this.options.icon
|
|
173
|
-
if (typeof icon === 'string') {
|
|
174
|
-
icon = {
|
|
175
|
-
name: icon,
|
|
176
|
-
type: 'info',
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return icon
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
}
|
|
183
|
-
</script>
|