@sinequa/atomic-angular 1.15.1 → 1.16.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.
@@ -211,7 +211,8 @@ declare const WorkspacesStore: _angular_core.Type<{
211
211
  create: ({ name, description, tags, owners, readers }: CreateWorkspaceInput) => Promise<Workspace>;
212
212
  update: (id: string, patch: UpdateContainerInput) => Promise<Workspace | null>;
213
213
  remove: (ids: string[]) => Promise<RemoveWorkspacesResult>;
214
- loadDocuments: (workspaceId: string) => Promise<WorkspaceDocument[]>;
214
+ forget: (id: string) => void;
215
+ loadDocuments: (workspaceId: string, signal?: AbortSignal) => Promise<WorkspaceDocument[]>;
215
216
  loadMoreDocuments: (workspaceId: string) => Promise<WorkspaceDocument[]>;
216
217
  addDocuments: (workspaceId: string, documents: WorkspaceDocument[]) => void;
217
218
  discardDocuments: (workspaceId: string, documentIds: string[]) => void;
@@ -868,7 +869,9 @@ type ViewMode$1 = "grid" | "list";
868
869
  * Each time it is shown it refreshes: workspaces created by others since the last
869
870
  * time appear, and the document counts match the server. The list already on
870
871
  * screen stays until the answer lands, and a failure leaves it untouched behind
871
- * an inline error.
872
+ * an inline error. While it stays shown, it keeps polling in the background
873
+ * (`injectLiveRefresh`) so a workspace created by someone else — one the current
874
+ * user only reads, say — appears on its own, with no action needed.
872
875
  */
873
876
  declare class WorkspaceList {
874
877
  /** A workspace was chosen for opening. */
@@ -897,7 +900,8 @@ declare class WorkspaceList {
897
900
  create: ({ name, description, tags, owners, readers }: _sinequa_atomic_angular_document_workspaces.CreateWorkspaceInput) => Promise<Workspace>;
898
901
  update: (id: string, patch: _sinequa_atomic.UpdateContainerInput) => Promise<Workspace | null>;
899
902
  remove: (ids: string[]) => Promise<_sinequa_atomic_angular_document_workspaces.RemoveWorkspacesResult>;
900
- loadDocuments: (workspaceId: string) => Promise<_sinequa_atomic_angular_document_workspaces.WorkspaceDocument[]>;
903
+ forget: (id: string) => void;
904
+ loadDocuments: (workspaceId: string, signal?: AbortSignal) => Promise<_sinequa_atomic_angular_document_workspaces.WorkspaceDocument[]>;
901
905
  loadMoreDocuments: (workspaceId: string) => Promise<_sinequa_atomic_angular_document_workspaces.WorkspaceDocument[]>;
902
906
  addDocuments: (workspaceId: string, documents: _sinequa_atomic_angular_document_workspaces.WorkspaceDocument[]) => void;
903
907
  discardDocuments: (workspaceId: string, documentIds: string[]) => void;
@@ -936,6 +940,7 @@ declare class WorkspaceList {
936
940
  clearQuery: () => void;
937
941
  };
938
942
  protected readonly selection: _sinequa_atomic_angular_document_workspaces.Selection<Workspace>;
943
+ private readonly liveRefresh;
939
944
  constructor();
940
945
  /** Shows what is cached at once and refreshes behind it, or loads for the first time. */
941
946
  protected load(): Promise<void>;
@@ -1123,6 +1128,9 @@ type ViewMode = "grid" | "list";
1123
1128
  * shown: it says what the user asked for (`back`, `deleted`, `askFollowUp`) and whoever
1124
1129
  * hosts it decides. The document open in the preview is the `previewedDocumentId` model.
1125
1130
  *
1131
+ * While a workspace stays open, it keeps polling its documents in the background
1132
+ * (`injectLiveRefresh`), so a document another user adds to it shows up on its own.
1133
+ *
1126
1134
  * Only someone who can write sees the edit, share, delete, upload and select controls.
1127
1135
  */
1128
1136
  declare class WorkspaceDetail {
@@ -1153,6 +1161,8 @@ declare class WorkspaceDetail {
1153
1161
  protected readonly refreshing: _angular_core.WritableSignal<boolean>;
1154
1162
  protected readonly loadingMore: _angular_core.WritableSignal<boolean>;
1155
1163
  protected readonly loadMoreError: _angular_core.WritableSignal<string | null>;
1164
+ /** The background refresh found the workspace gone from the server (deleted elsewhere). */
1165
+ protected readonly deletedElsewhere: _angular_core.WritableSignal<boolean>;
1156
1166
  protected readonly workspace: _angular_core.Signal<Workspace | undefined>;
1157
1167
  protected readonly documents: _angular_core.Signal<WorkspaceDocument[]>;
1158
1168
  protected readonly previewed: _angular_core.Signal<WorkspaceDocument | null>;
@@ -1167,8 +1177,15 @@ declare class WorkspaceDetail {
1167
1177
  protected readonly filtered: _angular_core.Signal<WorkspaceDocument[]>;
1168
1178
  private readonly writable;
1169
1179
  protected readonly selection: _sinequa_atomic_angular_document_workspaces.Selection<WorkspaceDocument>;
1180
+ private readonly liveRefresh;
1170
1181
  constructor();
1171
1182
  private open;
1183
+ /**
1184
+ * Leaving from here: once the background refresh found the workspace gone,
1185
+ * the cache still holds it (so its name kept showing) — drop it now, so a
1186
+ * stale card does not show it back on the list.
1187
+ */
1188
+ protected leave(): void;
1172
1189
  /** Reads the workspace (when it is not cached) and its documents, for the workspace currently shown. */
1173
1190
  protected load(): Promise<void>;
1174
1191
  /** Reads the documents again without leaving the view; a failure leaves the ones shown as they are. */
@@ -7,7 +7,7 @@ import { Upload } from 'tus-js-client';
7
7
  import { toSignal } from '@angular/core/rxjs-interop';
8
8
  import { TranslocoService, TranslocoPipe, provideTranslocoScope } from '@jsverse/transloco';
9
9
  import { Temporal } from 'temporal-polyfill';
10
- import { DialogComponent, ButtonComponent, DialogContentComponent, DialogFooterComponent, DialogTitleComponent, FolderIcon, UserIcon, UserRoundIcon, FileIcon, TooltipDirective, TrashIcon, DialogService, ListIcon, MagnifyingGlassIcon, PlusIcon, XMarkIcon, FileSpreadsheetIcon, FileTextIcon, UploadIcon, DialogHeaderComponent, ChatPlusIcon, ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, EyeIcon, KeyIcon, MenuComponent, MenuContentComponent, MenuItemComponent, PenToSquareIcon, RefreshIcon, TagIcon, CheckIcon, CircleXIcon, SyncIcon } from '@sinequa/ui';
10
+ import { DialogComponent, ButtonComponent, DialogContentComponent, DialogFooterComponent, DialogTitleComponent, FolderIcon, UserIcon, UserRoundIcon, FileIcon, TooltipDirective, TrashIcon, DialogService, ListIcon, MagnifyingGlassIcon, PlusIcon, XMarkIcon, FileSpreadsheetIcon, FileTextIcon, UploadIcon, FileSearchIcon, DialogHeaderComponent, ChatPlusIcon, ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, EyeIcon, KeyIcon, MenuComponent, MenuContentComponent, MenuItemComponent, PenToSquareIcon, RefreshIcon, TagIcon, CheckIcon, CircleXIcon, SyncIcon } from '@sinequa/ui';
11
11
  import { DomSanitizer } from '@angular/platform-browser';
12
12
  import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
13
13
 
@@ -164,8 +164,8 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
164
164
  };
165
165
  });
166
166
  };
167
- const loadDocuments = async (workspaceId) => {
168
- const res = await listContainerDocuments(workspaceId, { page: 1, pageSize: PAGE_SIZE });
167
+ const loadDocuments = async (workspaceId, signal) => {
168
+ const res = await listContainerDocuments(workspaceId, { page: 1, pageSize: PAGE_SIZE }, { signal });
169
169
  const documents = res.documents.map(mapContainerDocumentToWorkspaceDocument);
170
170
  patchState(store, state => ({
171
171
  documentPagination: { ...state.documentPagination, [workspaceId]: { page: res.page, total: res.total } }
@@ -314,6 +314,23 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
314
314
  }));
315
315
  return { succeeded: result.succeeded, failed: result.failed.map(f => ({ id: f.containerId, error: f.error })) };
316
316
  },
317
+ /**
318
+ * Local only — for when a read already proved a workspace is gone from the
319
+ * server (a 404), rather than this session deleting it: the detail view
320
+ * calls this once the user actually leaves a workspace its background
321
+ * refresh found deleted, instead of {@link remove}, which would call
322
+ * DELETE on something already deleted. Kept in the cache until then, so
323
+ * the view can still show its name while it explains what happened.
324
+ */
325
+ forget(id) {
326
+ patchState(store, state => ({
327
+ workspaces: state.workspaces.filter(w => w.id !== id),
328
+ pagination: state.workspaces.some(w => w.id === id) ? { ...state.pagination, total: Math.max(0, state.pagination.total - 1) } : state.pagination,
329
+ documents: Object.fromEntries(Object.entries(state.documents).filter(([wid]) => wid !== id)),
330
+ documentPagination: Object.fromEntries(Object.entries(state.documentPagination).filter(([wid]) => wid !== id)),
331
+ selectedIds: state.selectedIds.filter(selected => selected !== id)
332
+ }));
333
+ },
317
334
  loadDocuments,
318
335
  /** Without this, a workspace with more than one page of documents is silently truncated. */
319
336
  async loadMoreDocuments(workspaceId) {
@@ -2314,6 +2331,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
2314
2331
  args: [{ name: "highlightMatch" }]
2315
2332
  }] });
