frappe-ui 0.0.43 → 0.0.46

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,11 +1,12 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.43",
3
+ "version": "0.0.46",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
7
7
  "test": "npx prettier --check ./src",
8
- "prettier": "npx prettier -w ./src"
8
+ "prettier": "npx prettier -w ./src",
9
+ "prepare": "husky install"
9
10
  },
10
11
  "files": [
11
12
  "src"
@@ -32,5 +33,13 @@
32
33
  "postcss": "^8.4.5",
33
34
  "socket.io-client": "^4.5.1",
34
35
  "tailwindcss": "^3.0.12"
36
+ },
37
+ "devDependencies": {
38
+ "husky": ">=6",
39
+ "lint-staged": ">=10",
40
+ "prettier": "2.7.1"
41
+ },
42
+ "lint-staged": {
43
+ "*.{js,css,md,vue}": "prettier --write"
35
44
  }
36
45
  }
@@ -1,31 +1,23 @@
1
1
  <template>
2
- <Combobox v-model="selectedValue" nullable>
2
+ <Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
3
3
  <Popover class="w-full">
4
4
  <template #target="{ open: openPopover }">
5
- <div class="relative w-full">
6
- <ComboboxInput
7
- :displayValue="displayValue"
8
- class="w-full placeholder-gray-500 form-input"
9
- type="text"
10
- @change="
11
- (e) => {
12
- query = e.target.value
13
- openPopover()
14
- }
15
- "
16
- @focus="
5
+ <div class="w-full">
6
+ <ComboboxButton
7
+ class="flex items-center justify-between w-full py-1.5 pl-3 pr-2 rounded-md bg-gray-100"
8
+ :class="{ 'rounded-b-none': isComboboxOpen }"
9
+ @click="
17
10
  () => {
18
11
  openPopover()
19
- toggleCombobox(true)
20
12
  }
21
13
  "
22
- @keydown="toggleCombobox(true)"
23
- autocomplete="off"
24
- v-bind="$attrs"
25
- />
26
- <ComboboxButton
27
- class="absolute inset-y-0 right-0 flex items-center pr-2"
28
14
  >
15
+ <span class="text-base" v-if="selectedValue">
16
+ {{ displayValue(selectedValue) }}
17
+ </span>
18
+ <span class="text-base text-gray-500" v-else>
19
+ {{ placeholder || '' }}
20
+ </span>
29
21
  <FeatherIcon
30
22
  name="chevron-down"
31
23
  class="w-4 h-4 text-gray-500"
@@ -36,12 +28,27 @@
36
28
  </template>
37
29
  <template #body>
38
30
  <ComboboxOptions
39
- :class="[
40
- 'p-1.5 bg-white rounded-md shadow-md rounded-t-none max-h-[11rem] overflow-y-auto',
41
- { hidden: !showCombobox },
42
- ]"
43
- :static="true"
31
+ class="px-1.5 pb-1.5 bg-white rounded-md shadow-md rounded-t-none max-h-[11rem] overflow-y-auto"
32
+ static
33
+ v-show="isComboboxOpen"
44
34
  >
35
+ <div
36
+ class="flex items-st items-stretch space-x-1.5 sticky top-0 pt-1.5 mb-1.5 bg-white"
37
+ >
38
+ <ComboboxInput
39
+ class="w-full placeholder-gray-500 form-input"
40
+ type="text"
41
+ @change="
42
+ (e) => {
43
+ query = e.target.value
44
+ }
45
+ "
46
+ :value="query"
47
+ autocomplete="off"
48
+ placeholder="Search by keyword"
49
+ />
50
+ <Button icon="x" @click="selectedValue = null" />
51
+ </div>
45
52
  <ComboboxOption
46
53
  as="template"
47
54
  v-for="option in filteredOptions"
@@ -81,8 +88,7 @@ import Popover from './Popover.vue'
81
88
 
