@sinequa/atomic-angular 1.16.0 → 1.17.0
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/document-workspaces/i18n/de.json +4 -2
- package/document-workspaces/i18n/en.json +4 -2
- package/document-workspaces/i18n/fr.json +4 -2
- package/document-workspaces/i18n/ja.json +4 -2
- package/document-workspaces/index.d.ts +64 -21
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs +247 -203
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs.map +1 -1
- package/fesm2022/sinequa-atomic-angular-workspace-elements.mjs +2 -2
- package/fesm2022/sinequa-atomic-angular-workspace-elements.mjs.map +1 -1
- package/package.json +1 -5
- package/elements/index.d.ts +0 -346
- package/fesm2022/sinequa-atomic-angular-elements.mjs +0 -7526
- package/fesm2022/sinequa-atomic-angular-elements.mjs.map +0 -1
|
@@ -132,8 +132,8 @@ function withDocumentCountAdjusted(workspaces, id, delta) {
|
|
|
132
132
|
* touches the cache once the server has answered, so a rejected call leaves
|
|
133
133
|
* the state as it was — the call site reports the error.
|
|
134
134
|
*
|
|
135
|
-
* `addDocuments`
|
|
136
|
-
*
|
|
135
|
+
* `addDocuments`, `discardDocuments` and `patchDocument` stay purely local. The upload
|
|
136
|
+
* flow no longer uses them: the list is the index, and only the listing writes to it.
|
|
137
137
|
*/
|
|
138
138
|
const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Workspaces"), withState(initialState$1), withComputed(({ workspaces, pagination, selectedIds }) => ({
|
|
139
139
|
hasMore: computed(() => workspaces().length < pagination().total && workspaces().length < MAX_LOADED_WORKSPACES),
|
|
@@ -353,17 +353,16 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
|
|
|
353
353
|
return merged;
|
|
354
354
|
},
|
|
355
355
|
/**
|
|
356
|
-
* Local only
|
|
357
|
-
*
|
|
358
|
-
* transfer completes.
|
|
356
|
+
* Local only: adds documents to the cache without calling the API. The upload
|
|
357
|
+
* flow does not use it (a file enters the list once the listing returns it).
|
|
359
358
|
*/
|
|
360
359
|
addDocuments(workspaceId, documents) {
|
|
361
360
|
setDocuments(workspaceId, [...(store.documents()[workspaceId] ?? []), ...documents]);
|
|
362
361
|
adjustDocumentCount(workspaceId, documents.length);
|
|
363
362
|
},
|
|
364
363
|
/**
|
|
365
|
-
* Local only, the counterpart of {@link addDocuments}: forgets documents
|
|
366
|
-
*
|
|
364
|
+
* Local only, the counterpart of {@link addDocuments}: forgets documents without
|
|
365
|
+
* calling the API.
|
|
367
366
|
* `removeDocuments` is the one that deletes on the server.
|
|
368
367
|
*/
|
|
369
368
|
discardDocuments(workspaceId, documentIds) {
|
|
@@ -372,7 +371,7 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
|
|
|
372
371
|
setDocuments(workspaceId, after);
|
|
373
372
|
adjustDocumentCount(workspaceId, after.length - before.length);
|
|
374
373
|
},
|
|
375
|
-
/**
|
|
374
|
+
/** Local only: merges a patch into one cached document, matched by id. */
|
|
376
375
|
patchDocument(workspaceId, documentId, patch) {
|
|
377
376
|
patchState(store, state => ({
|
|
378
377
|
documents: {
|
|
@@ -437,7 +436,8 @@ const initialState = {
|
|
|
437
436
|
workspaces: [],
|
|
438
437
|
connection: null,
|
|
439
438
|
clearedCount: 0,
|
|
440
|
-
jobIndexing: {}
|
|
439
|
+
jobIndexing: {},
|
|
440
|
+
ignoredJobs: {}
|
|
441
441
|
};
|
|
442
442
|
const FINISHED = ["done", "error", "rejected"];
|
|
443
443
|
const FAILED = ["error", "rejected"];
|
|
@@ -493,6 +493,45 @@ function phaseProgress(uploads, jobIndexing) {
|
|
|
493
493
|
function updateOne(uploads, id, change) {
|
|
494
494
|
return uploads.map(upload => (upload.id === id ? change(upload) : upload));
|
|
495
495
|
}
|
|
496
|
+
/** `ignored` plus the jobs of `uploads`. */
|
|
497
|
+
function withIgnored(ignored, uploads) {
|
|
498
|
+
const next = { ...ignored };
|
|
499
|
+
for (const upload of uploads)
|
|
500
|
+
next[jobOf(upload)] = true;
|
|
501
|
+
return next;
|
|
502
|
+
}
|
|
503
|
+
/** The row standing for a whole job, whose files the API does not list: `<jobId>/`. */
|
|
504
|
+
const remoteJobRowId = (jobId) => `${jobId}/`;
|
|
505
|
+
/** Where a whole job stands, as one row: what the API says of its two phases. */
|
|
506
|
+
function remoteJobState(job) {
|
|
507
|
+
if (job.uploadStatus === "uploading")
|
|
508
|
+
return { status: "uploading", percent: job.uploadProgress?.percentage ?? 0 };
|
|
509
|
+
if (job.uploadStatus === "upload_interrupted")
|
|
510
|
+
return { status: "error", percent: 0 };
|
|
511
|
+
if (job.indexingStatus === "complete")
|
|
512
|
+
return { status: "done", percent: 100 };
|
|
513
|
+
if (job.indexingStatus === "failed" || job.indexingStatus === "interrupted")
|
|
514
|
+
return { status: "error", percent: 0 };
|
|
515
|
+
// The job's aggregate indexing percentage lives in `jobIndexing`: no file has its own.
|
|
516
|
+
return { status: "indexing", percent: 0 };
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* The rows a job started elsewhere shows as: one per file the API names (errors and
|
|
520
|
+
* rejections, the only per-file data it has), and one for the job itself — unless a
|
|
521
|
+
* named file already explains why it failed.
|
|
522
|
+
*/
|
|
523
|
+
function remoteRows({ job, workspaceId, mine }) {
|
|
524
|
+
const base = { type: "doc", workspaceId, retryable: false, percent: 0, remote: { mine } };
|
|
525
|
+
const files = [
|
|
526
|
+
...(job.errors ?? []).map(e => ({ ...base, id: `${job.jobId}/${e.fileName}`, name: e.fileName, status: "error", message: e.error ?? undefined })),
|
|
527
|
+
...(job.rejected ?? []).map(r => ({ ...base, id: `${job.jobId}/${r.fileName}`, name: r.fileName, status: "rejected", message: r.reason }))
|
|
528
|
+
];
|
|
529
|
+
const state = remoteJobState(job);
|
|
530
|
+
if (state.status === "error" && files.length)
|
|
531
|
+
return files;
|
|
532
|
+
return [{ ...base, ...state, id: remoteJobRowId(job.jobId), name: "" }, ...files];
|
|
533
|
+
}
|
|
534
|
+
const sameRow = (a, b) => a.status === b.status && a.percent === b.percent && a.message === b.message && a.workspaceId === b.workspaceId && a.remote?.mine === b.remote?.mine;
|
|
496
535
|
/**
|
|
497
536
|
* The state of every upload the tracker follows — pure state, no I/O.
|
|
498
537
|
*
|
|
@@ -515,6 +554,13 @@ const UploadTrackerStore = signalStore({ providedIn: "root" }, withDevtools("Upl
|
|
|
515
554
|
}),
|
|
516
555
|
/** Everything is over and nothing failed: the panel celebrates, then dismisses itself. */
|
|
517
556
|
allDone: computed(() => uploads().length > 0 && uploads().every(u => u.status === "done")),
|
|
557
|
+
/** Everything is over, whether it worked or not: nothing moves any more, and the panel may be closed. */
|
|
558
|
+
settled: computed(() => uploads().length > 0 && uploads().every(isFinished)),
|
|
559
|
+
/**
|
|
560
|
+
* A failure on an upload of the current user's. It is theirs to read and close: the panel
|
|
561
|
+
* stays until they do. A failure on someone else's upload is only worth the usual delay.
|
|
562
|
+
*/
|
|
563
|
+
authorFailed: computed(() => uploads().some(u => isFailed(u) && (!u.remote || u.remote.mine))),
|
|
518
564
|
/** Per workspace, how far each phase is — what the tracker's progress bars show. */
|
|
519
565
|
progress: computed(() => {
|
|
520
566
|
const byWorkspace = new Map();
|
|
@@ -638,22 +684,62 @@ const UploadTrackerStore = signalStore({ providedIn: "root" }, withDevtools("Upl
|
|
|
638
684
|
patchState(store, { uploads: next, jobIndexing });
|
|
639
685
|
return changed;
|
|
640
686
|
},
|
|
687
|
+
/**
|
|
688
|
+
* Feeds in the jobs the status API reports that this client is not following — the ones
|
|
689
|
+
* another user started, or its own from before a page reload. Each becomes a row (see
|
|
690
|
+
* `remoteRows`), updated on every call; a job this client sent itself is left to the
|
|
691
|
+
* local rows, and so is one the user already got rid of.
|
|
692
|
+
*
|
|
693
|
+
* @returns Whether anything changed, which paces the poller.
|
|
694
|
+
*/
|
|
695
|
+
applyRemoteJobs(entries) {
|
|
696
|
+
const current = store.uploads();
|
|
697
|
+
const ignored = store.ignoredJobs();
|
|
698
|
+
const ownJobs = new Set(current.filter(u => !u.remote).map(jobOf));
|
|
699
|
+
const rows = new Map(current.map(u => [u.id, u]));
|
|
700
|
+
const workspaces = [...store.workspaces()];
|
|
701
|
+
let changed = false;
|
|
702
|
+
for (const entry of entries) {
|
|
703
|
+
if (ignored[entry.job.jobId] || ownJobs.has(entry.job.jobId))
|
|
704
|
+
continue;
|
|
705
|
+
for (const row of remoteRows(entry)) {
|
|
706
|
+
const known = rows.get(row.id);
|
|
707
|
+
if (known && sameRow(known, row))
|
|
708
|
+
continue;
|
|
709
|
+
rows.set(row.id, known ? { ...known, ...row } : row);
|
|
710
|
+
changed = true;
|
|
711
|
+
}
|
|
712
|
+
if (!workspaces.some(w => w.id === entry.workspaceId)) {
|
|
713
|
+
workspaces.push({ id: entry.workspaceId, name: entry.workspaceName });
|
|
714
|
+
changed = true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (changed)
|
|
718
|
+
patchState(store, { uploads: [...rows.values()], workspaces });
|
|
719
|
+
return changed;
|
|
720
|
+
},
|
|
641
721
|
setConnection(connection) {
|
|
642
722
|
patchState(store, { connection });
|
|
643
723
|
},
|
|
644
724
|
remove(id) {
|
|
645
|
-
patchState(store, state =>
|
|
725
|
+
patchState(store, state => {
|
|
726
|
+
const uploads = state.uploads.filter(u => u.id !== id);
|
|
727
|
+
// The job is forgotten once its last row is gone; while one is left, the job is still followed.
|
|
728
|
+
const gone = state.uploads.filter(u => u.id === id && !uploads.some(other => jobOf(other) === jobOf(u)));
|
|
729
|
+
return { uploads, ignoredJobs: withIgnored(state.ignoredJobs, gone) };
|
|
730
|
+
});
|
|
646
731
|
},
|
|
647
732
|
/** Drops a finished workspace's files from the tracker; they stay counted as indexed. */
|
|
648
733
|
removeWorkspace(workspaceId) {
|
|
649
734
|
patchState(store, state => ({
|
|
650
735
|
uploads: state.uploads.filter(u => u.workspaceId !== workspaceId),
|
|
651
736
|
workspaces: state.workspaces.filter(w => w.id !== workspaceId),
|
|
652
|
-
clearedCount: state.clearedCount + state.uploads.filter(u => u.workspaceId === workspaceId).length
|
|
737
|
+
clearedCount: state.clearedCount + state.uploads.filter(u => u.workspaceId === workspaceId).length,
|
|
738
|
+
ignoredJobs: withIgnored(state.ignoredJobs, state.uploads.filter(u => u.workspaceId === workspaceId))
|
|
653
739
|
}));
|
|
654
740
|
},
|
|
655
741
|
reset() {
|
|
656
|
-
patchState(store, initialState);
|
|
742
|
+
patchState(store, state => ({ ...initialState, ignoredJobs: withIgnored(state.ignoredJobs, state.uploads) }));
|
|
657
743
|
}
|
|
658
744
|
})));
|
|
659
745
|
|
|
@@ -744,6 +830,12 @@ function isJobTerminal(job) {
|
|
|
744
830
|
function allJobs(res) {
|
|
745
831
|
return (res.containers ?? []).flatMap(c => c.jobs ?? []);
|
|
746
832
|
}
|
|
833
|
+
/** Like {@link allJobs}, but keeps the workspace each job belongs to. */
|
|
834
|
+
function containerJobs(res) {
|
|
835
|
+
return (res.containers ?? []).flatMap(c => (c.jobs ?? []).map(job => ({ job, containerId: c.containerId, containerName: c.containerName })));
|
|
836
|
+
}
|
|
837
|
+
/** How long the panel stays on screen once every upload is over. */
|
|
838
|
+
const FINISHED_UPLOAD_VISIBLE_MS = 2 * 60 * 1000;
|
|
747
839
|
|
|
748
840
|
/**
|
|
749
841
|
* `crypto.randomUUID()` is only available in a secure context (HTTPS, or HTTP on
|
|
@@ -969,9 +1061,6 @@ const UPLOAD_FACTORY = new InjectionToken("UPLOAD_FACTORY", {
|
|
|
969
1061
|
factory: () => createTusUpload
|
|
970
1062
|
});
|
|
971
1063
|
|
|
972
|
-
// One page is enough to refresh what the open workspace shows: the detail view
|
|
973
|
-
// itself loads page 1 at the same size.
|
|
974
|
-
const DOCUMENT_STATUS_PAGE_SIZE = 100;
|
|
975
1064
|
/**
|
|
976
1065
|
* Sends files to a workspace and follows them until they are indexed.
|
|
977
1066
|
*
|
|
@@ -986,6 +1075,10 @@ const DOCUMENT_STATUS_PAGE_SIZE = 100;
|
|
|
986
1075
|
* speaks for both. What the user sees lives in `UploadTrackerStore`; this
|
|
987
1076
|
* service only writes to it.
|
|
988
1077
|
*
|
|
1078
|
+
* It never writes a file into a workspace's list: the list is the index, and a file
|
|
1079
|
+
* enters it only once the container listing returns it. What the service does for the
|
|
1080
|
+
* list is re-read that listing while something is in progress.
|
|
1081
|
+
*
|
|
989
1082
|
* Root-scoped: an upload must outlive the view that started it. It registers its
|
|
990
1083
|
* window and auth listeners once, and removes them when the application is destroyed.
|
|
991
1084
|
*/
|
|
@@ -1053,37 +1146,20 @@ class UploadService {
|
|
|
1053
1146
|
const jobId = generateUUID();
|
|
1054
1147
|
const fileCount = files.length;
|
|
1055
1148
|
const totalBytes = files.reduce((total, f) => total + f.size, 0);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
// then the document has no real id to act on.
|
|
1064
|
-
id: `${jobId}/${file.name}`,
|
|
1065
|
-
name: file.name,
|
|
1066
|
-
workspaceId: workspace.id,
|
|
1067
|
-
type: getDocumentType(file.name),
|
|
1068
|
-
createdAt: now,
|
|
1069
|
-
size: file.size,
|
|
1070
|
-
status: "uploading",
|
|
1071
|
-
percent: 0,
|
|
1072
|
-
jobId,
|
|
1073
|
-
uploadedBy
|
|
1074
|
-
}));
|
|
1075
|
-
this.workspaces.addDocuments(workspace.id, documents);
|
|
1076
|
-
this.tracker.track(documents.map(d => ({ id: d.id, name: d.name, type: d.type })), workspace.id, workspace.name);
|
|
1077
|
-
documents.forEach((document, i) => {
|
|
1078
|
-
this.inFlight.add(document.id);
|
|
1079
|
-
this.startUpload(document, files[i], { jobId, fileCount, totalBytes }, options.endpoint);
|
|
1149
|
+
// The id is the key the tracker and this service follow the file by. It is NOT the
|
|
1150
|
+
// server's: the listing names a document `/DocUpload/…|<containerId>/<fileName>`.
|
|
1151
|
+
const items = files.map(file => ({ id: `${jobId}/${file.name}`, workspaceId: workspace.id }));
|
|
1152
|
+
this.tracker.track(files.map((file, i) => ({ id: items[i].id, name: file.name, type: getDocumentType(file.name) })), workspace.id, workspace.name);
|
|
1153
|
+
items.forEach((item, i) => {
|
|
1154
|
+
this.inFlight.add(item.id);
|
|
1155
|
+
this.startUpload(item, files[i], { jobId, fileCount, totalBytes }, options.endpoint);
|
|
1080
1156
|
});
|
|
1081
1157
|
this.poller.start();
|
|
1082
|
-
return
|
|
1158
|
+
return items.map(item => item.id);
|
|
1083
1159
|
}
|
|
1084
1160
|
/**
|
|
1085
1161
|
* Abandons a transfer and forgets it: stops it, and drops the file from the
|
|
1086
|
-
* tracker
|
|
1162
|
+
* tracker. Works while it is sending
|
|
1087
1163
|
* and once it has failed; once the bytes are in, indexing is the server's and
|
|
1088
1164
|
* there is nothing left to cancel.
|
|
1089
1165
|
*
|
|
@@ -1100,7 +1176,6 @@ class UploadService {
|
|
|
1100
1176
|
this.inFlight.delete(id);
|
|
1101
1177
|
this.clearRetryNotice(id);
|
|
1102
1178
|
this.tracker.remove(id);
|
|
1103
|
-
this.workspaces.discardDocuments(entry.workspaceId, [this.storedId(entry.workspaceId, id)]);
|
|
1104
1179
|
try {
|
|
1105
1180
|
await entry.handle.abort(true);
|
|
1106
1181
|
}
|
|
@@ -1115,7 +1190,6 @@ class UploadService {
|
|
|
1115
1190
|
if (!entry || entry.refused || upload?.status !== "error")
|
|
1116
1191
|
return;
|
|
1117
1192
|
this.tracker.restart(id);
|
|
1118
|
-
this.patchStored(entry.workspaceId, id, { status: "uploading", percent: 0 });
|
|
1119
1193
|
this.inFlight.add(id);
|
|
1120
1194
|
entry.handle.resume();
|
|
1121
1195
|
this.poller.start();
|
|
@@ -1132,17 +1206,15 @@ class UploadService {
|
|
|
1132
1206
|
}
|
|
1133
1207
|
/**
|
|
1134
1208
|
* Tells the service which workspace is open, so the status polling can be
|
|
1135
|
-
* scoped to it (the API documents that as the cheaper lookup) and
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1209
|
+
* scoped to it (the API documents that as the cheaper lookup) and re-read its
|
|
1210
|
+
* listing. `null` when none is. Starts the polling: a page reload loses every
|
|
1211
|
+
* local tus handle and the listing no longer says what is in progress, so the
|
|
1212
|
+
* status API is the only way to know. The loop stops by itself after one tick
|
|
1213
|
+
* when nothing is.
|
|
1139
1214
|
*/
|
|
1140
1215
|
watch(workspaceId) {
|
|
1141
1216
|
this.focusedWorkspaceId = workspaceId;
|
|
1142
|
-
if (
|
|
1143
|
-
return;
|
|
1144
|
-
const unfinished = this.workspaces.documentsOf(workspaceId).some(d => d.status === "uploading" || d.status === "indexing");
|
|
1145
|
-
if (unfinished)
|
|
1217
|
+
if (workspaceId)
|
|
1146
1218
|
this.poller.start();
|
|
1147
1219
|
}
|
|
1148
1220
|
startPolling() {
|
|
@@ -1152,11 +1224,11 @@ class UploadService {
|
|
|
1152
1224
|
this.poller.stop();
|
|
1153
1225
|
}
|
|
1154
1226
|
// ── sending ──
|
|
1155
|
-
startUpload(
|
|
1156
|
-
const workspaceId =
|
|
1227
|
+
startUpload(item, file, job, endpoint) {
|
|
1228
|
+
const workspaceId = item.workspaceId;
|
|
1157
1229
|
if (!endpoint) {
|
|
1158
|
-
this.inFlight.delete(
|
|
1159
|
-
this.markError(
|
|
1230
|
+
this.inFlight.delete(item.id);
|
|
1231
|
+
this.markError(item);
|
|
1160
1232
|
return;
|
|
1161
1233
|
}
|
|
1162
1234
|
const entry = {
|
|
@@ -1178,25 +1250,25 @@ class UploadService {
|
|
|
1178
1250
|
// again once the user is back.
|
|
1179
1251
|
void this.recoverSessionAndResume();
|
|
1180
1252
|
if (this.signedOut) {
|
|
1181
|
-
this.clearRetryNotice(
|
|
1253
|
+
this.clearRetryNotice(item.id);
|
|
1182
1254
|
return false;
|
|
1183
1255
|
}
|
|
1184
1256
|
}
|
|
1185
1257
|
else if (FATAL_UPLOAD_STATUSES.has(status)) {
|
|
1186
1258
|
entry.refused = true;
|
|
1187
|
-
this.clearRetryNotice(
|
|
1259
|
+
this.clearRetryNotice(item.id);
|
|
1188
1260
|
return false;
|
|
1189
1261
|
}
|
|
1190
1262
|
// tus waits retryDelays[attempt] before this attempt and passes that same
|
|
1191
1263
|
// index here, so the banner's countdown is the real delay, not an estimate.
|
|
1192
|
-
this.noteRetryScheduled(
|
|
1264
|
+
this.noteRetryScheduled(item.id, attempt);
|
|
1193
1265
|
return true;
|
|
1194
1266
|
},
|
|
1195
1267
|
// Anything that came back cleanly means the connection works again for this
|
|
1196
1268
|
// upload, whatever the retry bookkeeping still says.
|
|
1197
1269
|
responded: status => {
|
|
1198
1270
|
if (status < 400)
|
|
1199
|
-
this.noteHealthy(
|
|
1271
|
+
this.noteHealthy(item.id);
|
|
1200
1272
|
},
|
|
1201
1273
|
// A recovery kicked off by a previous 401 may still be in flight, and
|
|
1202
1274
|
// carrying the token it is fetching is the entire point of this retry.
|
|
@@ -1209,67 +1281,42 @@ class UploadService {
|
|
|
1209
1281
|
// processed: on a fast transfer a stale event can still arrive after
|
|
1210
1282
|
// success moved the file to indexing. Drop anything that comes once the
|
|
1211
1283
|
// upload phase is over, so it cannot stomp the indexing percentage.
|
|
1212
|
-
if (this.tracker.uploads().find(u => u.id ===
|
|
1284
|
+
if (this.tracker.uploads().find(u => u.id === item.id)?.status !== "uploading")
|
|
1213
1285
|
return;
|
|
1214
|
-
this.tracker.setProgress(
|
|
1286
|
+
this.tracker.setProgress(item.id, bytesTotal ? (bytesUploaded / bytesTotal) * 100 : 0);
|
|
1215
1287
|
},
|
|
1216
1288
|
success: () => {
|
|
1217
1289
|
// Bytes are in; indexing is now the server's business, so the poller owns
|
|
1218
1290
|
// this document from here. Nothing left to resume.
|
|
1219
|
-
this.noteHealthy(
|
|
1220
|
-
this.uploads.delete(
|
|
1221
|
-
this.markIndexing(
|
|
1291
|
+
this.noteHealthy(item.id);
|
|
1292
|
+
this.uploads.delete(item.id);
|
|
1293
|
+
this.markIndexing(item);
|
|
1222
1294
|
this.poller.start();
|
|
1223
1295
|
},
|
|
1224
1296
|
failed: () => {
|
|
1225
1297
|
// The handle is kept: the retry budget is spent, but the upload is not lost —
|
|
1226
1298
|
// `retry()` can pick it up again and tus resumes from the server's offset.
|
|
1227
|
-
this.clearRetryNotice(
|
|
1228
|
-
this.inFlight.delete(
|
|
1229
|
-
this.markError(
|
|
1299
|
+
this.clearRetryNotice(item.id);
|
|
1300
|
+
this.inFlight.delete(item.id);
|
|
1301
|
+
this.markError(item, { retryable: !entry.refused });
|
|
1230
1302
|
}
|
|
1231
1303
|
}
|
|
1232
1304
|
})
|
|
1233
1305
|
};
|
|
1234
|
-
this.uploads.set(
|
|
1306
|
+
this.uploads.set(item.id, entry);
|
|
1235
1307
|
entry.handle.begin();
|
|
1236
1308
|
}
|
|
1237
|
-
// ── what a
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
keyOf(document) {
|
|
1244
|
-
return document.jobId ? `${document.jobId}/${document.name}` : document.id;
|
|
1245
|
-
}
|
|
1246
|
-
/** The id the workspace store holds for the file followed under `key`. */
|
|
1247
|
-
storedId(workspaceId, key) {
|
|
1248
|
-
return this.workspaces.documentsOf(workspaceId).find(d => this.keyOf(d) === key)?.id ?? key;
|
|
1249
|
-
}
|
|
1250
|
-
patchStored(workspaceId, key, patch) {
|
|
1251
|
-
this.workspaces.patchDocument(workspaceId, this.storedId(workspaceId, key), patch);
|
|
1252
|
-
}
|
|
1253
|
-
markIndexing(document) {
|
|
1254
|
-
const key = this.keyOf(document);
|
|
1255
|
-
this.patchStored(document.workspaceId, key, { status: "indexing" });
|
|
1256
|
-
this.tracker.setIndexing(key);
|
|
1257
|
-
}
|
|
1258
|
-
markComplete(document) {
|
|
1259
|
-
const key = this.keyOf(document);
|
|
1260
|
-
this.patchStored(document.workspaceId, key, { status: null });
|
|
1261
|
-
this.tracker.setDone(key);
|
|
1262
|
-
}
|
|
1263
|
-
markError(document, options = {}) {
|
|
1264
|
-
const key = this.keyOf(document);
|
|
1265
|
-
this.patchStored(document.workspaceId, key, { status: "error" });
|
|
1266
|
-
this.tracker.setError(key, options);
|
|
1309
|
+
// ── what a file's state change writes ──
|
|
1310
|
+
markIndexing(item) {
|
|
1311
|
+
this.tracker.setIndexing(item.id);
|
|
1312
|
+
}
|
|
1313
|
+
markError(item, options = {}) {
|
|
1314
|
+
this.tracker.setError(item.id, options);
|
|
1267
1315
|
}
|
|
1268
1316
|
// ── following indexing ──
|
|
1269
1317
|
//
|
|
1270
|
-
//
|
|
1271
|
-
// - GET .../container/{id}/documents ->
|
|
1272
|
-
// - GET .../document-upload/status -> aggregate, errors[] and rejected[] PER JOB
|
|
1318
|
+
// - GET .../document-upload/status -> the tracker: aggregate, errors[] and rejected[] PER JOB
|
|
1319
|
+
// - GET .../container/{id}/documents -> the workspace's list, which is the index and nothing else
|
|
1273
1320
|
async pollUploadStatus(signal) {
|
|
1274
1321
|
try {
|
|
1275
1322
|
const result = await this.runPollTick(signal);
|
|
@@ -1294,84 +1341,51 @@ class UploadService {
|
|
|
1294
1341
|
// the tracker still follows a batch after the user navigates away from it.
|
|
1295
1342
|
const response = await fetchUploadStatus(workspaceId ? { containerId: workspaceId } : {}, { signal });
|
|
1296
1343
|
const jobs = allJobs(response);
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1344
|
+
// Jobs started elsewhere come first, so that `applyJobs` finds their rows to update.
|
|
1345
|
+
const remote = await this.remoteJobs(response, signal);
|
|
1346
|
+
let changed = this.tracker.applyRemoteJobs(remote);
|
|
1347
|
+
changed = this.tracker.applyJobs(jobs) || changed;
|
|
1301
1348
|
this.settleFinishedUploads();
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
changed = changed || applied.changed;
|
|
1308
|
-
}
|
|
1349
|
+
const pending = jobs.some(job => !isJobTerminal(job));
|
|
1350
|
+
// The list follows the index, not the status: a file shows up once the listing returns it.
|
|
1351
|
+
// The last tick of a loop reads it too, which is when a file that just got indexed comes in.
|
|
1352
|
+
if (workspaceId)
|
|
1353
|
+
changed = (await this.refreshListing(workspaceId, signal)) || changed;
|
|
1309
1354
|
return { pending: pending || this.inFlight.size > 0, changed };
|
|
1310
1355
|
}
|
|
1311
1356
|
/**
|
|
1312
|
-
* The
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1357
|
+
* The jobs of the response the panel should show although this client did not send
|
|
1358
|
+
* them. The server decides which ones the response holds — those in progress or just
|
|
1359
|
+
* ended, on the workspaces the user can write to — so none is filtered by role or date
|
|
1360
|
+
* here; only, with a workspace open, those that belong to it are kept. Whether the user
|
|
1361
|
+
* started one is read from `initiatedBy`, which is their `userId`, so it holds after a
|
|
1362
|
+
* page reload too.
|
|
1315
1363
|
*/
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1364
|
+
async remoteJobs(response, signal) {
|
|
1365
|
+
const me = (await this.currentUser.whenReady())?.id;
|
|
1366
|
+
if (signal.aborted)
|
|
1367
|
+
return [];
|
|
1368
|
+
const entries = [];
|
|
1369
|
+
for (const { job, containerId, containerName } of containerJobs(response)) {
|
|
1370
|
+
if (this.focusedWorkspaceId && this.focusedWorkspaceId !== containerId)
|
|
1319
1371
|
continue;
|
|
1320
|
-
this.
|
|
1321
|
-
|
|
1372
|
+
const workspaceName = this.workspaces.workspace(containerId)?.name ?? containerName;
|
|
1373
|
+
entries.push({ job, workspaceId: containerId, workspaceName, mine: !!me && job.initiatedBy === me });
|
|
1322
1374
|
}
|
|
1375
|
+
return entries;
|
|
1323
1376
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (!document)
|
|
1336
|
-
continue;
|
|
1337
|
-
const key = this.keyOf(document);
|
|
1338
|
-
// From now on the store holds the id the server will recognise: delete and move send it.
|
|
1339
|
-
if (document.id !== wire.id)
|
|
1340
|
-
this.workspaces.patchDocument(workspaceId, document.id, { id: wire.id });
|
|
1341
|
-
// While tus is still sending, the local byte-level progress is ahead of anything
|
|
1342
|
-
// the server can report: it owns the percentage until tus hands over. An error
|
|
1343
|
-
// is the one thing worth taking early.
|
|
1344
|
-
if (this.inFlight.has(key) && document.status === "uploading") {
|
|
1345
|
-
if (next === "error") {
|
|
1346
|
-
this.inFlight.delete(key);
|
|
1347
|
-
this.markError(document);
|
|
1348
|
-
changed = true;
|
|
1349
|
-
}
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
if (next === "indexing" && document.status !== "indexing") {
|
|
1353
|
-
this.markIndexing(document);
|
|
1354
|
-
changed = true;
|
|
1355
|
-
}
|
|
1356
|
-
else if (next === null && document.status !== null) {
|
|
1357
|
-
this.inFlight.delete(key);
|
|
1358
|
-
this.markComplete(document);
|
|
1359
|
-
changed = true;
|
|
1360
|
-
}
|
|
1361
|
-
else if (next === "error" && document.status !== "error") {
|
|
1362
|
-
this.inFlight.delete(key);
|
|
1363
|
-
this.markError(document);
|
|
1364
|
-
changed = true;
|
|
1365
|
-
}
|
|
1366
|
-
else if (next === "uploading" && typeof wire.percent === "number" && document.percent !== wire.percent) {
|
|
1367
|
-
// Not ours to transfer (another user's upload, or our own seen again after a
|
|
1368
|
-
// reload): the server's percentage is all there is.
|
|
1369
|
-
this.workspaces.patchDocument(workspaceId, wire.id, { percent: wire.percent });
|
|
1370
|
-
this.tracker.setProgress(key, wire.percent);
|
|
1371
|
-
changed = true;
|
|
1372
|
-
}
|
|
1377
|
+
/** Re-reads the workspace's listing. @returns Whether it differs from what was shown. */
|
|
1378
|
+
async refreshListing(workspaceId, signal) {
|
|
1379
|
+
const before = JSON.stringify(this.workspaces.documentsOf(workspaceId));
|
|
1380
|
+
await this.workspaces.loadDocuments(workspaceId, signal);
|
|
1381
|
+
return JSON.stringify(this.workspaces.documentsOf(workspaceId)) !== before;
|
|
1382
|
+
}
|
|
1383
|
+
/** The job-level status can finish a file this client was still holding the poller for: release it. */
|
|
1384
|
+
settleFinishedUploads() {
|
|
1385
|
+
for (const upload of this.tracker.uploads()) {
|
|
1386
|
+
if (upload.status === "done")
|
|
1387
|
+
this.inFlight.delete(upload.id);
|
|
1373
1388
|
}
|
|
1374
|
-
return { pending, changed };
|
|
1375
1389
|
}
|
|
1376
1390
|
// ── connection loss & session recovery ──
|
|
1377
1391
|
//
|
|
@@ -3147,6 +3161,7 @@ class WorkspaceList {
|
|
|
3147
3161
|
createRequested = output();
|
|
3148
3162
|
store = inject(WorkspacesStore);
|
|
3149
3163
|
dialogs = inject(DialogService);
|
|
3164
|
+
uploads = inject(UploadService);
|
|
3150
3165
|
transloco = inject(TranslocoService);
|
|
3151
3166
|
tabs = TABS;
|
|
3152
3167
|
viewMode = signal("grid", ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
|
|
@@ -3174,9 +3189,13 @@ class WorkspaceList {
|
|
|
3174
3189
|
const changed = JSON.stringify(this.store.workspaces()) !== before;
|
|
3175
3190
|
if (changed)
|
|
3176
3191
|
this.selection.exit(); // the ticks belonged to the list that was there before
|
|
3192
|
+
// An upload someone else is running on a workspace listed here: only the status API tells.
|
|
3193
|
+
// It stops by itself after a tick when nothing is moving.
|
|
3194
|
+
this.uploads.startPolling();
|
|
3177
3195
|
return changed;
|
|
3178
3196
|
}, { intervalMs: 20_000, maxIntervalMs: 120_000 });
|
|
3179
3197
|
constructor() {
|
|
3198
|
+
this.uploads.startPolling();
|
|
3180
3199
|
void this.load();
|
|
3181
3200
|
}
|
|
3182
3201
|
/** Shows what is cached at once and refreshes behind it, or loads for the first time. */
|
|
@@ -3511,6 +3530,15 @@ function truncateName(name, max) {
|
|
|
3511
3530
|
}
|
|
3512
3531
|
return `${name.slice(0, max - 1)}…`;
|
|
3513
3532
|
}
|
|
3533
|
+
/**
|
|
3534
|
+
* What to call a tracked row: the file's name — or, for a job started elsewhere whose files
|
|
3535
|
+
* the status API does not list, whose job it is.
|
|
3536
|
+
*/
|
|
3537
|
+
function uploadLabel(upload, max) {
|
|
3538
|
+
if (upload.name)
|
|
3539
|
+
return { text: truncateName(upload.name, max) };
|
|
3540
|
+
return trackerMessage(upload.remote?.mine ? "ownUpload" : "remoteUpload");
|
|
3541
|
+
}
|
|
3514
3542
|
/** `m:ss` for a duration in ms, clamped at 0 so a late tick cannot go negative. */
|
|
3515
3543
|
function formatClock(ms) {
|
|
3516
3544
|
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
@@ -5792,10 +5820,10 @@ class WorkspaceDetail {
|
|
|
5792
5820
|
/** The background refresh found the workspace gone from the server (deleted elsewhere). */
|
|
5793
5821
|
deletedElsewhere = signal(false, ...(ngDevMode ? [{ debugName: "deletedElsewhere" }] : []));
|
|
5794
5822
|
workspace = computed(() => this.store.workspace(this.workspaceId()), ...(ngDevMode ? [{ debugName: "workspace" }] : []));
|
|
5795
|
-
//
|
|
5796
|
-
// belong to the upload tracker, not to the workspace. A document with no
|
|
5797
|
-
// all is one the server does not track — it is simply there, so it stays listed.
|
|
5798
|
-
documents = computed(() => this.store.documentsOf(this.workspaceId()).filter(d => d.status
|
|
5823
|
+
// The list is the index: only a document the listing returns as indexed is shown. Transfers,
|
|
5824
|
+
// indexing and failures belong to the upload tracker, not to the workspace. A document with no
|
|
5825
|
+
// status at all is one the server does not track — it is simply there, so it stays listed.
|
|
5826
|
+
documents = computed(() => this.store.documentsOf(this.workspaceId()).filter(d => !d.status), ...(ngDevMode ? [{ debugName: "documents" }] : []));
|
|
5799
5827
|
previewed = computed(() => this.documents().find(d => d.id === this.previewedDocumentId()) ?? null, ...(ngDevMode ? [{ debugName: "previewed" }] : []));
|
|
5800
5828
|
/** The width the user dragged the preview to, if they did. */
|
|
5801
5829
|
userPreviewWidth = signal(null, ...(ngDevMode ? [{ debugName: "userPreviewWidth" }] : []));
|
|
@@ -5848,8 +5876,9 @@ class WorkspaceDetail {
|
|
|
5848
5876
|
return false; // the user moved to another workspace meanwhile
|
|
5849
5877
|
const documentsChanged = JSON.stringify(this.store.documentsOf(id)) !== beforeDocuments;
|
|
5850
5878
|
const workspaceChanged = JSON.stringify(this.store.workspace(id)) !== beforeWorkspace;
|
|
5851
|
-
|
|
5852
|
-
|
|
5879
|
+
// Every tick, changed or not: an upload someone else just started shows no document yet, and the
|
|
5880
|
+
// tracker only learns of it from the status API. It stops by itself after a tick when nothing is moving.
|
|
5881
|
+
this.uploads.watch(id);
|
|
5853
5882
|
return documentsChanged || workspaceChanged;
|
|
5854
5883
|
}, { intervalMs: 15_000, maxIntervalMs: 90_000 });
|
|
5855
5884
|
constructor() {
|
|
@@ -7114,6 +7143,10 @@ class UploadTrackerSection {
|
|
|
7114
7143
|
const uploads = this.uploads();
|
|
7115
7144
|
return uploads.length ? Math.round(uploads.reduce((sum, u) => sum + u.percent, 0) / uploads.length) : 0;
|
|
7116
7145
|
}, ...(ngDevMode ? [{ debugName: "average" }] : []));
|
|
7146
|
+
/** The translation key standing for a row with no file name: a job started elsewhere. Only read when `name` is empty. */
|
|
7147
|
+
label(upload) {
|
|
7148
|
+
return upload.remote?.mine ? "documentWorkspaces.tracker.ownUpload" : "documentWorkspaces.tracker.remoteUpload";
|
|
7149
|
+
}
|
|
7117
7150
|
round(value) {
|
|
7118
7151
|
return Math.round(value);
|
|
7119
7152
|
}
|
|
@@ -7190,8 +7223,8 @@ class UploadTrackerSection {
|
|
|
7190
7223
|
<span
|
|
7191
7224
|
class="min-w-0 flex-1 truncate text-xs text-(--font-neutral-black)"
|
|
7192
7225
|
[class.text-(--font-error-base)]="upload.status === 'error' || upload.status === 'rejected'"
|
|
7193
|
-
[attr.title]="upload.name"
|
|
7194
|
-
>{{ upload.name }}</span
|
|
7226
|
+
[attr.title]="upload.name || (label(upload) | transloco)"
|
|
7227
|
+
>{{ upload.name || (label(upload) | transloco) }}</span
|
|
7195
7228
|
>
|
|
7196
7229
|
@if (upload.message) {
|
|
7197
7230
|
<span class="max-w-40 shrink truncate text-[11px] text-(--font-neutral-muted)" [attr.title]="upload.message">{{ upload.message }}</span>
|
|
@@ -7200,9 +7233,12 @@ class UploadTrackerSection {
|
|
|
7200
7233
|
@switch (upload.status) {
|
|
7201
7234
|
@case ("uploading") {
|
|
7202
7235
|
<span class="shrink-0 text-xs text-(--font-neutral-muted) tabular-nums">{{ round(upload.percent) }}%</span>
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7236
|
+
<!-- A transfer this client does not hold cannot be stopped from here, and nor can it be closed while it goes on. -->
|
|
7237
|
+
@if (!upload.remote) {
|
|
7238
|
+
<button class="w-6" variant="ghost" size="xs" [iconOnly]="true" [attr.aria-label]="'documentWorkspaces.tracker.cancelUpload' | transloco" [tooltip]="'documentWorkspaces.tracker.cancelUpload' | transloco" (click)="discard.emit(upload.id)">
|
|
7239
|
+
<x-mark-icon />
|
|
7240
|
+
</button>
|
|
7241
|
+
}
|
|
7206
7242
|
}
|
|
7207
7243
|
@case ("indexing") {
|
|
7208
7244
|
<!-- No percentage here: the server only reports one per job, shown by the bar above. -->
|
|
@@ -7332,8 +7368,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
7332
7368
|
<span
|
|
7333
7369
|
class="min-w-0 flex-1 truncate text-xs text-(--font-neutral-black)"
|
|
7334
7370
|
[class.text-(--font-error-base)]="upload.status === 'error' || upload.status === 'rejected'"
|
|
7335
|
-
[attr.title]="upload.name"
|
|
7336
|
-
>{{ upload.name }}</span
|
|
7371
|
+
[attr.title]="upload.name || (label(upload) | transloco)"
|
|
7372
|
+
>{{ upload.name || (label(upload) | transloco) }}</span
|
|
7337
7373
|
>
|
|
7338
7374
|
@if (upload.message) {
|
|
7339
7375
|
<span class="max-w-40 shrink truncate text-[11px] text-(--font-neutral-muted)" [attr.title]="upload.message">{{ upload.message }}</span>
|
|
@@ -7342,9 +7378,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
7342
7378
|
@switch (upload.status) {
|
|
7343
7379
|
@case ("uploading") {
|
|
7344
7380
|
<span class="shrink-0 text-xs text-(--font-neutral-muted) tabular-nums">{{ round(upload.percent) }}%</span>
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7381
|
+
<!-- A transfer this client does not hold cannot be stopped from here, and nor can it be closed while it goes on. -->
|
|
7382
|
+
@if (!upload.remote) {
|
|
7383
|
+
<button class="w-6" variant="ghost" size="xs" [iconOnly]="true" [attr.aria-label]="'documentWorkspaces.tracker.cancelUpload' | transloco" [tooltip]="'documentWorkspaces.tracker.cancelUpload' | transloco" (click)="discard.emit(upload.id)">
|
|
7384
|
+
<x-mark-icon />
|
|
7385
|
+
</button>
|
|
7386
|
+
}
|
|
7348
7387
|
}
|
|
7349
7388
|
@case ("indexing") {
|
|
7350
7389
|
<!-- No percentage here: the server only reports one per job, shown by the bar above. -->
|
|
@@ -7385,7 +7424,9 @@ const SECTION_ORDER = ["uploading", "indexing", "done", "error", "rejected"];
|
|
|
7385
7424
|
const FLASH_MS = 3000;
|
|
7386
7425
|
const PEEK_MS = 3500;
|
|
7387
7426
|
const WORKSPACE_COMPLETE_MS = 3500;
|
|
7388
|
-
|
|
7427
|
+
// How long a panel whose uploads are all over stays: long enough to read the outcome, even of an
|
|
7428
|
+
// upload someone else started and the user was not watching.
|
|
7429
|
+
const AUTO_DISMISS_MS = FINISHED_UPLOAD_VISIBLE_MS;
|
|
7389
7430
|
const DISMISS_ANIMATION_MS = 320;
|
|
7390
7431
|
const HOVER_LEAVE_MS = 250;
|
|
7391
7432
|
// The text on the strip leaves, then the new one comes: half a transition each.
|
|
@@ -7472,7 +7513,7 @@ class UploadTracker {
|
|
|
7472
7513
|
const failed = this.store.failed();
|
|
7473
7514
|
const severalWorkspaces = this.store.workspaces().length > 1;
|
|
7474
7515
|
// One file is named, several are counted.
|
|
7475
|
-
const label = (uploads, key, max) => uploads.length === 1 ?
|
|
7516
|
+
const label = (uploads, key, max) => uploads.length === 1 ? uploadLabel(uploads[0], max) : trackerMessage(key, { count: uploads.length });
|
|
7476
7517
|
const chips = [];
|
|
7477
7518
|
if (groups.uploading.length)
|
|
7478
7519
|
chips.push({ kind: "uploading", busy: true, message: trackerMessage("uploadingCount", { count: groups.uploading.length }) });
|
|
@@ -7492,11 +7533,11 @@ class UploadTracker {
|
|
|
7492
7533
|
return peek.message;
|
|
7493
7534
|
const failed = this.store.failed();
|
|
7494
7535
|
if (failed.length)
|
|
7495
|
-
return failed.length === 1 ?
|
|
7536
|
+
return failed.length === 1 ? uploadLabel(failed[0], 22) : trackerMessage("errorCount", { count: failed.length });
|
|
7496
7537
|
const { uploading, indexing, done } = this.store.byStatus();
|
|
7497
7538
|
const active = uploading.length + indexing.length;
|
|
7498
7539
|
if (done.length && !active)
|
|
7499
|
-
return done.length === 1 ?
|
|
7540
|
+
return done.length === 1 ? uploadLabel(done[0], 22) : trackerMessage("indexedCount", { count: done.length });
|
|
7500
7541
|
return trackerMessage("fileCount", { count: active });
|
|
7501
7542
|
}, ...(ngDevMode ? [{ debugName: "pillMessage" }] : []));
|
|
7502
7543
|
state = computed(() => (this.mode() !== "minimized" ? "normal" : this.hover() ? "minimized-hover" : "minimized"), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
@@ -7526,10 +7567,12 @@ class UploadTracker {
|
|
|
7526
7567
|
this.completeWorkspace(complete);
|
|
7527
7568
|
});
|
|
7528
7569
|
});
|
|
7529
|
-
// Everything
|
|
7570
|
+
// Everything is over: hold the outcome for a while, then go away — unless a failure of the
|
|
7571
|
+
// user's own is there to be read, which stays until they close it. A new upload cancels
|
|
7572
|
+
// this (nothing is settled any more), and the delay starts afresh when it ends.
|
|
7530
7573
|
effect(() => {
|
|
7531
|
-
const
|
|
7532
|
-
untracked(() => (
|
|
7574
|
+
const leaves = this.store.settled() && !this.store.authorFailed();
|
|
7575
|
+
untracked(() => (leaves ? this.startAutoDismiss() : this.cancelAutoDismiss()));
|
|
7533
7576
|
});
|
|
7534
7577
|
// The strip of a minimized panel says what is going on; when that changes, the text is swapped, not cut.
|
|
7535
7578
|
effect(() => {
|
|
@@ -7590,9 +7633,9 @@ class UploadTracker {
|
|
|
7590
7633
|
}
|
|
7591
7634
|
this.mode.update(mode => (mode === "expanded" ? "inline" : "expanded"));
|
|
7592
7635
|
}
|
|
7593
|
-
/** Minimizes while there is work going on; once everything is over, closes for good. */
|
|
7636
|
+
/** Minimizes while there is work going on; once everything is over (failures included), closes for good. */
|
|
7594
7637
|
dismiss() {
|
|
7595
|
-
if (this.store.
|
|
7638
|
+
if (this.store.settled() || this.mode() === "minimized") {
|
|
7596
7639
|
this.reset();
|
|
7597
7640
|
}
|
|
7598
7641
|
else {
|
|
@@ -7648,14 +7691,14 @@ class UploadTracker {
|
|
|
7648
7691
|
const failed = finished.filter(isFailed);
|
|
7649
7692
|
if (done.length) {
|
|
7650
7693
|
const last = done[done.length - 1];
|
|
7651
|
-
const message =
|
|
7694
|
+
const message = uploadLabel(last, this.mode() === "minimized" ? 22 : 30);
|
|
7652
7695
|
if (this.mode() === "minimized")
|
|
7653
7696
|
this.notify(message, "success");
|
|
7654
7697
|
else if (this.mode() === "inline" && !this.store.allDone())
|
|
7655
7698
|
this.showFlash(message);
|
|
7656
7699
|
}
|
|
7657
7700
|
if (failed.length && this.mode() === "minimized") {
|
|
7658
|
-
this.notify(
|
|
7701
|
+
this.notify(uploadLabel(failed[failed.length - 1], 22), "error");
|
|
7659
7702
|
}
|
|
7660
7703
|
}
|
|
7661
7704
|
/**
|
|
@@ -7710,8 +7753,9 @@ class UploadTracker {
|
|
|
7710
7753
|
startAutoDismiss() {
|
|
7711
7754
|
if (this.dismissTimer)
|
|
7712
7755
|
return;
|
|
7713
|
-
// The success message is in the header: make sure it can be seen.
|
|
7714
|
-
this.
|
|
7756
|
+
// The success message is in the header: make sure it can be seen. Failures are in the sections: leave those open.
|
|
7757
|
+
if (this.store.allDone())
|
|
7758
|
+
this.mode.set("inline");
|
|
7715
7759
|
this.dismissTimer = this.replaceTimer(null, AUTO_DISMISS_MS, () => {
|
|
7716
7760
|
this.dismissTimer = null;
|
|
7717
7761
|
this.dismissing.set(true);
|
|
@@ -7847,8 +7891,8 @@ class UploadTracker {
|
|
|
7847
7891
|
variant="ghost"
|
|
7848
7892
|
size="xs"
|
|
7849
7893
|
[iconOnly]="true"
|
|
7850
|
-
[attr.aria-label]="(mode() === 'minimized' || store.
|
|
7851
|
-
[tooltip]="(mode() === 'minimized' || store.
|
|
7894
|
+
[attr.aria-label]="(mode() === 'minimized' || store.settled() ? 'documentWorkspaces.common.close' : 'documentWorkspaces.common.minimize') | transloco"
|
|
7895
|
+
[tooltip]="(mode() === 'minimized' || store.settled() ? 'documentWorkspaces.common.close' : 'documentWorkspaces.common.minimize') | transloco"
|
|
7852
7896
|
(click)="dismiss(); $event.stopPropagation()">
|
|
7853
7897
|
<x-mark-icon />
|
|
7854
7898
|
</button>
|
|
@@ -8053,8 +8097,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
8053
8097
|
variant="ghost"
|
|
8054
8098
|
size="xs"
|
|
8055
8099
|
[iconOnly]="true"
|
|
8056
|
-
[attr.aria-label]="(mode() === 'minimized' || store.
|
|
8057
|
-
[tooltip]="(mode() === 'minimized' || store.
|
|
8100
|
+
[attr.aria-label]="(mode() === 'minimized' || store.settled() ? 'documentWorkspaces.common.close' : 'documentWorkspaces.common.minimize') | transloco"
|
|
8101
|
+
[tooltip]="(mode() === 'minimized' || store.settled() ? 'documentWorkspaces.common.close' : 'documentWorkspaces.common.minimize') | transloco"
|
|
8058
8102
|
(click)="dismiss(); $event.stopPropagation()">
|
|
8059
8103
|
<x-mark-icon />
|
|
8060
8104
|
</button>
|