2316
2333
 
2334
+ /**
2335
+ * Keeps a view fresh while it stays open, without the user ever asking: `tick`
2336
+ * re-fetches whatever the view shows and reports whether the result differs
2337
+ * from before — the loop speeds back up on a change and backs off while idle,
2338
+ * exactly like {@link StatusPoller} already does for upload status.
2339
+ *
2340
+ * Paused while the browser reports no network (resumed the moment it is back),
2341
+ * and fully stopped once the injecting component is destroyed. The very first
2342
+ * scheduled tick is a no-op: whatever calls `start()` has normally just
2343
+ * fetched on its own, so re-fetching the same thing instantly would be
2344
+ * redundant — the first real poll happens one `intervalMs` later.
2345
+ *
2346
+ * Must be called from an injection context.
2347
+ */
2348
+ function injectLiveRefresh(tick, options = {}) {
2349
+ assertInInjectionContext(injectLiveRefresh);
2350
+ let shouldRun = false;
2351
+ let first = true;
2352
+ const poller = new StatusPoller({
2353
+ intervalMs: options.intervalMs,
2354
+ maxIntervalMs: options.maxIntervalMs,
2355
+ tick: async (signal) => {
2356
+ if (first) {
2357
+ first = false;
2358
+ return { pending: true, changed: true };
2359
+ }
2360
+ return { pending: true, changed: await tick(signal) };
2361
+ }
2362
+ });
2363
+ if (typeof window !== "undefined") {
2364
+ const resume = () => {
2365
+ if (shouldRun)
2366
+ poller.start();
2367
+ };
2368
+ const pause = () => poller.stop();
2369
+ window.addEventListener("online", resume);
2370
+ window.addEventListener("offline", pause);
2371
+ inject(DestroyRef).onDestroy(() => {
2372
+ window.removeEventListener("online", resume);
2373
+ window.removeEventListener("offline", pause);
2374
+ });
2375
+ }
2376
+ inject(DestroyRef).onDestroy(() => poller.stop());
2377
+ return {
2378
+ start() {
2379
+ shouldRun = true;
2380
+ if (typeof navigator === "undefined" || navigator.onLine)
2381
+ poller.start();
2382
+ },
2383
+ stop() {
2384
+ shouldRun = false;
2385
+ poller.stop();
2386
+ }
2387
+ };
2388
+ }
2389
+
2317
2390
  /**
2318
2391
  * Maps a caught error to the message shown in an inline error banner.
2319
2392
  *
@@ -3063,7 +3136,9 @@ const TABS = [
3063
3136
  * Each time it is shown it refreshes: workspaces created by others since the last
3064
3137
  * time appear, and the document counts match the server. The list already on
3065
3138
  * screen stays until the answer lands, and a failure leaves it untouched behind
3066
- * an inline error.
3139
+ * an inline error. While it stays shown, it keeps polling in the background
3140
+ * (`injectLiveRefresh`) so a workspace created by someone else — one the current
3141
+ * user only reads, say — appears on its own, with no action needed.
3067
3142
  */
3068
3143
  class WorkspaceList {
3069
3144
  /** A workspace was chosen for opening. */
@@ -3088,6 +3163,19 @@ class WorkspaceList {
3088
3163
  key: (workspace) => workspace.id,
3089
3164
  selectable: workspace => canWrite(workspace.currentUserRole)
3090
3165
  });
3166
+ // Background polling, started once the list has something to compare against:
3167
+ // each tick re-reloads and compares the full result to what was cached before
3168
+ // it, not just the set of ids — a workspace renamed, re-tagged or whose access
3169
+ // changed keeps the same id, and would otherwise look unchanged forever, which
3170
+ // would both hide the edit and widen the backoff on ticks that are not actually idle.
3171
+ liveRefresh = injectLiveRefresh(async () => {
3172
+ const before = JSON.stringify(this.store.workspaces());
3173
+ await this.store.reload();
3174
+ const changed = JSON.stringify(this.store.workspaces()) !== before;
3175
+ if (changed)
3176
+ this.selection.exit(); // the ticks belonged to the list that was there before
3177
+ return changed;
3178
+ }, { intervalMs: 20_000, maxIntervalMs: 120_000 });
3091
3179
  constructor() {
3092
3180
  void this.load();
3093
3181
  }
@@ -3097,12 +3185,14 @@ class WorkspaceList {
3097
3185
  if (this.store.loaded()) {
3098
3186
  this.status.set("ready");
3099
3187
  await this.refresh();
3188
+ this.liveRefresh.start();
3100
3189
  return;
3101
3190
  }
3102
3191
  this.status.set("loading");
3103
3192
  try {
3104
3193
  await this.store.ensureLoaded();
3105
3194
  this.status.set("ready");
3195
+ this.liveRefresh.start();
3106
3196
  }
3107
3197
  catch (error) {
3108
3198
  this.loadError.set(workspaceErrorMessage(error));
@@ -3889,6 +3979,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
3889
3979
  }]
3890
3980
  }], propDecorators: { document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: true }] }], workspaceRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "workspaceRole", required: true }] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], selecting: [{ type: i0.Input, args: [{ isSignal: true, alias: "selecting", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], deleteRequested: [{ type: i0.Output, args: ["deleteRequested"] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }] } });
3891
3981
 
