pimelon-ui 0.0.73 → 0.0.84

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": "pimelon-ui",
3
- "version": "0.0.73",
3
+ "version": "0.0.84",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -39,6 +39,7 @@
39
39
  "feather-icons": "^4.28.0",
40
40
  "idb-keyval": "^6.2.0",
41
41
  "postcss": "^8.4.5",
42
+ "showdown": "^2.1.0",
42
43
  "socket.io-client": "^4.5.1",
43
44
  "tailwindcss": "^3.0.12",
44
45
  "tippy.js": "^6.3.7"
@@ -4,6 +4,7 @@
4
4
  :class="buttonClasses"
5
5
  @click="handleClick"
6
6
  :disabled="isDisabled"
7
+ :aria-label="ariaLabel"
7
8
  >
8
9
  <LoadingIndicator
9
10
  v-if="loading"
@@ -136,6 +137,9 @@ export default {
136
137
  isDisabled() {
137
138
  return this.disabled || this.loading
138
139
  },
140
+ ariaLabel() {
141
+ return this.icon ? this.label : null
142
+ },
139
143
  },
140
144
  methods: {
141
145
  handleClick() {
@@ -10,7 +10,8 @@
10
10
  @close="open = false"
11
11
  >
12
12
  <div
13
- class="flex min-h-screen flex-col items-center justify-center px-4 pt-4 pb-20 text-center"
13
+ class="flex min-h-screen flex-col items-center px-4 pt-4 pb-20 text-center"
14
+ :class="dialogPositionClasses"
14
15
  >
15
16
  <TransitionChild
16
17
  as="template"
@@ -36,7 +37,20 @@
36
37
  leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
37
38
  >
38
39
  <div
39
- class="my-8 inline-block w-full max-w-lg transform overflow-hidden rounded-lg bg-white text-left align-middle shadow-xl transition-all"
40
+ class="my-8 inline-block w-full transform overflow-hidden rounded-lg bg-white text-left align-middle shadow-xl transition-all"
41
+ :class="{
42
+ 'max-w-7xl': options.size === '7xl',
43
+ 'max-w-6xl': options.size === '6xl',
44
+ 'max-w-5xl': options.size === '5xl',
45
+ 'max-w-4xl': options.size === '4xl',
46
+ 'max-w-3xl': options.size === '3xl',
47
+ 'max-w-2xl': options.size === '2xl',
48
+ 'max-w-xl': options.size === 'xl',
49
+ 'max-w-md': options.size === 'md',
50
+ 'max-w-lg': options.size === 'lg' || !options.size,
51
+ 'max-w-sm': options.size === 'sm',
52
+ 'max-w-xs': options.size === 'xs',
53
+ }"
40
54
  >
41
55
  <slot name="body">
42
56
  <slot name="body-main">
@@ -76,7 +90,10 @@
76
90
  </DialogTitle>
77
91
 
78
92
  <slot name="body-content">
79
- <p class="text-sm text-gray-600" v-if="options.message">
93
+ <p
94
+ class="text-base text-gray-600"
95
+ v-if="options.message"
96
+ >
80
97
  {{ options.message }}
81
98
  </p>
82
99
  </slot>
@@ -118,7 +135,8 @@ import {
118
135
  TransitionChild,
119
136
  TransitionRoot,
120
137
  } from '@headlessui/vue'
121
- import { Button, FeatherIcon } from 'pimelon-ui'
138
+ import Button from './Button.vue'
139
+ import FeatherIcon from './FeatherIcon.vue'
122
140
 
123
141
  export default {
124
142
  name: 'Dialog',
@@ -187,6 +205,13 @@ export default {
187
205
  }
188
206
  return icon
189
207
  },
208
+ dialogPositionClasses() {
209
+ let position = this.options?.position || 'center'
210
+ return {
211
+ 'justify-center': position === 'center',
212
+ 'pt-[20vh]': position === 'top',
213
+ }
214
+ },
190
215
  },
