@sinequa/atomic-angular 1.15.0 → 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.
@@ -2,12 +2,12 @@ import * as i0 from '@angular/core';
2
2
  import { computed, signal, Injectable, InjectionToken, inject, DestroyRef, assertInInjectionContext, effect, untracked, output, input, booleanAttribute, Directive, model, Pipe, viewChild, ChangeDetectionStrategy, Component, ElementRef, ViewEncapsulation } from '@angular/core';
3
3
  import { withDevtools } from '@angular-architects/ngrx-toolkit';
4
4
  import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
5
- import { listContainers, addContainerDocuments, deleteContainerDocuments, listContainerDocuments, deleteContainers, updateContainer, createContainer, getContainer, fetchPrincipal, globalConfig, tryAutoAuthentication, createHeaders, onAuthEvent, ApiError, fetchUploadStatus, searchPrincipals, fetchPrincipalsByIds, fetchPreview } from '@sinequa/atomic';
5
+ import { listContainerDocuments, listContainers, addContainerDocuments, deleteContainerDocuments, deleteContainers, updateContainer, createContainer, getContainer, fetchPrincipal, globalConfig, tryAutoAuthentication, createHeaders, onAuthEvent, ApiError, fetchUploadStatus, searchPrincipals, fetchPrincipalsByIds, fetchPreview } from '@sinequa/atomic';
6
6
  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
 