3982
+ /**
3983
+ * The file-search glyph on a teal tile — the same tile `EmptyFolderIcon` uses
3984
+ * for an empty workspace or an empty list, for a workspace or document the
3985
+ * server no longer has.
3986
+ */
3987
+ class NotFoundIcon {
3988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NotFoundIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
3989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.29", type: NotFoundIcon, isStandalone: true, selector: "not-found-icon", host: { attributes: { "data-testid": "not-found-icon" }, classAttribute: "mb-1 inline-flex size-14 shrink-0 items-center justify-center rounded-(--radius-xl) bg-(--teal-100,#E0F4F1) text-(--teal-400,#4EBFB0)" }, ngImport: i0, template: `<file-search-icon class="size-6" />`, isInline: true, dependencies: [{ kind: "component", type: FileSearchIcon, selector: "file-search-icon, FileSearchIcon, filesearchicon", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3990
+ }
3991
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: NotFoundIcon, decorators: [{
3992
+ type: Component,
3993
+ args: [{
3994
+ selector: "not-found-icon",
3995
+ imports: [FileSearchIcon],
3996
+ changeDetection: ChangeDetectionStrategy.OnPush,
3997
+ host: {
3998
+ class: "mb-1 inline-flex size-14 shrink-0 items-center justify-center rounded-(--radius-xl) bg-(--teal-100,#E0F4F1) text-(--teal-400,#4EBFB0)",
3999
+ "data-testid": "not-found-icon"
4000
+ },
4001
+ template: `<file-search-icon class="size-6" />`
4002
+ }]
4003
+ }] });
4004
+
3892
4005
  /** The files staged for upload, before anything is sent. */
