frappe-ui 0.0.2 → 0.0.6
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/Avatar.vue +1 -1
- package/src/components/Button.vue +43 -19
- package/src/components/Dialog.vue +166 -40
- package/src/components/Dropdown.vue +90 -197
- package/src/components/FileUploader.vue +220 -0
- package/src/index.js +3 -1
- package/src/utils/call.js +27 -2
- package/src/utils/debounce.js +15 -0
- package/src/utils/plugin.js +3 -2
- package/src/utils/resources.js +123 -44
- package/src/utils/tailwind.config.js +1 -3
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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,7 +8,7 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<div
|
|
10
10
|
v-else
|
|
11
|
-
class="flex items-center justify-center w-full h-full text-
|
|
11
|
+
class="flex items-center justify-center w-full h-full text-gray-600 uppercase bg-gray-200"
|
|
12
12
|
:class="{ sm: 'text-xs', md: 'text-base', lg: 'text-lg' }[size]"
|
|
13
13
|
>
|
|
14
14
|
{{ label && label[0] }}
|
|
@@ -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:shadow-outline-gray':
|
|
82
|
-
this.type === 'secondary',
|
|
83
|
-
'bg-red-500 hover:bg-red-400 text-white focus:shadow-outline-red':
|
|
84
|
-
this.type === 'danger',
|
|
85
|
-
'bg-white text-gray-900 shadow focus:ring 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>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<input
|
|
4
|
+
ref="input"
|
|
5
|
+
type="file"
|
|
6
|
+
:accept="fileTypes"
|
|
7
|
+
class="hidden"
|
|
8
|
+
@change="onFileAdd"
|
|
9
|
+
/>
|
|
10
|
+
<slot
|
|
11
|
+
v-bind="{
|
|
12
|
+
file,
|
|
13
|
+
uploading,
|
|
14
|
+
progress,
|
|
15
|
+
uploaded,
|
|
16
|
+
message,
|
|
17
|
+
error,
|
|
18
|
+
total,
|
|
19
|
+
success,
|
|
20
|
+
openFileSelector,
|
|
21
|
+
}"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
class FileUploader {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.listeners = {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
on(event, handler) {
|
|
33
|
+
this.listeners[event] = this.listeners[event] || []
|
|
34
|
+
this.listeners[event].push(handler)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
trigger(event, data) {
|
|
38
|
+
let handlers = this.listeners[event] || []
|
|
39
|
+
handlers.forEach((handler) => {
|
|
40
|
+
handler.call(this, data)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
upload(file, options) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
let xhr = new XMLHttpRequest()
|
|
47
|
+
xhr.upload.addEventListener('loadstart', () => {
|
|
48
|
+
this.trigger('start')
|
|
49
|
+
})
|
|
50
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
51
|
+
if (e.lengthComputable) {
|
|
52
|
+
this.trigger('progress', {
|
|
53
|
+
uploaded: e.loaded,
|
|
54
|
+
total: e.total,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
xhr.upload.addEventListener('load', () => {
|
|
59
|
+
this.trigger('finish')
|
|
60
|
+
})
|
|
61
|
+
xhr.addEventListener('error', () => {
|
|
62
|
+
this.trigger('error')
|
|
63
|
+
reject()
|
|
64
|
+
})
|
|
65
|
+
xhr.onreadystatechange = () => {
|
|
66
|
+
if (xhr.readyState == XMLHttpRequest.DONE) {
|
|
67
|
+
let error
|
|
68
|
+
if (xhr.status === 200) {
|
|
69
|
+
let r = null
|
|
70
|
+
try {
|
|
71
|
+
r = JSON.parse(xhr.responseText)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
r = xhr.responseText
|
|
74
|
+
}
|
|
75
|
+
let out = r.message || r
|
|
76
|
+
resolve(out)
|
|
77
|
+
} else if (xhr.status === 403) {
|
|
78
|
+
error = JSON.parse(xhr.responseText)
|
|
79
|
+
} else {
|
|
80
|
+
this.failed = true
|
|
81
|
+
try {
|
|
82
|
+
error = JSON.parse(xhr.responseText)
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// pass
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (error && error.exc) {
|
|
88
|
+
console.error(JSON.parse(error.exc)[0])
|
|
89
|
+
}
|
|
90
|
+
reject(error)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
xhr.open('POST', '/api/method/upload_file', true)
|
|
94
|
+
xhr.setRequestHeader('Accept', 'application/json')
|
|
95
|
+
if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
|
|
96
|
+
xhr.setRequestHeader('X-Frappe-CSRF-Token', window.csrf_token)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let form_data = new FormData()
|
|
100
|
+
if (file) {
|
|
101
|
+
form_data.append('file', file, file.name)
|
|
102
|
+
}
|
|
103
|
+
form_data.append('is_private', +(options.private || 0))
|
|
104
|
+
form_data.append('folder', options.folder || 'Home')
|
|
105
|
+
|
|
106
|
+
if (options.file_url) {
|
|
107
|
+
form_data.append('file_url', options.file_url)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.doctype && options.docname) {
|
|
111
|
+
form_data.append('doctype', options.doctype)
|
|
112
|
+
form_data.append('docname', options.docname)
|
|
113
|
+
if (options.fieldname) {
|
|
114
|
+
form_data.append('fieldname', options.fieldname)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.method) {
|
|
119
|
+
form_data.append('method', options.method)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (options.type) {
|
|
123
|
+
form_data.append('type', options.type)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
xhr.send(form_data)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default {
|
|
132
|
+
name: 'FileUploader',
|
|
133
|
+
props: ['fileTypes', 'uploadArgs', 'type', 'validateFile'],
|
|
134
|
+
data() {
|
|
135
|
+
return {
|
|
136
|
+
uploader: null,
|
|
137
|
+
uploading: false,
|
|
138
|
+
uploaded: 0,
|
|
139
|
+
error: null,
|
|
140
|
+
message: '',
|
|
141
|
+
total: 0,
|
|
142
|
+
file: null,
|
|
143
|
+
finishedUploading: false,
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
computed: {
|
|
147
|
+
progress() {
|
|
148
|
+
let value = Math.floor((this.uploaded / this.total) * 100)
|
|
149
|
+
return isNaN(value) ? 0 : value
|
|
150
|
+
},
|
|
151
|
+
success() {
|
|
152
|
+
return this.finishedUploading && !this.error
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
methods: {
|
|
156
|
+
openFileSelector() {
|
|
157
|
+
this.$refs['input'].click()
|
|
158
|
+
},
|
|
159
|
+
async onFileAdd(e) {
|
|
160
|
+
this.error = null
|
|
161
|
+
this.file = e.target.files[0]
|
|
162
|
+
|
|
163
|
+
if (this.file && this.validateFile) {
|
|
164
|
+
try {
|
|
165
|
+
let message = await this.validateFile(this.file)
|
|
166
|
+
if (message) {
|
|
167
|
+
this.error = message
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.error = error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!this.error) {
|
|
175
|
+
this.uploadFile(this.file)
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async uploadFile(file) {
|
|
179
|
+
this.error = null
|
|
180
|
+
this.uploaded = 0
|
|
181
|
+
this.total = 0
|
|
182
|
+
|
|
183
|
+
this.uploader = new FileUploader()
|
|
184
|
+
this.uploader.on('start', () => {
|
|
185
|
+
this.uploading = true
|
|
186
|
+
})
|
|
187
|
+
this.uploader.on('progress', (data) => {
|
|
188
|
+
this.uploaded = data.uploaded
|
|
189
|
+
this.total = data.total
|
|
190
|
+
})
|
|
191
|
+
this.uploader.on('error', () => {
|
|
192
|
+
this.uploading = false
|
|
193
|
+
this.error = 'Error Uploading File'
|
|
194
|
+
})
|
|
195
|
+
this.uploader.on('finish', () => {
|
|
196
|
+
this.uploading = false
|
|
197
|
+
this.finishedUploading = true
|
|
198
|
+
})
|
|
199
|
+
this.uploader
|
|
200
|
+
.upload(file, this.uploadArgs || {})
|
|
201
|
+
.then((data) => {
|
|
202
|
+
this.$emit('success', data)
|
|
203
|
+
})
|
|
204
|
+
.catch((error) => {
|
|
205
|
+
this.uploading = false
|
|
206
|
+
let errorMessage = 'Error Uploading File'
|
|
207
|
+
if (error._server_messages) {
|
|
208
|
+
errorMessage = JSON.parse(
|
|
209
|
+
JSON.parse(error._server_messages)[0]
|
|
210
|
+
).message
|
|
211
|
+
} else if (error.exc) {
|
|
212
|
+
errorMessage = JSON.parse(error.exc)[0].split('\n').slice(-2, -1)[0]
|
|
213
|
+
}
|
|
214
|
+
this.error = errorMessage
|
|
215
|
+
this.$emit('failure', error)
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
</script>
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { default as Dialog } from './components/Dialog.vue'
|
|
|
8
8
|
export { default as Dropdown } from './components/Dropdown.vue'
|
|
9
9
|
export { default as ErrorMessage } from './components/ErrorMessage.vue'
|
|
10
10
|
export { default as FeatherIcon } from './components/FeatherIcon.vue'
|
|
11
|
+
export { default as FileUploader } from './components/FileUploader.vue'
|
|
11
12
|
export { default as GreenCheckIcon } from './components/GreenCheckIcon.vue'
|
|
12
13
|
export { default as Input } from './components/Input.vue'
|
|
13
14
|
export { default as Link } from './components/Link.vue'
|
|
@@ -23,7 +24,8 @@ export { default as SuccessMessage } from './components/SuccessMessage.vue'
|
|
|
23
24
|
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|
|
24
25
|
|
|
25
26
|
// utilities
|
|
26
|
-
export { default as call } from './utils/call.js'
|
|
27
|
+
export { default as call, createCall } from './utils/call.js'
|
|
28
|
+
export { default as debounce } from './utils/debounce.js'
|
|
27
29
|
|
|
28
30
|
// plugin
|
|
29
31
|
export { default as FrappeUI } from './utils/plugin.js'
|
package/src/utils/call.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default async function call(method, args,
|
|
1
|
+
export default async function call(method, args, options = {}) {
|
|
2
2
|
if (!args) {
|
|
3
3
|
args = {}
|
|
4
4
|
}
|
|
@@ -9,7 +9,7 @@ export default async function call(method, args, _headers) {
|
|
|
9
9
|
'Content-Type': 'application/json; charset=utf-8',
|
|
10
10
|
'X-Frappe-Site-Name': window.location.hostname,
|
|
11
11
|
},
|
|
12
|
-
|
|
12
|
+
options.headers || {}
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
|
|
@@ -27,6 +27,20 @@ export default async function call(method, args, _headers) {
|
|
|
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()
|
|
@@ -49,6 +63,7 @@ export default async function call(method, args, _headers) {
|
|
|
49
63
|
let e = new Error(errorParts.join('\n'))
|
|
50
64
|
e.exc_type = error.exc_type
|
|
51
65
|
e.exc = exception
|
|
66
|
+
e.status = res.status
|
|
52
67
|
e.messages = error._server_messages
|
|
53
68
|
? JSON.parse(error._server_messages)
|
|
54
69
|
: []
|
|
@@ -67,6 +82,16 @@ export default async function call(method, args, _headers) {
|
|
|
67
82
|
: ['Internal Server Error']
|
|
68
83
|
}
|
|
69
84
|
|
|
85
|
+
if (options.onError) {
|
|
86
|
+
options.onError({ response: res, status: res.status, error: e })
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
throw e
|
|
71
90
|
}
|
|
72
91
|
}
|
|
92
|
+
|
|
93
|
+
export function createCall(options) {
|
|
94
|
+
return function customCall(method, args) {
|
|
95
|
+
return call(method, args, options)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default function debounce(func, wait, immediate) {
|
|
2
|
+
var timeout
|
|
3
|
+
return function () {
|
|
4
|
+
var context = this,
|
|
5
|
+
args = arguments
|
|
6
|
+
var later = function () {
|
|
7
|
+
timeout = null
|
|
8
|
+
if (!immediate) func.apply(context, args)
|
|
9
|
+
}
|
|
10
|
+
var callNow = immediate && !timeout
|
|
11
|
+
clearTimeout(timeout)
|
|
12
|
+
timeout = setTimeout(later, wait)
|
|
13
|
+
if (callNow) func.apply(context, args)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/utils/plugin.js
CHANGED
|
@@ -11,10 +11,11 @@ let defaultOptions = {
|
|
|
11
11
|
export default {
|
|
12
12
|
install(app, options = {}) {
|
|
13
13
|
options = Object.assign({}, defaultOptions, options)
|
|
14
|
-
options.resources && app.use(resources)
|
|
14
|
+
options.resources && app.use(resources, options.resources)
|
|
15
15
|
|
|
16
16
|
if (options.call) {
|
|
17
|
-
|
|
17
|
+
let callFunction = typeof options.call == 'function' ? options.call : call
|
|
18
|
+
app.config.globalProperties.$call = callFunction
|
|
18
19
|
}
|
|
19
20
|
if (options.socketio) {
|
|
20
21
|
app.config.globalProperties.$socket = socket
|
package/src/utils/resources.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import call from '
|
|
2
|
-
import {
|
|
1
|
+
import { call, debounce } from 'frappe-ui'
|
|
2
|
+
import { reactive, watch } from 'vue'
|
|
3
3
|
|
|
4
4
|
let cached = {}
|
|
5
5
|
|
|
6
|
-
function createResource(options, vm) {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
function createResource(options, vm, getResource) {
|
|
7
|
+
let cacheKey = null
|
|
8
|
+
if (options.cache) {
|
|
9
|
+
cacheKey = options.cache
|
|
10
|
+
if (typeof cacheKey === 'string') {
|
|
11
|
+
cacheKey = [cacheKey]
|
|
12
|
+
}
|
|
13
|
+
cacheKey = JSON.stringify(cacheKey)
|
|
14
|
+
if (cached[cacheKey]) {
|
|
15
|
+
return cached[cacheKey]
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
if (typeof options == 'string') {
|
|
@@ -15,79 +23,149 @@ function createResource(options, vm) {
|
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
let resourceFetcher = getResource || call
|
|
27
|
+
let fetchFunction = options.debounce
|
|
28
|
+
? debounce(fetch, options.debounce)
|
|
29
|
+
: fetch
|
|
30
|
+
|
|
18
31
|
let out = reactive({
|
|
19
32
|
data: options.initialData || null,
|
|
33
|
+
previousData: null,
|
|
20
34
|
loading: false,
|
|
21
35
|
fetched: false,
|
|
22
36
|
error: null,
|
|
37
|
+
auto: options.auto,
|
|
23
38
|
params: null,
|
|
24
|
-
fetch,
|
|
25
|
-
submit,
|
|
39
|
+
fetch: fetchFunction,
|
|
40
|
+
submit: fetchFunction,
|
|
41
|
+
update,
|
|
26
42
|
})
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} else {
|
|
36
|
-
out.params = options.params
|
|
37
|
-
}
|
|
44
|
+
async function fetch(params) {
|
|
45
|
+
if (params instanceof Event) {
|
|
46
|
+
params = null
|
|
47
|
+
}
|
|
48
|
+
out.params = params || options.params
|
|
49
|
+
out.previousData = out.data ? JSON.parse(JSON.stringify(out.data)) : null
|
|
50
|
+
out.loading = true
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
if (options.onFetch) {
|
|
53
|
+
options.onFetch.call(vm, out.params)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.validate) {
|
|
57
|
+
let invalidMessage
|
|
58
|
+
try {
|
|
59
|
+
invalidMessage = await options.validate.call(vm, out.params)
|
|
60
|
+
if (invalidMessage && typeof invalidMessage == 'string') {
|
|
61
|
+
let error = new Error(invalidMessage)
|
|
62
|
+
handleError(error)
|
|
63
|
+
out.loading = false
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
handleError(error)
|
|
68
|
+
out.loading = false
|
|
69
|
+
return
|
|
70
|
+
}
|
|
42
71
|
}
|
|
43
|
-
})
|
|
44
72
|
|
|
45
|
-
async function fetch() {
|
|
46
|
-
out.loading = true
|
|
47
|
-
out.data = options.initialData || null
|
|
48
73
|
try {
|
|
49
|
-
let data = await
|
|
74
|
+
let data = await resourceFetcher(options.method, params || options.params)
|
|
50
75
|
out.data = data
|
|
51
76
|
out.fetched = true
|
|
52
77
|
if (options.onSuccess) {
|
|
53
78
|
options.onSuccess.call(vm, data)
|
|
54
79
|
}
|
|
55
80
|
} catch (error) {
|
|
56
|
-
|
|
57
|
-
if (options.onError) {
|
|
58
|
-
options.onError.call(vm, error)
|
|
59
|
-
}
|
|
81
|
+
handleError(error)
|
|
60
82
|
}
|
|
61
83
|
out.loading = false
|
|
62
84
|
}
|
|
63
85
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
function update({ method, params, auto }) {
|
|
87
|
+
if (method && method !== options.method) {
|
|
88
|
+
options.method = method
|
|
89
|
+
}
|
|
90
|
+
if (params && params !== options.params) {
|
|
91
|
+
options.params = params
|
|
92
|
+
}
|
|
93
|
+
if (auto !== undefined && auto !== out.auto) {
|
|
94
|
+
out.auto = auto
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleError(error) {
|
|
99
|
+
console.error(error)
|
|
100
|
+
if (out.previousData) {
|
|
101
|
+
out.data = out.previousData
|
|
102
|
+
}
|
|
103
|
+
out.error = error
|
|
104
|
+
if (options.onError) {
|
|
105
|
+
options.onError.call(vm, error)
|
|
106
|
+
}
|
|
67
107
|
}
|
|
68
108
|
|
|
69
|
-
if (
|
|
70
|
-
cached[
|
|
109
|
+
if (cacheKey && !cached[cacheKey]) {
|
|
110
|
+
cached[cacheKey] = out
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
return out
|
|
74
114
|
}
|
|
75
115
|
|
|
76
|
-
let
|
|
116
|
+
let createMixin = (mixinOptions) => ({
|
|
77
117
|
created() {
|
|
78
118
|
if (this.$options.resources) {
|
|
79
|
-
this._resources = {}
|
|
119
|
+
this._resources = reactive({})
|
|
80
120
|
for (let key in this.$options.resources) {
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
let options = this.$options.resources[key]
|
|
122
|
+
|
|
123
|
+
if (typeof options == 'function') {
|
|
124
|
+
watch(
|
|
125
|
+
() => options.call(this),
|
|
126
|
+
(updatedOptions, oldVal) => {
|
|
127
|
+
let changed =
|
|
128
|
+
!oldVal ||
|
|
129
|
+
JSON.stringify(updatedOptions) !== JSON.stringify(oldVal)
|
|
130
|
+
|
|
131
|
+
if (!changed) return
|
|
132
|
+
|
|
133
|
+
let resource = this._resources[key]
|
|
134
|
+
if (!resource) {
|
|
135
|
+
resource = createResource(
|
|
136
|
+
updatedOptions,
|
|
137
|
+
this,
|
|
138
|
+
mixinOptions.getResource
|
|
139
|
+
)
|
|
140
|
+
this._resources[key] = resource
|
|
141
|
+
} else {
|
|
142
|
+
resource.update(updatedOptions)
|
|
143
|
+
}
|
|
144
|
+
if (resource.auto) {
|
|
145
|
+
resource.fetch()
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
immediate: true,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
} else {
|
|
153
|
+
let resource = createResource(options, this, mixinOptions.getResource)
|
|
154
|
+
this._resources[key] = resource
|
|
155
|
+
if (resource.auto) {
|
|
156
|
+
resource.fetch()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
85
159
|
}
|
|
86
160
|
}
|
|
87
161
|
},
|
|
88
162
|
methods: {
|
|
89
|
-
$
|
|
90
|
-
|
|
163
|
+
$refetchResource(cacheKey) {
|
|
164
|
+
let key = JSON.stringify(cacheKey)
|
|
165
|
+
if (cached[key]) {
|
|
166
|
+
let resource = cached[key]
|
|
167
|
+
resource.fetch()
|
|
168
|
+
}
|
|
91
169
|
},
|
|
92
170
|
},
|
|
93
171
|
computed: {
|
|
@@ -95,10 +173,11 @@ let Resources = {
|
|
|
95
173
|
return this._resources
|
|
96
174
|
},
|
|
97
175
|
},
|
|
98
|
-
}
|
|
176
|
+
})
|
|
99
177
|
|
|
100
178
|
export default {
|
|
101
179
|
install(app, options) {
|
|
102
|
-
|
|
180
|
+
let resourceMixin = createMixin(options)
|
|
181
|
+
app.mixin(resourceMixin)
|
|
103
182
|
},
|
|
104
183
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const plugin = require('tailwindcss/plugin')
|
|
2
|
-
|
|
3
1
|
module.exports = {
|
|
4
2
|
theme: {
|
|
5
3
|
extend: {
|
|
@@ -88,7 +86,7 @@ module.exports = {
|
|
|
88
86
|
plugins: [
|
|
89
87
|
require('@tailwindcss/forms'),
|
|
90
88
|
require('@tailwindcss/typography'),
|
|
91
|
-
plugin(function ({ addUtilities, theme }) {
|
|
89
|
+
require('tailwindcss/plugin')(function ({ addUtilities, theme }) {
|
|
92
90
|
addUtilities({
|
|
93
91
|
'.bg-gradient-blue': {
|
|
94
92
|
'background-image': `linear-gradient(180deg,#2c9af1 0%, ${theme(
|