@@ -102,6 +102,11 @@ function mapContainerDocumentToWorkspaceDocument(dto) {
102
102
 
103
103
  /** API maximum page size. */
104
104
  const PAGE_SIZE = 100;
105
+ /**
106
+ * Most workspaces the list keeps loaded: "load more" stops there, so the list never puts an
107
+ * unbounded number of cards in the DOM. Past it, {@link WorkspacesStore.truncated} is true.
108
+ */
109
+ const MAX_LOADED_WORKSPACES = 3 * PAGE_SIZE;
105
110
  const initialState$1 = {
106
111
  workspaces: [],
107
112
  loaded: false,
@@ -131,19 +136,56 @@ function withDocumentCountAdjusted(workspaces, id, delta) {
131
136
  * pre-transfer cache update.
132
137
  */
133
138
  const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Workspaces"), withState(initialState$1), withComputed(({ workspaces, pagination, selectedIds }) => ({
134
- hasMore: computed(() => workspaces().length < pagination().total),
139
+ hasMore: computed(() => workspaces().length < pagination().total && workspaces().length < MAX_LOADED_WORKSPACES),
140
+ /** The server holds more workspaces than the list is allowed to load. */
141
+ truncated: computed(() => workspaces().length < pagination().total && workspaces().length >= MAX_LOADED_WORKSPACES),
135
142
  selectedCount: computed(() => selectedIds().length),
136
143
  selectedIdSet: computed(() => new Set(selectedIds()))
137
144
  })), withMethods(store => {
138
145
  let loadingPromise = null;
146
+ let loadingMorePromise = null;
139
147
  const setDocuments = (workspaceId, documents) => {
140
148
  patchState(store, state => ({ documents: { ...state.documents, [workspaceId]: documents } }));
141
149
  };
142
150
  // Counts are moved by a delta rather than reset to the cached list's length:
143
151
  // the cache holds at most the pages loaded so far, while `documentCount` is
144
152
  // the server's total.
153
+ // The same goes for the documents' pagination total, which `hasMoreDocuments` compares
154
+ // to the cached list: left as it was loaded, a workspace emptied of its documents would
155
+ // still offer to load more.
145
156
  const adjustDocumentCount = (workspaceId, delta) => {
146
- patchState(store, state => ({ workspaces: withDocumentCountAdjusted(state.workspaces, workspaceId, delta) }));
157
+ patchState(store, state => {
158
+ const pagination = state.documentPagination[workspaceId];
159
+ return {
160
+ workspaces: withDocumentCountAdjusted(state.workspaces, workspaceId, delta),
161
+ documentPagination: pagination
162
+ ? { ...state.documentPagination, [workspaceId]: { ...pagination, total: Math.max(0, pagination.total + delta) } }
163
+ : state.documentPagination
164
+ };
165
+ });
166
+ };
167
+ const loadDocuments = async (workspaceId, signal) => {
168
+ const res = await listContainerDocuments(workspaceId, { page: 1, pageSize: PAGE_SIZE }, { signal });
169
+ const documents = res.documents.map(mapContainerDocumentToWorkspaceDocument);
170
+ patchState(store, state => ({
171
+ documentPagination: { ...state.documentPagination, [workspaceId]: { page: res.page, total: res.total } }
172
+ }));
173
+ setDocuments(workspaceId, documents);
174
+ return documents;
175
+ };
176
+ // Deleting everything that was loaded does not mean the workspace is empty: the server may hold
177
+ // pages that were never loaded. Show them rather than an empty state next to a "load more".
178
+ // The deletion already succeeded, so a failed refill is not an error here: the cache stays empty
179
+ // and "load more" (which then asks for the first page) lets the user retry.
180
+ const refillIfEmptied = async (workspaceId) => {
181
+ if ((store.documents()[workspaceId] ?? []).length || !(store.documentPagination()[workspaceId]?.total ?? 0))
182
+ return;
183
+ try {
184
+ await loadDocuments(workspaceId);
185
+ }
186
+ catch {
187
+ // see above
188
+ }
147
189
  };
148
190
  const load = async () => {
149
191
  patchState(store, { loading: true });
@@ -194,15 +236,30 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
194
236
  loadingPromise ??= load();
195
237
  return loadingPromise;
196
238
  },
197
- async loadMore() {
239
+ /** Shares an in-flight call, so a double click does not fetch (and append) the same page twice. */
240
+ loadMore() {
198
241
  if (!store.hasMore())
199
- return store.workspaces();
200
- const res = await listContainers({ page: store.pagination().page + 1, pageSize: PAGE_SIZE });
201
- patchState(store, state => ({
202
- workspaces: [...state.workspaces, ...res.containers.map(mapContainerToWorkspace)],
203
- pagination: { page: res.page, total: res.total }
204
- }));
205
- return store.workspaces();
242
+ return Promise.resolve(store.workspaces());
243
+ loadingMorePromise ??= (async () => {
244
+ try {
245
+ const current = store.workspaces();
246
+ // Deleting workspaces shifts the server's pages: `page + 1` would skip as many as were
247
+ // deleted since. Ask for the page holding the first one not loaded yet (it may overlap
248
+ // the loaded tail, hence the dedup).
249
+ const page = Math.min(store.pagination().page + 1, Math.floor(current.length / PAGE_SIZE) + 1);
250
+ const res = await listContainers({ page, pageSize: PAGE_SIZE });
251
+ patchState(store, state => {
252
+ const loaded = new Set(state.workspaces.map(w => w.id));
253
+ const fresh = res.containers.map(mapContainerToWorkspace).filter(w => !loaded.has(w.id));
254
+ return { workspaces: [...state.workspaces, ...fresh], pagination: { page: res.page, total: res.total } };
255
+ });
256
+ return store.workspaces();
257
+ }
258
+ finally {
259
+ loadingMorePromise = null;
260
+ }
261
+ })();
262
+ return loadingMorePromise;
206
263
  },
207
264
  /**
208
265
  * Fallback for a deep link opened before the list was loaded, or for an id
@@ -220,6 +277,8 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
220
277
  const workspace = mapContainerToWorkspace(await createContainer({ name, description, tags, owners, readers }));
221
278
  patchState(store, state => ({
222
279
  workspaces: [...state.workspaces, workspace],
280
+ // The server's total grew by one too: left alone, a full list would offer to load a phantom workspace.
281
+ pagination: { ...state.pagination, total: state.pagination.total + 1 },
223
282
  documents: { ...state.documents, [workspace.id]: [] }
224
283
  }));
225
284
  return workspace;
@@ -248,28 +307,45 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
248
307
  const succeeded = new Set(result.succeeded);
249
308
  patchState(store, state => ({
250
309
  workspaces: state.workspaces.filter(w => !succeeded.has(w.id)),
310
+ // Followed like the documents' total: an emptied list must not offer to load more.
311
+ pagination: { ...state.pagination, total: Math.max(0, state.pagination.total - result.succeeded.length) },
251
312
  documents: Object.fromEntries(Object.entries(state.documents).filter(([id]) => !succeeded.has(id))),
252
313
  selectedIds: state.selectedIds.filter(id => !succeeded.has(id))
253
314
  }));
254
315
  return { succeeded: result.succeeded, failed: result.failed.map(f => ({ id: f.containerId, error: f.error })) };
255
316
  },
256
- async loadDocuments(workspaceId) {
257
- const res = await listContainerDocuments(workspaceId, { page: 1, pageSize: PAGE_SIZE });
258
- const documents = res.documents.map(mapContainerDocumentToWorkspaceDocument);
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) {
259
326
  patchState(store, state => ({
260
- documentPagination: { ...state.documentPagination, [workspaceId]: { page: res.page, total: res.total } }
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)
261
332
  }));
262
- setDocuments(workspaceId, documents);
263
- return documents;
264
333
  },
334
+ loadDocuments,
265
335
  /** Without this, a workspace with more than one page of documents is silently truncated. */
266
336
  async loadMoreDocuments(workspaceId) {
267
337
  const current = store.documents()[workspaceId] ?? [];
268
338
  const pagination = store.documentPagination()[workspaceId];
269
339
  if (!pagination || current.length >= pagination.total)
270
340
  return current;
271
- const res = await listContainerDocuments(workspaceId, { page: pagination.page + 1, pageSize: PAGE_SIZE });
272
- const merged = [...current, ...res.documents.map(mapContainerDocumentToWorkspaceDocument)];
341
+ // Deleting documents shifts the server's pages: `pagination.page + 1` would skip as many
342
+ // documents as were deleted since. Ask for the page holding the first one not loaded yet
343
+ // (it may overlap the loaded tail, hence the dedup).
344
+ const page = Math.min(pagination.page + 1, Math.floor(current.length / PAGE_SIZE) + 1);
345
+ const res = await listContainerDocuments(workspaceId, { page, pageSize: PAGE_SIZE });
346
+ const loaded = new Set(current.map(d => d.id));
347
+ const fresh = res.documents.map(mapContainerDocumentToWorkspaceDocument).filter(d => !loaded.has(d.id));
348
+ const merged = [...current, ...fresh];
273
349
  patchState(store, state => ({
274
350
  documentPagination: { ...state.documentPagination, [workspaceId]: { page: res.page, total: res.total } }
275
351
  }));
@@ -322,6 +398,7 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
322
398
  const after = before.filter(d => !documentIds.includes(d.id));
323
399
  setDocuments(workspaceId, after);
324
400
  adjustDocumentCount(workspaceId, after.length - before.length);
401
+ await refillIfEmptied(workspaceId);
325
402
  },
326
403
  /**
327
404
  * "Move to workspace". There is no dedicated re-parent endpoint, so this
@@ -341,6 +418,7 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
341
418
  const remaining = from.filter(d => !documentIds.includes(d.id));
342
419
  setDocuments(fromWorkspaceId, remaining);
343
420
  adjustDocumentCount(fromWorkspaceId, remaining.length - from.length);
421
+ await refillIfEmptied(fromWorkspaceId);
344
422
  },
345
423
  toggleSelected(id, checked) {
346
424
  patchState(store, state => {
@@ -2253,6 +2331,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
2253
2331
  args: [{ name: "highlightMatch" }]
2254
2332
  }] });
2255
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
+
2256
2390
  /**
2257
2391
  * Maps a caught error to the message shown in an inline error banner.
2258
2392
  *
@@ -3002,7 +3136,9 @@ const TABS = [
3002
3136
  * Each time it is shown it refreshes: workspaces created by others since the last
3003
3137
  * time appear, and the document counts match the server. The list already on
3004
3138
  * screen stays until the answer lands, and a failure leaves it untouched behind
3005
- * 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.
3006
3142
  */
3007
3143
  class WorkspaceList {
3008
3144
  /** A workspace was chosen for opening. */
@@ -3017,6 +3153,8 @@ class WorkspaceList {
3017
3153
  status = signal("loading", ...(ngDevMode ? [{ debugName: "status" }] : []));
3018
3154
  loadError = signal(null, ...(ngDevMode ? [{ debugName: "loadError" }] : []));
3019
3155
  refreshError = signal(null, ...(ngDevMode ? [{ debugName: "refreshError" }] : []));
3156
+ loadingMore = signal(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : []));
3157
+ loadMoreError = signal(null, ...(ngDevMode ? [{ debugName: "loadMoreError" }] : []));
3020
3158
  filter = injectWorkspaceFilter(this.store.workspaces);
3021
3159
  // A workspace shared read-only cannot be deleted: it is not offered for selection either, so the
3022
3160
  // count, "select all" and the Delete button never include one.
@@ -3025,6 +3163,19 @@ class WorkspaceList {
3025
3163
  key: (workspace) => workspace.id,
3026
3164
  selectable: workspace => canWrite(workspace.currentUserRole)
3027
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 });
3028
3179
  constructor() {
3029
3180
  void this.load();
3030
3181
  }
@@ -3034,12 +3185,14 @@ class WorkspaceList {
3034
3185
  if (this.store.loaded()) {
3035
3186
  this.status.set("ready");
3036
3187
  await this.refresh();
3188
+ this.liveRefresh.start();
3037
3189
  return;
3038
3190
  }
3039
3191
  this.status.set("loading");
3040
3192
  try {
3041
3193
  await this.store.ensureLoaded();
3042
3194
  this.status.set("ready");
3195
+ this.liveRefresh.start();
3043
3196
  }
3044
3197
  catch (error) {
3045
3198
  this.loadError.set(workspaceErrorMessage(error));
@@ -3057,6 +3210,21 @@ class WorkspaceList {
3057
3210
  this.refreshError.set(this.translate(workspaceErrorMessage(error)));
3058
3211
  }
3059
3212
  }
3213
+ async loadMore() {
3214
+ if (this.loadingMore())
3215
+ return;
3216
+ this.loadMoreError.set(null);
3217
+ this.loadingMore.set(true);
3218
+ try {
3219
+ await this.store.loadMore();
3220
+ }
3221
+ catch (error) {
3222
+ this.loadMoreError.set(this.translate(workspaceErrorMessage(error)));
3223
+ }
3224
+ finally {
3225
+ this.loadingMore.set(false);
3226
+ }
3227
+ }
3060
3228
  onSearch(event) {
3061
3229
  this.filter.query.set(event.target.value);
3062
3230
  }
@@ -3101,7 +3269,7 @@ class WorkspaceList {
3101
3269
  return "key" in message ? this.transloco.translate(message.key, message.params) : message.text;
3102
3270
  }
3103
3271
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceList, deps: [], target: i0.ɵɵFactoryTarget.Component });
3104
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: WorkspaceList, isStandalone: true, selector: "workspace-list", outputs: { opened: "opened", createRequested: "createRequested" }, host: { classAttribute: "@container flex h-full min-h-0 flex-col p-3 pb-2 md:p-8 md:pb-4" }, providers: [provideTranslocoScope("document-workspaces")], ngImport: i0, template: "\n <div class=\"mb-1 flex items-center justify-between gap-4\">\n <h1 class=\"text-2xl leading-[31px] font-bold text-(--font-neutral-black)\">{{ \"documentWorkspaces.list.title\" | transloco }}</h1>\n <button size=\"sm\" variant=\"primary\" class=\"gap-1.5 rounded-full\" (click)=\"createRequested.emit()\">\n <plus-icon />\n {{ \"documentWorkspaces.list.addWorkspace\" | transloco }}\n </button>\n </div>\n <p class=\"mb-6 text-base text-(--font-primary-base)\">{{ \"documentWorkspaces.list.subtitle\" | transloco }}</p>\n\n <div role=\"tablist\" class=\"mb-3 flex w-fit max-w-full overflow-x-auto border-b border-(--stroke-primary-lighter)\">\n @for (item of tabs; track item.tab) {\n <button\n variant=\"none\"\n role=\"tab\"\n type=\"button\"\n class=\"flex h-11 items-center gap-1 rounded-t-lg border-b-2 border-transparent px-3 text-sm font-semibold whitespace-nowrap text-(--font-primary-base) hover:bg-(--bg-primary-lightest) aria-selected:border-(--stroke-primary-base) aria-selected:bg-(--bg-neutral-white)\"\n [attr.aria-selected]=\"filter.tab() === item.tab\"\n (click)=\"filter.tab.set(item.tab)\">\n @switch (item.tab) {\n @case (\"editor\") {\n <user-icon class=\"size-4\" />\n }\n @case (\"reader\") {\n <user-round-icon class=\"size-4\" />\n }\n }\n {{ item.labelKey | transloco }}\n <span\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-(--bg-primary-light) px-1 text-[10px] leading-[10px] font-semibold\"\n [attr.data-testid]=\"'badge-' + item.tab\"\n >{{ filter.counts()[item.tab] }}</span\n >\n </button>\n }\n </div>\n\n <div class=\"mb-6 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]=\"filter.query()\"\n [attr.placeholder]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [attr.aria-label]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n (input)=\"onSearch($event)\" />\n @if (filter.query()) {\n <button variant=\"none\" type=\"button\" class=\"inline-flex\" [attr.aria-label]=\"'documentWorkspaces.common.clearSearch' | transloco\" (click)=\"filter.clearQuery()\">\n <x-mark-icon class=\"size-4\" />\n </button>\n }\n </div>\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 <span class=\"text-sm text-(--font-primary-base)\" aria-live=\"polite\">{{ \"documentWorkspaces.list.selectedCount\" | transloco: { count: selection.count() } }}</span>\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 @if (refreshError(); as message) {\n <p role=\"alert\" class=\"text-sm text-(--font-error-base)\">{{ \"documentWorkspaces.list.refreshError\" | transloco: { error: message } }}</p>\n }\n\n <!-- Only the workspaces scroll: the title, the tabs and the toolbar stay in place. -->\n <div class=\"min-h-0 flex-1 overflow-y-auto\">\n @switch (status()) {\n @case (\"loading\") {\n <p class=\"text-sm text-(--font-primary-muted)\">{{ \"documentWorkspaces.list.loading\" | 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.list.loadError\" | transloco }}\n }\n </span>\n <button variant=\"secondary\" size=\"xs\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (!filter.filtered().length) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"empty\">\n <!-- The illustration is for a list with nothing in it, not for a search that found nothing. -->\n @if (!store.workspaces().length) {\n <empty-folder-icon />\n }\n <p class=\"text-sm text-(--font-primary-muted)\">\n {{ (store.workspaces().length ? \"documentWorkspaces.list.noMatch\" : \"documentWorkspaces.list.empty\") | transloco }}\n </p>\n </div>\n } @else if (viewMode() === \"grid\") {\n <div class=\"grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] content-start gap-4 pb-8\">\n @for (workspace of filter.filtered(); track workspace.id) {\n <workspace-card\n [workspace]=\"workspace\"\n [query]=\"filter.query()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(workspace)\"\n [selectable]=\"selection.canSelect(workspace)\"\n (opened)=\"opened.emit(workspace)\"\n (deleteRequested)=\"confirmDelete([workspace])\"\n (selectedChange)=\"selection.toggle(workspace, $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_auto_2.5rem] items-center gap-x-4 px-3 data-selecting:grid-cols-[2rem_minmax(0,1fr)_auto_auto_2.5rem] @xl:grid-cols-[minmax(0,1fr)_4.5rem_6.5rem_6.5rem_2.5rem] @xl:data-selecting:grid-cols-[2rem_minmax(0,1fr)_4.5rem_6.5rem_6.5rem_2.5rem] @3xl:grid-cols-[minmax(0,2fr)_5.5rem_7.5rem_9rem_minmax(5rem,1fr)_2.5rem] @3xl:data-selecting:grid-cols-[2rem_minmax(0,2fr)_5.5rem_7.5rem_9rem_minmax(5rem,1fr)_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.list.columnName\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.list.columnContent\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.list.columnOwner\" | transloco }}</div>\n <div role=\"columnheader\" class=\"hidden @xl:block\">{{ \"documentWorkspaces.list.columnDateUpdated\" | transloco }}</div>\n <div role=\"columnheader\" class=\"hidden @3xl:block\">{{ \"documentWorkspaces.list.columnTags\" | transloco }}</div>\n <div role=\"columnheader\"></div>\n </div>\n @for (workspace of filter.filtered(); track workspace.id) {\n <workspace-row\n [workspace]=\"workspace\"\n [query]=\"filter.query()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(workspace)\"\n [selectable]=\"selection.canSelect(workspace)\"\n (opened)=\"opened.emit(workspace)\"\n (deleteRequested)=\"confirmDelete([workspace])\"\n (selectedChange)=\"selection.toggle(workspace, $event)\" />\n }\n </div>\n }\n }\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { 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: PlusIcon, selector: "plus-icon, PlusIcon", 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: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: UserRoundIcon, selector: "user-round, UserRound, userround, user-round-icon, UserRoundIcon, userroundicon", inputs: ["class"] }, { kind: "component", type: WorkspaceCard, selector: "workspace-card", inputs: ["workspace", "query", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: WorkspaceMessage, selector: "workspace-message", inputs: ["message"] }, { kind: "component", type: WorkspaceRow, selector: "workspace-row", inputs: ["workspace", "query", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3272
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: WorkspaceList, isStandalone: true, selector: "workspace-list", outputs: { opened: "opened", createRequested: "createRequested" }, host: { classAttribute: "@container flex h-full min-h-0 flex-col p-3 pb-2 md:p-8 md:pb-4" }, providers: [provideTranslocoScope("document-workspaces")], ngImport: i0, template: "\n <div class=\"mb-1 flex items-center justify-between gap-4\">\n <h1 class=\"text-2xl leading-[31px] font-bold text-(--font-neutral-black)\">{{ \"documentWorkspaces.list.title\" | transloco }}</h1>\n <button size=\"sm\" variant=\"primary\" class=\"gap-1.5 rounded-full\" (click)=\"createRequested.emit()\">\n <plus-icon />\n {{ \"documentWorkspaces.list.addWorkspace\" | transloco }}\n </button>\n </div>\n <p class=\"mb-6 text-base text-(--font-primary-base)\">{{ \"documentWorkspaces.list.subtitle\" | transloco }}</p>\n\n <div role=\"tablist\" class=\"mb-3 flex w-fit max-w-full overflow-x-auto border-b border-(--stroke-primary-lighter)\">\n @for (item of tabs; track item.tab) {\n <button\n variant=\"none\"\n role=\"tab\"\n type=\"button\"\n class=\"flex h-11 items-center gap-1 rounded-t-lg border-b-2 border-transparent px-3 text-sm font-semibold whitespace-nowrap text-(--font-primary-base) hover:bg-(--bg-primary-lightest) aria-selected:border-(--stroke-primary-base) aria-selected:bg-(--bg-neutral-white)\"\n [attr.aria-selected]=\"filter.tab() === item.tab\"\n (click)=\"filter.tab.set(item.tab)\">\n @switch (item.tab) {\n @case (\"editor\") {\n <user-icon class=\"size-4\" />\n }\n @case (\"reader\") {\n <user-round-icon class=\"size-4\" />\n }\n }\n {{ item.labelKey | transloco }}\n <span\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-(--bg-primary-light) px-1 text-[10px] leading-[10px] font-semibold\"\n [attr.data-testid]=\"'badge-' + item.tab\"\n >{{ filter.counts()[item.tab] }}</span\n >\n </button>\n }\n </div>\n\n <div class=\"mb-6 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]=\"filter.query()\"\n [attr.placeholder]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [attr.aria-label]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n (input)=\"onSearch($event)\" />\n @if (filter.query()) {\n <button variant=\"none\" type=\"button\" class=\"inline-flex\" [attr.aria-label]=\"'documentWorkspaces.common.clearSearch' | transloco\" (click)=\"filter.clearQuery()\">\n <x-mark-icon class=\"size-4\" />\n </button>\n }\n </div>\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 <span class=\"text-sm text-(--font-primary-base)\" aria-live=\"polite\">{{ \"documentWorkspaces.list.selectedCount\" | transloco: { count: selection.count() } }}</span>\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 @if (refreshError(); as message) {\n <p role=\"alert\" class=\"text-sm text-(--font-error-base)\">{{ \"documentWorkspaces.list.refreshError\" | transloco: { error: message } }}</p>\n }\n\n <!-- Only the workspaces scroll: the title, the tabs and the toolbar stay in place. -->\n <div class=\"min-h-0 flex-1 overflow-y-auto\">\n @switch (status()) {\n @case (\"loading\") {\n <p class=\"text-sm text-(--font-primary-muted)\">{{ \"documentWorkspaces.list.loading\" | 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.list.loadError\" | transloco }}\n }\n </span>\n <button variant=\"secondary\" size=\"xs\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (!filter.filtered().length) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"empty\">\n <!-- The illustration is for a list with nothing in it, not for a search that found nothing. -->\n @if (!store.workspaces().length) {\n <empty-folder-icon />\n }\n <p class=\"text-sm text-(--font-primary-muted)\">\n {{ (store.workspaces().length ? \"documentWorkspaces.list.noMatch\" : \"documentWorkspaces.list.empty\") | transloco }}\n </p>\n </div>\n } @else if (viewMode() === \"grid\") {\n <div class=\"grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] content-start gap-4 pb-8\">\n @for (workspace of filter.filtered(); track workspace.id) {\n <workspace-card\n [workspace]=\"workspace\"\n [query]=\"filter.query()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(workspace)\"\n [selectable]=\"selection.canSelect(workspace)\"\n (opened)=\"opened.emit(workspace)\"\n (deleteRequested)=\"confirmDelete([workspace])\"\n (selectedChange)=\"selection.toggle(workspace, $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_auto_2.5rem] items-center gap-x-4 px-3 data-selecting:grid-cols-[2rem_minmax(0,1fr)_auto_auto_2.5rem] @xl:grid-cols-[minmax(0,1fr)_4.5rem_6.5rem_6.5rem_2.5rem] @xl:data-selecting:grid-cols-[2rem_minmax(0,1fr)_4.5rem_6.5rem_6.5rem_2.5rem] @3xl:grid-cols-[minmax(0,2fr)_5.5rem_7.5rem_9rem_minmax(5rem,1fr)_2.5rem] @3xl:data-selecting:grid-cols-[2rem_minmax(0,2fr)_5.5rem_7.5rem_9rem_minmax(5rem,1fr)_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.list.columnName\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.list.columnContent\" | transloco }}</div>\n <div role=\"columnheader\">{{ \"documentWorkspaces.list.columnOwner\" | transloco }}</div>\n <div role=\"columnheader\" class=\"hidden @xl:block\">{{ \"documentWorkspaces.list.columnDateUpdated\" | transloco }}</div>\n <div role=\"columnheader\" class=\"hidden @3xl:block\">{{ \"documentWorkspaces.list.columnTags\" | transloco }}</div>\n <div role=\"columnheader\"></div>\n </div>\n @for (workspace of filter.filtered(); track workspace.id) {\n <workspace-row\n [workspace]=\"workspace\"\n [query]=\"filter.query()\"\n [selecting]=\"selection.active()\"\n [selected]=\"selection.isSelected(workspace)\"\n [selectable]=\"selection.canSelect(workspace)\"\n (opened)=\"opened.emit(workspace)\"\n (deleteRequested)=\"confirmDelete([workspace])\"\n (selectedChange)=\"selection.toggle(workspace, $event)\" />\n }\n </div>\n }\n\n @if (store.hasMore() || store.truncated()) {\n <div class=\"flex flex-col items-center gap-2 pb-8\" data-testid=\"load-more\">\n <p class=\"text-sm text-(--font-primary-muted)\">\n {{ \"documentWorkspaces.list.partial\" | transloco: { shown: store.workspaces().length, total: store.pagination().total } }}\n </p>\n @if (loadMoreError(); as message) {\n <p role=\"alert\" class=\"text-sm text-(--font-error-base)\">{{ \"documentWorkspaces.list.loadMoreError\" | transloco: { error: message } }}</p>\n }\n @if (store.hasMore()) {\n <button variant=\"secondary\" size=\"sm\" [disabled]=\"loadingMore()\" (click)=\"loadMore()\">\n {{ \"documentWorkspaces.list.loadMore\" | transloco }}\n </button>\n }\n </div>\n }\n }\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { 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: PlusIcon, selector: "plus-icon, PlusIcon", 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: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: UserRoundIcon, selector: "user-round, UserRound, userround, user-round-icon, UserRoundIcon, userroundicon", inputs: ["class"] }, { kind: "component", type: WorkspaceCard, selector: "workspace-card", inputs: ["workspace", "query", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: WorkspaceMessage, selector: "workspace-message", inputs: ["message"] }, { kind: "component", type: WorkspaceRow, selector: "workspace-row", inputs: ["workspace", "query", "selecting", "selected", "selectable"], outputs: ["opened", "deleteRequested", "selectedChange"] }, { kind: "component", type: XMarkIcon, selector: "XMarkIcon, xmark-icon, x-mark-icon", inputs: ["class"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3105
3273
  }
3106
3274
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceList, decorators: [{
3107
3275
  type: Component,
@@ -3305,6 +3473,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
3305
3473
  }
3306
3474
  </div>
3307
3475
  }
3476
+
3477
+ @if (store.hasMore() || store.truncated()) {
3478
+ <div class="flex flex-col items-center gap-2 pb-8" data-testid="load-more">
3479
+ <p class="text-sm text-(--font-primary-muted)">
3480
+ {{ "documentWorkspaces.list.partial" | transloco: { shown: store.workspaces().length, total: store.pagination().total } }}
3481
+ </p>
3482
+ @if (loadMoreError(); as message) {
3483
+ <p role="alert" class="text-sm text-(--font-error-base)">{{ "documentWorkspaces.list.loadMoreError" | transloco: { error: message } }}</p>
3484
+ }
3485
+ @if (store.hasMore()) {
3486
+ <button variant="secondary" size="sm" [disabled]="loadingMore()" (click)="loadMore()">
3487
+ {{ "documentWorkspaces.list.loadMore" | transloco }}
3488
+ </button>
3489
+ }
3490
+ </div>
3491
+ }
3308
3492
  }
