frappe-ui 0.0.12 → 0.0.16

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.12",
3
+ "version": "0.0.16",
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]
@@ -86,25 +86,25 @@ export default {
86
86
  onClick,
87
87
  }
88
88
  },
89
- },
90
- computed: {
91
- dropdownItems() {
92
- return (this.options || [])
89
+ filterOptions(options) {
90
+ return (options || [])
93
91
  .filter(Boolean)
94
92
  .filter((option) => (option.condition ? option.condition() : true))
95
93
  .map((option) => this.normalizeDropdownItem(option))
96
94
  },
95
+ },
96
+ computed: {
97
97
  groups() {
98
98
  let groups = this.options[0]?.group
99
99
  ? this.options
100
- : [{ group: '', items: this.options }]
100
+ : [{ group: '', items: this.filterOptions(this.options) }]
101
101
 
102
102
  return groups.map((group, i) => {
103
103
  return {
104
104
  key: i,
105
105
  group: group.group,
106
106
  hideLabel: group.hideLabel || false,
107
- items: group.items.map((item) => this.normalizeDropdownItem(item)),
107
+ items: this.filterOptions(group.items),
108
108
  }
109
109
  })
110
110
  },
@@ -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"
@@ -68,6 +68,7 @@ import { debounce } from 'frappe-ui'
68
68
  export default {
69
69
  name: 'Input',
70
70
  inheritAttrs: false,
71
+ expose: ['getInputValue'],
71
72
  props: {
72
73
  label: {
73
74
  type: String,
@@ -122,9 +123,10 @@ export default {
122
123
  this.$refs.input.blur()
123
124
  },
124
125
  getInputValue(e) {
125
- let value = e.target.value
126
+ let $input = e ? e.target : this.$refs.input
127
+ let value = $input.value
126
128
  if (this.type == 'checkbox') {
127
- value = e.target.checked
129
+ value = $input.checked
128
130
  }
129
131
  return value
130
132
  },
@@ -134,7 +136,7 @@ export default {
134
136
  if ('value' in this.$attrs) {
135
137
  return this.$attrs.value
136
138
  }
137
- return this.modelValue
139
+ return this.modelValue || null
138
140
  },
139
141
  inputAttributes() {
140
142
  let onInput = (e) => {
@@ -1,17 +1,29 @@
1
1
  <template>
2
2
  <div ref="reference">
3
- <div class="h-full">
4
- <slot name="target" :togglePopover="togglePopover"></slot>
3
+ <div
4
+ class="h-full"
5
+ ref="target"
6
+ @click="updatePosition"
7
+ @focusin="updatePosition"
8
+ @keydown="updatePosition"
9
+ >
10
+ <slot
11
+ name="target"
12
+ v-bind="{ togglePopover, updatePosition, open, close }"
13
+ ></slot>
5
14
  </div>
6
- <teleport to="#popovers">
15
+ <teleport to="#frappeui-popper-root">
7
16
  <div
8
17
  ref="popover"
9
18
  :class="popoverClass"
10
- class="bg-white rounded-md border shadow-md popover-container relative"
11
- v-show="isOpen"
19
+ class="relative z-[100] popover-container"
20
+ :style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
12
21
  >
13
22
  <div v-if="!hideArrow" class="popover-arrow" ref="popover-arrow"></div>
14
- <slot name="content" :togglePopover="togglePopover"></slot>
23
+ <slot
24
+ name="content"
25
+ v-bind="{ togglePopover, updatePosition, open, close }"
26
+ ></slot>
15
27
  </div>
16
28
  </teleport>
17
29
  </div>
@@ -25,9 +37,9 @@ export default {
25
37
  props: {
26
38
  hideArrow: {
27
39
  type: Boolean,
28
- default: false,
40
+ default: true,
29
41
  },
30
- showPopup: {
42
+ show: {
31
43
  default: null,
32
44
  },
33
45
  right: Boolean,
@@ -39,18 +51,28 @@ export default {
39
51
  },
40
52
  emits: ['init', 'open', 'close'],
41
53
  watch: {
42
- showPopup(value) {
43
- if (value === true) {
44
- this.open()
45
- }
46
- if (value === false) {
47
- this.close()
48
- }
54
+ show: {
55
+ immediate: true,
56
+ handler(val) {
57
+ if (val) {
58
+ this.open()
59
+ } else {
60
+ this.close()
61
+ }
62
+ },
49
63
  },
50
64
  },
51
65
  data() {
52
66
  return {
53
67
  isOpen: false,
68
+ targetWidth: null,
69
+ }
70
+ },
71
+ created() {
72
+ if (!document.getElementById('frappeui-popper-root')) {
73
+ const root = document.createElement('div')
74
+ root.id = 'frappeui-popper-root'
75
+ document.body.appendChild(root)
54
76
  }
55
77
  },
56
78
  mounted() {
@@ -67,6 +89,9 @@ export default {
67
89
  if (this.show == null) {
68
90
  document.addEventListener('click', this.listener)
69
91
  }
92
+ this.$nextTick(() => {
93
+ this.targetWidth = this.$refs['target'].clientWidth
94
+ })
70
95
  },
71
96
  beforeDestroy() {
72
97
  this.popper && this.popper.destroy()
@@ -95,10 +120,13 @@ export default {
95
120
  : [],
96
121
  })
97
122
  } else {
98
- this.popper.update()
123
+ this.updatePosition()
99
124
  }
100
125
  this.$emit('init')
101
126
  },
127
+ updatePosition() {
128
+ this.popper && this.popper.update()
129
+ },
102
130
  togglePopover(flag) {
103
131
  if (flag == null) {
104
132
  flag = !this.isOpen
@@ -115,9 +143,7 @@ export default {
115
143
  return
116
144
  }
117
145
  this.isOpen = true
118
- this.$nextTick(() => {
119
- this.setupPopper()
120
- })
146
+ this.$nextTick(() => this.setupPopper())
121
147
  this.$emit('open')
122
148
  },
123
149
  close() {
@@ -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>
@@ -41,7 +41,7 @@ export function createResource(options, vm, getResource) {
41
41
  setData,
42
42
  })
43
43
 
44
- async function fetch(params) {
44
+ async function fetch(params, tempOptions = {}) {
45
45
  if (params instanceof Event) {
46
46
  params = null
47
47
  }
@@ -57,18 +57,22 @@ export function createResource(options, vm, getResource) {
57
57
  options.onFetch.call(vm, out.params)
58
58
  }
59
59
 
60
- if (options.validate) {
60
+ let validateFunction = tempOptions.validate || options.validate
61
+ let errorFunction = tempOptions.onError || options.onError
62
+ let successFunction = tempOptions.onSuccess || options.onSuccess
63
+
64
+ if (validateFunction) {
61
65
  let invalidMessage
62
66
  try {
63
- invalidMessage = await options.validate.call(vm, out.params)
67
+ invalidMessage = await validateFunction.call(vm, out.params)
64
68
  if (invalidMessage && typeof invalidMessage == 'string') {
65
69
  let error = new Error(invalidMessage)
66
- handleError(error)
70
+ handleError(error, errorFunction)
67
71
  out.loading = false
68
72
  return
69
73
  }
70
74
  } catch (error) {
71
- handleError(error)
75
+ handleError(error, errorFunction)
72
76
  out.loading = false
73
77
  return
74
78
  }
@@ -78,11 +82,11 @@ export function createResource(options, vm, getResource) {
78
82
  let data = await resourceFetcher(options.method, params || options.params)
79
83
  out.data = data
80
84
  out.fetched = true
81
- if (options.onSuccess) {
82
- options.onSuccess.call(vm, data)
85
+ if (successFunction) {
86
+ successFunction.call(vm, data)
83
87
  }
84
88
  } catch (error) {
85
- handleError(error)
89
+ handleError(error, errorFunction)
86
90
  }
87
91
  out.loading = false
88
92
  }
@@ -109,14 +113,14 @@ export function createResource(options, vm, getResource) {
109
113
  out.auto = options.auto
110
114
  }
111
115
 
112
- function handleError(error) {
116
+ function handleError(error, errorFunction) {
113
117
  console.error(error)
114
118
  if (out.previousData) {
115
119
  out.data = out.previousData
116
120
  }
117
121
  out.error = error
118
- if (options.onError) {
119
- options.onError.call(vm, error)
122
+ if (errorFunction) {
123
+ errorFunction.call(vm, error)
120
124
  }
121
125
  }
122
126
 
@@ -138,6 +142,8 @@ export function createResource(options, vm, getResource) {
138
142
  }
139
143
 
140
144
  export function createDocumentResource(options, vm) {
145
+ if (!(options.doctype && options.name)) return
146
+
141
147
  let cacheKey = getCacheKey([options.doctype, options.name])
142
148
  if (documentCache[cacheKey]) {
143
149
  return documentCache[cacheKey]
@@ -153,7 +159,7 @@ export function createDocumentResource(options, vm) {
153
159
  }
154
160
  },
155
161
  onSuccess(data) {
156
- out.doc = data
162
+ out.doc = postprocess(data)
157
163
  },
158
164
  }
159
165
 
@@ -170,7 +176,7 @@ export function createDocumentResource(options, vm) {
170
176
  }
171
177
  },
172
178
  onSuccess(data) {
173
- out.doc = data
179
+ out.doc = postprocess(data)
174
180
  },
175
181
  }),
176
182
  setValue: createResource(setValueOptions),
@@ -191,14 +197,54 @@ export function createDocumentResource(options, vm) {
191
197
  },
192
198
  }),
193
199
  update,
200
+ reload,
194
201
  })
195
202
 
203
+ for (let method in options.whitelistedMethods) {
204
+ let methodName = options.whitelistedMethods[method]
205
+ out[method] = createResource({
206
+ method: 'run_doc_method',
207
+ makeParams(values) {
208
+ return {
209
+ dt: out.doctype,
210
+ dn: out.name,
211
+ method: methodName,
212
+ args: JSON.stringify(values),
213
+ }
214
+ },
215
+ onSuccess(data) {
216
+ if (data.docs) {
217
+ for (let doc of data.docs) {
218
+ if (doc.doctype === out.doctype && doc.name === out.name) {
219
+ out.doc = postprocess(doc)
220
+ break
221
+ }
222
+ }
223
+ }
224
+ },
225
+ })
226
+ }
227
+
196
228
  function update(updatedOptions) {
197
229
  out.doctype = updatedOptions.doctype
198
230
  out.name = updatedOptions.name
199
231
  out.get.fetch()
200
232
  }
201
233
 
234
+ function reload() {
235
+ out.get.fetch()
236
+ }
237
+
238
+ function postprocess(doc) {
239
+ if (options.postprocess) {
240
+ let returnValue = options.postprocess(doc)
241
+ if (typeof returnValue === 'object') {
242
+ return returnValue
243
+ }
244
+ }
245
+ return doc
246
+ }
247
+
202
248
  // fetch the doc
203
249
  out.get.fetch()
204
250
  // cache
@@ -206,10 +252,64 @@ export function createDocumentResource(options, vm) {
206
252
  return out
207
253
  }
208
254
 
255
+ function createListResource(options, vm, getResource) {
256
+ if (!options.doctype) return
257
+
258
+ let out = reactive({
259
+ doctype: options.doctype,
260
+ fields: options.fields,
261
+ filters: options.filters,
262
+ data: null,
263
+ list: createResource({
264
+ method: 'frappe.client.get_list',
265
+ makeParams() {
266
+ return {
267
+ doctype: out.doctype,
268
+ fields: out.fields,
269
+ filters: out.filters,
270
+ }
271
+ },
272
+ onSuccess(data) {
273
+ out.data = data
274
+ },
275
+ }),
276
+ insert: createResource({
277
+ method: 'frappe.client.insert',
278
+ makeParams(values) {
279
+ return {
280
+ doc: {
281
+ doctype: out.doctype,
282
+ ...values,
283
+ },
284
+ }
285
+ },
286
+ onSuccess() {
287
+ out.list.fetch()
288
+ },
289
+ }),
290
+ update,
291
+ })
292
+
293
+ function update(updatedOptions) {
294
+ out.doctype = updatedOptions.doctype
295
+ out.fields = updatedOptions.fields
296
+ out.filters = updatedOptions.filters
297
+ out.list.fetch()
298
+ }
299
+
300
+ // fetch list
301
+ out.list.fetch()
302
+
303
+ return out
304
+ }
305
+
209
306
  function createResourceForOptions(options, vm, getResource) {
210
307
  if (options.type === 'document') {
211
308
  return createDocumentResource(options, vm, getResource)
212
309
  }
310
+ if (options.type === 'list') {
311
+ return createListResource(options, vm, getResource)
312
+ }
213
313
  return createResource(options, vm, getResource)
214
314
  }
215
315
 
@@ -248,7 +348,7 @@ let createMixin = (mixinOptions) => ({
248
348
  } else {
249
349
  resource.update(updatedOptions)
250
350
  }
251
- if (resource.auto) {
351
+ if (resource && resource.auto) {
252
352
  resource.fetch()
253
353
  }
254
354
  },