frappe-ui 0.0.57 → 0.0.60

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.57",
3
+ "version": "0.0.60",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -36,6 +36,7 @@
36
36
  "@tiptap/vue-3": "^2.0.0-beta.96",
37
37
  "autoprefixer": "^10.4.2",
38
38
  "feather-icons": "^4.28.0",
39
+ "idb-keyval": "^6.2.0",
39
40
  "postcss": "^8.4.5",
40
41
  "socket.io-client": "^4.5.1",
41
42
  "tailwindcss": "^3.0.12",
@@ -31,7 +31,7 @@
31
31
  </template>
32
32
  <template #body>
33
33
  <ComboboxOptions
34
- class="max-h-[11rem] overflow-y-auto rounded-md rounded-t-none bg-white px-1.5 pb-1.5 shadow-md"
34
+ class="max-h-[15rem] overflow-y-auto rounded-md rounded-t-none bg-white px-1.5 pb-1.5 shadow-md"
35
35
  static
36
36
  v-show="isComboboxOpen"
37
37
  >
@@ -52,24 +52,36 @@
52
52
  />
53
53
  <Button icon="x" @click="selectedValue = null" />
54
54
  </div>
55
- <ComboboxOption
56
- as="template"
57
- v-for="option in filteredOptions"
58
- :key="option.value"
59
- :value="option"
60
- v-slot="{ active, selected }"
55
+ <div
56
+ v-for="group in groups"
57
+ :key="group.key"
58
+ v-show="group.items.length > 0"
61
59
  >
62
- <li
63
- :class="[
64
- 'rounded-md px-2.5 py-1.5 text-base',
65
- { 'bg-gray-100': active },
66
- ]"
60
+ <div
61
+ v-if="group.group && !group.hideLabel"
62
+ class="px-2 py-1 text-xs font-semibold uppercase tracking-wider text-gray-500"
63
+ >
64
+ {{ group.group }}
65
+ </div>
66
+ <ComboboxOption
67
+ as="template"
68
+ v-for="option in group.items"
69
+ :key="option.value"
70
+ :value="option"
71
+ v-slot="{ active, selected }"
67
72
  >
68
- {{ option.label }}
69
- </li>
70
- </ComboboxOption>
73
+ <li
74
+ :class="[
75
+ 'rounded-md px-2.5 py-1.5 text-base',
76
+ { 'bg-gray-100': active },
77
+ ]"
78
+ >
79
+ {{ option.label }}
80
+ </li>
81
+ </ComboboxOption>
82
+ </div>
71
83
  <li
72
- v-if="filteredOptions.length == 0"
84
+ v-if="groups.length == 0"
73
85
  class="rounded-md px-2.5 py-1.5 text-base text-gray-600"
74
86
  >
75
87
  No results found
@@ -119,11 +131,31 @@ export default {
119
131
  this.$emit(this.valuePropPassed ? 'change' : 'update:modelValue', val)
120
132
  },
121
133
  },
