frappe-ui 0.0.13 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.13",
3
+ "version": "0.0.17",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  "author": "Frappe Technologies Pvt. Ltd.",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@headlessui/vue": "^1.4.3",
20
+ "@headlessui/vue": "^1.5.0",
21
21
  "@popperjs/core": "^2.11.2",
22
22
  "@tailwindcss/forms": "^0.4.0",
23
23
  "@tailwindcss/typography": "^0.5.0",
@@ -44,7 +44,7 @@ export default {
44
44
  computed: {
45
45
  styleClasses() {
46
46
  const sizeClasses = {
47
- sm: 'w-4 h-4',
47
+ sm: 'w-5 h-5',
48
48
  md: 'w-8 h-8',
49
49
  lg: 'w-12 h-12',
50
50
  }[this.size]
@@ -40,7 +40,7 @@
40
40
  active ? 'bg-gray-100' : 'text-gray-900',
41
41
  'group flex rounded-md items-center w-full px-2 py-2 text-sm',
42
42
  ]"
43
- :onClick="item.onClick"
43
+ @click="item.onClick"
44
44
  >
45
45
  <FeatherIcon
46
46
  v-if="item.icon"
@@ -97,7 +97,7 @@ export default {
97
97
  groups() {
98
98
  let groups = this.options[0]?.group
99
99
  ? this.options
100
- : [{ group: '', items: this.filterOptions(this.options) }]
100
+ : [{ group: '', items: this.options }]
101
101
 
102
102
  return groups.map((group, i) => {
103
103
  return {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <label :class="type == 'checkbox' ? 'flex' : 'block'">
2
+ <label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]">
3
3
  <span
4
4
  v-if="label && type != 'checkbox'"
5
5
  class="block mb-2 text-sm leading-4 text-gray-700"
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="inline-flex px-1 py-1 bg-white">
3
3
  <div class="inline-flex items-center gap-1">
4
- <template v-for="button in menuButtons" :key="button.label">
4
+ <template v-for="button in buttons" :key="button.label">
5
5
  <div
6
6
  class="border-l w-[2px] h-4"
7
7
  v-if="button.type === 'separator'"
@@ -9,8 +9,8 @@
9
9
  <button
10
10
  v-else
11
11
  class="flex p-1 text-gray-800 transition-colors rounded"
12
- :class="button.isActive() ? 'bg-gray-100' : 'hover:bg-gray-100'"
13
- @click="button.action"
12
+ :class="button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'"
13
+ @click="() => button.action(editor)"
14
14
  :title="button.label"
15
15
  >
16
16
  <FeatherIcon v-if="button.icon" :name="button.icon" class="w-4" />
@@ -26,96 +26,7 @@
26
26
  import { FeatherIcon } from 'frappe-ui'
27
27
  export default {
28
28
  name: 'TipTapMenu',
29
- props: ['editor'],
29
+ props: ['editor', 'buttons'],
30
30
  components: { FeatherIcon },
31
- computed: {
32
- menuButtons() {
33
- return [
34
- {
35
- label: 'Paragraph',
36
- icon: 'type',
37
- action: () => this.editor.chain().focus().setParagraph().run(),
38
- isActive: () => this.editor.isActive('paragraph'),
39
- },
40
- {
41
- label: 'Heading 2',
42
- text: 'H2',
43
- action: () =>
44
- this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
45
- isActive: () => this.editor.isActive('heading', { level: 2 }),
46
- },
47
- {
48
- label: 'Heading 3',
49
- text: 'H3',
50
- action: () =>
51
- this.editor.chain().focus().toggleHeading({ level: 3 }).run(),
52
- isActive: () => this.editor.isActive('heading', { level: 3 }),
53
- },
54
- {
55
- type: 'separator',
56
- },
57
- {
58
- label: 'Bold',
59
- icon: 'bold',
60
- action: () => this.editor.chain().focus().toggleBold().run(),
61
- isActive: () => this.editor.isActive('bold'),
62
- },
63
- {
64
- label: 'Italic',
65
- icon: 'italic',
66
- action: () => this.editor.chain().focus().toggleItalic().run(),
67
- isActive: () => this.editor.isActive('italic'),
68
- },
69
- {
70
- type: 'separator',
71
- },
72
- {
73
- label: 'Bullet List',
74
- icon: 'list',
75
- action: () => this.editor.chain().focus().toggleBulletList().run(),
76
- isActive: () => this.editor.isActive('bulletList'),
77
- },
78
- {
79
- label: 'Numbered List',
80
- text: '1.',
81
- action: () => this.editor.chain().focus().toggleOrderedList().run(),
82
- isActive: () => this.editor.isActive('orderedList'),
83
- },
84
- {
85
- label: 'Blockquote',
86
- icon: 'chevron-right',
87
- action: () => this.editor.chain().focus().toggleBlockquote().run(),
88
- isActive: () => this.editor.isActive('blockquote'),
89
- },
90
- {
91
- label: 'Code',
92
- icon: 'code',
93
- action: () => this.editor.chain().focus().toggleCodeBlock().run(),
94
- isActive: () => this.editor.isActive('codeBlock'),
95
- },
96
- {
97
- label: 'Horizontal Rule',
98
- icon: 'minus',
99
- action: () => this.editor.chain().focus().setHorizontalRule().run(),
100
- isActive: () => false,
101
- },
102
- {
103
- type: 'separator',
104
- },
105
- {
106
- label: 'Undo',
107
- icon: 'corner-up-left',
108
- action: () => this.editor.chain().focus().undo().run(),
109
- isActive: () => false,
110
- },
111
- {
112
- label: 'Redo',
113
- icon: 'corner-up-right',
114
- action: () => this.editor.chain().focus().redo().run(),
115
- isActive: () => false,
116
- },
117
- ]
118
- },
119
- },
120
31
  }
121
32
  </script>
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <div class="relative w-full" :class="{ 'pt-6': showMenu }" v-if="editor">
2
+ <div class="relative w-full" :class="$attrs.class" v-if="editor">
3
3
  <BubbleMenu
4
- v-if="showBubbleMenu"
4
+ v-if="bubbleMenuButtons"
5
5
  class="bubble-menu"
6
6
  :tippy-options="{ duration: 100 }"
7
7
  :editor="editor"
@@ -9,27 +9,48 @@
9
9
  <Menu
10
10
  :editor="editor"
11
11
  class="border border-gray-100 rounded-md shadow-sm"
12
+ :buttons="bubbleMenuButtons"
12
13
  />
13
14
  </BubbleMenu>
14
15
 
15
16
  <Menu
16
- v-if="showMenu"
17
- class="absolute top-0 left-0 right-0 border rounded-t-lg border-gray-50 border-b-gray-100"
17
+ v-if="fixedMenuButtons"
18
+ class="w-full border rounded-t-lg border-gray-50 border-b-gray-100"
18
19
  :editor="editor"
20
+ :buttons="fixedMenuButtons"
19
21
  />
20
- <editor-content
22
+
23
+ <FloatingMenu
24
+ v-if="floatingMenuButtons"
25
+ :tippy-options="{ duration: 100 }"
21
26
  :editor="editor"
22
- :class="$attrs.class || 'prose-sm prose'"
23
- />
27
+ class="flex"
28
+ >
29
+ <button
30
+ v-for="button in floatingMenuButtons"
31
+ :key="button.label"
32
+ class="flex p-1 text-gray-800 transition-colors rounded"
33
+ :class="button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'"
34
+ @click="() => button.action(editor)"
35
+ :title="button.label"
36
+ >
37
+ <FeatherIcon v-if="button.icon" :name="button.icon" class="w-4" />
38
+ <span class="inline-block h-4 text-sm leading-4 min-w-[1rem]" v-else>
39
+ {{ button.text }}
40
+ </span>
41
+ </button>
42
+ </FloatingMenu>
43
+ <editor-content :editor="editor" />
24
44
  </div>
25
45
  </template>
26
46
 
27
47
  <script>
28
- import { Editor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
48
+ import { Editor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/vue-3'
29
49
  import StarterKit from '@tiptap/starter-kit'
30
50
  import Placeholder from '@tiptap/extension-placeholder'
31
51
  import Image from '@tiptap/extension-image'
32
52
  import Menu from './Menu.vue'
53
+ import commands from './commands'
33
54
 
34
55
  export default {
35
56
  name: 'TextEditor',
@@ -37,14 +58,17 @@ export default {
37
58
  components: {
38
59
  EditorContent,
39
60
  BubbleMenu,
61
+ FloatingMenu,
40
62
  Menu,
41
63
  },
42
64
  props: [
43
65
  'content',
44
66
  'placeholder',
45
67
  'editorClass',
46
- 'showMenu',
47
- 'showBubbleMenu',
68
+ 'fixedMenu',
69
+ 'bubbleMenu',
70
+ 'floatingMenu',
71
+ 'extensions',
48
72
  ],
49
73
  emits: ['change'],
50
74
  expose: ['editor'],
@@ -58,7 +82,7 @@ export default {
58
82
  content: this.content || '<p></p>',
59
83
  editorProps: {
60
84
  attributes: {
61
- class: ['prose-p:my-1', this.editorClass].join(' '),
85
+ class: ['prose prose-sm prose-p:my-1', this.editorClass].join(' '),
62
86
  },
63
87
  },
64
88
  extensions: [
@@ -67,6 +91,7 @@ export default {
67
91
  Placeholder.configure({
68
92
  placeholder: this.placeholder || 'Write something...',
69
93
  }),
94
+ ...(this.extensions || []),
70
95
  ],
71
96
  onUpdate: ({ editor }) => {
72
97
  this.$emit('change', editor.getHTML())
@@ -76,6 +101,85 @@ export default {
76
101
  beforeUnmount() {
77
102
  this.editor.destroy()
78
103
  },
104
+ computed: {
105
+ fixedMenuButtons() {
106
+ if (!this.fixedMenu) return false
107
+
108
+ let buttons
109
+ if (Array.isArray(this.fixedMenu)) {
110
+ buttons = this.fixedMenu
111
+ } else {
112
+ buttons = [
113
+ 'Paragraph',
114
+ 'Heading 2',
115
+ 'Heading 3',
116
+ 'Separator',
117
+ 'Bold',
118
+ 'Italic',
119
+ 'Separator',
120
+ 'Bullet List',
121
+ 'Numbered List',
122
+ 'Blockquote',
123
+ 'Code',
124
+ 'Horizontal Rule',
125
+ 'Separator',
126
+ 'Undo',
127
+ 'Redo',
128
+ ]
129
+ }
130
+ return buttons.map(createEditorButton)
131
+ },
132
+ bubbleMenuButtons() {
133
+ if (!this.bubbleMenu) return false
134
+
135
+ let buttons
136
+ if (Array.isArray(this.bubbleMenu)) {
137
+ buttons = this.bubbleMenu
138
+ } else {
139
+ buttons = [
140
+ 'Paragraph',
141
+ 'Heading 2',
142
+ 'Heading 3',
143
+ 'Separator',
144
+ 'Bold',
145
+ 'Italic',
146
+ 'Separator',
147
+ 'Bullet List',
148
+ 'Numbered List',
149
+ 'Blockquote',
150
+ 'Code',
151
+ ]
152
+ }
153
+ return buttons.map(createEditorButton)
154
+ },
155
+ floatingMenuButtons() {
156
+ if (!this.floatingMenu) return false
157
+
158
+ let buttons
159
+ if (Array.isArray(this.floatingMenu)) {
160
+ buttons = this.floatingMenu
161
+ } else {
162
+ buttons = [
163
+ 'Paragraph',
164
+ 'Heading 2',
165
+ 'Heading 3',
166
+ 'Bullet List',
167
+ 'Numbered List',
168
+ 'Blockquote',
169
+ 'Code',
170
+ 'Horizontal Rule',
171
+ ]
172
+ }
173
+ return buttons.map(createEditorButton)
174
+ },
175
+ },
176
+ }
177
+
178
+ function createEditorButton(option) {
179
+ if (typeof option == 'object') {
180
+ return option
181
+ }
182
+ return commands[option]
79
183
  }
80
184
  </script>
81
185
  <style>
@@ -0,0 +1,79 @@
1
+ export default {
2
+ Paragraph: {
3
+ label: 'Paragraph',
4
+ icon: 'type',
5
+ action: (editor) => editor.chain().focus().setParagraph().run(),
6
+ isActive: (editor) => editor.isActive('paragraph'),
7
+ },
8
+ 'Heading 2': {
9
+ label: 'Heading 2',
10
+ text: 'H2',
11
+ action: (editor) =>
12
+ editor.chain().focus().toggleHeading({ level: 2 }).run(),
13
+ isActive: (editor) => editor.isActive('heading', { level: 2 }),
14
+ },
15
+ 'Heading 3': {
16
+ label: 'Heading 3',
17
+ text: 'H3',
18
+ action: (editor) =>
19
+ editor.chain().focus().toggleHeading({ level: 3 }).run(),
20
+ isActive: (editor) => editor.isActive('heading', { level: 3 }),
21
+ },
22
+ Bold: {
23
+ label: 'Bold',
24
+ icon: 'bold',
25
+ action: (editor) => editor.chain().focus().toggleBold().run(),
26
+ isActive: (editor) => editor.isActive('bold'),
27
+ },
28
+ Italic: {
29
+ label: 'Italic',
30
+ icon: 'italic',
31
+ action: (editor) => editor.chain().focus().toggleItalic().run(),
32
+ isActive: (editor) => editor.isActive('italic'),
33
+ },
34
+ 'Bullet List': {
35
+ label: 'Bullet List',
36
+ icon: 'list',
37
+ action: (editor) => editor.chain().focus().toggleBulletList().run(),
38
+ isActive: (editor) => editor.isActive('bulletList'),
39
+ },
40
+ 'Numbered List': {
41
+ label: 'Numbered List',
42
+ text: '1.',
43
+ action: (editor) => editor.chain().focus().toggleOrderedList().run(),
44
+ isActive: (editor) => editor.isActive('orderedList'),
45
+ },
46
+ Blockquote: {
47
+ label: 'Blockquote',
48
+ icon: 'chevron-right',
49
+ action: (editor) => editor.chain().focus().toggleBlockquote().run(),
50
+ isActive: (editor) => editor.isActive('blockquote'),
51
+ },
52
+ Code: {
53
+ label: 'Code',
54
+ icon: 'code',
55
+ action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
56
+ isActive: (editor) => editor.isActive('codeBlock'),
57
+ },
58
+ 'Horizontal Rule': {
59
+ label: 'Horizontal Rule',
60
+ icon: 'minus',
61
+ action: (editor) => editor.chain().focus().setHorizontalRule().run(),
62
+ isActive: (editor) => false,
63
+ },
64
+ Undo: {
65
+ label: 'Undo',
66
+ icon: 'corner-up-left',
67
+ action: (editor) => editor.chain().focus().undo().run(),
68
+ isActive: (editor) => false,
69
+ },
70
+ Redo: {
71
+ label: 'Redo',
72
+ icon: 'corner-up-right',
73
+ action: (editor) => editor.chain().focus().redo().run(),
74
+ isActive: (editor) => false,
75
+ },
76
+ Separator: {
77
+ type: 'separator',
78
+ },
79
+ }
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <teleport to="#frappeui-toast-root">
3
+ <transition :name="position.includes('top') ? 'toast-top' : 'toast-bottom'">
4
+ <div
5
+ v-if="shown"
6
+ :style="style"
7
+ :class="[
8
+ 'absolute transition duration-200 ease-out m-4 pointer-events-auto',
9
+ position.includes('center') ? '-translate-x-1/2' : '',
10
+ ]"
11
+ >
12
+ <div
13
+ class="px-2.5 py-2 bg-white border rounded-lg shadow-md min-w-[15rem]"
14
+ >
15
+ <div class="flex items-center justify-between">
16
+ <div class="text-lg">
17
+ <slot> Toast Content </slot>
18
+ </div>
19
+ <div>
20
+ <slot name="actions">
21
+ <Button icon="x" @click="shown = false" />
22
+ </slot>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </transition>
28
+ </teleport>
29
+ </template>
30
+ <script>
31
+ const positions = [
32
+ 'top-right',
33
+ 'top-center',
34
+ 'top-left',
35
+ 'bottom-right',
36
+ 'bottom-center',
37
+ 'bottom-left',
38
+ ]
39
+
40
+ export default {
41
+ name: 'Toast',
42
+ props: {
43
+ position: {
44
+ type: String,
45
+ default: 'top-right',
46
+ },
47
+ text: {
48
+ type: String,
49
+ },
50
+ },
51
+ created() {
52
+ if (!document.getElementById('frappeui-toast-root')) {
53
+ const root = document.createElement('div')
54
+ root.id = 'frappeui-toast-root'
55
+ root.style.position = 'fixed'
56
+ root.style.top = '16px'
57
+ root.style.right = '16px'
58
+ root.style.bottom = '16px'
59
+ root.style.left = '16px'
60
+ root.style.zIndex = '9999'
61
+ root.style.pointerEvents = 'none'
62
+ document.body.appendChild(root)
63
+ }
64
+ },
65
+ mounted() {
66
+ this.shown = true
67
+ // setTimeout(() => {
68
+ // this.shown = false
69
+ // }, 3000)
70
+ },
71
+ data() {
72
+ return {
73
+ shown: false,
74
+ }
75
+ },
76
+ computed: {
77
+ style() {
78
+ let style = {}
79
+ if (this.position.includes('top')) {
80
+ style.top = 0
81
+ }
82
+ if (this.position.includes('bottom')) {
83
+ style.bottom = 0
84
+ }
85
+ if (this.position.includes('right')) {
86
+ style.right = 0
87
+ }
88
+ if (this.position.includes('left')) {
89
+ style.left = 0
90
+ }
91
+ if (this.position.includes('center')) {
92
+ style.left = '50%'
93
+ // style.transform = 'translateX(-50%)'
94
+ }
95
+ return style
96
+ },
97
+ transitionProps() {
98
+ let props = {
99
+ enterActiveClass: 'transition duration-200 ease-out',
100
+ enterFromClass: 'opacity-0',
101
+ enterToClass: 'translate-y-0 opacity-100',
102
+ leaveActiveClass: 'transition duration-100 ease-in',
103
+ leaveFromClass: 'scale-100 translate-y-0 opacity-100',
104
+ leaveToClass: 'scale-75 translate-y-4 opacity-0',
105
+ }
106
+
107
+ if (this.position.includes('top')) {
108
+ props.enterFromClass += ' -translate-y-12'
109
+ }
110
+ if (this.position.includes('bottom')) {
111
+ props.enterFromClass += ' translate-y-12'
112
+ }
113
+ return props
114
+ },
115
+ },
116
+ }
117
+ </script>
118
+ <style>
119
+ .toast-top-enter-active,
120
+ .toast-bottom-enter-active {
121
+ transition: all 200ms ease-out;
122
+ }
123
+ .toast-top-leave-active,
124
+ .toast-bottom-leave-active {
125
+ transition: all 100ms ease-in;
126
+ }
127
+ .toast-top-enter-from {
128
+ opacity: 0;
129
+ transform: translateY(0);
130
+ }
131
+ .toast-top-enter-to {
132
+ opacity: 1;
133
+ transform: translateY(0);
134
+ }
135
+ </style>
@@ -167,61 +167,74 @@ export function createDocumentResource(options, vm) {
167
167
  doctype: options.doctype,
168
168
  name: options.name,
169
169
  doc: null,
170
- get: createResource({
171
- method: 'frappe.client.get',
172
- makeParams() {
173
- return {
174
- doctype: out.doctype,
175
- name: out.name,
176
- }
170
+ get: createResource(
171
+ {
172
+ method: 'frappe.client.get',
173
+ makeParams() {
174
+ return {
175
+ doctype: out.doctype,
176
+ name: out.name,
177
+ }
178
+ },
179
+ onSuccess(data) {
180
+ out.doc = postprocess(data)
181
+ },
177
182
  },
178
- onSuccess(data) {
179
- out.doc = postprocess(data)
183
+ vm
184
+ ),
185
+ setValue: createResource(setValueOptions, vm),
186
+ setValueDebounced: createResource(
187
+ {
188
+ ...setValueOptions,
189
+ debounce: options.debounce || 500,
180
190
  },
181
- }),
182
- setValue: createResource(setValueOptions),
183
- setValueDebounced: createResource({
184
- ...setValueOptions,
185
- debounce: options.debounce || 500,
186
- }),
187
- delete: createResource({
188
- method: 'frappe.client.delete',
189
- makeParams() {
190
- return {
191
- doctype: out.doctype,
192
- name: out.name,
193
- }
194
- },
195
- onSuccess() {
196
- out.doc = null
191
+ vm
192
+ ),
193
+ delete: createResource(
194
+ {
195
+ method: 'frappe.client.delete',
196
+ makeParams() {
197
+ return {
198
+ doctype: out.doctype,
199
+ name: out.name,
200
+ }
201
+ },
202
+ onSuccess() {
203
+ out.doc = null
204
+ },
197
205
  },
198
- }),
206
+ vm
207
+ ),
199
208
  update,
209
+ reload,
200
210
  })
201
211
 
202
212
  for (let method in options.whitelistedMethods) {
203
213
  let methodName = options.whitelistedMethods[method]
204
- out[method] = createResource({
205
- method: 'run_doc_method',
206
- makeParams(values) {
207
- return {
208
- dt: out.doctype,
209
- dn: out.name,
210
- method: methodName,
211
- args: JSON.stringify(values),
212
- }
213
- },
214
- onSuccess(data) {
215
- if (data.docs) {
216
- for (let doc of data.docs) {
217
- if (doc.doctype === out.doctype && doc.name === out.name) {
218
- out.doc = postprocess(doc)
219
- break
214
+ out[method] = createResource(
215
+ {
216
+ method: 'run_doc_method',
217
+ makeParams(values) {
218
+ return {
219
+ dt: out.doctype,
220
+ dn: out.name,
221
+ method: methodName,
222
+ args: JSON.stringify(values),
223
+ }
224
+ },
225
+ onSuccess(data) {
226
+ if (data.docs) {
227
+ for (let doc of data.docs) {
228
+ if (doc.doctype === out.doctype && doc.name === out.name) {
229
+ out.doc = postprocess(doc)
230
+ break
231
+ }
220
232
  }
221
233
  }
222
- }
234
+ },
223
235
  },
224
- })
236
+ vm
237
+ )
225
238
  }
226
239
 
227
240
  function update(updatedOptions) {
@@ -230,9 +243,13 @@ export function createDocumentResource(options, vm) {
230
243
  out.get.fetch()
231
244
  }
232
245
 
246
+ function reload() {
247
+ out.get.fetch()
248
+ }
249
+
233
250
  function postprocess(doc) {
234
251
  if (options.postprocess) {
235
- let returnValue = options.postprocess(doc)
252
+ let returnValue = options.postprocess.call(vm, doc)
236
253
  if (typeof returnValue === 'object') {
237
254
  return returnValue
238
255
  }
@@ -247,10 +264,79 @@ export function createDocumentResource(options, vm) {
247
264
  return out
248
265
  }
249
266
 
267
+ function createListResource(options, vm, getResource) {
268
+ if (!options.doctype) return
269
+
270
+ let out = reactive({
271
+ doctype: options.doctype,
272
+ fields: options.fields,
273
+ filters: options.filters,
274
+ order_by: options.order_by,
275
+ start: options.start,
276
+ limit: options.limit,
277
+ data: null,
278
+ list: createResource(
279
+ {
280
+ method: 'frappe.client.get_list',
281
+ makeParams() {
282
+ return {
283
+ doctype: out.doctype,
284
+ fields: out.fields,
285
+ filters: out.filters,
286
+ order_by: out.order_by,
287
+ limit_start: out.start,
288
+ limit_page_length: out.limit,
289
+ }
290
+ },
291
+ onSuccess(data) {
292
+ out.data = data
293
+ },
294
+ },
295
+ vm
296
+ ),
297
+ insert: createResource(
298
+ {
299
+ method: 'frappe.client.insert',
300
+ makeParams(values) {
301
+ return {
302
+ doc: {
303
+ doctype: out.doctype,
304
+ ...values,
305
+ },
306
+ }
307
+ },
308
+ onSuccess() {
309
+ out.list.fetch()
310
+ },
311
+ },
312
+ vm
313
+ ),
314
+ update,
315
+ })
316
+
317
+ function update(updatedOptions) {
318
+ out.doctype = updatedOptions.doctype
319
+ out.fields = updatedOptions.fields
320
+ out.filters = updatedOptions.filters
321
+ out.order_by = updatedOptions.order_by
322
+ out.start = updatedOptions.start
323
+ out.limit = updatedOptions.limit
324
+ out.list.fetch()
325
+ }
326
+
327
+ // fetch list
328
+ out.list.fetch()
329
+
330
+ return out
331
+ }
332
+
250
333
  function createResourceForOptions(options, vm, getResource) {
251
334
  if (options.type === 'document') {
252
335
  return createDocumentResource(options, vm, getResource)
253
336
  }
337
+ if (options.type === 'list') {
338
+ return createListResource(options, vm, getResource)
339
+ }
254
340
  return createResource(options, vm, getResource)
255
341
  }
256
342
 
@@ -270,8 +356,19 @@ let createMixin = (mixinOptions) => ({
270
356
 
271
357
  if (typeof options == 'function') {
272
358
  watch(
273
- () => options.call(this),
359
+ () => {
360
+ try {
361
+ return options.call(this)
362
+ } catch (error) {
363
+ console.warn('Failed to get resource options\n\n', error)
364
+ return null
365
+ }
366
+ },
274
367
  (updatedOptions, oldVal) => {
368
+ if (!updatedOptions) {
369
+ return
370
+ }
371
+
275
372
  let changed =
276
373
  !oldVal ||
277
374
  JSON.stringify(updatedOptions) !== JSON.stringify(oldVal)