@xuda.io/xuda-widget-plugin-xuda-drive 1.0.142 → 1.0.144

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/src/runtime.mjs CHANGED
@@ -3,6 +3,7 @@ import 'filepond/dist/filepond.min.css';
3
3
 
4
4
  window.FilePond = FilePond;
5
5
 
6
+
6
7
  import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
7
8
 
8
9
  import * as pintura from './vendors/pintura/pintura/pintura.js';
@@ -26,6 +27,11 @@ import * as fs from './vendors/fs-lightbox.js';
26
27
  import { useDropZone } from '@vueuse/core';
27
28
 
28
29
  export async function upload(fields, e) {
30
+ const container = e.container_node;
31
+ if (!container) {
32
+ e.report_error?.('xuda-drive: upload skipped — host provided no container_node (element not yet in DOM, e.g. a collapsed section)', true);
33
+ return;
34
+ }
29
35
  return new Promise(async (resolve, reject) => {
30
36
  var overrideAction = 'replace';
31
37
  var existingFiles = {};
@@ -39,7 +45,7 @@ export async function upload(fields, e) {
39
45
  var pond = undefined;
40
46
 
41
47
  var init_files_fn = (files) => {
42
- $.each(files, function (key, val) {
48
+ Object.entries(files || {}).forEach(function ([key, val]) {
43
49
  if (val?.server_fileName) val = val.server_fileName;
44
50
  val = func.utils.get_drive_url(e.SESSION_ID, val).value;
45
51
 
@@ -119,12 +125,15 @@ export async function upload(fields, e) {
119
125
  }, 100);
120
126
  };
121
127
 
122
- var $pallet_wrapper = $(`<div class="image_wrapper" style="border: none !important;">`);
123
-
124
128
  var input_id = new Date().valueOf();
125
- var $imgInp = $(`<input class="filepond ${widget_cssClass}" id="${input_id}" />`).appendTo($pallet_wrapper);
126
-
127
- e.$containerP.html($pallet_wrapper);
129
+ var pallet_wrapper = document.createElement('div');
130
+ pallet_wrapper.className = 'image_wrapper';
131
+ pallet_wrapper.style.setProperty('border', 'none', 'important');
132
+ var imgInp = document.createElement('input');
133
+ imgInp.className = `filepond ${widget_cssClass || ''}`.trim();
134
+ imgInp.id = String(input_id);
135
+ pallet_wrapper.appendChild(imgInp);
136
+ container.replaceChildren(pallet_wrapper);
128
137
 
129
138
  FilePond.registerPlugin(FilePondPluginImageEditor);
130
139
  FilePond.registerPlugin(FilePondPluginFileValidateType);
@@ -144,15 +153,15 @@ export async function upload(fields, e) {
144
153
  };
145
154
 
146
155
  const axios_ajax = function (url, data) {
147
- // const e = new URLSearchParams();
156
+ const body = new URLSearchParams();
148
157
 
149
- $.each(data, function (key, val) {
150
- e.append(key, val);
158
+ Object.entries(data || {}).forEach(function ([key, val]) {
159
+ body.append(key, val);
151
160
  });
152
161
 
153
162
  fetch(url, {
154
163
  method: 'POST',
155
- body: e,
164
+ body,
156
165
  }).then((res) => {
157
166
  console.log('Request complete! response:', res);
158
167
  });
@@ -162,7 +171,7 @@ export async function upload(fields, e) {
162
171
  allowProcess: true,
163
172
  allowRemove: true,
164
173
  allowRevert: true,
165
- maxParallelUploads: 50,
174
+ maxParallelUploads: 25,
166
175
 
167
176
  imageCropAspectRatio: 1,
168
177
  imageResizeTargetWidth: 200,
@@ -407,7 +416,7 @@ export async function upload(fields, e) {
407
416
  opt.styleButtonProcessItemPosition = 'right bottom';
408
417
  }
409
418
 
410
- pond = FilePond.create($imgInp[0], opt);
419
+ pond = FilePond.create(imgInp, opt);
411
420
 
412
421
  pond.on('processfiles', (e) => {
413
422
  do_callback();
@@ -433,6 +442,10 @@ export async function upload(fields, e) {
433
442
 
434
443
  export async function viewer(fields, e) {
435
444
  const { app_id, gtp_token, app_token, api_key } = e._session;
445
+ if (!e.container_node) {
446
+ e.report_error?.('xuda-drive: viewer skipped — host provided no container_node (element not yet in DOM, e.g. a collapsed section)', true);
447
+ return;
448
+ }
436
449
 
437
450
  var sepcialLoader = {
438
451
  template: `
@@ -633,7 +646,7 @@ export async function viewer(fields, e) {
633
646
 
634
647
  <div>
635
648
  <button
636
- @click="$refs.fileInput.click()"
649
+ @click="uploadModal = true"
637
650
  type="button"
638
651
  class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide text-white transition-colors duration-200 rounded-md bg-neutral-950 hover:bg-neutral-900 focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none"
639
652
  >
@@ -751,137 +764,129 @@ export async function viewer(fields, e) {
751
764
  </div>
752
765
  </div>
753
766
 
754
- <!-- Hidden native file input -->
755
- <input type="file" ref="fileInput" :multiple="options.file_upload_allow_multiple_files" :accept="acceptTypes" class="hidden" @change="onFilesSelected" />
756
-
757
- <!-- Consent override modal -->
758
767
  <teleport to="body">
759
- <div @keydown.escape.window="showExistModal = false" v-show="showExistModal" class="fixed top-0 left-0 z-[99] w-screen h-screen" x-cloak>
760
- <div v-show="showExistModal" @click="showExistModal=false" class="absolute inset-0 w-full h-full bg-white backdrop-blur-sm bg-opacity-70"></div>
768
+ <div @keydown.escape.window="uploadModal = false" v-show="uploadModal" class="fixed top-0 left-0 z-[99] w-screen h-screen" x-cloak>
769
+ <div v-show="uploadModal" @click="uploadModal=false" class="absolute inset-0 w-full h-full bg-white backdrop-blur-sm bg-opacity-70"></div>
770
+
761
771
  <transition key="wrapper" enter-from-class="opacity-0" enter-active-class="ease-out duration-300" enter-to-class="opacity-100" leave-from-class="opacity-100" leave-active-class="ease-in duration-300" leave-to-class="opacity-0">
762
- <div class="flex items-center justify-center size-full" v-show="showExistModal">
763
- <transition key="inner" enter-active-class="ease-out duration-300" enter-from-class="opacity-0 -translate-y-2 sm:scale-95" enter-to-class="opacity-100 translate-y-0 sm:scale-100" leave-active-class="ease-in duration-200" leave-from-class="opacity-100 translate-y-0 sm:scale-100" leave-to-class="opacity-0 -translate-y-2 sm:scale-95">
764
- <div v-show="showExistModal" class="relative w-full py-6 bg-white border shadow-lg px-7 border-neutral-200 sm:max-w-lg sm:rounded-lg overflow-hidden">
772
+ <div class="flex items-center justify-center size-full" v-show="uploadModal">
773
+ <transition
774
+ key="inner"
775
+ enter-active-class="ease-out duration-300"
776
+ enter-from-class="opacity-0 -translate-y-2 sm:scale-95"
777
+ enter-to-class="opacity-100 translate-y-0 sm:scale-100"
778
+ leave-active-class="ease-in duration-200"
779
+ leave-from-class="opacity-100 translate-y-0 sm:scale-100"
780
+ leave-to-class="opacity-0 -translate-y-2 sm:scale-95"
781
+ >
782
+ <div v-show="uploadModal" class="relative w-full py-6 bg-white border shadow-lg px-7 border-neutral-200 sm:max-w-lg sm:rounded-lg overflow-hidden">
765
783
  <div class="flex items-center justify-between pb-3">
766
- <h3 class="text-lg font-semibold">Upload options</h3>
767
- <button @click="showExistModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
784
+ <h3 class="text-lg font-semibold" v-text="'Upload file' + (options.file_upload_allow_multiple_files ? 's' : '')"></h3>
785
+ <button @click="uploadModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
768
786
  <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
769
787
  </button>
770
788
  </div>
771
- <div class="flex-1">
772
- <p class="text-gray-800">One or more items already exists in this location. Do you want to replace the existing items with a new version or keep both items? Replacing the items won't change sharing settings.</p>
773
- <fieldset>
774
- <div class="mt-6 space-y-5 pl-2">
775
- <div v-for="option in replaceOptions" :key="option.value">
776
- <label :for="option.value" class="flex items-center cursor-pointer">
777
- <input :id="option.value" name="notification-method" type="radio" :value="option.value" v-model="overrideAction" :checked="option.value === overrideAction" class="option-radio peer" />
778
- <div class="ml-3 block font-medium peer-checked:text-black text-light-700 cursor-pointer">{{ option.label }}</div>
779
- </label>
780
- </div>
781
- </div>
782
- </fieldset>
783
- </div>
784
- <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-6">
785
- <button @click="showExistModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2">
789
+ <div id="imageUploaderComponent" class="relative w-auto pb-8 h-48 [&>div]:h-full [&>div>div]:h-full"></div>
790
+ <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
791
+ <button @click="uploadModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2">
786
792
  <div>Dismiss</div>
787
793
  </button>
788
- <button @click="startUpload(true)" type="button" class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900">
789
- Upload
794
+ <button
795
+ @click="uploadFiles(false)"
796
+ :disabled="uploaderLoading || checkingFiles"
797
+ type="button"
798
+ class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed"
799
+ >
800
+ <template v-if="checkingFiles">
801
+ <sepcial-loader class="size-5" />
802
+ Checking files...
803
+ </template>
804
+
805
+ <template v-else-if="uploaderLoading">
806
+ <sepcial-loader class="size-5" />
807
+ Uploading
808
+ </template>
809
+
810
+ <template v-else>Upload</template>
790
811
  </button>
791
812
  </div>
813
+
814
+ <div v-show="showExistModal" class="absolute inset-0 bg-white py-6 px-7 flex flex-col">
815
+ <div class="flex items-center justify-between pb-3">
816
+ <h3 class="text-lg font-semibold">Upload options</h3>
817
+ <button @click="showExistModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
818
+ <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
819
+ </button>
820
+ </div>
821
+
822
+ <div class="flex-1">
823
+ <p class="text-gray-800">One or more items already exists in this location. Do you want to replace the existing items with a new version or keep both items? Replacing the items won't change sharing settings.</p>
824
+
825
+ <fieldset>
826
+ <div class="mt-6 space-y-5 pl-2">
827
+ <div v-for="option in replaceOptions" :key="option.value">
828
+ <label :for="option.value" class="flex items-center cursor-pointer">
829
+ <input :id="option.value" name="notification-method" type="radio" :value="option.value" v-model="overrideAction" :checked="option.value === overrideAction" class="option-radio peer" />
830
+ <div class="ml-3 block font-medium peer-checked:text-black text-light-700 cursor-pointer">{{ option.label }}</div>
831
+ </label>
832
+ </div>
833
+ </div>
834
+ </fieldset>
835
+ </div>
836
+
837
+ <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
838
+ <button @click="showExistModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2">
839
+ <div>Dismiss</div>
840
+ </button>
841
+ <button
842
+ @click="uploadFiles(true)"
843
+ :disabled="uploaderLoading"
844
+ type="button"
845
+ class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed"
846
+ >
847
+ Upload
848
+ </button>
849
+ </div>
850
+ </div>
851
+
852
+ <div v-show="checkerFailedModal" class="absolute inset-0 bg-white py-6 px-7 flex flex-col">
853
+ <div class="flex items-center justify-between pb-3">
854
+ <h3 class="text-lg font-semibold">Checker failed</h3>
855
+ <button @click="checkerFailedModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
856
+ <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
857
+ </button>
858
+ </div>
859
+
860
+ <div class="flex-1">
861
+ <p class="text-gray-800">We couldn't complete your file upload due to a network error. This might be due to a temporary connection issue.
862
+
863
+ </p>
864
+
865
+
866
+ </div>
867
+
868
+ <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
869
+ <button @click="checkerFailedModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2">
870
+ <div>Dismiss</div>
871
+ </button>
872
+ <button
873
+ @click="uploadFiles(false), (checkerFailedModal = false)"
874
+ :disabled="uploaderLoading"
875
+ type="button"
876
+ class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed"
877
+ >
878
+ Retry
879
+ </button>
880
+ </div>
881
+ </div>
882
+
883
+
884
+
792
885
  </div>
793
886
  </transition>
794
887
  </div>
795
888
  </transition>
796
889
  </div>
797
-
798
- <!-- Upload job window (Google Drive style) -->
799
- <div v-if="uploadJobs.length || checkingFiles" class="fixed bottom-4 right-4 z-[100] w-96 bg-white rounded-lg shadow-2xl border border-neutral-200 overflow-hidden" style="font-family: system-ui, -apple-system, sans-serif;">
800
- <!-- Header -->
801
- <div class="flex items-center justify-between px-4 py-3 bg-neutral-900 text-white cursor-pointer" @click="jobWindowCollapsed = !jobWindowCollapsed">
802
- <div class="flex items-center gap-2.5 font-medium text-sm">
803
- <!-- Circular progress -->
804
- <svg v-if="uploadJobs.length && !checkingFiles && uploadsInProgress > 0" class="w-5 h-5 -rotate-90 shrink-0" viewBox="0 0 20 20">
805
- <circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.2" />
806
- <circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" :stroke-dasharray="50.265" :stroke-dashoffset="50.265 - (50.265 * overallProgress / 100)" class="transition-all duration-300" />
807
- </svg>
808
- <div>
809
- <template v-if="checkingFiles">Checking {{ checkingFilesCount }} file{{ checkingFilesCount > 1 ? 's' : '' }}</template>
810
- <template v-else-if="uploadsInProgress > 0">Uploading {{ uploadsInProgress }} item{{ uploadsInProgress > 1 ? 's' : '' }}<template v-if="uploadsFailed">, {{ uploadsFailed }} failed</template></template>
811
- <template v-else>
812
- {{ uploadsDone }} uploaded<template v-if="uploadsFailed">, {{ uploadsFailed }} failed</template>
813
- </template>
814
- </div>
815
- </div>
816
- <div class="flex items-center gap-2">
817
- <button @click.stop="jobWindowCollapsed = !jobWindowCollapsed" class="text-white/70 hover:text-white">
818
- <svg :class="{ 'rotate-180': jobWindowCollapsed }" class="w-5 h-5 transition-transform" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>
819
- </button>
820
- <button v-if="uploadsInProgress === 0 && !checkingFiles" @click.stop="uploadJobs = []" class="text-white/70 hover:text-white">
821
- <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
822
- </button>
823
- </div>
824
- </div>
825
- <!-- Failed summary bar -->
826
- <div v-if="uploadsFailed > 0 && uploadsInProgress === 0 && !jobWindowCollapsed" class="flex items-center justify-between px-4 py-2 bg-red-50 border-b border-red-100">
827
- <div class="flex items-center gap-2 text-red-600 text-sm font-medium">
828
- <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" /></svg>
829
- {{ uploadsFailed }} failed
830
- </div>
831
- <div class="flex items-center gap-1.5">
832
- <!-- Navigate up/down between failed items -->
833
- <button @click.stop="navigateFailed(-1)" class="p-0.5 rounded hover:bg-red-100 text-red-500 transition-colors">
834
- <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" /></svg>
835
- </button>
836
- <button @click.stop="navigateFailed(1)" class="p-0.5 rounded hover:bg-red-100 text-red-500 transition-colors">
837
- <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>
838
- </button>
839
- <!-- Retry all failed -->
840
- <button @click.stop="retryAllFailed" class="text-xs text-red-600 hover:text-red-800 font-medium px-2 py-1 rounded hover:bg-red-100 transition-colors">
841
- Retry all
842
- </button>
843
- </div>
844
- </div>
845
- <!-- File list -->
846
- <div v-show="!jobWindowCollapsed" ref="jobListRef" class="max-h-64 overflow-y-auto divide-y divide-neutral-100">
847
- <!-- Checking files row -->
848
- <div v-if="checkingFiles" class="flex items-center gap-3 px-4 py-2.5">
849
- <div class="shrink-0">
850
- <sepcial-loader class="size-5 text-neutral-500" />
851
- </div>
852
- <div class="flex-1 min-w-0">
853
- <div class="text-sm">Checking {{ checkingFilesCount }} file{{ checkingFilesCount > 1 ? 's' : '' }} for duplicates...</div>
854
- </div>
855
- </div>
856
- <!-- Upload jobs -->
857
- <div v-for="job in uploadJobs" :key="job.id" :ref="'job-' + job.id" class="flex items-center gap-3 px-4 py-2.5">
858
- <div class="shrink-0">
859
- <!-- Pending (queued) -->
860
- <svg v-if="job.status === 'pending'" class="w-5 h-5 text-neutral-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
861
- <!-- Uploading spinner -->
862
- <svg v-else-if="job.status === 'uploading'" class="w-5 h-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /></svg>
863
- <!-- Success icon -->
864
- <svg v-else-if="job.status === 'done'" class="w-5 h-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clip-rule="evenodd" /></svg>
865
- <!-- Error icon -->
866
- <svg v-else-if="job.status === 'error'" class="w-5 h-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" /></svg>
867
- </div>
868
- <div class="flex-1 min-w-0">
869
- <div class="text-sm truncate" v-text="job.name"></div>
870
- <!-- Progress bar -->
871
- <div v-if="job.status === 'uploading'" class="mt-1 h-1 bg-neutral-100 rounded-full overflow-hidden">
872
- <div class="h-full bg-blue-500 rounded-full transition-all duration-300" :style="{ width: job.progress + '%' }"></div>
873
- </div>
874
- </div>
875
- <div class="shrink-0 flex items-center gap-1">
876
- <span v-if="job.status === 'uploading'" class="text-xs text-neutral-400">{{ Math.round(job.progress) }}%</span>
877
- <!-- Retry button for failed uploads -->
878
- <button v-if="job.status === 'error'" @click.stop="retryUpload(job)" class="text-xs text-blue-500 hover:text-blue-700 font-medium px-1.5 py-0.5 rounded hover:bg-blue-50 transition-colors">
879
- Retry
880
- </button>
881
- </div>
882
- </div>
883
- </div>
884
- </div>
885
890
  </teleport>
886
891
  </div>
887
892
 
@@ -929,7 +934,7 @@ export async function viewer(fields, e) {
929
934
  };
930
935
 
931
936
  var containerId = crypto.randomUUID();
932
- e.$containerP.attr('widget-id', containerId);
937
+ e.container_node.setAttribute('widget-id', containerId);
933
938
 
934
939
  var app = createApp({
935
940
  template: html,
@@ -1000,21 +1005,18 @@ export async function viewer(fields, e) {
1000
1005
  folders: [],
1001
1006
  files: [],
1002
1007
  view: fields.default_view_type || 'gallery',
1008
+ uploadModal: false,
1009
+ uploaderLoading: false,
1010
+ checkingFiles: false,
1003
1011
  loadingSearch: false,
1004
1012
  fetcher,
1005
1013
  formatBytes,
1014
+ initUploader: upload,
1006
1015
  swiper: null,
1007
1016
  existingTags: '',
1008
1017
  showSearchPopper: false,
1009
1018
  isOverDropZone: false,
1010
1019
  removeChipCount: 0,
1011
- // Upload job window state
1012
- uploadJobs: [],
1013
- jobWindowCollapsed: false,
1014
- pendingFiles: [],
1015
- checkingFiles: false,
1016
- checkingFilesCount: 0,
1017
- failedNavIndex: -1,
1018
1020
  searchOptions: [
1019
1021
  {
1020
1022
  label: 'Filename',
@@ -1046,7 +1048,10 @@ export async function viewer(fields, e) {
1046
1048
  existingFiles: {},
1047
1049
  allLoading: false,
1048
1050
  showExistModal: false,
1051
+ checkerFailedModal: false,
1049
1052
  overrideAction: 'replace',
1053
+ pond: null,
1054
+ processFiles: null,
1050
1055
  };
1051
1056
  },
1052
1057
  methods: {
@@ -1093,189 +1098,67 @@ export async function viewer(fields, e) {
1093
1098
  refreshFsLightbox();
1094
1099
  });
1095
1100
  },
1096
- onFilesSelected(event) {
1097
- const files = Array.from(event.target.files);
1098
- if (!files.length) return;
1099
- this.pendingFiles = files;
1100
- // Reset the input so re-selecting the same files works
1101
- event.target.value = '';
1102
- this.checkAndUpload();
1103
- },
1104
- checkAndUpload() {
1105
- this.existingFiles = {};
1106
- this.checkingFiles = true;
1107
- this.checkingFilesCount = this.pendingFiles.length;
1108
- this.jobWindowCollapsed = false;
1109
- var file_paths = this.pendingFiles.map((f) => '/' + f.name);
1110
-
1111
- this.fetcher('check_drive_files_workspace', {
1112
- file_paths,
1113
- type: 'file',
1114
- })
1115
- .then(() => {
1116
- this.checkingFiles = false;
1117
- // No conflicts — upload all
1118
- this.startUpload(false);
1119
- })
1120
- .catch((err) => {
1121
- this.checkingFiles = false;
1122
- if (err.code === -764) {
1123
- (err.files || []).forEach((file, index) => {
1124
- if (file.code === -764) {
1125
- this.existingFiles[this.pendingFiles[index].name] = file.duplicates;
1126
- }
1127
- });
1101
+ uploadFiles(is_after_confirm) {
1102
+ this.uploaderLoading = true;
1128
1103
 
1129
- if (Object.keys(this.existingFiles).length) {
1130
- this.showExistModal = true;
1131
- } else {
1132
- this.startUpload(false);
1133
- }
1134
- } else {
1135
- // Network error — upload anyway, server will handle it
1136
- this.startUpload(false);
1137
- }
1138
- });
1139
- },
1140
- startUpload(isAfterConsent) {
1141
- if (isAfterConsent) {
1104
+ if (is_after_confirm) {
1142
1105
  this.showExistModal = false;
1143
- }
1144
-
1145
- const filesToUpload = this.pendingFiles;
1146
- this.pendingFiles = [];
1147
- this.jobWindowCollapsed = false;
1148
-
1149
- filesToUpload.forEach((file) => {
1150
- const jobId = crypto.randomUUID();
1151
- const isUpdate = this.overrideAction === 'replace' && file.name in this.existingFiles;
1152
- this.uploadJobs.push({
1153
- id: jobId,
1154
- name: file.name,
1155
- progress: 0,
1156
- status: 'pending',
1157
- file: file,
1158
- isUpdate: isUpdate,
1106
+ // this.processCb();
1107
+ this.processFiles(null, {
1108
+ overrideAction: this.overrideAction,
1109
+ existingFiles: this.existingFiles,
1159
1110
  });
1160
- });
1161
1111
 
1162
- this.processQueue();
1163
- },
1164
- processQueue() {
1165
- const MAX_CONCURRENT = 10;
1166
- const active = this.uploadJobs.filter(j => j.status === 'uploading').length;
1167
- const pending = this.uploadJobs.filter(j => j.status === 'pending');
1168
- const slotsAvailable = MAX_CONCURRENT - active;
1169
-
1170
- for (let i = 0; i < Math.min(slotsAvailable, pending.length); i++) {
1171
- const job = pending[i];
1172
- job.status = 'uploading';
1173
- this.uploadSingleFile(job.file, job.id, job.isUpdate);
1174
- }
1175
- },
1176
- uploadSingleFile(file, jobId, isUpdate) {
1177
- let formData = new FormData();
1178
- formData.append('file', file, file.name);
1179
-
1180
- const { app_id: appId, gtp_token, app_token, api_key } = e._session;
1181
- formData.append('app_id_reference', APP_OBJ?.[appId]?.app_id_reference);
1182
- formData.append('app_id', appId);
1183
- formData.append('gtp_token', gtp_token);
1184
- formData.append('app_token', app_token);
1185
- formData.append('drive_type', 'workspace');
1186
- if (api_key) formData.append('api_key', api_key);
1187
-
1188
- if (fields.file_upload_folder) formData.append('folder', fields.file_upload_folder);
1189
- if (fields.public_file) formData.append('make_public', fields.public_file);
1190
-
1191
- let tags = fields.assign_file_tags || [];
1192
- let file_tags = [];
1193
- if (fields.auto_tag_generator) {
1194
- let identifier = fields.auto_tag_identifier || ' ';
1195
- const filename = file.name.split('.').slice(0, -1).join('.');
1196
- file_tags = filename.split(identifier);
1197
- }
1198
- formData.append('tags', [...tags, ...file_tags]);
1199
-
1200
- const request = new XMLHttpRequest();
1201
-
1202
- let _domain = e._session.domain;
1203
- if (e._session.is_deployment) {
1204
- _domain = e._session.opt.regional_server === 'dev.xuda.ai' ? 'dev.xuda.ai' : 'xuda.ai';
1205
- }
1112
+ // this.pond.on("processfile", (e) => {
1113
+ // this.uploaderLoading = false;
1114
+ // this.uploadModal = false;
1115
+ // this.pond.removeFiles();
1206
1116
 
1207
- if (isUpdate) {
1208
- formData.append('file_path', '/' + file.name);
1209
- formData.append('file_name', file.name);
1210
- request.open('POST', 'https://' + _domain + '/cpi/update_drive_file_workspace');
1117
+ // this.refreshDirectory();
1118
+ // });
1211
1119
  } else {
1212
- request.open('POST', 'https://' + _domain + '/cpi/upload_drive_file_workspace');
1213
- }
1214
-
1215
- request.setRequestHeader('xu-gtp-token', gtp_token);
1216
- request.setRequestHeader('xu-app-token', app_token);
1120
+ this.existingFiles = {};
1121
+ this.checkingFiles = true;
1217
1122
 
1218
- request.upload.onprogress = (ev) => {
1219
- if (ev.lengthComputable) {
1220
- const job = this.uploadJobs.find((j) => j.id === jobId);
1221
- if (job) job.progress = (ev.loaded / ev.total) * 100;
1222
- }
1223
- };
1224
-
1225
- const onComplete = () => {
1226
- this.processQueue();
1227
- const hasPending = this.uploadJobs.some(j => j.status === 'pending');
1228
- if (this.uploadsInProgress === 0 && !hasPending) {
1229
- this.refreshDirectory(false, true);
1230
- }
1231
- };
1123
+ var filesMapped = this.pond.getFiles().map((e) => ({ id: e.id, filename: e.file.name }));
1124
+ var file_paths = filesMapped.map(({ filename }) => '/' + filename);
1232
1125
 
1233
- request.onload = () => {
1234
- const job = this.uploadJobs.find((j) => j.id === jobId);
1235
- try {
1236
- const res = JSON.parse(request.responseText);
1237
- if (request.status >= 200 && request.status < 300 && res.code > 0) {
1238
- if (job) { job.status = 'done'; job.progress = 100; }
1239
- } else {
1240
- if (job) job.status = 'error';
1241
- }
1242
- } catch (_e) {
1243
- if (job) job.status = 'error';
1244
- }
1245
- onComplete();
1246
- };
1126
+ this.fetcher(`check_drive_files_workspace`, {
1127
+ file_paths,
1128
+ type: 'file',
1129
+ })
1130
+ .then(() => {
1131
+ this.checkingFiles = false;
1247
1132
 
1248
- request.onerror = () => {
1249
- const job = this.uploadJobs.find((j) => j.id === jobId);
1250
- if (job) job.status = 'error';
1251
- onComplete();
1252
- };
1133
+ // No conflicts process all files
1134
+ filesMapped.forEach((e) => {
1135
+ this.processFiles(e.id);
1136
+ });
1137
+ })
1138
+ .catch((err) => {
1139
+ this.checkingFiles = false;
1140
+
1141
+ if (err.code === -764) {
1142
+ (err.files || []).forEach((file, index) => {
1143
+ if (file.code === -764) {
1144
+ this.existingFiles[filesMapped[index].filename] = file.duplicates;
1145
+ }
1146
+ });
1147
+
1148
+ if (Object.keys(this.existingFiles).length) {
1149
+ this.showExistModal = true;
1150
+ }
1253
1151
 
1254
- request.send(formData);
1255
- },
1256
- retryUpload(job) {
1257
- job.status = 'uploading';
1258
- job.progress = 0;
1259
- this.uploadSingleFile(job.file, job.id, job.isUpdate);
1260
- },
1261
- retryAllFailed() {
1262
- this.uploadJobs.filter(j => j.status === 'error').forEach(job => {
1263
- job.status = 'pending';
1264
- job.progress = 0;
1265
- });
1266
- this.processQueue();
1267
- },
1268
- navigateFailed(direction) {
1269
- const failedJobs = this.uploadJobs.filter(j => j.status === 'error');
1270
- if (!failedJobs.length) return;
1271
- this.failedNavIndex = (this.failedNavIndex + direction + failedJobs.length) % failedJobs.length;
1272
- const targetJob = failedJobs[this.failedNavIndex];
1273
- const refEl = this.$refs['job-' + targetJob.id];
1274
- const el = Array.isArray(refEl) ? refEl[0] : refEl;
1275
- if (el && this.$refs.jobListRef) {
1276
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1277
- el.classList.add('bg-red-50');
1278
- setTimeout(() => el.classList.remove('bg-red-50'), 1500);
1152
+ // Process non-conflicting files
1153
+ filesMapped
1154
+ .filter((e) => !Object.keys(this.existingFiles).includes(e.filename))
1155
+ .forEach((e) => {
1156
+ this.processFiles(e.id);
1157
+ });
1158
+ } else {
1159
+ this.checkerFailedModal = true;
1160
+ }
1161
+ });
1279
1162
  }
1280
1163
  },
1281
1164
 
@@ -1308,10 +1191,10 @@ export async function viewer(fields, e) {
1308
1191
  moment,
1309
1192
  },
1310
1193
  mounted() {
1311
- var onDrop = (droppedFiles) => {
1194
+ var onDrop = (files) => {
1312
1195
  if (this.view === 'slider') return;
1313
- this.pendingFiles = Array.from(droppedFiles);
1314
- this.checkAndUpload();
1196
+ this.uploadModal = true;
1197
+ this.pond.addFiles(files);
1315
1198
  };
1316
1199
 
1317
1200
  const { isOverDropZone } = useDropZone(this.$refs.dropZoneRef, {
@@ -1321,31 +1204,6 @@ export async function viewer(fields, e) {
1321
1204
  this.isOverDropZone = isOverDropZone;
1322
1205
  },
1323
1206
  computed: {
1324
- uploadsInProgress() {
1325
- return this.uploadJobs.filter((j) => j.status === 'uploading' || j.status === 'pending').length;
1326
- },
1327
- uploadsDone() {
1328
- return this.uploadJobs.filter(j => j.status === 'done').length;
1329
- },
1330
- uploadsFailed() {
1331
- return this.uploadJobs.filter(j => j.status === 'error').length;
1332
- },
1333
- overallProgress() {
1334
- if (!this.uploadJobs.length) return 0;
1335
- return this.uploadJobs.reduce((sum, j) => sum + j.progress, 0) / this.uploadJobs.length;
1336
- },
1337
- acceptTypes() {
1338
- if (fields.file_upload_mime_type_preset) {
1339
- switch (fields.file_upload_mime_type_preset) {
1340
- case 'image': return 'image/*';
1341
- case 'video': return 'video/*';
1342
- case 'audio': return 'audio/*';
1343
- default: return 'image/*';
1344
- }
1345
- }
1346
- if (fields.file_upload_custom_mime_types) return fields.file_upload_custom_mime_types;
1347
- return null;
1348
- },
1349
1207
  searchTags() {
1350
1208
  return this.opts.search_string.split(' ').filter((e) => e.includes(':'));
1351
1209
  },
@@ -1424,6 +1282,43 @@ export async function viewer(fields, e) {
1424
1282
  async created() {
1425
1283
  this.refreshDirectory();
1426
1284
 
1285
+ this.$nextTick(async () => {
1286
+ // debugger;
1287
+ const uploaderContainer = e.container_node.querySelector('#imageUploaderComponent');
1288
+ if (!uploaderContainer) return; // viewer-only (e.g. a read-only diagram) — no uploader element to init
1289
+ var { pond, processFiles } = await this.initUploader(
1290
+ { ...fields, instant_file_upload: 'N' },
1291
+ {
1292
+ ...e,
1293
+ container_node: uploaderContainer,
1294
+ container_data: uploaderContainer ? func.runtime.ui.get_data(uploaderContainer) : {},
1295
+ },
1296
+ );
1297
+
1298
+ this.pond = pond;
1299
+ this.processFiles = processFiles;
1300
+
1301
+ // Avoid per-file DOM removal — let processfiles handle cleanup in bulk
1302
+ this.pond.on('processfile', (e, { id }) => {
1303
+ // if (!e?.type !== 'error') {
1304
+ // this.pond.removeFiles(id);
1305
+ // }
1306
+
1307
+ // if (!this.pond.getFiles().length) {
1308
+ // this.uploaderLoading = false;
1309
+ // this.uploadModal = false;
1310
+ // this.pond.removeFiles();
1311
+ // }
1312
+ });
1313
+
1314
+ this.pond.on('processfiles', () => {
1315
+ this.uploaderLoading = false;
1316
+ this.uploadModal = false;
1317
+ this.pond.removeFiles();
1318
+ this.refreshDirectory(false, true);
1319
+ });
1320
+ });
1321
+
1427
1322
  this.$watch('opts', (value) => {
1428
1323
  this.refreshDirectory();
1429
1324
  });