@xuda.io/xuda-widget-plugin-xuda-drive 1.0.136 → 1.0.138

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.
Files changed (3) hide show
  1. package/dist/runtime.mjs +4229 -4166
  2. package/package.json +1 -1
  3. package/src/runtime.mjs +92 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/xuda-widget-plugin-xuda-drive",
3
- "version": "1.0.136",
3
+ "version": "1.0.138",
4
4
  "description": "Xuda Drive widget plugin",
5
5
  "scripts": {
6
6
  "pub": "npm version patch --force && npm publish --access public",
package/src/runtime.mjs CHANGED
@@ -796,27 +796,60 @@ export async function viewer(fields, e) {
796
796
  </div>
797
797
 
798
798
  <!-- Upload job window (Google Drive style) -->
799
- <div v-if="uploadJobs.length" 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;">
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
800
  <!-- Header -->
801
801
  <div class="flex items-center justify-between px-4 py-3 bg-neutral-900 text-white cursor-pointer" @click="jobWindowCollapsed = !jobWindowCollapsed">
802
802
  <div class="font-medium text-sm">
803
- <template v-if="uploadsInProgress > 0">Uploading {{ uploadsInProgress }} item{{ uploadsInProgress > 1 ? 's' : '' }}</template>
804
- <template v-else>{{ uploadJobs.length }} upload{{ uploadJobs.length > 1 ? 's' : '' }} complete</template>
803
+ <template v-if="checkingFiles">Checking {{ checkingFilesCount }} file{{ checkingFilesCount > 1 ? 's' : '' }}</template>
804
+ <template v-else-if="uploadsInProgress > 0">Uploading {{ uploadsInProgress }} item{{ uploadsInProgress > 1 ? 's' : '' }}</template>
805
+ <template v-else>
806
+ {{ uploadsDone }} uploaded<template v-if="uploadsFailed">, {{ uploadsFailed }} failed</template>
807
+ </template>
805
808
  </div>
806
809
  <div class="flex items-center gap-2">
807
810
  <button @click.stop="jobWindowCollapsed = !jobWindowCollapsed" class="text-white/70 hover:text-white">
808
811
  <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>
809
812
  </button>
810
- <button v-if="uploadsInProgress === 0" @click.stop="uploadJobs = []" class="text-white/70 hover:text-white">
813
+ <button v-if="uploadsInProgress === 0 && !checkingFiles" @click.stop="uploadJobs = []" class="text-white/70 hover:text-white">
811
814
  <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>
812
815
  </button>
813
816
  </div>
814
817
  </div>
818
+ <!-- Failed summary bar -->
819
+ <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">
820
+ <div class="flex items-center gap-2 text-red-600 text-sm font-medium">
821
+ <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>
822
+ {{ uploadsFailed }} failed
823
+ </div>
824
+ <div class="flex items-center gap-1.5">
825
+ <!-- Navigate up/down between failed items -->
826
+ <button @click.stop="navigateFailed(-1)" class="p-0.5 rounded hover:bg-red-100 text-red-500 transition-colors">
827
+ <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>
828
+ </button>
829
+ <button @click.stop="navigateFailed(1)" class="p-0.5 rounded hover:bg-red-100 text-red-500 transition-colors">
830
+ <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>
831
+ </button>
832
+ <!-- Retry all failed -->
833
+ <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">
834
+ Retry all
835
+ </button>
836
+ </div>
837
+ </div>
815
838
  <!-- File list -->
816
- <div v-show="!jobWindowCollapsed" class="max-h-64 overflow-y-auto divide-y divide-neutral-100">
817
- <div v-for="job in uploadJobs" :key="job.id" class="flex items-center gap-3 px-4 py-2.5">
839
+ <div v-show="!jobWindowCollapsed" ref="jobListRef" class="max-h-64 overflow-y-auto divide-y divide-neutral-100">
840
+ <!-- Checking files row -->
841
+ <div v-if="checkingFiles" class="flex items-center gap-3 px-4 py-2.5">
842
+ <div class="shrink-0">
843
+ <sepcial-loader class="size-5 text-neutral-500" />
844
+ </div>
845
+ <div class="flex-1 min-w-0">
846
+ <div class="text-sm">Checking {{ checkingFilesCount }} file{{ checkingFilesCount > 1 ? 's' : '' }} for duplicates...</div>
847
+ </div>
848
+ </div>
849
+ <!-- Upload jobs -->
850
+ <div v-for="job in uploadJobs" :key="job.id" :ref="'job-' + job.id" class="flex items-center gap-3 px-4 py-2.5">
818
851
  <div class="shrink-0">
819
- <!-- Upload icon -->
852
+ <!-- Uploading spinner -->
820
853
  <svg v-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>
821
854
  <!-- Success icon -->
822
855
  <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>
@@ -830,8 +863,12 @@ export async function viewer(fields, e) {
830
863
  <div class="h-full bg-blue-500 rounded-full transition-all duration-300" :style="{ width: job.progress + '%' }"></div>
831
864
  </div>
832
865
  </div>
833
- <div class="shrink-0 text-xs text-neutral-400">
834
- <span v-if="job.status === 'uploading'">{{ Math.round(job.progress) }}%</span>
866
+ <div class="shrink-0 flex items-center gap-1">
867
+ <span v-if="job.status === 'uploading'" class="text-xs text-neutral-400">{{ Math.round(job.progress) }}%</span>
868
+ <!-- Retry button for failed uploads -->
869
+ <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">
870
+ Retry
871
+ </button>
835
872
  </div>
836
873
  </div>
837
874
  </div>
@@ -966,6 +1003,9 @@ export async function viewer(fields, e) {
966
1003
  uploadJobs: [],
967
1004
  jobWindowCollapsed: false,
968
1005
  pendingFiles: [],
1006
+ checkingFiles: false,
1007
+ checkingFilesCount: 0,
1008
+ failedNavIndex: -1,
969
1009
  searchOptions: [
970
1010
  {
971
1011
  label: 'Filename',
@@ -1054,6 +1094,9 @@ export async function viewer(fields, e) {
1054
1094
  },
1055
1095
  checkAndUpload() {
1056
1096
  this.existingFiles = {};
1097
+ this.checkingFiles = true;
1098
+ this.checkingFilesCount = this.pendingFiles.length;
1099
+ this.jobWindowCollapsed = false;
1057
1100
  var file_paths = this.pendingFiles.map((f) => '/' + f.name);
1058
1101
 
1059
1102
  this.fetcher('check_drive_files_workspace', {
@@ -1061,10 +1104,12 @@ export async function viewer(fields, e) {
1061
1104
  type: 'file',
1062
1105
  })
1063
1106
  .then(() => {
1107
+ this.checkingFiles = false;
1064
1108
  // No conflicts — upload all
1065
1109
  this.startUpload(false);
1066
1110
  })
1067
1111
  .catch((err) => {
1112
+ this.checkingFiles = false;
1068
1113
  if (err.code === -764) {
1069
1114
  (err.files || []).forEach((file, index) => {
1070
1115
  if (file.code === -764) {
@@ -1094,16 +1139,19 @@ export async function viewer(fields, e) {
1094
1139
 
1095
1140
  filesToUpload.forEach((file) => {
1096
1141
  const jobId = crypto.randomUUID();
1142
+ const isUpdate = this.overrideAction === 'replace' && file.name in this.existingFiles;
1097
1143
  this.uploadJobs.push({
1098
1144
  id: jobId,
1099
1145
  name: file.name,
1100
1146
  progress: 0,
1101
1147
  status: 'uploading',
1148
+ file: file,
1149
+ isUpdate: isUpdate,
1102
1150
  });
1103
- this.uploadSingleFile(file, jobId);
1151
+ this.uploadSingleFile(file, jobId, isUpdate);
1104
1152
  });
1105
1153
  },
1106
- uploadSingleFile(file, jobId) {
1154
+ uploadSingleFile(file, jobId, isUpdate) {
1107
1155
  let formData = new FormData();
1108
1156
  formData.append('file', file, file.name);
1109
1157
 
@@ -1134,7 +1182,7 @@ export async function viewer(fields, e) {
1134
1182
  _domain = e._session.opt.regional_server === 'dev.xuda.ai' ? 'dev.xuda.ai' : 'xuda.ai';
1135
1183
  }
1136
1184
 
1137
- if (this.overrideAction === 'replace' && file.name in this.existingFiles) {
1185
+ if (isUpdate) {
1138
1186
  formData.append('file_path', '/' + file.name);
1139
1187
  formData.append('file_name', file.name);
1140
1188
  request.open('POST', 'https://' + _domain + '/cpi/update_drive_file_workspace');
@@ -1174,10 +1222,36 @@ export async function viewer(fields, e) {
1174
1222
  request.onerror = () => {
1175
1223
  const job = this.uploadJobs.find((j) => j.id === jobId);
1176
1224
  if (job) job.status = 'error';
1225
+ if (this.uploadsInProgress === 0) {
1226
+ this.refreshDirectory(false, true);
1227
+ }
1177
1228
  };
1178
1229
 
1179
1230
  request.send(formData);
1180
1231
  },
1232
+ retryUpload(job) {
1233
+ job.status = 'uploading';
1234
+ job.progress = 0;
1235
+ this.uploadSingleFile(job.file, job.id, job.isUpdate);
1236
+ },
1237
+ retryAllFailed() {
1238
+ this.uploadJobs.filter(j => j.status === 'error').forEach(job => {
1239
+ this.retryUpload(job);
1240
+ });
1241
+ },
1242
+ navigateFailed(direction) {
1243
+ const failedJobs = this.uploadJobs.filter(j => j.status === 'error');
1244
+ if (!failedJobs.length) return;
1245
+ this.failedNavIndex = (this.failedNavIndex + direction + failedJobs.length) % failedJobs.length;
1246
+ const targetJob = failedJobs[this.failedNavIndex];
1247
+ const refEl = this.$refs['job-' + targetJob.id];
1248
+ const el = Array.isArray(refEl) ? refEl[0] : refEl;
1249
+ if (el && this.$refs.jobListRef) {
1250
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1251
+ el.classList.add('bg-red-50');
1252
+ setTimeout(() => el.classList.remove('bg-red-50'), 1500);
1253
+ }
1254
+ },
1181
1255
 
1182
1256
  removeLastSearch($event) {
1183
1257
  if (!$event.target.value) {
@@ -1224,6 +1298,12 @@ export async function viewer(fields, e) {
1224
1298
  uploadsInProgress() {
1225
1299
  return this.uploadJobs.filter((j) => j.status === 'uploading').length;
1226
1300
  },
1301
+ uploadsDone() {
1302
+ return this.uploadJobs.filter(j => j.status === 'done').length;
1303
+ },
1304
+ uploadsFailed() {
1305
+ return this.uploadJobs.filter(j => j.status === 'error').length;
1306
+ },
1227
1307
  acceptTypes() {
1228
1308
  if (fields.file_upload_mime_type_preset) {
1229
1309
  switch (fields.file_upload_mime_type_preset) {