3309
3493
  }
3310
3494
  </div>
@@ -3795,6 +3979,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
3795
3979
  }]
3796
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"] }] } });
3797
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
+
3798
4005
  /** The files staged for upload, before anything is sent. */
3799
4006
  class UploadFileList {
3800
4007
  files = input.required(...(ngDevMode ? [{ debugName: "files" }] : []));
@@ -5549,6 +5756,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5549
5756
  * shown: it says what the user asked for (`back`, `deleted`, `askFollowUp`) and whoever
5550
5757
  * hosts it decides. The document open in the preview is the `previewedDocumentId` model.
5551
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
+ *
5552
5762
  * Only someone who can write sees the edit, share, delete, upload and select controls.
5553
5763
  */
5554
5764
  class WorkspaceDetail {
@@ -5579,6 +5789,8 @@ class WorkspaceDetail {
5579
5789
  refreshing = signal(false, ...(ngDevMode ? [{ debugName: "refreshing" }] : []));
5580
5790
  loadingMore = signal(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : []));
5581
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" }] : []));
5582
5794
  workspace = computed(() => this.store.workspace(this.workspaceId()), ...(ngDevMode ? [{ debugName: "workspace" }] : []));
5583
5795
  // Only what is really uploaded or being indexed is listed: transfers and failures
5584
5796
  // belong to the upload tracker, not to the workspace. A document with no status at
@@ -5604,6 +5816,42 @@ class WorkspaceDetail {
5604
5816
  // Selecting only serves deleting, which a reader cannot do.
5605
5817
  selectable: () => this.writable()
5606
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 });
5607
5855
  constructor() {
5608
5856
  effect(() => {
5609
5857
  this.workspaceId();
@@ -5629,8 +5877,19 @@ class WorkspaceDetail {
5629
5877
  this.query.set("");
5630
5878
  this.refreshError.set(null);
5631
5879
  this.loadMoreError.set(null);
5880
+ this.deletedElsewhere.set(false);
5632
5881
  await this.load();
5633
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
+ }
5634
5893
  /** Reads the workspace (when it is not cached) and its documents, for the workspace currently shown. */
5635
5894
  async load() {
5636
5895
  const id = this.workspaceId();
@@ -5653,6 +5912,7 @@ class WorkspaceDetail {
5653
5912
  return;
5654
5913
  this.status.set("ready");
5655
5914
  this.uploads.watch(id);
5915
+ this.liveRefresh.start();
5656
5916
  }
5657
5917
  /** Reads the documents again without leaving the view; a failure leaves the ones shown as they are. */
5658
5918
  async refresh() {
@@ -5752,7 +6012,7 @@ class WorkspaceDetail {
5752
6012
  return "key" in message ? this.transloco.translate(message.key, message.params) : message.text;
5753
6013
  }
5754
6014
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceDetail, deps: [], target: i0.ɵɵFactoryTarget.Component });
5755
- 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 });
5756
6016
  }
5757
6017
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: WorkspaceDetail, decorators: [{
5758
6018
  type: Component,
@@ -5769,6 +6029,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5769
6029
  GridIcon,
5770
6030
  ListIcon,
5771
6031
  MagnifyingGlassIcon,
6032
+ NotFoundIcon,
5772
6033
  PenToSquareIcon,
5773
6034
  RefreshIcon,
5774
6035
  ResizeHandle,
@@ -5792,7 +6053,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5792
6053
  [iconOnly]="true"
5793
6054
  [attr.aria-label]="'documentWorkspaces.detail.back' | transloco"
5794
6055
  [tooltip]="'documentWorkspaces.detail.back' | transloco"
5795
- (click)="back.emit()">
6056
+ (click)="leave()">
5796
6057
  <chevron-down-icon class="rotate-90" />
5797
6058
  </button>
5798
6059
 
@@ -5800,34 +6061,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5800
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>
5801
6062
 
5802
6063
  <!-- Beside the title while they fit next to its 16rem, on a line of their own under it once they do not. -->
5803
- <div class="ml-auto flex shrink-0 items-center gap-1">
5804
- <button size="sm"
5805
- variant="ghost"
5806
- class="gap-1.5 text-(--font-error-base) hover:bg-(--bg-error-lightest)"
5807
- [writerOnly]="workspace.currentUserRole"
5808
- (click)="confirmDeleteWorkspace(workspace)">
5809
- <trash-icon />
5810
- {{ "documentWorkspaces.common.delete" | transloco }}
5811
- </button>
5812
- <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="share(workspace)">
5813
- <user-round-icon />
5814
- {{ "documentWorkspaces.share.title" | transloco }}
5815
- </button>
5816
- <button size="sm" variant="ghost" class="gap-1.5" [writerOnly]="workspace.currentUserRole" (click)="edit(workspace)">
5817
- <pen-to-square-icon />
5818
- {{ "documentWorkspaces.common.edit" | transloco }}
5819
- </button>
5820
- <button size="sm"
5821
- variant="ghost"
5822
- class="w-6"
5823
- [iconOnly]="true"
5824
- [disabled]="refreshing()"
5825
- [attr.aria-label]="'documentWorkspaces.common.refresh' | transloco"
5826
- [tooltip]="'documentWorkspaces.common.refresh' | transloco"
5827
- (click)="refresh()">
5828
- <refresh-icon />
5829
- </button>
5830
- </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
+ }
5831
6094
  }
5832
6095
  </div>
5833
6096
 
@@ -5842,6 +6105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5842
6105
  </p>
5843
6106
  }
5844
6107
 
6108
+ @if (!deletedElsewhere()) {
5845
6109
  <div class="mb-3 flex flex-wrap items-center gap-2">
5846
6110
  <div role="group" class="inline-flex gap-1 rounded-full bg-(--bg-primary-lighter) p-0.5">
5847
6111
  <button
@@ -5912,6 +6176,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5912
6176
  {{ "documentWorkspaces.detail.fileCount" | transloco: { count: workspace.documentCount } }}
5913
6177
  }
5914
6178
  </p>
6179
+ }
5915
6180
  }
5916
6181
 
5917
6182
  @if (refreshError(); as message) {
@@ -5937,7 +6202,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
5937
6202
  </div>
5938
6203
  }
5939
6204
  @default {
5940
- @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) {
5941
6212
  @if (!documents().length) {
5942
6213
  <div class="flex flex-col items-center gap-1 py-8 text-center" data-testid="empty">
5943
6214
  <empty-folder-icon />