3893
4006
  class UploadFileList {
3894
4007
  files = input.required(...(ngDevMode ? [{ debugName: "files" }] : []));
@@ -5643,6 +5756,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5643
5756
  * shown: it says what the user asked for (`back`, `deleted`, `askFollowUp`) and whoever
5644
5757
  * hosts it decides. The document open in the preview is the `previewedDocumentId` model.
5645
5758
  *
5759
+ * While a workspace stays open, it keeps polling its documents in the background
5760
+ * (`injectLiveRefresh`), so a document another user adds to it shows up on its own.
5761
+ *
5646
5762
  * Only someone who can write sees the edit, share, delete, upload and select controls.
5647
5763
  */
5648
5764
  class WorkspaceDetail {
@@ -5673,6 +5789,8 @@ class WorkspaceDetail {
5673
5789
  refreshing = signal(false, ...(ngDevMode ? [{ debugName: "refreshing" }] : []));
5674
5790
  loadingMore = signal(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : []));
5675
5791
  loadMoreError = signal(null, ...(ngDevMode ? [{ debugName: "loadMoreError" }] : []));
5792
+ /** The background refresh found the workspace gone from the server (deleted elsewhere). */
5793
+ deletedElsewhere = signal(false, ...(ngDevMode ? [{ debugName: "deletedElsewhere" }] : []));
5676
5794
  workspace = computed(() => this.store.workspace(this.workspaceId()), ...(ngDevMode ? [{ debugName: "workspace" }] : []));
5677
5795
  // Only what is really uploaded or being indexed is listed: transfers and failures
5678
5796
  // belong to the upload tracker, not to the workspace. A document with no status at
@@ -5698,6 +5816,42 @@ class WorkspaceDetail {
5698
5816
  // Selecting only serves deleting, which a reader cannot do.
5699
5817
  selectable: () => this.writable()
5700
5818
  });
5819
+ // Background polling, re-scoped to whatever `workspaceId` is current on each
5820
+ // tick (no restart needed when the user switches workspaces): refreshes both
5821
+ // the workspace's own metadata (title, description, tags, owners/readers,
5822
+ // the current user's role) and its documents, so an edit or an access change
5823
+ // made elsewhere shows up here too, not just additions to the document list.
5824
+ // Comparing full snapshots (not just ids) catches an in-place edit that an
5825
+ // id-only diff would miss — a renamed workspace, a demoted role — and keeps
5826
+ // the backoff from widening on a tick that only looks unchanged by id.
5827
+ liveRefresh = injectLiveRefresh(async (signal) => {
5828
+ const id = this.workspaceId();
5829
+ const beforeWorkspace = JSON.stringify(this.store.workspace(id));
5830
+ const beforeDocuments = JSON.stringify(this.store.documentsOf(id));
5831
+ try {
5832
+ await Promise.all([this.store.fetch(id), this.store.loadDocuments(id, signal)]);
5833
+ }
5834
+ catch (error) {
5835
+ // Someone else deleted it: flag it and stop polling — there is nothing
5836
+ // left here to discover. The workspace stays in the shared cache (so its
5837
+ // name/description keep showing instead of the view going blank); `leave()`
5838
+ // drops it once the user actually goes back to the list.
5839
+ if (error instanceof ApiError && error.status === 404 && this.workspaceId() === id) {
5840
+ this.deletedElsewhere.set(true);
5841
+ this.selection.exit();
5842
+ this.liveRefresh.stop();
5843
+ return true;
5844
+ }
5845
+ throw error;
5846
+ }
5847
+ if (this.workspaceId() !== id)
5848
+ return false; // the user moved to another workspace meanwhile
5849
+ const documentsChanged = JSON.stringify(this.store.documentsOf(id)) !== beforeDocuments;
5850
+ const workspaceChanged = JSON.stringify(this.store.workspace(id)) !== beforeWorkspace;
5851
+ if (documentsChanged)
5852
+ this.uploads.watch(id); // a document that just appeared may still be indexing for someone else
5853
+ return documentsChanged || workspaceChanged;
5854
+ }, { intervalMs: 15_000, maxIntervalMs: 90_000 });
5701
5855
  constructor() {
5702
5856
  effect(() => {
5703
5857
  this.workspaceId();
@@ -5723,8 +5877,19 @@ class WorkspaceDetail {
5723
5877
  this.query.set("");
5724
5878
  this.refreshError.set(null);
5725
5879
  this.loadMoreError.set(null);
5880
+ this.deletedElsewhere.set(false);
5726
5881
  await this.load();
5727
5882
  }
5883
+ /**
5884
+ * Leaving from here: once the background refresh found the workspace gone,
5885
+ * the cache still holds it (so its name kept showing) — drop it now, so a
5886
+ * stale card does not show it back on the list.
5887
+ */
5888
+ leave() {
5889
+ if (this.deletedElsewhere())
5890
+ this.store.forget(this.workspaceId());
5891
+ this.back.emit();
5892
+ }
5728
5893
  /** Reads the workspace (when it is not cached) and its documents, for the workspace currently shown. */
5729
5894
  async load() {
5730
5895
  const id = this.workspaceId();
@@ -5747,6 +5912,7 @@ class WorkspaceDetail {
5747
5912
  return;
5748
5913
  this.status.set("ready");
5749
5914
  this.uploads.watch(id);
5915
+ this.liveRefresh.start();
5750
5916
  }
5751
5917
  /** Reads the documents again without leaving the view; a failure leaves the ones shown as they are. */
5752
5918
  async refresh() {
@@ -5846,7 +6012,7 @@ class WorkspaceDetail {
5846
6012
  return "key" in message ? this.transloco.translate(message.key, message.params) : message.text;
5847
6013
  }
5848
6014
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceDetail, deps: [], target: i0.ɵɵFactoryTarget.Component });
5849
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: WorkspaceDetail, isStandalone: true, selector: "workspace-detail", inputs: { workspaceId: { classPropertyName: "workspaceId", publicName: "workspaceId", isSignal: true, isRequired: true, transformFunction: null }, uploadEndpoint: { classPropertyName: "uploadEndpoint", publicName: "uploadEndpoint", isSignal: true, isRequired: false, transformFunction: null }, previewedDocumentId: { classPropertyName: "previewedDocumentId", publicName: "previewedDocumentId", isSignal: true, isRequired: false, transformFunction: null }, queryName: { classPropertyName: "queryName", publicName: "queryName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { previewedDocumentId: "previewedDocumentIdChange", back: "back", deleted: "deleted", workspaceUpdated: "workspaceUpdated", askFollowUp: "askFollowUp" }, host: { classAttribute: "flex h-full min-h-0" }, providers: [provideTranslocoScope("document-workspaces")], ngImport: i0, template: "\n <div class=\"@container flex min-h-0 min-w-0 flex-1 flex-col p-3 pb-2 md:p-8 md:pb-4\">\n <div class=\"mb-3 flex flex-wrap items-center gap-x-3 gap-y-2\">\n <button class=\"w-6\" size=\"sm\"\n variant=\"ghost\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'documentWorkspaces.detail.back' | transloco\"\n [tooltip]=\"'documentWorkspaces.detail.back' | transloco\"\n (click)=\"back.emit()\">\n <chevron-down-icon class=\"rotate-90\" />\n </button>\n\n @if (workspace(); as workspace) {\n <h1 class=\"min-w-28 flex-[1_1_16rem] text-2xl leading-[31px] font-bold break-words text-(--font-neutral-black)\">{{ workspace.name }}</h1>\n\n <!-- Beside the title while they fit next to its 16rem, on a line of their own under it once they do not. -->\n <div class=\"ml-auto flex shrink-0 items-center gap-1\">\n <button size=\"sm\"\n variant=\"ghost\"\n class=\"gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)\"\n [writerOnly]=\"workspace.currentUserRole\"\n (click)=\"confirmDeleteWorkspace(workspace)\">\n <trash-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"ghost\" class=\"gap-1.5\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"share(workspace)\">\n <user-round-icon />\n {{ \"documentWorkspaces.share.title\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"ghost\" class=\"gap-1.5\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"edit(workspace)\">\n <pen-to-square-icon />\n {{ \"documentWorkspaces.common.edit\" | transloco }}\n </button>\n <button size=\"sm\"\n variant=\"ghost\"\n class=\"w-6\"\n [iconOnly]=\"true\"\n [disabled]=\"refreshing()\"\n [attr.aria-label]=\"'documentWorkspaces.common.refresh' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.refresh' | transloco\"\n (click)=\"refresh()\">\n <refresh-icon />\n </button>\n </div>\n }\n </div>\n\n @if (workspace(); as workspace) {\n @if (workspace.description) {\n <p class=\"mb-3 text-base text-(--font-primary-base)\">{{ workspace.description }}</p>\n }\n @if (workspace.tags.length) {\n <p class=\"mb-6 flex items-center gap-2 text-sm text-(--font-primary-base)\" data-testid=\"tags\">\n <tag-icon class=\"size-4\" />\n {{ workspace.tags.join(\", \") }}\n </p>\n }\n\n <div class=\"mb-3 flex flex-wrap items-center gap-2\">\n <div role=\"group\" class=\"inline-flex gap-1 rounded-full bg-(--bg-primary-lighter) p-0.5\">\n <button\n variant=\"none\"\n class=\"inline-flex size-6 items-center justify-center rounded-full text-(--font-primary-base) aria-pressed:bg-(--bg-neutral-white) aria-pressed:shadow-md\"\n [attr.aria-pressed]=\"viewMode() === 'grid'\"\n [attr.aria-label]=\"'documentWorkspaces.common.gridView' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.gridView' | transloco\"\n (click)=\"viewMode.set('grid')\">\n <grid-icon class=\"size-3.5\" />\n </button>\n <button\n variant=\"none\"\n class=\"inline-flex size-6 items-center justify-center rounded-full text-(--font-primary-base) aria-pressed:bg-(--bg-neutral-white) aria-pressed:shadow-md\"\n [attr.aria-pressed]=\"viewMode() === 'list'\"\n [attr.aria-label]=\"'documentWorkspaces.common.listView' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.listView' | transloco\"\n (click)=\"viewMode.set('list')\">\n <list-icon class=\"size-3.5\" />\n </button>\n </div>\n\n <div\n role=\"search\"\n class=\"flex h-9 min-w-32 flex-[0_1_15rem] items-center gap-2 rounded-full border border-(--stroke-primary-light) bg-(--bg-neutral-white) px-3 focus-within:border-(--stroke-primary-base)\">\n <magnifying-glass-icon class=\"size-4 shrink-0 text-(--font-primary-muted)\" />\n <input\n type=\"text\"\n class=\"min-w-0 flex-1 bg-transparent text-xs text-(--font-neutral-black) outline-none placeholder:text-(--font-primary-muted)\"\n [value]=\"query()\"\n [attr.placeholder]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [attr.aria-label]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n (input)=\"onSearch($event)\" />\n @if (query()) {\n <button variant=\"none\" type=\"button\" class=\"inline-flex\" [attr.aria-label]=\"'documentWorkspaces.common.clearSearch' | transloco\" (click)=\"query.set('')\">\n <x-mark-icon class=\"size-4\" />\n </button>\n }\n </div>\n\n <button class=\"gap-1.5\" size=\"sm\" variant=\"primary\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"upload(workspace)\">\n <upload-icon />\n {{ \"documentWorkspaces.detail.upload\" | transloco }}\n </button>\n\n @if (selection.selectableItems().length) {\n @if (selection.active()) {\n <div class=\"ml-auto flex items-center gap-2 @max-xl:order-last @max-xl:ml-0 @max-xl:basis-full\" data-testid=\"selection-bar\">\n <button variant=\"ghost\" size=\"xs\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)\" variant=\"ghost\" size=\"xs\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <trash-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"ghost\" size=\"xs\" class=\"@max-xl:ml-auto\" [class.ml-auto]=\"!selection.active()\" (click)=\"selection.toggleMode()\">\n {{ (selection.active() ? \"documentWorkspaces.common.cancel\" : \"documentWorkspaces.common.select\") | transloco }}\n </button>\n }\n </div>\n\n <p class=\"mb-6 text-sm text-(--font-primary-base)\" aria-live=\"polite\" data-testid=\"count\">\n @if (selection.active()) {\n {{ \"documentWorkspaces.detail.selectedDocumentCount\" | transloco: { count: selection.count() } }}\n } @else {\n {{ \"documentWorkspaces.detail.fileCount\" | transloco: { count: workspace.documentCount } }}\n }\n </p>\n }\n\n @if (refreshError(); as message) {\n <p role=\"alert\" class=\"text-(--font-error-base) text-sm\">{{ \"documentWorkspaces.detail.refreshError\" | transloco: { error: message } }}</p>\n }\n\n <!-- Only the documents scroll: the title, the toolbar and the count stay in place. -->\n <div class=\"min-h-0 flex-1 overflow-y-auto\">\n @switch (status()) {\n @case (\"loading\") {\n <p class=\"text-(--font-primary-muted) text-sm\">{{ \"documentWorkspaces.detail.docsLoading\" | transloco }}</p>\n }\n @case (\"error\") {\n <div class=\"flex items-center gap-3 text-sm\" data-testid=\"load-error\">\n <span class=\"text-(--font-error-base)\">\n @if (loadError(); as message) {\n <workspace-message [message]=\"message\" />\n } @else {\n {{ \"documentWorkspaces.detail.docsLoadError\" | transloco }}\n }\n </span>\n <button variant=\"secondary\" size=\"xs\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (workspace(); as workspace) {\n @if (!documents().length) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"empty\">\n <empty-folder-icon />\n <h5 class=\"text-sm font-semibold text-(--font-primary-dark)\">{{ \"documentWorkspaces.detail.emptyTitle\" | transloco }}</h5>\n <p class=\"max-w-70 text-sm text-(--font-primary-base)\">{{ \"documentWorkspaces.detail.emptySubtitle\" | transloco }}</p>\n </div>\n } @else if (!filtered().length) {\n <p class=\"text-(--font-primary-muted) py-8 text-center text-sm\" data-testid=\"no-match\">{{ \"documentWorkspaces.detail.noMatch\" | transloco }}</p>\n } @else if (viewMode() === \"grid\") {\n <div class=\"grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] content-start gap-4 pb-8\">\n @for (document of filtered(); track document.id) {\n <document-card\n [document]=\"document\"\n [workspaceRole]=\"workspace.currentUserRole\"\n [query]=\"query()\"\n [active]=\"document.id === previewedDocumentId()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(document)\"\n [selectable]=\"selection.canSelect(document)\"\n (opened)=\"togglePreview(document)\"\n (deleteRequested)=\"confirmDeleteDocuments([document])\"\n (selectedChange)=\"selection.toggle(document, $event)\" />\n }\n </div>\n } @else {\n <div role=\"table\" class=\"flex flex-col gap-3 pb-8\">\n <div role=\"row\" [attr.data-selecting]=\"selection.active() ? '' : null\" class=\"grid grid-cols-[minmax(0,1fr)_auto_2.5rem] items-center gap-x-4 px-3 data-selecting:grid-cols-[2rem_minmax(0,1fr)_auto_2.5rem] @3xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_6rem_10rem_2.5rem] @3xl:data-selecting:grid-cols-[2rem_minmax(0,2fr)_minmax(0,1fr)_6rem_10rem_2.5rem] py-1 text-xs font-semibold text-(--font-primary-muted)\">\n @if (selection.active()) {\n <div role=\"columnheader\">\n <input\n type=\"checkbox\"\n class=\"accent-(--bg-primary-base) size-4\"\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [attr.aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (change)=\"selection.toggleAll()\" />\n\n </div>\n }\n <div role=\"columnheader\">{{ \"documentWorkspaces.detail.columnName\" | transloco }}</div>\n <div role=\"columnheader\" class=\"@max-3xl:hidden\">{{ \"documentWorkspaces.detail.columnSource\" | transloco }}</div>\n <div role=\"columnheader\" class=\"@max-3xl:hidden\">{{ \"documentWorkspaces.detail.columnSize\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.detail.columnDate\" | transloco }}</div>\n <div role=\"columnheader\"></div>\n </div>\n @for (document of filtered(); track document.id) {\n <document-row\n [document]=\"document\"\n [workspaceRole]=\"workspace.currentUserRole\"\n [query]=\"query()\"\n [active]=\"document.id === previewedDocumentId()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(document)\"\n [selectable]=\"selection.canSelect(document)\"\n (opened)=\"togglePreview(document)\"\n (deleteRequested)=\"confirmDeleteDocuments([document])\"\n (selectedChange)=\"selection.toggle(document, $event)\" />\n }\n </div>\n }\n\n @if (hasMore()) {\n <div class=\"flex flex-col items-center gap-2\">\n @if (loadMoreError(); as message) {\n <p role=\"alert\" class=\"text-(--font-error-base) text-sm\">{{ \"documentWorkspaces.detail.loadMoreError\" | transloco: { error: message } }}</p>\n }\n <button variant=\"secondary\" size=\"xs\" [disabled]=\"loadingMore()\" (click)=\"loadMore()\">\n {{ \"documentWorkspaces.detail.loadMore\" | transloco }}\n </button>\n </div>\n }\n } @else {\n <p class=\"text-(--font-primary-muted) py-8 text-center text-sm\" data-testid=\"not-found\">{{ \"documentWorkspaces.errors.notFound\" | transloco }}</p>\n }\n }\n }\n </div>\n </div>\n\n <!-- The list gives up part of its width to the preview, and takes it back when the preview closes. -->\n <div\n #handle=\"resizeHandle\"\n class=\"group/handle flex w-3 shrink-0 cursor-col-resize items-center justify-center max-md:hidden\"\n [class.hidden]=\"!previewed()\"\n [resizeHandle]=\"previewWidth()\"\n (resizeHandleChange)=\"userPreviewWidth.set($event)\"\n [resizeMin]=\"280\"\n [resizeMax]=\"maxPreviewWidth()\"\n [attr.aria-label]=\"'documentWorkspaces.preview.resize' | transloco\">\n <span class=\"h-6 w-0.5 rounded-full bg-(--stroke-primary-light) opacity-0 group-hover/handle:opacity-100 group-focus-visible/handle:opacity-100 group-data-resizing/handle:opacity-100\"></span>\n </div>\n <aside\n class=\"sticky top-3 my-3 mr-3 flex h-[calc(100dvh-1.5rem)] max-h-[calc(100%-1.5rem)] shrink-0 self-start overflow-hidden rounded-(--radius-xl) bg-(--bg-neutral-white) transition-[width] duration-[350ms] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none data-open:border data-open:border-(--stroke-primary-lighter) data-open:shadow-[0_4px_6px_-2px_rgba(45,57,58,0.05),0_10px_15px_-3px_rgba(45,57,58,0.1)] max-md:data-open:fixed max-md:data-open:inset-0 max-md:data-open:z-50 max-md:data-open:m-0 max-md:data-open:w-full! max-md:data-open:max-h-none max-md:data-open:rounded-none\"\n [class.transition-none]=\"handle.resizing()\"\n [style.width.px]=\"previewed() ? previewWidth() : 0\"\n [attr.data-open]=\"previewed() ? '' : null\"\n [attr.aria-hidden]=\"previewed() ? null : 'true'\"\n data-testid=\"preview\">\n @if (previewed(); as previewed) {\n <!-- Its own width, so the content does not reflow while the card opens. -->\n <document-preview-panel\n class=\"shrink-0 max-md:w-full!\"\n [style.width.px]=\"previewWidth()\"\n [document]=\"previewed\"\n [queryName]=\"queryName()\"\n (closed)=\"previewedDocumentId.set(null)\"\n (askFollowUp)=\"askFollowUp.emit($event)\" />\n }\n </aside>\n ", isInline: true, dependencies: [{ kind: "component", type: ChevronDownIcon, selector: "chevron-down, ChevronDown, chevrondown, chevron-down-icon, ChevronDownIcon, chevrondownicon", inputs: ["class"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: DocumentCard, selector: "document-card", inputs: ["document", "workspaceRole", "query", "active", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: DocumentPreviewPanel, selector: "document-preview-panel", inputs: ["document", "queryName"], outputs: ["closed", "askFollowUp"] }, { kind: "component", type: DocumentRow, selector: "document-row", inputs: ["document", "workspaceRole", "query", "active", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: EmptyFolderIcon, selector: "empty-folder-icon" }, { kind: "component", type: GridIcon, selector: "grid-icon", inputs: ["class"] }, { kind: "component", type: ListIcon, selector: "list-icon, ListIcon", inputs: ["class"] }, { kind: "component", type: MagnifyingGlassIcon, selector: "magnifying-glass-icon, MagnifyingGlassIcon", inputs: ["class"] }, { kind: "component", type: PenToSquareIcon, selector: "pen-to-square-icon, PenToSquareIcon", inputs: ["class"] }, { kind: "component", type: RefreshIcon, selector: "refresh-icon, RefreshIcon, refreshicon", inputs: ["class"] }, { kind: "directive", type: ResizeHandle, selector: "[resizeHandle]", inputs: ["resizeHandle", "resizeMin", "resizeMax", "resizeStep", "resizeEdge"], outputs: ["resizeHandleChange"], exportAs: ["resizeHandle"] }, { kind: "component", type: TagIcon, selector: "tag-icon, TagIcon", inputs: ["class"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip]", inputs: ["tooltip", "strategy", "tooltip-position", "tooltip-delay", "tooltip-duration", "tooltip-offset"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: UploadIcon, selector: "upload-icon, UploadIcon, uploadicon", inputs: ["class"] }, { kind: "component", type: UserRoundIcon, selector: "user-round, UserRound, userround, user-round-icon, UserRoundIcon, userroundicon", inputs: ["class"] }, { kind: "component", type: WorkspaceMessage, selector: "workspace-message", inputs: ["message"] }, { kind: "directive", type: WriterOnly, selector: "[writerOnly]", inputs: ["writerOnly"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6015
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: WorkspaceDetail, isStandalone: true, selector: "workspace-detail", inputs: { workspaceId: { classPropertyName: "workspaceId", publicName: "workspaceId", isSignal: true, isRequired: true, transformFunction: null }, uploadEndpoint: { classPropertyName: "uploadEndpoint", publicName: "uploadEndpoint", isSignal: true, isRequired: false, transformFunction: null }, previewedDocumentId: { classPropertyName: "previewedDocumentId", publicName: "previewedDocumentId", isSignal: true, isRequired: false, transformFunction: null }, queryName: { classPropertyName: "queryName", publicName: "queryName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { previewedDocumentId: "previewedDocumentIdChange", back: "back", deleted: "deleted", workspaceUpdated: "workspaceUpdated", askFollowUp: "askFollowUp" }, host: { classAttribute: "flex h-full min-h-0" }, providers: [provideTranslocoScope("document-workspaces")], ngImport: i0, template: "\n <div class=\"@container flex min-h-0 min-w-0 flex-1 flex-col p-3 pb-2 md:p-8 md:pb-4\">\n <div class=\"mb-3 flex flex-wrap items-center gap-x-3 gap-y-2\">\n <button class=\"w-6\" size=\"sm\"\n variant=\"ghost\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'documentWorkspaces.detail.back' | transloco\"\n [tooltip]=\"'documentWorkspaces.detail.back' | transloco\"\n (click)=\"leave()\">\n <chevron-down-icon class=\"rotate-90\" />\n </button>\n\n @if (workspace(); as workspace) {\n <h1 class=\"min-w-28 flex-[1_1_16rem] text-2xl leading-[31px] font-bold break-words text-(--font-neutral-black)\">{{ workspace.name }}</h1>\n\n <!-- Beside the title while they fit next to its 16rem, on a line of their own under it once they do not. -->\n @if (!deletedElsewhere()) {\n <div class=\"ml-auto flex shrink-0 items-center gap-1\">\n <button size=\"sm\"\n variant=\"ghost\"\n class=\"gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)\"\n [writerOnly]=\"workspace.currentUserRole\"\n (click)=\"confirmDeleteWorkspace(workspace)\">\n <trash-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"ghost\" class=\"gap-1.5\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"share(workspace)\">\n <user-round-icon />\n {{ \"documentWorkspaces.share.title\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"ghost\" class=\"gap-1.5\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"edit(workspace)\">\n <pen-to-square-icon />\n {{ \"documentWorkspaces.common.edit\" | transloco }}\n </button>\n <button size=\"sm\"\n variant=\"ghost\"\n class=\"w-6\"\n [iconOnly]=\"true\"\n [disabled]=\"refreshing()\"\n [attr.aria-label]=\"'documentWorkspaces.common.refresh' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.refresh' | transloco\"\n (click)=\"refresh()\">\n <refresh-icon />\n </button>\n </div>\n }\n }\n </div>\n\n @if (workspace(); as workspace) {\n @if (workspace.description) {\n <p class=\"mb-3 text-base text-(--font-primary-base)\">{{ workspace.description }}</p>\n }\n @if (workspace.tags.length) {\n <p class=\"mb-6 flex items-center gap-2 text-sm text-(--font-primary-base)\" data-testid=\"tags\">\n <tag-icon class=\"size-4\" />\n {{ workspace.tags.join(\", \") }}\n </p>\n }\n\n @if (!deletedElsewhere()) {\n <div class=\"mb-3 flex flex-wrap items-center gap-2\">\n <div role=\"group\" class=\"inline-flex gap-1 rounded-full bg-(--bg-primary-lighter) p-0.5\">\n <button\n variant=\"none\"\n class=\"inline-flex size-6 items-center justify-center rounded-full text-(--font-primary-base) aria-pressed:bg-(--bg-neutral-white) aria-pressed:shadow-md\"\n [attr.aria-pressed]=\"viewMode() === 'grid'\"\n [attr.aria-label]=\"'documentWorkspaces.common.gridView' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.gridView' | transloco\"\n (click)=\"viewMode.set('grid')\">\n <grid-icon class=\"size-3.5\" />\n </button>\n <button\n variant=\"none\"\n class=\"inline-flex size-6 items-center justify-center rounded-full text-(--font-primary-base) aria-pressed:bg-(--bg-neutral-white) aria-pressed:shadow-md\"\n [attr.aria-pressed]=\"viewMode() === 'list'\"\n [attr.aria-label]=\"'documentWorkspaces.common.listView' | transloco\"\n [tooltip]=\"'documentWorkspaces.common.listView' | transloco\"\n (click)=\"viewMode.set('list')\">\n <list-icon class=\"size-3.5\" />\n </button>\n </div>\n\n <div\n role=\"search\"\n class=\"flex h-9 min-w-32 flex-[0_1_15rem] items-center gap-2 rounded-full border border-(--stroke-primary-light) bg-(--bg-neutral-white) px-3 focus-within:border-(--stroke-primary-base)\">\n <magnifying-glass-icon class=\"size-4 shrink-0 text-(--font-primary-muted)\" />\n <input\n type=\"text\"\n class=\"min-w-0 flex-1 bg-transparent text-xs text-(--font-neutral-black) outline-none placeholder:text-(--font-primary-muted)\"\n [value]=\"query()\"\n [attr.placeholder]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [attr.aria-label]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n (input)=\"onSearch($event)\" />\n @if (query()) {\n <button variant=\"none\" type=\"button\" class=\"inline-flex\" [attr.aria-label]=\"'documentWorkspaces.common.clearSearch' | transloco\" (click)=\"query.set('')\">\n <x-mark-icon class=\"size-4\" />\n </button>\n }\n </div>\n\n <button class=\"gap-1.5\" size=\"sm\" variant=\"primary\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"upload(workspace)\">\n <upload-icon />\n {{ \"documentWorkspaces.detail.upload\" | transloco }}\n </button>\n\n @if (selection.selectableItems().length) {\n @if (selection.active()) {\n <div class=\"ml-auto flex items-center gap-2 @max-xl:order-last @max-xl:ml-0 @max-xl:basis-full\" data-testid=\"selection-bar\">\n <button variant=\"ghost\" size=\"xs\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)\" variant=\"ghost\" size=\"xs\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <trash-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"ghost\" size=\"xs\" class=\"@max-xl:ml-auto\" [class.ml-auto]=\"!selection.active()\" (click)=\"selection.toggleMode()\">\n {{ (selection.active() ? \"documentWorkspaces.common.cancel\" : \"documentWorkspaces.common.select\") | transloco }}\n </button>\n }\n </div>\n\n <p class=\"mb-6 text-sm text-(--font-primary-base)\" aria-live=\"polite\" data-testid=\"count\">\n @if (selection.active()) {\n {{ \"documentWorkspaces.detail.selectedDocumentCount\" | transloco: { count: selection.count() } }}\n } @else {\n {{ \"documentWorkspaces.detail.fileCount\" | transloco: { count: workspace.documentCount } }}\n }\n </p>\n }\n }\n\n @if (refreshError(); as message) {\n <p role=\"alert\" class=\"text-(--font-error-base) text-sm\">{{ \"documentWorkspaces.detail.refreshError\" | transloco: { error: message } }}</p>\n }\n\n <!-- Only the documents scroll: the title, the toolbar and the count stay in place. -->\n <div class=\"min-h-0 flex-1 overflow-y-auto\">\n @switch (status()) {\n @case (\"loading\") {\n <p class=\"text-(--font-primary-muted) text-sm\">{{ \"documentWorkspaces.detail.docsLoading\" | transloco }}</p>\n }\n @case (\"error\") {\n <div class=\"flex items-center gap-3 text-sm\" data-testid=\"load-error\">\n <span class=\"text-(--font-error-base)\">\n @if (loadError(); as message) {\n <workspace-message [message]=\"message\" />\n } @else {\n {{ \"documentWorkspaces.detail.docsLoadError\" | transloco }}\n }\n </span>\n <button variant=\"secondary\" size=\"xs\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (deletedElsewhere()) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"not-found\">\n <not-found-icon />\n <p class=\"max-w-70 text-sm text-(--font-primary-base)\">{{ \"documentWorkspaces.errors.notFound\" | transloco }}</p>\n <button variant=\"secondary\" size=\"sm\" class=\"mt-2\" (click)=\"leave()\">{{ \"documentWorkspaces.detail.back\" | transloco }}</button>\n </div>\n } @else if (workspace(); as workspace) {\n @if (!documents().length) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"empty\">\n <empty-folder-icon />\n <h5 class=\"text-sm font-semibold text-(--font-primary-dark)\">{{ \"documentWorkspaces.detail.emptyTitle\" | transloco }}</h5>\n <p class=\"max-w-70 text-sm text-(--font-primary-base)\">{{ \"documentWorkspaces.detail.emptySubtitle\" | transloco }}</p>\n </div>\n } @else if (!filtered().length) {\n <p class=\"text-(--font-primary-muted) py-8 text-center text-sm\" data-testid=\"no-match\">{{ \"documentWorkspaces.detail.noMatch\" | transloco }}</p>\n } @else if (viewMode() === \"grid\") {\n <div class=\"grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] content-start gap-4 pb-8\">\n @for (document of filtered(); track document.id) {\n <document-card\n [document]=\"document\"\n [workspaceRole]=\"workspace.currentUserRole\"\n [query]=\"query()\"\n [active]=\"document.id === previewedDocumentId()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(document)\"\n [selectable]=\"selection.canSelect(document)\"\n (opened)=\"togglePreview(document)\"\n (deleteRequested)=\"confirmDeleteDocuments([document])\"\n (selectedChange)=\"selection.toggle(document, $event)\" />\n }\n </div>\n } @else {\n <div role=\"table\" class=\"flex flex-col gap-3 pb-8\">\n <div role=\"row\" [attr.data-selecting]=\"selection.active() ? '' : null\" class=\"grid grid-cols-[minmax(0,1fr)_auto_2.5rem] items-center gap-x-4 px-3 data-selecting:grid-cols-[2rem_minmax(0,1fr)_auto_2.5rem] @3xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_6rem_10rem_2.5rem] @3xl:data-selecting:grid-cols-[2rem_minmax(0,2fr)_minmax(0,1fr)_6rem_10rem_2.5rem] py-1 text-xs font-semibold text-(--font-primary-muted)\">\n @if (selection.active()) {\n <div role=\"columnheader\">\n <input\n type=\"checkbox\"\n class=\"accent-(--bg-primary-base) size-4\"\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [attr.aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (change)=\"selection.toggleAll()\" />\n\n </div>\n }\n <div role=\"columnheader\">{{ \"documentWorkspaces.detail.columnName\" | transloco }}</div>\n <div role=\"columnheader\" class=\"@max-3xl:hidden\">{{ \"documentWorkspaces.detail.columnSource\" | transloco }}</div>\n <div role=\"columnheader\" class=\"@max-3xl:hidden\">{{ \"documentWorkspaces.detail.columnSize\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.detail.columnDate\" | transloco }}</div>\n <div role=\"columnheader\"></div>\n </div>\n @for (document of filtered(); track document.id) {\n <document-row\n [document]=\"document\"\n [workspaceRole]=\"workspace.currentUserRole\"\n [query]=\"query()\"\n [active]=\"document.id === previewedDocumentId()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(document)\"\n [selectable]=\"selection.canSelect(document)\"\n (opened)=\"togglePreview(document)\"\n (deleteRequested)=\"confirmDeleteDocuments([document])\"\n (selectedChange)=\"selection.toggle(document, $event)\" />\n }\n </div>\n }\n\n @if (hasMore()) {\n <div class=\"flex flex-col items-center gap-2\">\n @if (loadMoreError(); as message) {\n <p role=\"alert\" class=\"text-(--font-error-base) text-sm\">{{ \"documentWorkspaces.detail.loadMoreError\" | transloco: { error: message } }}</p>\n }\n <button variant=\"secondary\" size=\"xs\" [disabled]=\"loadingMore()\" (click)=\"loadMore()\">\n {{ \"documentWorkspaces.detail.loadMore\" | transloco }}\n </button>\n </div>\n }\n } @else {\n <p class=\"text-(--font-primary-muted) py-8 text-center text-sm\" data-testid=\"not-found\">{{ \"documentWorkspaces.errors.notFound\" | transloco }}</p>\n }\n }\n }\n </div>\n </div>\n\n <!-- The list gives up part of its width to the preview, and takes it back when the preview closes. -->\n <div\n #handle=\"resizeHandle\"\n class=\"group/handle flex w-3 shrink-0 cursor-col-resize items-center justify-center max-md:hidden\"\n [class.hidden]=\"!previewed()\"\n [resizeHandle]=\"previewWidth()\"\n (resizeHandleChange)=\"userPreviewWidth.set($event)\"\n [resizeMin]=\"280\"\n [resizeMax]=\"maxPreviewWidth()\"\n [attr.aria-label]=\"'documentWorkspaces.preview.resize' | transloco\">\n <span class=\"h-6 w-0.5 rounded-full bg-(--stroke-primary-light) opacity-0 group-hover/handle:opacity-100 group-focus-visible/handle:opacity-100 group-data-resizing/handle:opacity-100\"></span>\n </div>\n <aside\n class=\"sticky top-3 my-3 mr-3 flex h-[calc(100dvh-1.5rem)] max-h-[calc(100%-1.5rem)] shrink-0 self-start overflow-hidden rounded-(--radius-xl) bg-(--bg-neutral-white) transition-[width] duration-[350ms] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none data-open:border data-open:border-(--stroke-primary-lighter) data-open:shadow-[0_4px_6px_-2px_rgba(45,57,58,0.05),0_10px_15px_-3px_rgba(45,57,58,0.1)] max-md:data-open:fixed max-md:data-open:inset-0 max-md:data-open:z-50 max-md:data-open:m-0 max-md:data-open:w-full! max-md:data-open:max-h-none max-md:data-open:rounded-none\"\n [class.transition-none]=\"handle.resizing()\"\n [style.width.px]=\"previewed() ? previewWidth() : 0\"\n [attr.data-open]=\"previewed() ? '' : null\"\n [attr.aria-hidden]=\"previewed() ? null : 'true'\"\n data-testid=\"preview\">\n @if (previewed(); as previewed) {\n <!-- Its own width, so the content does not reflow while the card opens. -->\n <document-preview-panel\n class=\"shrink-0 max-md:w-full!\"\n [style.width.px]=\"previewWidth()\"\n [document]=\"previewed\"\n [queryName]=\"queryName()\"\n (closed)=\"previewedDocumentId.set(null)\"\n (askFollowUp)=\"askFollowUp.emit($event)\" />\n }\n </aside>\n ", isInline: true, dependencies: [{ kind: "component", type: ChevronDownIcon, selector: "chevron-down, ChevronDown, chevrondown, chevron-down-icon, ChevronDownIcon, chevrondownicon", inputs: ["class"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: DocumentCard, selector: "document-card", inputs: ["document", "workspaceRole", "query", "active", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: DocumentPreviewPanel, selector: "document-preview-panel", inputs: ["document", "queryName"], outputs: ["closed", "askFollowUp"] }, { kind: "component", type: DocumentRow, selector: "document-row", inputs: ["document", "workspaceRole", "query", "active", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: EmptyFolderIcon, selector: "empty-folder-icon" }, { kind: "component", type: GridIcon, selector: "grid-icon", inputs: ["class"] }, { kind: "component", type: ListIcon, selector: "list-icon, ListIcon", inputs: ["class"] }, { kind: "component", type: MagnifyingGlassIcon, selector: "magnifying-glass-icon, MagnifyingGlassIcon", inputs: ["class"] }, { kind: "component", type: NotFoundIcon, selector: "not-found-icon" }, { kind: "component", type: PenToSquareIcon, selector: "pen-to-square-icon, PenToSquareIcon", inputs: ["class"] }, { kind: "component", type: RefreshIcon, selector: "refresh-icon, RefreshIcon, refreshicon", inputs: ["class"] }, { kind: "directive", type: ResizeHandle, selector: "[resizeHandle]", inputs: ["resizeHandle", "resizeMin", "resizeMax", "resizeStep", "resizeEdge"], outputs: ["resizeHandleChange"], exportAs: ["resizeHandle"] }, { kind: "component", type: TagIcon, selector: "tag-icon, TagIcon", inputs: ["class"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip]", inputs: ["tooltip", "strategy", "tooltip-position", "tooltip-delay", "tooltip-duration", "tooltip-offset"] }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: UploadIcon, selector: "upload-icon, UploadIcon, uploadicon", inputs: ["class"] }, { kind: "component", type: UserRoundIcon, selector: "user-round, UserRound, userround, user-round-icon, UserRoundIcon, userroundicon", inputs: ["class"] }, { kind: "component", type: WorkspaceMessage, selector: "workspace-message", inputs: ["message"] }, { kind: "directive", type: WriterOnly, selector: "[writerOnly]", inputs: ["writerOnly"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5850
6016
  }
5851
6017
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceDetail, decorators: [{
5852
6018
  type: Component,
@@ -5863,6 +6029,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5863
6029
  GridIcon,
5864
6030
  ListIcon,
5865
6031
  MagnifyingGlassIcon,
6032
+ NotFoundIcon,
5866
6033
  PenToSquareIcon,
5867
6034
  RefreshIcon,
5868
6035
  ResizeHandle,
@@ -5886,7 +6053,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5886
6053
  [iconOnly]="true"
5887
6054
  [attr.aria-label]="'documentWorkspaces.detail.back' | transloco"
5888
6055
  [tooltip]="'documentWorkspaces.detail.back' | transloco"
5889
- (click)="back.emit()">
6056
+ (click)="leave()">
5890
6057
  <chevron-down-icon class="rotate-90" />
5891
6058
  </button>
5892
6059
 
@@ -5894,34 +6061,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5894
6061
  <h1 class="min-w-28 flex-[1_1_16rem] text-2xl leading-[31px] font-bold break-words text-(--font-neutral-black)">{{ workspace.name }}</h1>
5895
6062
 
5896
6063
  <!-- Beside the title while they fit next to its 16rem, on a line of their own under it once they do not. -->
5897
- <div class="ml-auto flex shrink-0 items-center gap-1">
5898
- <button size="sm"
5899
- variant="ghost"
5900
- class="gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)"
5901
- [writerOnly]="workspace.currentUserRole"
5902
- (click)="confirmDeleteWorkspace(workspace)">
5903
- <trash-icon />
5904
- {{ "documentWorkspaces.common.delete" | transloco }}
5905
- </button>
5906
- <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="share(workspace)">
5907
- <user-round-icon />
5908
- {{ "documentWorkspaces.share.title" | transloco }}
5909
- </button>
5910
- <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="edit(workspace)">
5911
- <pen-to-square-icon />
5912
- {{ "documentWorkspaces.common.edit" | transloco }}
5913
- </button>
5914
- <button size="sm"
5915
- variant="ghost"
5916
- class="w-6"
5917
- [iconOnly]="true"
5918
- [disabled]="refreshing()"
5919
- [attr.aria-label]="'documentWorkspaces.common.refresh' | transloco"
5920
- [tooltip]="'documentWorkspaces.common.refresh' | transloco"
5921
- (click)="refresh()">
5922
- <refresh-icon />
5923
- </button>
5924
- </div>
6064
+ @if (!deletedElsewhere()) {
6065
+ <div class="ml-auto flex shrink-0 items-center gap-1">
6066
+ <button size="sm"
6067
+ variant="ghost"
6068
+ class="gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)"
6069
+ [writerOnly]="workspace.currentUserRole"
6070
+ (click)="confirmDeleteWorkspace(workspace)">
6071
+ <trash-icon />
6072
+ {{ "documentWorkspaces.common.delete" | transloco }}
6073
+ </button>
6074
+ <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="share(workspace)">
6075
+ <user-round-icon />
6076
+ {{ "documentWorkspaces.share.title" | transloco }}
6077
+ </button>
6078
+ <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="edit(workspace)">
6079
+ <pen-to-square-icon />
6080
+ {{ "documentWorkspaces.common.edit" | transloco }}
6081
+ </button>
6082
+ <button size="sm"
6083
+ variant="ghost"
6084
+ class="w-6"
6085
+ [iconOnly]="true"
6086
+ [disabled]="refreshing()"
6087
+ [attr.aria-label]="'documentWorkspaces.common.refresh' | transloco"
6088
+ [tooltip]="'documentWorkspaces.common.refresh' | transloco"
6089
+ (click)="refresh()">
6090
+ <refresh-icon />
6091
+ </button>
6092
+ </div>
6093
+ }
5925
6094
  }
5926
6095
  </div>
5927
6096
 
@@ -5936,6 +6105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5936
6105
  </p>
5937
6106
  }
5938
6107
 
6108
+ @if (!deletedElsewhere()) {
5939
6109
  <div class="mb-3 flex flex-wrap items-center gap-2">
5940
6110
  <div role="group" class="inline-flex gap-1 rounded-full bg-(--bg-primary-lighter) p-0.5">
5941
6111
  <button
@@ -6006,6 +6176,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
6006
6176
  {{ "documentWorkspaces.detail.fileCount" | transloco: { count: workspace.documentCount } }}
6007
6177
  }
6008
6178
  </p>
6179
+ }
6009
6180
  }
6010
6181
 
6011
6182
  @if (refreshError(); as message) {
@@ -6031,7 +6202,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
6031
6202
  </div>
6032
6203
  }
6033
6204
  @default {
6034
- @if (workspace(); as workspace) {
6205
+ @if (deletedElsewhere()) {
6206
+ <div class="flex flex-col items-center gap-1 py-8 text-center" data-testid="not-found">
6207
+ <not-found-icon />
6208
+ <p class="max-w-70 text-sm text-(--font-primary-base)">{{ "documentWorkspaces.errors.notFound" | transloco }}</p>
6209
+ <button variant="secondary" size="sm" class="mt-2" (click)="leave()">{{ "documentWorkspaces.detail.back" | transloco }}</button>
6210
+ </div>
6211
+ } @else if (workspace(); as workspace) {
6035
6212
  @if (!documents().length) {
6036
6213
  <div class="flex flex-col items-center gap-1 py-8 text-center" data-testid="empty">
6037
6214
  <empty-folder-icon />