82
89
  export default {
83
90
  name: 'Autocomplete',
84
- inheritAttrs: false,
85
- props: ['modelValue', 'options'],
91
+ props: ['modelValue', 'options', 'placeholder'],
86
92
  emits: ['update:modelValue', 'change'],
87
93
  components: {
88
94
  Popover,
@@ -94,7 +100,6 @@ export default {
94
100
  },
95
101
  data() {
96
102
  return {
97
- showCombobox: false,
98
103
  query: '',
99
104
  }
100
105
  },
@@ -107,7 +112,7 @@ export default {
107
112
  return this.valuePropPassed ? this.$attrs.value : this.modelValue
108
113
  },
109
114
  set(val) {
110
- setTimeout(() => this.toggleCombobox(false), 0)
115
+ this.query = ''
111
116
  this.$emit(this.valuePropPassed ? 'change' : 'update:modelValue', val)
112
117
  },
113
118
  },
@@ -130,12 +135,6 @@ export default {
130
135
  }
131
136
  return option?.label
132
137
  },
133
- toggleCombobox(value) {
134
- value = Boolean(value)
135
- if (this.showCombobox !== value) {
136
- this.showCombobox = value
137
- }
138
- },
139
138
  },
140
139
  }
141
140
  </script>
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <TransitionRoot as="template" :show="open">
2
+ <TransitionRoot
3
+ as="template"
4
+ :show="open"
5
+ @after-leave="$emit('after-leave')"
6
+ >
3
7
  <HDialog
4
8
  as="div"
5
9
  class="fixed inset-0 z-10 overflow-y-auto"
@@ -137,7 +141,7 @@ export default {
137
141
  },
138
142
  },
139
143
  },
