Upload documents
Stage files, send them to a workspace as one batch over tus, and follow each one from upload to indexed — with retries and session recovery.
Uploading goes through UploadService: it sends the files to the tus endpoint, then follows the indexing of each
one until the document is in the workspace. <upload-tracker> shows that progress, and UploadDocuments is the
dialog that stages the files first.
Minimal example
import { Component } from "@angular/core";
import { UploadDocuments, UploadTracker, type Workspace } from "@sinequa/atomic-angular/document-workspaces";
@Component({
selector: "upload-example",
imports: [UploadTracker],
template: `
<button (click)="upload()">Upload documents…</button>
<upload-tracker />
`,
})
export class UploadExample {
/** A workspace the user can write to, e.g. the one a detail view is showing. */
workspace!: Pick<Workspace, "id" | "name" | "currentUserRole">;
upload() {
void UploadDocuments.call({ workspace: this.workspace, endpoint: "/tus" });
}
}UploadDocuments.call() without an injector needs provideCallable() from @sinequa/galactik in the providers.
The dialog refuses to open for someone who cannot write to the workspace and ends with dialog-cancel.
How it works
The dialog only stages: nothing leaves the browser until "Upload" is pressed, so the whole selection goes out as
one ingest job instead of each drop starting its own. Dismissing the dialog any other way discards the selection.
Two files with the same name collapse into one, because a document is addressed as <jobId>/<fileName> and two
files sharing a name inside a job would collide on the server.
While a job is moving, the service matches the server's documents listing to the tracked files by job and file name
(the server names a document /DocUpload/DocUpload/|<containerId>/<fileName>, not <jobId>/<fileName>), and a file
takes the server's id as soon as the listing shows it — that is the id a delete needs. sent_for_indexing counts as
indexing, and a file completes when its job does. The server only reports indexing per job (indexed files out of
total), so the tracker's uploading and indexing bars show the overall progress of the phase (UploadTrackerStore.progress()),
and files being indexed show animated dots rather than the job's percentage. The workspace detail only lists documents
that are indexed or being indexed: transfers and failures stay in the tracker.
The tracker only reads UploadTrackerStore, so it shows whatever is in it — which is also how you can drive it in
a test or a demo, without any network.
Recipes
Send files without the dialog
import { Component, inject } from "@angular/core";
import { UploadService } from "@sinequa/atomic-angular/document-workspaces";
@Component({
selector: "drop-example",
template: `<input type="file" multiple (change)="send($event)" />`,
})
export class DropExample {
private readonly uploads = inject(UploadService);
send(event: Event) {
const files = Array.from((event.target as HTMLInputElement).files ?? []);
this.uploads.addFiles({ id: "w1", name: "Legal" }, files, { endpoint: "/tus" });
}
}addFiles returns the tracked id of each file, in order. cancel(id), retry(id) and resumeStalled() act on
tracked uploads. watch(workspaceId) tells the service which workspace is open, so that the status polling is scoped
to it and starts when that workspace already holds documents that are not finished — a page reload loses every
local upload handle, and the listing is then the only way to know. <workspace-detail> calls it for the workspace it shows.
Pitfalls
What's next
List and detail
Lay out the workspace list and a single workspace yourself — each says what the user asked for, and your host decides where it goes.
Share a workspace
Open the "Manage access" dialog to give people the owner or reader role on a workspace, change a role or take access away — each change is saved at once.