191
216
  }
192
217
  </script>
@@ -64,7 +64,7 @@
64
64
 
65
65
  <script>
66
66
  import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
67
- import { FeatherIcon } from 'pimelon-ui'
67
+ import FeatherIcon from './FeatherIcon.vue'
68
68
 
69
69
  export default {
70
70
  name: 'NewDropdown',
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <slot v-bind="{ openDialog, resetAddImage }"></slot>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
3
  <Dialog
4
4
  :options="{ title: 'Add Image' }"
5
5
  v-model="addImageDialog.show"
6
- @after-leave="resetAddImage"
6
+ @after-leave="reset"
7
7
  >
8
8
  <template #body-content>
9
9
  <label
@@ -29,12 +29,14 @@
29
29
  <Button appearance="primary" @click="addImage(addImageDialog.url)">
30
30
  Insert Image
31
31
  </Button>
32
+ <Button @click="reset"> Cancel </Button>
32
33
  </template>
33
34
  </Dialog>
34
35
  </template>
35
36
  <script>
36
37
  import fileToBase64 from '../../utils/file-to-base64'
37
38
  import Dialog from '../Dialog.vue'
39
+ import Button from '../Button.vue'
38
40
 
39
41
  export default {
40
42
  name: 'InsertImage',
@@ -45,7 +47,7 @@ export default {
45
47
  addImageDialog: { url: '', file: null, show: false },
46
48
  }
47
49
  },
48
- components: { Dialog },
50
+ components: { Button, Dialog },
49
51
  methods: {
50
52
  openDialog() {
51
53
  this.addImageDialog.show = true
@@ -62,10 +64,10 @@ export default {
62
64
  },
63
65
  addImage(src) {
64
66
  this.editor.chain().focus().setImage({ src }).run()
65
- this.resetAddImage()
67
+ this.reset()
66
68
  },
67
- resetAddImage() {
68
- this.addImageDialog = { show: false, url: null, file: null }
69
+ reset() {
70
+ this.addImageDialog = this.$options.data().addImageDialog
69
71
  },
70
72
  },
71
73
  }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Set Link' }"
5
+ v-model="setLinkDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <Input
10
+ type="text"
11
+ label="URL"
12
+ v-model="setLinkDialog.url"
13
+ @keydown.enter="(e) => setLink(e.target.value)"
14
+ />
15
+ </template>
16
+ <template #actions>
17
+ <Button appearance="primary" @click="setLink(setLinkDialog.url)">
18
+ Save
19
+ </Button>
20
+ </template>
21
+ </Dialog>
22
+ </template>
23
+ <script>
24
+ import Dialog from '../Dialog.vue'
25
+ import Button from '../Button.vue'
26
+ import Input from '../Input.vue'
27
+
28
+ export default {
29
+ name: 'InsertLink',
30
+ props: ['editor'],
31
+ components: { Button, Input, Dialog },
32
+ data() {
33
+ return {
34
+ setLinkDialog: { url: '', show: false },
35
+ }
36
+ },
37
+ methods: {
38
+ openDialog() {
39
+ let existingURL = this.editor.getAttributes('link').href
40
+ if (existingURL) {
41
+ this.setLinkDialog.url = existingURL
42
+ }
43
+ this.setLinkDialog.show = true
44
+ },
45
+ setLink(url) {
46
+ // empty
47
+ if (url === '') {
48
+ this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
49
+ } else {
50
+ // update link
51
+ this.editor
52
+ .chain()
53
+ .focus()
54
+ .extendMarkRange('link')
55
+ .setLink({ href: url })
56
+ .run()
57
+ }
58
+
59
+ this.setLinkDialog.show = false
60
+ this.setLinkDialog.url = ''
61
+ },
62
+ reset() {
63
+ this.setLinkDialog = this.$options.data().setLinkDialog
64
+ },
65
+ },
66
+ }
67
+ </script>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Add Video' }"
5
+ v-model="addVideoDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <FileUploader
10
+ file-types="video/*"
11
+ @success="(file) => (addVideoDialog.url = file.file_url)"
12
+ >
13
+ <template v-slot="{ file, progress, uploading, openFileSelector }">
14
+ <div class="flex items-center space-x-2">
15
+ <Button @click="openFileSelector">
16
+ {{
17
+ uploading
18
+ ? `Uploading ${progress}%`
19
+ : addVideoDialog.url
20
+ ? 'Change Video'
21
+ : 'Upload Video'
22
+ }}
23
+ </Button>
24
+ <Button
25
+ v-if="addVideoDialog.url"
26
+ @click="
27
+ () => {
28
+ addVideoDialog.url = null
29
+ addVideoDialog.file = null
30
+ }
31
+ "
32
+ >
33
+ Remove
34
+ </Button>
35
+ </div>
36
+ </template>
37
+ </FileUploader>
38
+ <video
39
+ v-if="addVideoDialog.url"
40
+ :src="addVideoDialog.url"
41
+ class="mt-2 w-full rounded-lg"
42
+ type="video/mp4"
43
+ controls
44
+ />
45
+ </template>
46
+ <template #actions>
47
+ <Button appearance="primary" @click="addVideo(addVideoDialog.url)">
48
+ Insert Video
49
+ </Button>
50
+ <Button @click="reset">Cancel</Button>
51
+ </template>
52
+ </Dialog>
53
+ </template>
54
+ <script>
55
+ import Button from '../Button.vue'
56
+ import Dialog from '../Dialog.vue'
57
+ import FileUploader from '../FileUploader.vue'
58
+
59
+ export default {
60
+ name: 'InsertImage',
61
+ props: ['editor'],
62
+ expose: ['openDialog'],
63
+ data() {
64
+ return {
65
+ addVideoDialog: { url: '', file: null, show: false },
66
+ }
67
+ },
68
+ components: { Button, Dialog, FileUploader },
69
+ methods: {
70
+ openDialog() {
71
+ this.addVideoDialog.show = true
72
+ },
73
+ onVideoSelect(e) {
74
+ let file = e.target.files[0]
75
+ if (!file) {
76
+ return
77
+ }
78
+ this.addVideoDialog.file = file
79
+ },
80
+
81
+ addVideo(src) {
82
+ this.editor
83
+ .chain()
84
+ .focus()
85
+ .insertContent(`<video src="${src}"></video>`)
86
+ .run()
87
+ this.reset()
88
+ },
89
+ reset() {
90
+ this.addVideoDialog = this.$options.data().addVideoDialog
91
+ },
92
+ },
93
+ }
94
+ </script>
@@ -38,7 +38,7 @@
38
38
  class="w-full rounded px-2 py-1 text-left text-base hover:bg-gray-50"
39
39
  @click="
40
40
  () => {
41
- onClick(option)
41
+ onButtonClick(option)
42
42
  close()
43
43
  }
44
44
  "
@@ -50,88 +50,47 @@
50
50
  </template>
51
51
  </Popover>
52
52
  </div>
53
- <button
54
- v-else
55
- class="flex rounded p-1 text-gray-800 transition-colors"
56
- :class="button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'"
57
- @click="onClick(button)"
58
- :title="button.label"
59
- >
60
- <component v-if="button.icon" :is="button.icon" class="h-4 w-4" />
61
- <span class="inline-block h-4 min-w-[1rem] text-sm leading-4" v-else>
62
- {{ button.text }}
63
- </span>
64
- </button>
53
+ <component v-else :is="button.component || 'div'" v-bind="{ editor }">
54
+ <template v-slot="componentSlotProps">
55
+ <button
56
+ class="flex rounded p-1 text-gray-800 transition-colors"
57
+ :class="
58
+ button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'
59
+ "
60
+ @click="
61
+ componentSlotProps?.onClick
62
+ ? componentSlotProps.onClick(button)
63
+ : onButtonClick(button)
64
+ "
65
+ :title="button.label"
66
+ >
67
+ <component v-if="button.icon" :is="button.icon" class="h-4 w-4" />
68
+ <span
69
+ class="inline-block h-4 min-w-[1rem] text-sm leading-4"
70
+ v-else
71
+ >
72
+ {{ button.text }}
73
+ </span>
74
+ </button>
75
+ </template>
76
+ </component>
65
77
  </template>
66
78
  </div>
67
-
68
- <Dialog :options="{ title: 'Set Link' }" v-model="setLinkDialog.show">
69
- <template #body-content>
70
- <Input
71
- type="text"
72
- label="URL"
73
- v-model="setLinkDialog.url"
74
- @keydown.enter="(e) => setLink(e.target.value)"
75
- />
76
- </template>
77
- <template #actions>
78
- <Button appearance="primary" @click="setLink(setLinkDialog.url)">
79
- Save
80
- </Button>
81
- </template>
82
- </Dialog>
83
- <InsertImage ref="insertImage" :editor="editor" />
84
79
  </div>
85
80
  </template>
86
81
  <script>
87
- import { Popover, Dialog, Input, Button } from 'pimelon-ui'
88
- import InsertImage from './InsertImage.vue'
82
+ import Popover from '../Popover.vue'
83
+
89
84
  export default {
90
85
  name: 'TipTapMenu',
91
86
  props: ['buttons'],
92
87
  inject: ['editor'],
93
88
  components: {
94
89
  Popover,
95
- Dialog,
96
- Input,
97
- Button,
98
- InsertImage,
99
- },
100
- data() {
101
- return {
102
- setLinkDialog: { url: '', show: false },
103
- }
104
90
  },
105
91
  methods: {
106
- onClick(button) {
107
- if (button.label === 'Link') {
108
- this.setLinkDialog.show = true
109
- let existingURL = this.editor.getAttributes('link').href
110
- if (existingURL) {
111
- this.setLinkDialog.url = existingURL
112
- }
113
- } else if (button.label === 'Image') {
114
- this.$refs.insertImage.openDialog()
115
- } else {
116
- button.action(this.editor)
117
- }
118
- },
119
- setLink(url) {
120
- // empty
121
- if (url === '') {
122
- this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
123
- } else {
124
- // update link
125
- this.editor
126
- .chain()
127
- .focus()
128
- .extendMarkRange('link')
129
- .setLink({ href: url })
130
- .run()
131
- }
132
-
133
- this.setLinkDialog.show = false
134
- this.setLinkDialog.url = ''
92
+ onButtonClick(button) {
93
+ button.action(this.editor)
135
94
  },
136
95
  },
137
96
  }
@@ -24,11 +24,14 @@ import TableCell from '@tiptap/extension-table-cell'
24
24
  import TableHeader from '@tiptap/extension-table-header'
25
25
  import TableRow from '@tiptap/extension-table-row'
26
26
  import Image from './image-extension'
27
+ import Video from './video-extension'
27
28
  import Link from '@tiptap/extension-link'
28
29
  import configureMention from './mention'
29
30
  import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
30
31
  import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
31
32
  import TextEditorFloatingMenu from './TextEditorFloatingMenu.vue'
33
+ import { detectMarkdown, markdownToHTML } from '../../utils/markdown'
34
+ import { DOMParser } from 'prosemirror-model'
32
35
 
33
36
  export default {
34
37
  name: 'TextEditor',
@@ -133,6 +136,7 @@ export default {
133
136
  types: ['heading', 'paragraph'],
134
137
  }),
135
138
  Image,
139
+ Video,
136
140
  Link,
137
141
  Placeholder.configure({
138
142
  showOnlyWhenEditable: false,
@@ -161,6 +165,26 @@ export default {
161
165
  this.editorClass,
162
166
  ]),
163
167
  },
168
+ clipboardTextParser: (text, $context) => {
169
+ if (!detectMarkdown(text)) return
170
+ if (
171
+ !confirm(
172
+ 'Do you want to convert markdown content to HTML before pasting?'
173
+ )
174
+ )
175
+ return
176
+
177
+ let dom = document.createElement('div')
178
+ dom.innerHTML = markdownToHTML(text)
179
+ let parser =
180
+ this.editor.view.someProp('clipboardParser') ||
181
+ this.editor.view.someProp('domParser') ||
182
+ DOMParser.fromSchema(this.editor.schema)
183
+ return parser.parseSlice(dom, {
184
+ preserveWhitespace: true,
185
+ context: $context,
186
+ })
187
+ },
164
188
  }
165
189
  },
166
190
  },
