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 CHANGED
@@ -1,17 +1,23 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.4",
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': type == 'primary',
12
- 'text-gray-600': type == 'secondary',
13
- 'text-red-200': type == 'danger',
11
+ 'text-white': appearance == 'primary',
12
+ 'text-gray-600': appearance == 'secondary',
13
+ 'text-red-200': appearance == 'danger',
14
14
  }"
15
15
  />
16
- <FeatherIcon v-else-if="iconLeft" :name="iconLeft" class="w-4 h-4 mr-1.5" />
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 v-if="iconRight" :name="iconRight" class="w-4 h-4 ml-2" />
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
- type: {
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': this.isDisabled,
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
- this.type === 'primary',
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
- <Modal
3
- :show="modelValue"
4
- @change="handleChange"
5
- :dismissable="dismissable"
6
- :full="full"
7
- >
8
- <div class="px-4 pb-4 bg-white sm:px-6 sm:pb-4">
9
- <div class="sm:flex sm:items-start">
10
- <div class="relative w-full sm:text-left">
11
- <div class="sticky top-0 py-4 bg-white">
12
- <h3 class="text-xl font-medium leading-6 text-gray-900">
13
- {{ title }}
14
- </h3>
15
- </div>
16
- <button
17
- v-if="dismissable"
18
- class="absolute top-0 right-0"
19
- @click="handleChange(false)"
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
+ &#8203;
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
- <FeatherIcon name="x" class="w-4 h-4 mt-4" />
22
- </button>
23
- <div class="leading-5 text-gray-800">
24
- <slot></slot>
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
- </div>
115
+ </TransitionChild>
27
116
  </div>
28
- </div>
29
- <div
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 Modal from './Modal.vue'
40
- import FeatherIcon from './FeatherIcon.vue'
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: ['title', 'modelValue', 'dismissable', 'full'],
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
- Modal,
148
+ HDialog,
149
+ DialogOverlay,
150
+ DialogTitle,
151
+ TransitionChild,
152
+ TransitionRoot,
153
+ Button,
48
154
  FeatherIcon,
49
155
  },
50
- methods: {
51
- handleChange(value) {
52
- this.$emit('update:modelValue', value)
53
- if (value === false) {
54
- this.$emit('close')
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
- <Popover
3
- :show-popup="isShown"
4
- :hide-arrow="true"
5
- :placement="right ? 'bottom-end' : 'bottom-start'"
6
- @init="updateTargetWidth"
7
- >
8
- <template v-slot:target>
9
- <div
10
- class="h-full"
11
- ref="target"
12
- v-on-outside-click="() => (isShown = false)"
13
- >
14
- <slot
15
- :toggleDropdown="toggleDropdown"
16
- :highlightItemUp="highlightItemUp"
17
- :highlightItemDown="highlightItemDown"
18
- :selectHighlightedItem="selectHighlightedItem"
19
- ></slot>
20
- </div>
21
- </template>
22
- <template v-slot:content>
23
- <div
24
- class="z-10 rounded-md w-fullbg-white min-w-40"
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="p-1 overflow-auto text-sm max-h-64">
28
- <div v-if="isLoading" class="p-2 text-gray-600">
29
- {{ _('Loading...') }}
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
- <template v-else>
32
- <div v-for="d in dropdownItems" :key="d.label">
33
- <div
34
- v-if="d.isGroup"
35
- class="px-2 pt-2 pb-1 text-xs font-semibold tracking-wider text-gray-500 uppercase"
36
- >
37
- {{ d.label }}
38
- </div>
39
- <a
40
- v-else
41
- :ref="setItemRef"
42
- class="block p-2 truncate rounded-md cursor-pointer first:mt-0"
43
- :class="d.index === highlightedIndex ? 'bg-gray-100' : ''"
44
- @mouseenter="highlightedIndex = d.index"
45
- @mouseleave="highlightedIndex = -1"
46
- @click="selectItem(d)"
47
- >
48
- <component :is="d.component" v-if="d.component" />
49
- <template v-else>{{ d.label }}</template>
50
- </a>
51
- </div>
52
- </template>
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
- </div>
55
- </template>
56
- </Popover>
57
+ </MenuItems>
58
+ </transition>
59
+ </Menu>
57
60
  </template>
58
61
 
59
62
  <script>
60
- import Popover from './Popover.vue'
61
- import onOutsideClick from '../directives/onOutsideClick'
63
+ import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
64
+ import { FeatherIcon } from 'frappe-ui'
62
65
 
63
66
  export default {
64
- name: 'Dropdown',
65
- directives: {
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
- Popover,
70
+ Menu,
71
+ MenuButton,
72
+ MenuItems,
73
+ MenuItem,
74
+ FeatherIcon,
92
75
  },
93
- data() {
94
- return {
95
- targetWidth: undefined,
96
- isShown: false,
97
- itemRefs: [],
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
- let groupNames = [
107
- ...new Set(
108
- this.items
109
- .map((d) => d.group)
110
- .filter(Boolean)
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
- let items = this.items
92
+ return (this.options || [])
121
93
  .filter(Boolean)
122
- .filter((d) => (d.condition ? d.condition() : true))
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
- toggleDropdown(flag) {
167
- if (flag == null) {
168
- this.isShown = !this.isShown
169
- } else {
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
- this.$nextTick(() => {
200
- this.scrollToHighlighted()
201
- })
202
- },
203
- scrollToHighlighted() {
204
- let highlightedElement = this.$refs.items[this.highlightedIndex]
205
- highlightedElement &&
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()
@@ -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
- &#8203;
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>