140
- emits: ['update:modelValue', 'close'],
144
+ emits: ['update:modelValue', 'close', 'after-leave'],
141
145
  components: {
142
146
  HDialog,
143
147
  DialogOverlay,
@@ -48,7 +48,7 @@ export default {
48
48
  'stroke-width': this.strokeWidth,
49
49
  width: null,
50
50
  height: null,
51
- class: [icon.attrs.class],
51
+ class: [icon.attrs.class, 'shrink-0'],
52
52
  innerHTML: icon.contents,
53
53
  },
54
54
  this.$attrs
@@ -208,10 +208,13 @@ export default {
208
208
  this.isOpen = false
209
209
  },
210
210
  onMouseover() {
211
+ this.mouseover = true
211
212
  if (this.trigger === 'hover') {
212
213
  if (this.hoverDelay) {
213
214
  this.hoverTimer = setTimeout(() => {
214
- this.open()
215
+ if (this.mouseover) {
216
+ this.open()
217
+ }
215
218
  }, Number(this.hoverDelay) * 1000)
216
219
  } else {
217
220
  this.open()
@@ -219,6 +222,7 @@ export default {
219
222
  }
220
223
  },
221
224
  onMouseleave() {
225
+ this.mouseover = false
222
226
  if (this.hoverTimer) {
223
227
  clearTimeout(this.hoverTimer)
224
228
  }
@@ -67,6 +67,39 @@
67
67
  </Button>
68
68
  </template>
69
69
  </Dialog>
70
+ <Dialog
71
+ :options="{ title: 'Add Image' }"
72
+ v-model="addImageDialog.show"
73
+ @after-leave="resetAddImage"
74
+ >
75
+ <template #body-content>
76
+ <label
77
+ class="relative py-1 bg-gray-100 rounded-lg cursor-pointer focus-within:bg-gray-200 hover:bg-gray-200"
78
+ >
79
+ <input
80
+ type="file"
81
+ class="w-full opacity-0"
82
+ @change="onImageSelect"
83
+ accept="image/*"
84
+ />
85
+ <span class="absolute inset-0 px-2 py-1 text-base select-none">
86
+ {{
87
+ addImageDialog.file ? 'Select another image' : 'Select an image'
88
+ }}
89
+ </span>
90
+ </label>
91
+ <img
92
+ v-if="addImageDialog.url"
93
+ :src="addImageDialog.url"
94
+ class="w-full mt-2 rounded-lg"
95
+ />
96
+ </template>
97
+ <template #actions>
98
+ <Button appearance="primary" @click="addImage(addImageDialog.url)">
99
+ Insert Image
100
+ </Button>
101
+ </template>
102
+ </Dialog>
70
103
  </div>
71
104
  </template>
72
105
  <script>
@@ -83,6 +116,7 @@ export default {
83
116
  data() {
84
117
  return {
85
118
  setLinkDialog: { url: '', show: false },
119
+ addImageDialog: { url: '', file: null, show: false },
86
120
  }
87
121
  },
88
122
  methods: {
@@ -93,6 +127,8 @@ export default {
93
127
  if (existingURL) {
94
128
  this.setLinkDialog.url = existingURL
95
129
  }
130
+ } else if (button.label === 'Image') {
131
+ this.addImageDialog.show = true
96
132
  } else {
97
133
  button.action(this.editor)
98
134
  }
@@ -114,6 +150,26 @@ export default {
114
150
  this.setLinkDialog.show = false
115
151
  this.setLinkDialog.url = ''
116
152
  },
153
+ onImageSelect(e) {
154
+ let file = e.target.files[0]
155
+ if (!file) {
156
+ return
157
+ }
158
+ this.addImageDialog.file = file
159
+ let reader = new FileReader()
160
+ reader.onloadend = () => {
161
+ let base64string = reader.result
162
+ this.addImageDialog.url = base64string
163
+ }
164
+ reader.readAsDataURL(file)
165
+ },
166
+ addImage(src) {
167
+ this.editor.chain().focus().setImage({ src }).run()
168
+ this.resetAddImage()
169
+ },
170
+ resetAddImage() {
171
+ this.addImageDialog = { show: false, url: null, file: null }
172
+ },
117
173
  },
118
174
  }
119
175
  </script>
@@ -43,7 +43,7 @@
43
43
  <editor-content :editor="editor" />
44
44
  <span
45
45
  v-if="!content"
46
- class="absolute inset-y-0 text-base text-gray-500 pointer-events-none"
46
+ class="absolute top-0 text-base text-gray-500 pointer-events-none"
47
47
  >
48
48
  {{ placeholder }}
49
49
  </span>
@@ -119,7 +119,9 @@ export default {
119
119
  TextAlign.configure({
120
120
  types: ['heading', 'paragraph'],
121
121
  }),
122
- Image,
122
+ Image.configure({
123
+ allowBase64: true,
124
+ }),
123
125
  Link,
124
126
  Placeholder.configure({
125
127
  placeholder: this.placeholder || 'Write something...',
@@ -163,6 +165,7 @@ export default {
163
165
  'Align Center',
164
166
  'Align Right',
165
167
  'Separator',
168
+ 'Image',
166
169
  'Blockquote',
167
170
  'Code',
168
171
  'Horizontal Rule',
@@ -220,10 +223,7 @@ export default {
220
223
  editorProps() {
221
224
  return {
222
225
  attributes: {
223
- class: normalizeClass([
224
- 'prose prose-sm prose-p:my-1',
225
- this.editorClass,
226
- ]),
226
+ class: normalizeClass(['prose prose-p:my-1', this.editorClass]),
227
227
  },
228
228
  }
229
229
  },
@@ -16,6 +16,7 @@ import ListUnordered from './icons/list-unordered.vue'
16
16
  import DoubleQuotes from './icons/double-quotes-r.vue'
17
17
  import CodeView from './icons/code-view.vue'
18
18
  import Link from './icons/link.vue'
19
+ import Image from './icons/image-add-line.vue'
19
20
  import ArrowGoBack from './icons/arrow-go-back-line.vue'
20
21
  import ArrowGoForward from './icons/arrow-go-forward-line.vue'
21
22
  import Separator from './icons/separator.vue'
@@ -146,6 +147,11 @@ export default {
146
147
  icon: Link,
147
148
  isActive: (editor) => editor.isActive('link'),
148
149
  },
150
+ Image: {
151
+ label: 'Image',
152
+ icon: Image,
153
+ isActive: (editor) => false,
154
+ },
149
155
  Undo: {
150
156
  label: 'Undo',
151
157
  icon: ArrowGoBack,
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ width="24"
6
+ height="24"
7
+ >
8
+ <path fill="none" d="M0 0h24v24H0z" />
9
+ <path
10
+ d="M21 15v3h3v2h-3v3h-2v-3h-3v-2h3v-3h2zm.008-12c.548 0 .992.445.992.993V13h-2V5H4v13.999L14 9l3 3v2.829l-3-3L6.827 19H14v2H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016zM8 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"
11
+ fill="currentColor"
12
+ />
13
+ </svg>
14
+ </template>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <Popover trigger="hover" :hoverDelay="hoverDelay" :placement="placement">
3
+ <template #target>
4
+ <slot />
5
+ </template>
6
+ <template #body>
7
+ <slot name="body">
8
+ <div
9
+ class="px-2 py-1 text-xs text-white bg-gray-800 border border-gray-100 rounded-lg shadow-xl"
10
+ >
11
+ {{ text }}
12
+ </div>
13
+ </slot>
14
+ </template>
15
+ </Popover>
16
+ </template>
17
+ <script>
18
+ import Popover from './Popover.vue'
19
+ export default {
20
+ name: 'Tooltip',
21
+ components: { Popover },
22
+ props: {
23
+ hoverDelay: {
24
+ default: 0.5,
25
+ },
26
+ placement: {
27
+ default: 'bottom-start',
28
+ },
29
+ text: {
30
+ type: String,
31
+ default: '',
32
+ },
33
+ },
34
+ }
35
+ </script>
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ export { default as Resource } from './components/Resource.vue'
23
23
  export { default as Spinner } from './components/Spinner.vue'
24
24
  export { default as SuccessMessage } from './components/SuccessMessage.vue'
25
25
  export { default as TextEditor } from './components/TextEditor'
26
+ export { default as Tooltip } from './components/Tooltip.vue'
26
27
 
27
28
  // directives
28
29
  export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
@@ -1,6 +1,6 @@
1
1
  import resources from './resources'
2
2
  import call from './call'
3
- import socket from './socketio'
3
+ import initSocket from './socketio'
4
4
 
5
5
  let defaultOptions = {
6
6
  resources: true,
@@ -18,7 +18,7 @@ export default {
18
18
  app.config.globalProperties.$call = callFunction
19
19
  }
20
20
  if (options.socketio) {
21
- app.config.globalProperties.$socket = socket
21
+ app.config.globalProperties.$socket = initSocket(options.socketio)
22
22
  }
23
23
  },
24
24
  }
@@ -201,6 +201,7 @@ export function createDocumentResource(options, vm) {
201
201
  doctype: options.doctype,
202
202
  name: options.name,
203
203
  doc: null,
204
+ auto: true,
204
205
  get: createResource(
205
206
  {
206
207
  method: 'frappe.client.get',
@@ -212,6 +213,7 @@ export function createDocumentResource(options, vm) {
212
213
  },
213
214
  onSuccess(data) {
214
215
  out.doc = transform(data)
216
+ options.onSuccess?.call(vm, out.doc)
215
217
  },
216
218
  onError: options.onError,
217
219
  },
@@ -237,6 +239,8 @@ export function createDocumentResource(options, vm) {
237
239
  onSuccess() {
238
240
  out.doc = null
239
241
  options.delete?.onSuccess?.call(vm, data)
242
+ // delete from list resources
243
+ deleteRowInListResource(out.doctype, out.name)
240
244
  },
241
245
  onError: options.delete?.onError,
242
246
  },
@@ -294,8 +298,6 @@ export function createDocumentResource(options, vm) {
294
298
  return doc
295
299
  }
296
300
 
297
- // fetch the doc
298
- out.get.fetch()
299
301
  // cache
300
302
  documentCache[cacheKey] = out
301
303
  return out
@@ -322,6 +324,7 @@ export function createListResource(options, vm, getResource) {
322
324
  data: null,
323
325
  next,
324
326
  hasNextPage: true,
327
+ auto: true,
325
328
  list: createResource(
326
329
  {
327
330
  method: 'frappe.client.get_list',
@@ -450,7 +453,6 @@ export function createListResource(options, vm, getResource) {
450
453
  out.order_by = updatedOptions.order_by
451
454
  out.start = updatedOptions.start
452
455
  out.limit = updatedOptions.limit
453
- out.list.fetch()
454
456
  }
455
457
 
456
458
  function transform(data) {
@@ -480,9 +482,6 @@ export function createListResource(options, vm, getResource) {
480
482
  out.list.fetch()
481
483
  }
482
484
 
483
- // fetch list
484
- out.list.fetch()
485
-
486
485
  if (cacheKey) {
487
486
  // cache
488
487
  listCache[cacheKey] = out
@@ -495,7 +494,7 @@ export function createListResource(options, vm, getResource) {
495
494
  }
496
495
 
497
496
  function updateRowInListResource(doctype, doc) {
498
- let resources = listResources[doctype]
497
+ let resources = listResources[doctype] || []
499
498
  for (let resource of resources) {
500
499
  if (resource.originalData) {
501
500
  for (let row of resource.originalData) {
@@ -514,8 +513,20 @@ function updateRowInListResource(doctype, doc) {
514
513
  }
515
514
  }
516
515
 
516
+ function deleteRowInListResource(doctype, docname) {
517
+ let resources = listResources[doctype] || []
518
+ for (let resource of resources) {
519
+ if (resource.originalData) {
520
+ resource.originalData = resource.originalData.filter(
521
+ (row) => row.name !== docname
522
+ )
523
+ resource.data = resource.transform(resource.originalData)
524
+ }
525
+ }
526
+ }
527
+
517
528
  function revertRowInListResource(doctype, doc) {
518
- let resources = listResources[doctype]
529
+ let resources = listResources[doctype] || []
519
530
  for (let resource of resources) {
520
531
  if (resource.originalData) {
521
532
  for (let row of resource.originalData) {
@@ -592,7 +603,7 @@ let createMixin = (mixinOptions) => ({
592
603
  resource.update(updatedOptions)
593
604
  }
594
605
  if (resource && resource.auto) {
595
- resource.fetch()
606
+ resource.reload()
596
607
  }
597
608
  },
598
609
  {
@@ -606,8 +617,8 @@ let createMixin = (mixinOptions) => ({
606
617
  mixinOptions.getResource
607
618
  )
608
619
  this._resources[key] = resource
609
- if (resource.auto) {
610
- resource.fetch()
620
+ if (resource && resource.auto) {
621
+ resource.reload()
611
622
  }
612
623
  }
613
624
  }
@@ -1,13 +1,11 @@
1
1
  import { io } from 'socket.io-client'
2
2
 
3
- function initSocket() {
3
+ export default function initSocket(options = {}) {
4
4
  let host = window.location.hostname
5
- let port = window.location.port ? ':9000' : ''
5
+ let socketio_port = options.port || 9000
6
+ let port = window.location.port ? `:${socketio_port}` : ''
6
7
  let protocol = port ? 'http' : 'https'
7
8
  let url = `${protocol}://${host}${port}`
8
- return io(url)
9
+ let socket = io(url)
10
+ return socket
9
11
  }
10
-
11
- let socket = initSocket()
12
-
13
- export default socket