frappe-ui 0.0.2 → 0.0.3
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/Button.vue +4 -4
- package/src/components/FileUploader.vue +220 -0
- package/src/components/NewDialog.vue +183 -0
- package/src/index.js +4 -1
- package/src/utils/call.js +13 -2
- package/src/utils/debounce.js +15 -0
- package/src/utils/plugin.js +3 -2
- package/src/utils/resources.js +91 -41
- package/src/utils/tailwind.config.js +1 -3
package/package.json
CHANGED
|
@@ -76,13 +76,13 @@ export default {
|
|
|
76
76
|
this.icon ? 'p-1.5' : 'px-3 py-1',
|
|
77
77
|
{
|
|
78
78
|
'opacity-50 cursor-not-allowed pointer-events-none': this.isDisabled,
|
|
79
|
-
'bg-gradient-blue hover:bg-gradient-none hover:bg-blue-500 text-white focus:
|
|
79
|
+
'bg-gradient-blue hover:bg-gradient-none hover:bg-blue-500 text-white focus:ring-2 focus:ring-offset-2 focus:ring-blue-500':
|
|
80
80
|
this.type === 'primary',
|
|
81
|
-
'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:
|
|
81
|
+
'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500':
|
|
82
82
|
this.type === 'secondary',
|
|
83
|
-
'bg-red-500 hover:bg-red-400 text-white focus:
|
|
83
|
+
'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500':
|
|
84
84
|
this.type === 'danger',
|
|
85
|
-
'bg-white text-gray-900
|
|
85
|
+
'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400':
|
|
86
86
|
this.type === 'white',
|
|
87
87
|
},
|
|
88
88
|
]
|
|
@@ -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>
|
|
@@ -0,0 +1,183 @@
|
|
|
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>
|
package/src/index.js
CHANGED
|
@@ -5,9 +5,11 @@ 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'
|
|
8
9
|
export { default as Dropdown } from './components/Dropdown.vue'
|
|
9
10
|
export { default as ErrorMessage } from './components/ErrorMessage.vue'
|
|
10
11
|
export { default as FeatherIcon } from './components/FeatherIcon.vue'
|
|
12
|
+
export { default as FileUploader } from './components/FileUploader.vue'
|
|
11
13
|
export { default as GreenCheckIcon } from './components/GreenCheckIcon.vue'
|
|
12
14
|
export { default as Input } from './components/Input.vue'
|
|
13
15
|
export { default as Link } from './components/Link.vue'
|
|
@@ -23,7 +25,8 @@ export { default as SuccessMessage } from './components/SuccessMessage.vue'
|
|
|
23
25
|
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|
|
24
26
|
|
|
25
27
|
// utilities
|
|
26
|
-
export { default as call } from './utils/call.js'
|
|
28
|
+
export { default as call, createCall } from './utils/call.js'
|
|
29
|
+
export { default as debounce } from './utils/debounce.js'
|
|
27
30
|
|
|
28
31
|
// plugin
|
|
29
32
|
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 }}') {
|
|
@@ -49,6 +49,7 @@ export default async function call(method, args, _headers) {
|
|
|
49
49
|
let e = new Error(errorParts.join('\n'))
|
|
50
50
|
e.exc_type = error.exc_type
|
|
51
51
|
e.exc = exception
|
|
52
|
+
e.status = res.status
|
|
52
53
|
e.messages = error._server_messages
|
|
53
54
|
? JSON.parse(error._server_messages)
|
|
54
55
|
: []
|
|
@@ -67,6 +68,16 @@ export default async function call(method, args, _headers) {
|
|
|
67
68
|
: ['Internal Server Error']
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
if (options.onError) {
|
|
72
|
+
options.onError({ response: res, status: res.status, error: e })
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
throw e
|
|
71
76
|
}
|
|
72
77
|
}
|
|
78
|
+
|
|
79
|
+
export function createCall(options) {
|
|
80
|
+
return function customCall(method, args) {
|
|
81
|
+
return call(method, args, options)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -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,44 +23,40 @@ 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
|
-
if (options.auto) {
|
|
32
|
-
fetch()
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
} else {
|
|
36
|
-
out.params = options.params
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onMounted(() => {
|
|
40
|
-
if (options.auto) {
|
|
41
|
-
fetch()
|
|
44
|
+
async function fetch(params) {
|
|
45
|
+
if (params instanceof Event) {
|
|
46
|
+
params = null
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async function fetch() {
|
|
48
|
+
out.params = params || options.params
|
|
46
49
|
out.loading = true
|
|
47
|
-
out.data = options.initialData || null
|
|
48
50
|
try {
|
|
49
|
-
let data = await
|
|
51
|
+
let data = await resourceFetcher(options.method, params || options.params)
|
|
52
|
+
out.previousData = out.data || null
|
|
50
53
|
out.data = data
|
|
51
54
|
out.fetched = true
|
|
52
55
|
if (options.onSuccess) {
|
|
53
56
|
options.onSuccess.call(vm, data)
|
|
54
57
|
}
|
|
55
58
|
} catch (error) {
|
|
59
|
+
console.error(error)
|
|
56
60
|
out.error = error
|
|
57
61
|
if (options.onError) {
|
|
58
62
|
options.onError.call(vm, error)
|
|
@@ -61,33 +65,78 @@ function createResource(options, vm) {
|
|
|
61
65
|
out.loading = false
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
function update({ method, params, auto }) {
|
|
69
|
+
if (method && method !== options.method) {
|
|
70
|
+
options.method = method
|
|
71
|
+
}
|
|
72
|
+
if (params && params !== options.params) {
|
|
73
|
+
options.params = params
|
|
74
|
+
}
|
|
75
|
+
if (auto !== undefined && auto !== out.auto) {
|
|
76
|
+
out.auto = auto
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
if (
|
|
70
|
-
cached[
|
|
80
|
+
if (cacheKey && !cached[cacheKey]) {
|
|
81
|
+
cached[cacheKey] = out
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
return out
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
let
|
|
87
|
+
let createMixin = (mixinOptions) => ({
|
|
77
88
|
created() {
|
|
78
89
|
if (this.$options.resources) {
|
|
79
|
-
this._resources = {}
|
|
90
|
+
this._resources = reactive({})
|
|
80
91
|
for (let key in this.$options.resources) {
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
let options = this.$options.resources[key]
|
|
93
|
+
|
|
94
|
+
if (typeof options == 'function') {
|
|
95
|
+
watch(
|
|
96
|
+
() => options.call(this),
|
|
97
|
+
(updatedOptions, oldVal) => {
|
|
98
|
+
let changed =
|
|
99
|
+
!oldVal ||
|
|
100
|
+
JSON.stringify(updatedOptions) !== JSON.stringify(oldVal)
|
|
101
|
+
|
|
102
|
+
if (!changed) return
|
|
103
|
+
|
|
104
|
+
let resource = this._resources[key]
|
|
105
|
+
if (!resource) {
|
|
106
|
+
resource = createResource(
|
|
107
|
+
updatedOptions,
|
|
108
|
+
this,
|
|
109
|
+
mixinOptions.getResource
|
|
110
|
+
)
|
|
111
|
+
this._resources[key] = resource
|
|
112
|
+
} else {
|
|
113
|
+
resource.update(updatedOptions)
|
|
114
|
+
}
|
|
115
|
+
if (resource.auto) {
|
|
116
|
+
resource.fetch()
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
immediate: true,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
} else {
|
|
124
|
+
let resource = createResource(options, this, mixinOptions.getResource)
|
|
125
|
+
this._resources[key] = resource
|
|
126
|
+
if (resource.auto) {
|
|
127
|
+
resource.fetch()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
85
130
|
}
|
|
86
131
|
}
|
|
87
132
|
},
|
|
88
133
|
methods: {
|
|
89
|
-
$
|
|
90
|
-
|
|
134
|
+
$refetchResource(cacheKey) {
|
|
135
|
+
let key = JSON.stringify(cacheKey)
|
|
136
|
+
if (cached[key]) {
|
|
137
|
+
let resource = cached[key]
|
|
138
|
+
resource.fetch()
|
|
139
|
+
}
|
|
91
140
|
},
|
|
92
141
|
},
|
|
93
142
|
computed: {
|
|
@@ -95,10 +144,11 @@ let Resources = {
|
|
|
95
144
|
return this._resources
|
|
96
145
|
},
|
|
97
146
|
},
|
|
98
|
-
}
|
|
147
|
+
})
|
|
99
148
|
|
|
100
149
|
export default {
|
|
101
150
|
install(app, options) {
|
|
102
|
-
|
|
151
|
+
let resourceMixin = createMixin(options)
|
|
152
|
+
app.mixin(resourceMixin)
|
|
103
153
|
},
|
|
104
154
|
}
|
|
@@ -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(
|