frappe-ui 0.1.262 → 0.1.263

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.
@@ -68,7 +68,7 @@ export const usersWithAccess = createResource({
68
68
  })
69
69
 
70
70
  export const updateAccess = createResource({
71
- url: 'drive.api.files.call_controller_method',
71
+ url: 'drive.api.files.update_access',
72
72
  makeParams: (params) => ({ ...params, method: params.method || 'share' }),
73
73
  onError: (error) => toast.error(error.messages[0]),
74
74
  })
@@ -96,11 +96,10 @@ export const getTeam = createResource({
96
96
  })
97
97
 
98
98
  export const rename = createResource({
99
- url: 'drive.api.files.call_controller_method',
99
+ url: 'drive.api.files.rename',
100
100
  method: 'POST',
101
101
  makeParams: (data) => {
102
102
  return {
103
- method: 'rename',
104
103
  ...data,
105
104
  }
106
105
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.262",
3
+ "version": "0.1.263",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="inline-flex space-x-2 rounded transition"
3
+ class="inline-flex gap-2 rounded transition"
4
4
  :class="{
5
5
  'px-2.5 py-1.5': padding && size === 'sm',
6
6
  'px-3 py-2': padding && size === 'md',
@@ -26,7 +26,7 @@
26
26
  >
27
27
  <div class="flex flex-1 items-center gap-2">
28
28
  <div
29
- class="w-13 flex-shrink-0 pl-2 text-end text-base text-ink-gray-5"
29
+ class="w-13 flex-shrink-0 ps-2 text-end text-base text-ink-gray-5"
30
30
  >
31
31
  {{ i == 0 ? 'Where' : 'And' }}
32
32
  </div>
@@ -2,7 +2,7 @@
2
2
  <div class="flex items-center">
3
3
  <button
4
4
  @click="toggleGroup"
5
- class="ml-[3px] mr-[11px] rounded p-1 hover:bg-surface-gray-2"
5
+ class="ms-[3px] me-[11px] rounded p-1 hover:bg-surface-gray-2"
6
6
  >
7
7
  <DownSolid
8
8
  class="h-4 w-4 text-ink-gray-6 transition-transform duration-200"
@@ -10,7 +10,7 @@
10
10
  />
11
11
  </button>
12
12
  <slot>
13
- <div class="w-full py-1.5 pr-2">
13
+ <div class="w-full py-1.5 pe-2">
14
14
  <component
15
15
  v-if="list.slots['group-header']"
16
16
  :is="list.slots['group-header']"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
3
+ class="mb-2 grid items-center gap-4 rounded bg-surface-gray-2 p-2"
4
4
  :style="{
5
5
  gridTemplateColumns: getGridTemplateColumns(
6
6
  list.columns,
@@ -5,7 +5,7 @@
5
5
  :class="item.align ? alignmentMap[item.align] : 'justify-between'"
6
6
  >
7
7
  <div
8
- class="flex items-center space-x-2 truncate text-sm text-ink-gray-5"
8
+ class="flex items-center gap-2 truncate text-sm text-ink-gray-5"
9
9
  :class="$attrs.class"
10
10
  >
11
11
  <slot name="prefix" v-bind="{ item }" />
@@ -22,7 +22,7 @@
22
22
  class="[all:unset] hover:[all:unset]"
23
23
  >
24
24
  <div
25
- class="grid items-center space-x-4 px-2"
25
+ class="grid items-center gap-4 px-2"
26
26
  :style="{
27
27
  height: rowHeight,
28
28
  gridTemplateColumns: getGridTemplateColumns(
@@ -33,7 +33,7 @@
33
33
  >
34
34
  <div
35
35
  v-if="list.options.selectable"
36
- class="w-fit pr-2 py-3 flex"
36
+ class="w-fit pe-2 py-3 flex"
37
37
  @click.stop.prevent
38
38
  @dblclick.stop
39
39
  >
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex items-center space-x-2" :class="alignmentMap[align]">
2
+ <div class="flex items-center gap-2" :class="alignmentMap[align]">
3
3
  <slot name="prefix">
4
4
  <component
5
5
  v-if="column.prefix"
@@ -12,7 +12,7 @@
12
12
  class="absolute inset-x-0 bottom-6 mx-auto w-max text-base"
13
13
  >
14
14
  <div
15
- class="flex min-w-[596px] items-center space-x-3 rounded-lg bg-surface-white px-4 py-2 shadow-2xl"
15
+ class="flex min-w-[596px] items-center gap-3 rounded-lg bg-surface-white px-4 py-2 shadow-2xl"
16
16
  :class="$attrs.class"
17
17
  >
18
18
  <slot
@@ -26,7 +26,7 @@
26
26
  <div
27
27
  class="flex flex-1 justify-between border-r border-outline-gray-2 text-ink-gray-9"
28
28
  >
29
- <div class="flex items-center space-x-3">
29
+ <div class="flex items-center gap-3">
30
30
  <Checkbox
31
31
  :modelValue="true"
32
32
  :disabled="true"
@@ -34,7 +34,7 @@
34
34
  />
35
35
  <div>{{ selectedText }}</div>
36
36
  </div>
37
- <div class="mr-3">
37
+ <div class="me-3">
38
38
  <slot
39
39
  name="actions"
40
40
  v-bind="{
@@ -46,7 +46,7 @@
46
46
  />
47
47
  </div>
48
48
  </div>
49
- <div class="flex items-center space-x-1">
49
+ <div class="flex items-center gap-1">
50
50
  <Button
51
51
  class="w- text-ink-gray-7"
52
52
  :disabled="list.allRowsSelected"
@@ -16,7 +16,7 @@ const model = defineModel<string | number>({ default: 0 })
16
16
  const indicatorXCss = `left-0 bottom-0 h-[2px] w-[--reka-tabs-indicator-size] transition-[width,transform]
17
17
  translate-x-[--reka-tabs-indicator-position] translate-y-[1px]`
18
18
 
19
- const indicatorYCss = `right-0 top-0 w-[2px] h-[--reka-tabs-indicator-size]
19
+ const indicatorYCss = `end-0 top-0 w-[2px] h-[--reka-tabs-indicator-size]
20
20
  translate-y-[--reka-tabs-indicator-position] transition-[height,transform]`
21
21
 
22
22
  // Using a plain <button> element via `h('button')` to avoid picking up
@@ -41,7 +41,7 @@ defineSlots<{
41
41
  v-model="model"
42
42
  >
43
43
  <TabsList
44
- class="relative min-h-fit flex data-[orientation=vertical]:flex-col p-1 border-b data-[orientation=vertical]:border-r gap-5"
44
+ class="relative min-h-fit flex data-[orientation=vertical]:flex-col p-1 border-b data-[orientation=vertical]:border-e gap-5"
45
45
  :class="{
46
46
  'overflow-x-auto overflow-y-hidden px-5': !props.vertical,
47
47
  'py-3': props.vertical,
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
- import { ref, onMounted, onUnmounted, computed } from 'vue'
2
+ import { ref, onMounted, onUnmounted, computed, h } from 'vue'
3
3
  import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3'
4
4
  import LoadingIndicator from '../../LoadingIndicator.vue'
5
5
  import Tooltip from '../../Tooltip/Tooltip.vue'
6
+ import { localFileMap } from '../extensions/image/image-extension'
6
7
  import { ErrorMessage } from '../../ErrorMessage'
7
8
  import LucideAlignLeft from '~icons/lucide/align-left'
8
9
  import LucideAlignCenter from '~icons/lucide/align-center'
@@ -13,6 +14,7 @@ import LucideFloatRight from '~icons/lucide/align-horizontal-justify-end'
13
14
  import LucideNoFloat from '~icons/lucide/align-vertical-space-around'
14
15
  import LucideCaptions from '~icons/lucide/captions'
15
16
  import LucideMoveDiagonal2 from '~icons/lucide/move-diagonal-2'
17
+ import LucideRotateCw from '~icons/lucide/rotate-cw'
16
18
 
17
19
  const props = defineProps(nodeViewProps)
18
20
 
@@ -30,6 +32,8 @@ const floatButtonRef = ref<HTMLElement | null>(null)
30
32
 
31
33
  const showCaption = ref(props.node.attrs.alt ? true : false)
32
34
  const isVideo = computed(() => props.node.type.name === 'video')
35
+ const isUploaded = computed(() => Boolean(props.node.attrs.src))
36
+ const fileContent = computed(() => localFileMap.get(props.node.attrs.uploadId)?.b64)
33
37
 
34
38
  const currentAlignIcon = computed(() => {
35
39
  return (
@@ -273,14 +277,16 @@ const wrapperClasses = (float: string) => [
273
277
  ]" :style="{
274
278
  width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
275
279
  }">
276
- <div v-if="node.attrs.src" class="relative">
277
- <img v-if="!isVideo" ref="mediaRef" class="rounded-[2px]" :src="node.attrs.src" :alt="node.attrs.alt || ''"
278
- :width="node.attrs.width" :height="node.attrs.height" @click.stop="selectMedia" @load="handleMediaLoaded" />
279
- <video v-else ref="mediaRef" class="rounded-[2px]" :src="node.attrs.src" :width="node.attrs.width"
280
- :height="node.attrs.height" :autoplay="node.attrs.autoplay" :loop="node.attrs.loop" :muted="node.attrs.muted"
281
- controls @click.stop="selectMedia" @loadedmetadata="handleMediaLoaded" />
282
-
283
- <div class="absolute top-2 right-2 items-center bg-black/65 px-1.5 py-1 gap-2 rounded group-hover:flex"
280
+ <div v-if="isUploaded || fileContent" class="relative">
281
+ <img v-if="!isVideo" ref="mediaRef" class="rounded-[2px]" :class="!isUploaded && 'opacity-40'" :src="node.attrs.src || fileContent"
282
+ :alt="node.attrs.alt || ''" :width="node.attrs.width" :height="node.attrs.height"
283
+ @click.stop="selectMedia" @load="handleMediaLoaded" />
284
+ <video v-else ref="mediaRef" class="rounded-[2px]" :class="!isUploaded && 'opacity-40'" :src="node.attrs.src || fileContent"
285
+ :width="node.attrs.width" :height="node.attrs.height" :autoplay="node.attrs.autoplay"
286
+ :loop="node.attrs.loop" :muted="node.attrs.muted" :controls="isUploaded" @click.stop="selectMedia"
287
+ @loadedmetadata="handleMediaLoaded" />
288
+
289
+ <div v-if="isUploaded" class="absolute top-2 right-2 items-center bg-black/65 px-1.5 py-1 gap-2 rounded group-hover:flex"
284
290
  :class="selected && isEditable ? 'flex' : 'hidden'">
285
291
  <button>
286
292
  <LucideCaptions @click="toggleCaptions" class="size-4"
@@ -350,7 +356,16 @@ const wrapperClasses = (float: string) => [
350
356
  </div>
351
357
  </div>
352
358
 
353
- <button v-if="selected && isEditable" class="absolute bottom-2 right-2 cursor-nw-resize bg-black/65 rounded p-1"
359
+ <Button
360
+ v-else
361
+ variant="solid"
362
+ class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
363
+ :icon-left="h(LucideRotateCw, { class: 'size-4' })"
364
+ label="Try again"
365
+ @click="isVideo ? editor.commands.reuploadVideo(node.attrs.uploadId) : editor.commands.reuploadImage(node.attrs.uploadId)"
366
+ />
367
+
368
+ <button v-if="selected && isEditable && isUploaded" class="absolute bottom-2 right-2 cursor-nw-resize bg-black/65 rounded p-1"
354
369
  @mousedown.prevent="startResize">
355
370
  <LucideMoveDiagonal2 class="text-white size-4" />
356
371
  </button>
@@ -364,14 +379,18 @@ const wrapperClasses = (float: string) => [
364
379
  </div>
365
380
  </div>
366
381
  </div>
382
+ <div v-else class="flex flex-col items-center justify-center gap-2 border rounded text-ink-gray-6 text-sm py-5 max-w-full" :class="{ 'border-none': selected }" :style="{ width: node.attrs.width + 'px', aspectRatio: node.attrs.width && node.attrs.height ? `${node.attrs.width} / ${node.attrs.height}` : undefined,}">
383
+ <div class="text-ink-gray-8 text-base">This {{ isVideo ? 'video' : 'image' }} hasn't yet been uploaded.</div>
384
+ <div v-if="node.attrs.error" class="text-sm text-ink-red-4">Upload failed: {{ node.attrs.error }}</div>
385
+ </div>
367
386
 
368
387
  <input v-if="(node.attrs.alt || showCaption) && !node.attrs.error" v-model="caption"
369
388
  class="w-full text-center bg-transparent text-sm text-ink-gray-6 h-7 border-none focus:ring-0 placeholder-ink-gray-4"
370
389
  placeholder="Add caption" :disabled="!isEditable" @change="updateCaption" @keydown="handleKeydown" />
371
390
 
372
- <div v-if="node.attrs.error" class="w-full py-1.5">
373
- <ErrorMessage :message="`Upload Failed: ${node.attrs.error}`" />
391
+ <div v-if="node.attrs.error && fileContent" class="w-full py-1.5 text-center">
392
+ <ErrorMessage :message="`Upload failed: ${node.attrs.error}`" />
374
393
  </div>
375
394
  </div>
376
395
  </NodeViewWrapper>
377
- </template>
396
+ </template>
@@ -11,6 +11,8 @@ import { Node } from '@tiptap/pm/model'
11
11
  import { fileToBase64 } from '../../../../index'
12
12
  import { UploadedFile } from '../../../../utils/useFileUpload'
13
13
 
14
+ export const localFileMap = new Map()
15
+
14
16
  export interface ImageExtensionOptions {
15
17
  /**
16
18
  * Function to handle image uploads
@@ -58,6 +60,10 @@ declare module '@tiptap/core' {
58
60
  * Set image float for text wrapping
59
61
  */
60
62
  setImageFloat: (float: 'left' | 'right' | null) => ReturnType
63
+ /**
64
+ * Re-upload a failed image using the file stored in localFileMap
65
+ */
66
+ reuploadImage: (uploadId: string) => ReturnType
61
67
  }
62
68
  }
63
69
  }
@@ -218,6 +224,45 @@ export const ImageExtension = NodeExtension.create<ImageExtensionOptions>({
218
224
  input.click()
219
225
  return true
220
226
  },
227
+
228
+ reuploadImage:
229
+ (uploadId: string) =>
230
+ ({ editor }) => {
231
+ const fileData = localFileMap.get(uploadId)
232
+ if (!fileData) {
233
+ console.error('reuploadImage: no file with uploadId', uploadId)
234
+ return false
235
+ }
236
+
237
+ // Find the node position
238
+ let nodePos: number | null = null
239
+ editor.view.state.doc.descendants((node, pos) => {
240
+ if (
241
+ node.type.name === 'image' &&
242
+ node.attrs.uploadId === uploadId
243
+ ) {
244
+ nodePos = pos
245
+ return false
246
+ }
247
+ })
248
+
249
+ if (nodePos === null) {
250
+ console.error(
251
+ 'reuploadImage: could not find node with uploadId',
252
+ uploadId,
253
+ )
254
+ return false
255
+ }
256
+
257
+ // Re-run the upload using the stored file, replacing the node at its position
258
+ return uploadImageBase(
259
+ fileData.file,
260
+ editor.view,
261
+ nodePos,
262
+ this.options,
263
+ 'replace',
264
+ )
265
+ },
221
266
  }
222
267
  },
223
268
 
@@ -369,6 +414,7 @@ function findInsertPosition(
369
414
  }
370
415
 
371
416
  // Base upload function shared by all image upload methods
417
+ type ImageDimensions = { width: number | null; height: number | null }
372
418
  function uploadImageBase(
373
419
  file: File,
374
420
  view: EditorView,
@@ -387,10 +433,19 @@ function uploadImageBase(
387
433
 
388
434
  fileToBase64(file)
389
435
  .then((base64Result: string) => {
436
+ localFileMap.set(uploadId, { b64: base64Result, file })
437
+
438
+ return getImageDimensions(base64Result)
439
+ .catch(() => ({ width: null, height: null }))
440
+ .then((dimensions) => dimensions)
441
+ })
442
+ .then((dimensions: ImageDimensions) => {
390
443
  const node = view.state.schema.nodes.image.create({
391
444
  loading: true,
392
445
  uploadId,
393
- src: base64Result,
446
+ src: null,
447
+ width: dimensions.width,
448
+ height: dimensions.height,
394
449
  })
395
450
 
396
451
  const tr = view.state.tr
@@ -438,22 +493,6 @@ function uploadImageBase(
438
493
 
439
494
  return options.uploadFunction(file)
440
495
  })
441
- .then((uploadedImage: UploadedFile) => {
442
- return getImageDimensions(uploadedImage.file_url)
443
- .then((dimensions) => {
444
- return {
445
- ...uploadedImage,
446
- width: dimensions.width,
447
- height: dimensions.height,
448
- } as UploadedFile & { width: number; height: number }
449
- })
450
- .catch(() => {
451
- return uploadedImage as UploadedFile & {
452
- width: number
453
- height: number
454
- }
455
- })
456
- })
457
496
  .then((uploadedImage) => {
458
497
  const transaction = view.state.tr
459
498
 
@@ -466,6 +505,7 @@ function uploadImageBase(
466
505
  height: uploadedImage.height || node.attrs.height,
467
506
  loading: false,
468
507
  })
508
+ localFileMap.delete(node.attrs.uploadId)
469
509
  return false
470
510
  }
471
511
  })
@@ -10,6 +10,7 @@ import { EditorView } from '@tiptap/pm/view'
10
10
  import { Node } from '@tiptap/pm/model'
11
11
  import { fileToBase64 } from '../../../index'
12
12
  import { UploadedFile } from '../../../utils/useFileUpload'
13
+ import { localFileMap } from './image/image-extension'
13
14
 
14
15
  export interface VideoExtensionOptions {
15
16
  /**
@@ -58,6 +59,11 @@ declare module '@tiptap/core' {
58
59
  * Set video floating
59
60
  */
60
61
  setVideoFloat: (float: 'left' | 'right' | null) => ReturnType
62
+
63
+ /**
64
+ * Re-upload a failed video using the file stored in localFileMap
65
+ */
66
+ reuploadVideo: (uploadId: string) => ReturnType
61
67
  }
62
68
  }
63
69
  }
@@ -206,6 +212,48 @@ export const VideoExtension = NodeExtension.create<VideoExtensionOptions>({
206
212
  input.click()
207
213
  return true
208
214
  },
215
+
216
+ reuploadVideo:
217
+ (uploadId: string) =>
218
+ ({ editor }) => {
219
+ const fileData = localFileMap.get(uploadId)
220
+ if (!fileData) {
221
+ console.error(
222
+ 'reuploadVideo: no file found in localFileMap for uploadId',
223
+ uploadId,
224
+ )
225
+ return false
226
+ }
227
+
228
+ // Find the node position
229
+ let nodePos: number | null = null
230
+ editor.view.state.doc.descendants((node, pos) => {
231
+ if (
232
+ node.type.name === 'video' &&
233
+ node.attrs.uploadId === uploadId
234
+ ) {
235
+ nodePos = pos
236
+ return false
237
+ }
238
+ })
239
+
240
+ if (nodePos === null) {
241
+ console.error(
242
+ 'reuploadVideo: could not find node with uploadId',
243
+ uploadId,
244
+ )
245
+ return false
246
+ }
247
+
248
+ // Re-run the upload using the stored file, replacing the node at its position
249
+ return uploadVideoBase(
250
+ fileData.file,
251
+ editor.view,
252
+ nodePos,
253
+ this.options,
254
+ 'replace',
255
+ )
256
+ },
209
257
  }
210
258
  },
211
259
 
@@ -353,6 +401,8 @@ function findInsertPosition(
353
401
  }
354
402
 
355
403
  // Base upload function shared by all video upload methods
404
+ type VideoDimensions = { width: number | null; height: number | null }
405
+
356
406
  function uploadVideoBase(
357
407
  file: File,
358
408
  view: EditorView,
@@ -371,10 +421,23 @@ function uploadVideoBase(
371
421
 
372
422
  fileToBase64(file)
373
423
  .then((base64Result: string) => {
424
+ localFileMap.set(uploadId, { b64: base64Result, file })
425
+
426
+ const objectUrl = URL.createObjectURL(file)
427
+ return getVideoDimensions(objectUrl)
428
+ .catch((): VideoDimensions => ({ width: null, height: null }))
429
+ .then((dimensions: VideoDimensions) => {
430
+ URL.revokeObjectURL(objectUrl)
431
+ return dimensions
432
+ })
433
+ })
434
+ .then((dimensions: VideoDimensions) => {
374
435
  const node = view.state.schema.nodes.video.create({
375
436
  loading: true,
376
437
  uploadId,
377
- src: base64Result,
438
+ src: null,
439
+ width: dimensions.width,
440
+ height: dimensions.height,
378
441
  })
379
442
 
380
443
  const tr = view.state.tr
@@ -422,22 +485,6 @@ function uploadVideoBase(
422
485
 
423
486
  return options.uploadFunction(file)
424
487
  })
425
- .then((uploadedVideo: UploadedFile) => {
426
- return getVideoDimensions(uploadedVideo.file_url)
427
- .then((dimensions) => {
428
- return {
429
- ...uploadedVideo,
430
- width: dimensions.width,
431
- height: dimensions.height,
432
- } as UploadedFile & { width: number; height: number }
433
- })
434
- .catch(() => {
435
- return uploadedVideo as UploadedFile & {
436
- width: number
437
- height: number
438
- }
439
- })
440
- })
441
488
  .then((uploadedVideo) => {
442
489
  const transaction = view.state.tr
443
490
 
@@ -450,6 +497,7 @@ function uploadVideoBase(
450
497
  height: uploadedVideo.height || node.attrs.height,
451
498
  loading: false,
452
499
  })
500
+ localFileMap.delete(node.attrs.uploadId)
453
501
  return false
454
502
  }
455
503
  })
@@ -466,6 +514,7 @@ function uploadVideoBase(
466
514
 
467
515
  view.state.doc.descendants((node, pos) => {
468
516
  if (node.type.name === 'video' && node.attrs.uploadId === uploadId) {
517
+ // width/height are preserved from ...node.attrs (pre-fetched from local file)
469
518
  transaction.setNodeMarkup(pos, undefined, {
470
519
  ...node.attrs,
471
520
  loading: false,
@@ -541,7 +590,7 @@ function updateNodeWithDimensions(
541
590
  }
542
591
  })
543
592
  .catch((error) => {
544
- console.error('Could not upload video', error)
593
+ console.error('Could not get video dimensions', error)
545
594
  })
546
595
  }
547
596
 
@@ -98,6 +98,7 @@ export function createResource(options, vm) {
98
98
  try {
99
99
  out.promise = resourceFetcher({
100
100
  ...options,
101
+ onError: undefined,
101
102
  params: params || options.params,
102
103
  })
103
104
  let data = await out.promise