@@ -187,6 +211,11 @@ export default {
187
211
  height: 0;
188
212
  }
189
213
 
214
+ .ProseMirror-selectednode video,
215
+ img.ProseMirror-selectednode {
216
+ border: 2px solid theme('colors.blue.300');
217
+ }
218
+
190
219
  /* Mentions */
191
220
  .mention {
192
221
  font-weight: 600;
@@ -42,6 +42,7 @@ export default {
42
42
  'Numbered List',
43
43
  'Separator',
44
44
  'Image',
45
+ 'Video',
45
46
  'Blockquote',
46
47
  'Code',
47
48
  [
@@ -39,6 +39,7 @@ export default {
39
39
  'Align Right',
40
40
  'Separator',
41
41
  'Image',
42
+ 'Video',
42
43
  'Link',
43
44
  'Blockquote',
44
45
  'Code',
@@ -1,3 +1,4 @@
1
+ import { defineAsyncComponent } from 'vue'
1
2
  import H1 from './icons/h-1.vue'
2
3
  import H2 from './icons/h-2.vue'
3
4
  import H3 from './icons/h-3.vue'
@@ -17,6 +18,7 @@ import DoubleQuotes from './icons/double-quotes-r.vue'
17
18
  import CodeView from './icons/code-view.vue'
18
19
  import Link from './icons/link.vue'
19
20
  import Image from './icons/image-add-line.vue'
21
+ import Video from './icons/video-add-line.vue'
20
22
  import ArrowGoBack from './icons/arrow-go-back-line.vue'
21
23
  import ArrowGoForward from './icons/arrow-go-forward-line.vue'
22
24
  import Separator from './icons/separator.vue'
@@ -147,11 +149,19 @@ export default {
147
149
  label: 'Link',
148
150
  icon: Link,
149
151
  isActive: (editor) => editor.isActive('link'),
152
+ component: defineAsyncComponent(() => import('./InsertLink.vue')),
150
153
  },
151
154
  Image: {
152
155
  label: 'Image',
153
156
  icon: Image,
154
157
  isActive: (editor) => false,
158
+ component: defineAsyncComponent(() => import('./InsertImage.vue')),
159
+ },
160
+ Video: {
161
+ label: 'Video',
162
+ icon: Video,
163
+ isActive: (editor) => false,
164
+ component: defineAsyncComponent(() => import('./InsertVideo.vue')),
155
165
  },
156
166
  Undo: {
157
167
  label: 'Undo',
@@ -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="M16 4c.552 0 1 .448 1 1v4.2l5.213-3.65c.226-.158.538-.103.697.124.058.084.09.184.09.286v12.08c0 .276-.224.5-.5.5-.103 0-.203-.032-.287-.09L17 14.8V19c0 .552-.448 1-1 1H2c-.552 0-1-.448-1-1V5c0-.552.448-1 1-1h14zm-1 2H3v12h12V6zM8 8h2v3h3v2H9.999L10 16H8l-.001-3H5v-2h3V8zm13 .841l-4 2.8v.718l4 2.8V8.84z"
11
+ fill="currentColor"
12
+ />
13
+ </svg>
14
+ </template>
@@ -1 +1,5 @@
1
1
  export { default } from './TextEditor.vue'
2
+ export { default as TextEditor } from './TextEditor.vue'
3
+ export { default as TextEditorBubbleMenu } from './TextEditorBubbleMenu.vue'
4
+ export { default as TextEditorFixedMenu } from './TextEditorFixedMenu.vue'
5
+ export { default as TextEditorFloatingMenu } from './TextEditorFloatingMenu.vue'
@@ -0,0 +1,60 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core'
2
+ // Inspired by this blog: https://www.codemzy.com/blog/tiptap-video-embed-extension
3
+
4
+ const Video = Node.create({
5
+ name: 'video',
6
+ group: 'block',
7
+ selectable: true,
8
+ draggable: true,
9
+ atom: true,
10
+
11
+ addAttributes() {
12
+ return {
13
+ src: {
14
+ default: null,
15
+ },
16
+ }
17
+ },
18
+
19
+ parseHTML() {
20
+ return [
21
+ {
22
+ tag: 'video',
23
+ },
24
+ ]
25
+ },
26
+
27
+ renderHTML({ HTMLAttributes }) {
28
+ return ['video', mergeAttributes(HTMLAttributes)]
29
+ },
30
+
31
+ addNodeView() {
32
+ return ({ editor, node }) => {
33
+ const div = document.createElement('div')
34
+ div.className =
35
+ 'relative aspect-w-16 aspect-h-9' +
36
+ (editor.isEditable ? ' cursor-pointer' : '')
37
+
38
+ const video = document.createElement('video')
39
+ if (editor.isEditable) {
40
+ video.className = 'pointer-events-none'
41
+ }
42
+ video.src = node.attrs.src
43
+ if (!editor.isEditable) {
44
+ video.setAttribute('controls', '')
45
+ } else {
46
+ let videoPill = document.createElement('div')
47
+ videoPill.className =
48
+ 'absolute top-0 right-0 text-xs m-2 bg-gray-800 text-white px-2 py-1 rounded-md'
49
+ videoPill.innerHTML = 'Video'
50
+ div.append(videoPill)
51
+ }
52
+ div.append(video)
53
+ return {
54
+ dom: div,
55
+ }
56
+ }
57
+ },
58
+ })
59
+
60
+ export default Video
@@ -47,7 +47,7 @@
47
47
  </teleport>
48
48
  </template>
49
49
  <script>
50
- import { FeatherIcon } from 'pimelon-ui'
50
+ import FeatherIcon from './FeatherIcon.vue'
51
51
  const positions = [
52
52
  'top-right',
53
53
  'top-center',
@@ -40,6 +40,8 @@ export function createListResource(options, vm, getResource) {
40
40
  fields: out.fields,
41
41
  filters: out.filters,
42
42
  order_by: out.order_by,
43
+ start: out.start,
44
+ limit: out.limit,
43
45
  limit_start: out.start,
44
46
  limit_page_length: out.limit,
45
47
  parent: out.parent,
@@ -77,18 +79,9 @@ export function createListResource(options, vm, getResource) {
77
79
  onSuccess(data) {
78
80
  if (data.length > 0 && out.originalData) {
79
81
  let doc = data[0]
80
- let index = out.originalData.findIndex((d) => d.name === doc.name)
81
- out.originalData = out.originalData.filter(
82
- (d) => d.name !== doc.name
83
- )
84
- out.originalData = [
85
- out.originalData.slice(0, index),
86
- data,
87
- out.originalData.slice(index),
88
- ].flat()
82
+ updateRowInListResource(out.doctype, doc)
89
83
  }
90
84
 
91
- out.data = transform(out.originalData)
92
85
  options.fetchOne?.onSuccess?.call(vm, out.data)
93
86
  },
94
87
  onError: options.fetchOne?.onError,
@@ -125,9 +118,9 @@ export function createListResource(options, vm, getResource) {
125
118
  fieldname: values,
126
119
  }
127
120
  },
128
- onSuccess(data) {
129
- out.list.fetch()
130
- options.setValue?.onSuccess?.call(vm, data)
121
+ onSuccess(doc) {
122
+ updateRowInListResource(out.doctype, doc)
123
+ options.setValue?.onSuccess?.call(vm, doc)
131
124
  },
132
125
  onError: options.setValue?.onError,
133
126
  },
@@ -189,7 +182,7 @@ export function createListResource(options, vm, getResource) {
189
182
  function transform(data) {
190
183
  if (options.transform) {
191
184
  let returnValue = options.transform.call(vm, data)
192
- if (typeof returnValue != null) {
185
+ if (returnValue != null) {
193
186
  return returnValue
194
187
  }
195
188
  }
@@ -256,6 +249,7 @@ export function getCachedListResource(cacheKey) {
256
249
  }
257
250
 
258
251
  export function updateRowInListResource(doctype, doc) {
252
+ if (!doc.name) return
259
253
  let resources = resourcesByDocType[doctype] || []
260
254
  for (let resource of resources) {
261
255
  if (resource.originalData) {
@@ -31,6 +31,7 @@ export function createResource(options, vm, getResource) {
31
31
  loading: false,
32
32
  fetched: false,
33
33
  error: null,
34
+ promise: null,
34
35
  auto: options.auto,
35
36
  params: null,
36
37
  fetch: fetchFunction,
@@ -85,7 +86,8 @@ export function createResource(options, vm, getResource) {
85
86
  }
86
87
 
87
88
  try {
88
- let data = await resourceFetcher(options.method, params || options.params)
89
+ out.promise = resourceFetcher(options.method, params || options.params)
90
+ let data = await out.promise
89
91
  saveLocal(cacheKey, data)
90
92
  out.data = transform(data)
91
93
  out.fetched = true
@@ -155,7 +157,7 @@ export function createResource(options, vm, getResource) {
155
157
  function transform(data) {
156
158
  if (options.transform) {
157
159
  let returnValue = options.transform.call(vm, data)
158
- if (typeof returnValue != null) {
160
+ if (returnValue != null) {
159
161
  return returnValue
160
162
  }
161
163
  }
@@ -0,0 +1,29 @@
1
+ import showdown from 'showdown'
2
+
3
+ export function markdownToHTML(text) {
4
+ const converter = new showdown.Converter()
5
+ return converter.makeHtml(text)
6
+ }
7
+
8
+ export function htmlToMarkdown(text) {
9
+ const converter = new showdown.Converter()
10
+ return converter.makeMarkdown(text)
11
+ }
12
+
13
+ export function detectMarkdown(text) {
14
+ const lines = text.split('\n')
15
+ const markdown = lines.filter(
16
+ (line) =>
17
+ line.startsWith('![') ||
18
+ line.startsWith('#') ||
19
+ line.startsWith('> ') ||
20
+ line.startsWith('*') ||
21
+ line.startsWith('- ') ||
22
+ line.startsWith('1. ') ||
23
+ line.startsWith('```') ||
24
+ line.startsWith('`') ||
25
+ line.startsWith('[') ||
26
+ line.startsWith('---')
27
+ )
28
+ return markdown.length > 0
29
+ }
@@ -71,6 +71,13 @@ module.exports = {
71
71
  50: '#F8F8FC',
72
72
  },
73
73
  },
74
+ typography: (theme) => ({
75
+ gray: {
76
+ css: {
77
+ '--tw-prose-body': theme('colors.gray.900'),
78
+ },
79
+ },
80
+ }),
74
81
  },
75
82
  container: {
76
83
  padding: {