122
- filteredOptions() {
134
+ groups() {
135
+ if (!this.options || this.options.length == 0) return []
136
+
137
+ let groups = this.options[0]?.group
138
+ ? this.options
139
+ : [{ group: '', items: this.options }]
140
+
141
+ return groups
142
+ .map((group, i) => {
143
+ return {
144
+ key: i,
145
+ group: group.group,
146
+ hideLabel: group.hideLabel || false,
147
+ items: this.filterOptions(group.items),
148
+ }
149
+ })
150
+ .filter((group) => group.items.length > 0)
151
+ },
152
+ },
153
+ methods: {
154
+ filterOptions(options) {
123
155
  if (!this.query) {
124
- return this.options
156
+ return options
125
157
  }
126
- return this.options.filter((option) => {
158
+ return options.filter((option) => {
127
159
  let searchTexts = [option.label, option.value]
128
160
  return searchTexts.some((text) =>
129
161
  (text || '')
@@ -133,8 +165,6 @@ export default {
133
165
  )
134
166
  })
135
167
  },
136
- },
137
- methods: {
138
168
  displayValue(option) {
139
169
  if (typeof option === 'string') {
140
170
  return option
@@ -7,23 +7,22 @@
7
7
  </span>
8
8
  </template>
9
9
  <script>
10
+ const DEFAULT_COLOR_MAP = {
11
+ Pending: 'yellow',
12
+ Running: 'yellow',
13
+ Success: 'green',
14
+ Failure: 'red',
15
+ Active: 'green',
16
+ Broken: 'red',
17
+ Updating: 'blue',
18
+ Rejected: 'red',
19
+ Published: 'green',
20
+ Approved: 'green',
21
+ }
22
+
10
23
  export default {
11
24
  name: 'Badge',
12
25
  props: ['color', 'status', 'colorMap'],
13
- data: {
14
- defaultColorMap: {
15
- Pending: 'yellow',
16
- Running: 'yellow',
17
- Success: 'green',
18
- Failure: 'red',
19
- Active: 'green',
20
- Broken: 'red',
21
- Updating: 'blue',
22
- Rejected: 'red',
23
- Published: 'green',
24
- Approved: 'green',
25
- },
26
- },
27
26
  computed: {
28
27
  classes() {
29
28
  let color = this.getBadgeColor()
@@ -46,10 +45,7 @@ export default {
46
45
  return color
47
46
  }
48
47
 
49
- let statusColorMap = Object.assign(
50
- this.defaultColorMap,
51
- this.colorMap || {}
52
- )
48
+ let statusColorMap = Object.assign(DEFAULT_COLOR_MAP, this.colorMap || {})
53
49
  color = statusColorMap[this.status] || 'gray'
54
50
 
55
51
  return color
@@ -1,20 +1,22 @@
1
1
  <template>
2
- <div
3
- v-if="items.length"
4
- class="min-w-40 rounded-lg border bg-white p-1 text-base shadow-lg"
5
- >
6
- <button
7
- :class="[
8
- index === selectedIndex ? 'bg-gray-100' : 'text-gray-900',
9
- 'flex w-full items-center whitespace-nowrap rounded-md px-2 py-2 text-sm',
10
- ]"
11
- v-for="(item, index) in items"
12
- :key="index"
13
- @click="selectItem(index)"
14
- @mouseover="selectedIndex = index"
2
+ <div>
3
+ <div
4
+ v-if="items.length"
5
+ class="min-w-40 rounded-lg border bg-white p-1 text-base shadow-lg"
15
6
  >
16
- {{ item.label }}
17
- </button>
7
+ <button
8
+ :class="[
9
+ index === selectedIndex ? 'bg-gray-100' : 'text-gray-900',
10
+ 'flex w-full items-center whitespace-nowrap rounded-md px-2 py-2 text-sm',
11
+ ]"
12
+ v-for="(item, index) in items"
13
+ :key="index"
14
+ @click="selectItem(index)"
15
+ @mouseover="selectedIndex = index"
16
+ >
17
+ {{ item.label }}
18
+ </button>
19
+ </div>
18
20
  </div>
19
21
  </template>
20
22
 
@@ -2,13 +2,13 @@
2
2
  <div class="relative w-full" :class="$attrs.class" v-if="editor">
3
3
  <BubbleMenu
4
4
  v-if="bubbleMenuButtons"
5
- class="bubble-menu"
5
+ class="bubble-menu rounded-md shadow-sm"
6
6
  :tippy-options="{ duration: 100 }"
7
7
  :editor="editor"
8
8
  >
9
9
  <Menu
10
10
  :editor="editor"
11
- class="rounded-md border border-gray-100 shadow-sm"
11
+ class="rounded-md border border-gray-100 shadow-lg"
12
12
  :buttons="bubbleMenuButtons"
13
13
  />
14
14
  </BubbleMenu>
@@ -131,9 +131,11 @@ export default {
131
131
  editorProps: {
132
132
  deep: true,
133
133
  handler(value) {
134
- this.editor.setOptions({
135
- editorProps: value,
136
- })
134
+ if (this.editor) {
135
+ this.editor.setOptions({
136
+ editorProps: value,
137
+ })
138
+ }
137
139
  },
138
140
  },
139
141
  },
@@ -247,8 +249,25 @@ export default {
247
249
  'Separator',
248
250
  'Bullet List',
249
251
  'Numbered List',
252
+ 'Separator',
253
+ 'Image',
250
254
  'Blockquote',
251
255
  'Code',
256
+ [
257
+ 'InsertTable',
258
+ 'AddColumnBefore',
259
+ 'AddColumnAfter',
260
+ 'DeleteColumn',
261
+ 'AddRowBefore',
262
+ 'AddRowAfter',
263
+ 'DeleteRow',
264
+ 'MergeCells',
265
+ 'SplitCell',
266
+ 'ToggleHeaderColumn',
267
+ 'ToggleHeaderRow',
268
+ 'ToggleHeaderCell',
269
+ 'DeleteTable',
270
+ ],
252
271
  ]
253
272
  }
254
273
  return buttons.map(createEditorButton)
@@ -97,13 +97,13 @@ export default {
97
97
  },
98
98
  'Bullet List': {
99
99
  label: 'Bullet List',
100
- icon: ListOrdered,
100
+ icon: ListUnordered,
101
101
  action: (editor) => editor.chain().focus().toggleBulletList().run(),
102
102
  isActive: (editor) => editor.isActive('bulletList'),
103
103
  },
104
104
  'Numbered List': {
105
105
  label: 'Numbered List',
106
- icon: ListUnordered,
106
+ icon: ListOrdered,
107
107
  action: (editor) => editor.chain().focus().toggleOrderedList().run(),
108
108
  isActive: (editor) => editor.isActive('orderedList'),
109
109
  },
@@ -1,28 +1,34 @@
1
- let instances = []
1
+ const instanceMap = new Map()
2
2
 
3
3
  function onDocumentClick(e, el, fn) {
4
4
  let target = e.target
5
5
  if (el !== target && !el.contains(target)) {
6
- fn(e)
6
+ fn?.(e)
7
7
  }
8
8
  }
9
9
 
10
10
  export default {
11
11
  beforeMount(el, binding) {
12
- el.dataset.outsideClickIndex = instances.length
13
-
14
12
  const fn = binding.value
15
- const click = function (e) {
13
+ const clickHandler = function (e) {
16
14
  onDocumentClick(e, el, fn)
17
15
  }
18
16
 
19
- document.addEventListener('click', click)
20
- instances.push(click)
17
+ removeHandlerIfPresent(el)
18
+ instanceMap.set(el, clickHandler)
19
+ document.addEventListener('click', clickHandler)
21
20
  },
22
21
  unmounted(el) {
23
- const index = el.dataset.outsideClickIndex
24
- const handler = instances[index]
25
- document.addEventListener('click', handler)
26
- instances.splice(index, 1)
22
+ removeHandlerIfPresent(el)
27
23
  },
28
24
  }
25
+
26
+ function removeHandlerIfPresent(el) {
27
+ const clickHandler = instanceMap.get(el)
28
+ if (!clickHandler) {
29
+ return
30
+ }
31
+
32
+ instanceMap.delete(el)
33
+ document.removeEventListener('click', clickHandler)
34
+ }
package/src/index.js CHANGED
@@ -36,7 +36,7 @@ export {
36
36
  createResource,
37
37
  createDocumentResource,
38
38
  createListResource,
39
- } from './utils/resources.js'
39
+ } from './resources'
40
40
 
41
41
  // plugin
42
42
  export { default as FrappeUI } from './utils/plugin.js'
@@ -0,0 +1,171 @@
1
+ import { reactive } from 'vue'
2
+ import { getCacheKey, createResource } from './resources'
3
+ import {
4
+ updateRowInListResource,
5
+ deleteRowInListResource,
6
+ revertRowInListResource,
7
+ } from './listResource'
8
+ import { getLocal, saveLocal } from './local'
9
+
10
+ let documentCache = reactive({})
11
+
12
+ export function createDocumentResource(options, vm) {
13
+ if (!(options.doctype && options.name)) return
14
+
15
+ let cacheKey = getCacheKey([options.doctype, options.name])
16
+ if (documentCache[cacheKey]) {
17
+ return documentCache[cacheKey]
18
+ }
19
+
20
+ let setValueOptions = {
21
+ method: 'frappe.client.set_value',
22
+ makeParams(values) {
23
+ return {
24
+ doctype: out.doctype,
25
+ name: out.name,
26
+ fieldname: values,
27
+ }
28
+ },
29
+ beforeSubmit(params) {
30
+ out.previousDoc = JSON.stringify(out.doc)
31
+ Object.assign(out.doc, params.fieldname || {})
32
+ // update data in list resources
33
+ updateRowInListResource(out.doctype, out.doc)
34
+ },
35
+ onSuccess(data) {
36
+ out.doc = transform(data)
37
+ options.setValue?.onSuccess?.call(vm, data)
38
+ },
39
+ onError(error) {
40
+ out.doc = JSON.parse(out.previousDoc)
41
+ options.setValue?.onError?.call(vm, error)
42
+ // revert data in list resource
43
+ revertRowInListResource(out.doctype, out.doc)
44
+ },
45
+ }
46
+
47
+ let out = reactive({
48
+ doctype: options.doctype,
49
+ name: options.name,
50
+ doc: null,
51
+ auto: true,
52
+ get: createResource(
53
+ {
54
+ method: 'frappe.client.get',
55
+ makeParams() {
56
+ return {
57
+ doctype: out.doctype,
58
+ name: out.name,
59
+ }
60
+ },
61
+ onSuccess(data) {
62
+ saveLocal(cacheKey, data)
63
+ out.doc = transform(data)
64
+ options.onSuccess?.call(vm, out.doc)
65
+ },
66
+ onError: options.onError,
67
+ },
68
+ vm
69
+ ),
70
+ setValue: createResource(setValueOptions, vm),
71
+ setValueDebounced: createResource(
72
+ {
73
+ ...setValueOptions,
74
+ debounce: options.debounce || 500,
75
+ },
76
+ vm
77
+ ),
78
+ delete: createResource(
79
+ {
80
+ method: 'frappe.client.delete',
81
+ makeParams() {
82
+ return {
83
+ doctype: out.doctype,
84
+ name: out.name,
85
+ }
86
+ },
87
+ onSuccess() {
88
+ out.doc = null
89
+ options.delete?.onSuccess?.call(vm)
90
+ // delete from list resources
91
+ deleteRowInListResource(out.doctype, out.name)
92
+ },
93
+ onError: options.delete?.onError,
94
+ },
95
+ vm
96
+ ),
97
+ update,
98
+ reload,
99
+ setDoc,
100
+ })
101
+
102
+ for (let method in options.whitelistedMethods) {
103
+ let methodName = options.whitelistedMethods[method]
104
+ out[method] = createResource(
105
+ {
106
+ method: 'run_doc_method',
107
+ makeParams(values) {
108
+ return {
109
+ dt: out.doctype,
110
+ dn: out.name,
111
+ method: methodName,
112
+ args: JSON.stringify(values),
113
+ }
114
+ },
115
+ onSuccess(data) {
116
+ if (data.docs) {
117
+ for (let doc of data.docs) {
118
+ if (doc.doctype === out.doctype && doc.name === out.name) {
119
+ out.doc = transform(doc)
120
+ break
121
+ }
122
+ }
123
+ }
124
+ },
125
+ },
126
+ vm
127
+ )
128
+ }
129
+
130
+ function update(updatedOptions) {
131
+ out.doctype = updatedOptions.doctype
132
+ out.name = updatedOptions.name
133
+ out.get.fetch()
134
+ }
135
+
136
+ function reload() {
137
+ return out.get.fetch()
138
+ }
139
+
140
+ function setDoc(doc) {
141
+ if (typeof doc === 'function') {
142
+ doc = doc.call(vm, out.doc)
143
+ }
144
+ out.doc = transform(doc)
145
+ }
146
+
147
+ function transform(doc) {
148
+ if (options.transform) {
149
+ let returnValue = options.transform.call(vm, doc)
150
+ if (typeof returnValue === 'object') {
151
+ return returnValue
152
+ }
153
+ }
154
+ return doc
155
+ }
156
+
157
+ // cache
158
+ documentCache[cacheKey] = out
159
+ // offline
160
+ getLocal(cacheKey).then((data) => {
161
+ if (out.get.loading && data) {
162
+ out.doc = transform(data)
163
+ }
164
+ })
165
+ return out
166
+ }
167
+
168
+ export function getCachedDocumentResource(doctype, name) {
169
+ let cacheKey = getCacheKey([doctype, name])
170
+ return documentCache[cacheKey] || null
171
+ }
@@ -0,0 +1,3 @@
1
+ export { createResource } from './resources'
2
+ export { createDocumentResource } from './documentResource'
3
+ export { createListResource } from './listResource'