@sinequa/atomic-angular 2.15.0 → 2.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,7 +2,7 @@ import * as i0 from "@angular/core";
2
2
  import { ChangeDetectionStrategy, Component, DestroyRef, Directive, ElementRef, Injectable, InjectionToken, Injector, Pipe, ViewEncapsulation, afterNextRender, assertInInjectionContext, booleanAttribute, computed, effect, forwardRef, inject, input, model, output, signal, untracked, viewChild } from "@angular/core";
3
3
  import { withDevtools } from "@ngrx-toolkit/core";
4
4
  import { patchState, signalStore, withComputed, withMethods, withState } from "@ngrx/signals";
5
- import { ApiError, addWorkspaceDocuments, createHeaders, createWorkspace, deleteWorkspaceDocuments, deleteWorkspaces, fetchPreview, fetchPrincipal, fetchPrincipalsByIds, fetchUploadStatus, getWorkspace, globalConfig, listWorkspaceDocuments, listWorkspaces, onAuthEvent, searchPrincipals, tryAutoAuthentication, updateWorkspace } from "@sinequa/atomic";
5
+ import { ApiError, addWorkspaceDocuments, createHeaders, createWorkspace, deleteWorkspaceDocuments, deleteWorkspaces, fetchPreview, fetchPrincipal, fetchPrincipalsByIds, fetchUploadStatus, fetchWorkspace, fetchWorkspaceContent, fetchWorkspaces, globalConfig, onAuthEvent, searchPrincipals, tryAutoAuthentication, updateWorkspace } from "@sinequa/atomic";
6
6
  import { Upload } from "tus-js-client";
7
7
  import { toSignal } from "@angular/core/rxjs-interop";
8
8
  import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
@@ -42,7 +42,46 @@ function mapWireWorkspaceToWorkspace(dto) {
42
42
  creatorId: dto.creator,
43
43
  ownerIds: dto.owners ?? [],
44
44
  readerIds: dto.readers ?? [],
45
- currentUserRole: dto.currentUserRole
45
+ currentUserRole: dto.currentUserRole,
46
+ membersKnown: true
47
+ };
48
+ }
49
+ function parseMeta(record) {
50
+ const meta = record.meta;
51
+ if (typeof meta !== "string") return {};
52
+ try {
53
+ const parsed = JSON.parse(meta);
54
+ return parsed && typeof parsed === "object" ? parsed : {};
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
59
+ const asString = (value) => typeof value === "string" ? value : void 0;
60
+ function mapWorkspaceRecordToWorkspace(record) {
61
+ const flags = record;
62
+ const meta = parseMeta(record);
63
+ return {
64
+ id: record.id,
65
+ name: record.title ?? "",
66
+ color: workspaceColorFor(record.id),
67
+ description: asString(meta["description"]) ?? "",
68
+ tags: Array.isArray(meta["tags"]) ? meta["tags"].filter((t) => typeof t === "string") : [],
69
+ documentCount: void 0,
70
+ creatorId: "",
71
+ ownerIds: [],
72
+ readerIds: [],
73
+ currentUserRole: flags.isCreator ? "creator" : flags.isOwner ? "owner" : "reader",
74
+ membersKnown: false
75
+ };
76
+ }
77
+ function mapDocumentRecordToWorkspaceDocument(record, workspaceId) {
78
+ const name = record.title ?? "";
79
+ return {
80
+ id: record.id,
81
+ name,
82
+ workspaceId,
83
+ type: getDocumentType(name),
84
+ createdAt: asString(parseMeta(record)["createdAt"])
46
85
  };
47
86
  }
48
87
  function mapDocumentStatus(status) {
@@ -71,6 +110,7 @@ function mapWireDocumentToWorkspaceDocument(dto) {
71
110
  };
72
111
  }
73
112
  const PAGE_SIZE = 100;
113
+ const WORKSPACES_QUERY_NAME = "query_Web";
74
114
  const initialState$1 = {
75
115
  workspaces: [],
76
116
  loaded: false,
@@ -81,10 +121,12 @@ const initialState$1 = {
81
121
  },
82
122
  documents: {},
83
123
  documentPagination: {},
84
- selectedIds: []
124
+ selectedIds: [],
125
+ searchText: "",
126
+ documentSearch: {}
85
127
  };
86
128
  function withDocumentCountAdjusted(workspaces, id, delta) {
87
- return workspaces.map((w) => w.id === id ? {
129
+ return workspaces.map((w) => w.id === id && w.documentCount !== void 0 ? {
88
130
  ...w,
89
131
  documentCount: Math.max(0, w.documentCount + delta)
90
132
  } : w);
@@ -118,19 +160,30 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
118
160
  };
119
161
  });
120
162
  };
163
+ const documentText = (workspaceId) => store.documentSearch()[workspaceId] ?? "";
121
164
  const loadDocuments = async (workspaceId, signal) => {
122
- const res = await listWorkspaceDocuments(workspaceId, {
165
+ const text = documentText(workspaceId);
166
+ const res = await fetchWorkspaceContent(workspaceId, {
167
+ queryName: WORKSPACES_QUERY_NAME,
168
+ text,
123
169
  page: 1,
124
170
  pageSize: PAGE_SIZE
125
171
  }, { signal });
126
- const documents = res.documents.map(mapWireDocumentToWorkspaceDocument);
127
- patchState(store, (state) => ({ documentPagination: {
128
- ...state.documentPagination,
129
- [workspaceId]: {
130
- page: res.page,
131
- total: res.total
132
- }
133
- } }));
172
+ if (documentText(workspaceId) !== text) return store.documents()[workspaceId] ?? [];
173
+ const documents = res.records.map((r) => mapDocumentRecordToWorkspaceDocument(r, workspaceId));
174
+ patchState(store, (state) => ({
175
+ documentPagination: {
176
+ ...state.documentPagination,
177
+ [workspaceId]: {
178
+ page: res.page,
179
+ total: res.rowCount
180
+ }
181
+ },
182
+ workspaces: text ? state.workspaces : state.workspaces.map((w) => w.id === workspaceId ? {
183
+ ...w,
184
+ documentCount: res.rowCount
185
+ } : w)
186
+ }));
134
187
  setDocuments(workspaceId, documents);
135
188
  return documents;
136
189
  };
@@ -140,25 +193,32 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
140
193
  await loadDocuments(workspaceId);
141
194
  } catch {}
142
195
  };
196
+ let loadSeq = 0;
143
197
  const load = async () => {
198
+ const seq = ++loadSeq;
144
199
  patchState(store, { loading: true });
145
200
  try {
146
- const res = await listWorkspaces({
201
+ const res = await fetchWorkspaces({
202
+ queryName: WORKSPACES_QUERY_NAME,
203
+ text: store.searchText(),
147
204
  page: 1,
148
205
  pageSize: PAGE_SIZE
149
206
  });
207
+ if (seq !== loadSeq) return store.workspaces();
150
208
  patchState(store, {
151
- workspaces: res.workspaces.map(mapWireWorkspaceToWorkspace),
209
+ workspaces: res.records.map(mapWorkspaceRecordToWorkspace),
152
210
  pagination: {
153
211
  page: res.page,
154
- total: res.total
212
+ total: res.rowCount
155
213
  },
156
214
  loaded: true
157
215
  });
158
216
  return store.workspaces();
159
217
  } finally {
160
- patchState(store, { loading: false });
161
- loadingPromise = null;
218
+ if (seq === loadSeq) {
219
+ patchState(store, { loading: false });
220
+ loadingPromise = null;
221
+ }
162
222
  }
163
223
  };
164
224
  return {
@@ -181,24 +241,35 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
181
241
  loadingPromise ??= load();
182
242
  return loadingPromise;
183
243
  },
244
+ search(text) {
245
+ const value = text.trim();
246
+ if (value === store.searchText()) return loadingPromise ?? Promise.resolve(store.workspaces());
247
+ patchState(store, { searchText: value });
248
+ loadingPromise = load();
249
+ return loadingPromise;
250
+ },
184
251
  loadMore() {
185
252
  if (!store.hasMore()) return Promise.resolve(store.workspaces());
186
253
  loadingMorePromise ??= (async () => {
187
254
  try {
188
255
  const current = store.workspaces();
189
256
  const page = Math.min(store.pagination().page + 1, Math.floor(current.length / PAGE_SIZE) + 1);
190
- const res = await listWorkspaces({
257
+ const text = store.searchText();
258
+ const res = await fetchWorkspaces({
259
+ queryName: WORKSPACES_QUERY_NAME,
260
+ text,
191
261
  page,
192
262
  pageSize: PAGE_SIZE
193
263
  });
264
+ if (text !== store.searchText()) return store.workspaces();
194
265
  patchState(store, (state) => {
195
266
  const loaded = new Set(state.workspaces.map((w) => w.id));
196
- const fresh = res.workspaces.map(mapWireWorkspaceToWorkspace).filter((w) => !loaded.has(w.id));
267
+ const fresh = res.records.map(mapWorkspaceRecordToWorkspace).filter((w) => !loaded.has(w.id));
197
268
  return {
198
269
  workspaces: [...state.workspaces, ...fresh],
199
270
  pagination: {
200
271
  page: res.page,
201
- total: res.total
272
+ total: res.rowCount
202
273
  }
203
274
  };
204
275
  });
@@ -210,7 +281,17 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
210
281
  return loadingMorePromise;
211
282
  },
212
283
  async fetch(id) {
213
- const workspace = mapWireWorkspaceToWorkspace(await getWorkspace(id));
284
+ const fetched = mapWorkspaceRecordToWorkspace(await fetchWorkspace(id, { queryName: WORKSPACES_QUERY_NAME }));
285
+ if (fetched.id !== id) throw new ApiError(`Workspace ${id} not found`, 404);
286
+ const cached = store.workspaces().find((w) => w.id === id);
287
+ const workspace = cached ? {
288
+ ...fetched,
289
+ documentCount: cached.documentCount,
290
+ creatorId: cached.creatorId,
291
+ ownerIds: cached.ownerIds,
292
+ readerIds: cached.readerIds,
293
+ membersKnown: cached.membersKnown
294
+ } : fetched;
214
295
  patchState(store, (state) => ({ workspaces: state.workspaces.some((w) => w.id === id) ? state.workspaces.map((w) => w.id === id ? workspace : w) : [...state.workspaces, workspace] }));
215
296
  return workspace;
216
297
  },
@@ -270,27 +351,48 @@ const WorkspacesStore = signalStore({ providedIn: "root" }, withDevtools("Worksp
270
351
  } : state.pagination,
271
352
  documents: Object.fromEntries(Object.entries(state.documents).filter(([wid]) => wid !== id)),
272
353
  documentPagination: Object.fromEntries(Object.entries(state.documentPagination).filter(([wid]) => wid !== id)),
354
+ documentSearch: Object.fromEntries(Object.entries(state.documentSearch).filter(([wid]) => wid !== id)),
273
355
  selectedIds: state.selectedIds.filter((selected) => selected !== id)
274
356
  }));
275
357
  },
276
358
  loadDocuments,
359
+ searchDocuments(workspaceId, text, signal) {
360
+ const value = text.trim();
361
+ if (value === documentText(workspaceId)) return Promise.resolve(store.documents()[workspaceId] ?? []);
362
+ patchState(store, (state) => ({ documentSearch: {
363
+ ...state.documentSearch,
364
+ [workspaceId]: value
365
+ } }));
366
+ return loadDocuments(workspaceId, signal);
367
+ },
368
+ resetDocumentSearch(workspaceId) {
369
+ if (!documentText(workspaceId)) return;
370
+ patchState(store, (state) => ({ documentSearch: {
371
+ ...state.documentSearch,
372
+ [workspaceId]: ""
373
+ } }));
374
+ },
277
375
  async loadMoreDocuments(workspaceId) {
278
376
  const current = store.documents()[workspaceId] ?? [];
279
377
  const pagination = store.documentPagination()[workspaceId];
280
378
  if (!pagination || current.length >= pagination.total) return current;
281
379
  const page = Math.min(pagination.page + 1, Math.floor(current.length / PAGE_SIZE) + 1);
282
- const res = await listWorkspaceDocuments(workspaceId, {
380
+ const text = documentText(workspaceId);
381
+ const res = await fetchWorkspaceContent(workspaceId, {
382
+ queryName: WORKSPACES_QUERY_NAME,
383
+ text,
283
384
  page,
284
385
  pageSize: PAGE_SIZE
285
386
  });
387
+ if (documentText(workspaceId) !== text) return store.documents()[workspaceId] ?? [];
286
388
  const loaded = new Set(current.map((d) => d.id));
287
- const fresh = res.documents.map(mapWireDocumentToWorkspaceDocument).filter((d) => !loaded.has(d.id));
389
+ const fresh = res.records.map((r) => mapDocumentRecordToWorkspaceDocument(r, workspaceId)).filter((d) => !loaded.has(d.id));
288
390
  const merged = [...current, ...fresh];
289
391
  patchState(store, (state) => ({ documentPagination: {
290
392
  ...state.documentPagination,
291
393
  [workspaceId]: {
292
394
  page: res.page,
293
- total: res.total
395
+ total: res.rowCount
294
396
  }
295
397
  } }));
296
398
  setDocuments(workspaceId, merged);
@@ -2089,7 +2191,7 @@ function injectWorkspaceFilter(workspaces, options = {}) {
2089
2191
  }, ...ngDevMode ? [{ debugName: "counts" }] : /* istanbul ignore next */ []),
2090
2192
  filtered: computed(() => workspaces().filter((w) => {
2091
2193
  const inTab = tab() === "all" || tab() === "editor" === canWrite(w.currentUserRole);
2092
- const inQuery = matchesText(w.name, query()) || options.fields?.() === "name+description" && matchesText(w.description, query());
2194
+ const inQuery = options.serverSide || matchesText(w.name, query()) || options.fields?.() === "name+description" && matchesText(w.description, query());
2093
2195
  return inTab && inQuery;
2094
2196
  }), ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []),
2095
2197
  clearQuery: () => query.set("")
@@ -5123,8 +5225,10 @@ var WorkspaceRow = class WorkspaceRow {
5123
5225
  </div>
5124
5226
 
5125
5227
  <div role="cell" class="flex items-center gap-1 text-xs text-(--font-primary-base)">
5126
- <file-icon class="size-3 text-(--font-primary-muted)" />
5127
- {{ workspace().documentCount }}
5228
+ @if (workspace().documentCount !== undefined) {
5229
+ <file-icon class="size-3 text-(--font-primary-muted)" />
5230
+ {{ workspace().documentCount }}
5231
+ }
5128
5232
  </div>
5129
5233
 
5130
5234
  <div role="cell"><workspace-role-tag [role]="workspace().currentUserRole" /></div>
@@ -5320,8 +5424,10 @@ i0.ɵɵngDeclareClassMetadata({
5320
5424
  </div>
5321
5425
 
5322
5426
  <div role="cell" class="flex items-center gap-1 text-xs text-(--font-primary-base)">
5323
- <file-icon class="size-3 text-(--font-primary-muted)" />
5324
- {{ workspace().documentCount }}
5427
+ @if (workspace().documentCount !== undefined) {
5428
+ <file-icon class="size-3 text-(--font-primary-muted)" />
5429
+ {{ workspace().documentCount }}
5430
+ }
5325
5431
  </div>
5326
5432
 
5327
5433
  <div role="cell"><workspace-role-tag [role]="workspace().currentUserRole" /></div>
@@ -5537,10 +5643,12 @@ var WorkspaceCard = class WorkspaceCard {
5537
5643
  <div class="mb-3 h-px bg-(--stroke-primary-lighter)"></div>
5538
5644
  <div class="flex items-center justify-between gap-2">
5539
5645
  <workspace-role-tag [role]="workspace().currentUserRole" />
5540
- <span class="ml-auto flex shrink-0 items-center gap-1 text-xs text-(--font-primary-muted)">
5541
- <file-icon class="size-3" />
5542
- {{ workspace().documentCount }}
5543
- </span>
5646
+ @if (workspace().documentCount !== undefined) {
5647
+ <span class="ml-auto flex shrink-0 items-center gap-1 text-xs text-(--font-primary-muted)">
5648
+ <file-icon class="size-3" />
5649
+ {{ workspace().documentCount }}
5650
+ </span>
5651
+ }
5544
5652
  </div>
5545
5653
  `,
5546
5654
  isInline: true,
@@ -5727,10 +5835,12 @@ i0.ɵɵngDeclareClassMetadata({
5727
5835
  <div class="mb-3 h-px bg-(--stroke-primary-lighter)"></div>
5728
5836
  <div class="flex items-center justify-between gap-2">
5729
5837
  <workspace-role-tag [role]="workspace().currentUserRole" />
5730
- <span class="ml-auto flex shrink-0 items-center gap-1 text-xs text-(--font-primary-muted)">
5731
- <file-icon class="size-3" />
5732
- {{ workspace().documentCount }}
5733
- </span>
5838
+ @if (workspace().documentCount !== undefined) {
5839
+ <span class="ml-auto flex shrink-0 items-center gap-1 text-xs text-(--font-primary-muted)">
5840
+ <file-icon class="size-3" />
5841
+ {{ workspace().documentCount }}
5842
+ </span>
5843
+ }
5734
5844
  </div>
5735
5845
  `
5736
5846
  }]
@@ -5804,6 +5914,7 @@ const TABS = [
5804
5914
  labelKey: "documentWorkspaces.list.tabReader"
5805
5915
  }
5806
5916
  ];
5917
+ const SEARCH_DEBOUNCE_MS$1 = 300;
5807
5918
  var WorkspaceList = class WorkspaceList {
5808
5919
  opened = output();
5809
5920
  createRequested = output();
@@ -5818,7 +5929,7 @@ var WorkspaceList = class WorkspaceList {
5818
5929
  refreshError = signal(null, ...ngDevMode ? [{ debugName: "refreshError" }] : /* istanbul ignore next */ []);
5819
5930
  loadingMore = signal(false, ...ngDevMode ? [{ debugName: "loadingMore" }] : /* istanbul ignore next */ []);
5820
5931
  loadMoreError = signal(null, ...ngDevMode ? [{ debugName: "loadMoreError" }] : /* istanbul ignore next */ []);
5821
- filter = injectWorkspaceFilter(this.store.workspaces);
5932
+ filter = injectWorkspaceFilter(this.store.workspaces, { serverSide: true });
5822
5933
  searchForm = form(this.filter.query);
5823
5934
  selection = injectSelection({
5824
5935
  items: this.filter.filtered,
@@ -5839,6 +5950,24 @@ var WorkspaceList = class WorkspaceList {
5839
5950
  constructor() {
5840
5951
  this.uploads.startPolling();
5841
5952
  this.load();
5953
+ effect((onCleanup) => {
5954
+ const value = this.filter.query().trim();
5955
+ const timer = setTimeout(() => untracked(() => void this.search(value)), value ? SEARCH_DEBOUNCE_MS$1 : 0);
5956
+ onCleanup(() => clearTimeout(timer));
5957
+ });
5958
+ inject(DestroyRef).onDestroy(() => {
5959
+ if (this.store.searchText()) this.store.search("").catch(() => void 0);
5960
+ });
5961
+ }
5962
+ async search(value) {
5963
+ if (this.status() !== "ready" || value === this.store.searchText()) return;
5964
+ this.refreshError.set(null);
5965
+ try {
5966
+ await this.store.search(value);
5967
+ this.selection.exit();
5968
+ } catch (error) {
5969
+ this.refreshError.set(this.translate(workspaceErrorMessage(error)));
5970
+ }
5842
5971
  }
5843
5972
  async load() {
5844
5973
  this.loadError.set(null);
@@ -5931,7 +6060,7 @@ var WorkspaceList = class WorkspaceList {
5931
6060
  host: { classAttribute: "@container flex h-full min-h-0 flex-col p-3 pb-2 md:p-8 md:pb-4" },
5932
6061
  providers: [provideTranslocoScope("document-workspaces")],
5933
6062
  ngImport: i0,
5934
- 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 (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 <Tabs class=\"mb-3 max-w-full\">\n <TabList\n variant=\"inner\"\n class=\"w-fit max-w-full\"\n selectionMode=\"explicit\"\n [softDisabled]=\"false\"\n [attr.aria-label]=\"'documentWorkspaces.list.title' | transloco\"\n [selectedTab]=\"filter.tab()\"\n (selectedTabChange)=\"onTabChange($event)\">\n @for (item of tabs; track item.tab) {\n <Tab [value]=\"item.tab\" variant=\"inner\">\n @switch (item.tab) {\n @case (\"editor\") {\n <user-icon class=\"shrink-0\" />\n }\n @case (\"reader\") {\n <user-round-icon class=\"shrink-0\" />\n }\n }\n <span>{{ item.labelKey | transloco }}</span>\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"shrink-0\" [attr.data-testid]=\"'badge-' + item.tab\">{{ filter.counts()[item.tab] }}</Badge>\n </Tab>\n }\n </TabList>\n </Tabs>\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 <SearchInput\n role=\"search\"\n class=\"min-w-32 flex-[0_1_15rem]\"\n [formField]=\"searchForm\"\n [placeholder]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [aria-label]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [clearLabel]=\"'documentWorkspaces.common.clearSearch' | transloco\" />\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=\"tertiary\" scheme=\"neutral\" size=\"sm\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\" variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" 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=\"sm\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (!filter.filtered().length) {\n <p class=\"py-8 text-center text-sm text-(--font-primary-muted)\" data-testid=\"empty\">\n {{ (store.workspaces().length ? \"documentWorkspaces.list.noMatch\" : \"documentWorkspaces.list.empty\") | transloco }}\n </p>\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 <checkbox\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (checkedChange)=\"selection.toggleAll()\" />\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 ",
6063
+ 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 (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 <Tabs class=\"mb-3 max-w-full\">\n <TabList\n variant=\"inner\"\n class=\"w-fit max-w-full\"\n selectionMode=\"explicit\"\n [softDisabled]=\"false\"\n [attr.aria-label]=\"'documentWorkspaces.list.title' | transloco\"\n [selectedTab]=\"filter.tab()\"\n (selectedTabChange)=\"onTabChange($event)\">\n @for (item of tabs; track item.tab) {\n <Tab [value]=\"item.tab\" variant=\"inner\">\n @switch (item.tab) {\n @case (\"editor\") {\n <user-icon class=\"shrink-0\" />\n }\n @case (\"reader\") {\n <user-round-icon class=\"shrink-0\" />\n }\n }\n <span>{{ item.labelKey | transloco }}</span>\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"shrink-0\" [attr.data-testid]=\"'badge-' + item.tab\">{{ filter.counts()[item.tab] }}</Badge>\n </Tab>\n }\n </TabList>\n </Tabs>\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 <SearchInput\n role=\"search\"\n class=\"min-w-32 flex-[0_1_15rem]\"\n [formField]=\"searchForm\"\n [placeholder]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [aria-label]=\"'documentWorkspaces.list.searchPlaceholder' | transloco\"\n [clearLabel]=\"'documentWorkspaces.common.clearSearch' | transloco\" />\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=\"tertiary\" scheme=\"neutral\" size=\"sm\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\" variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" 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=\"sm\" (click)=\"load()\">{{ \"documentWorkspaces.common.retry\" | transloco }}</button>\n </div>\n }\n @default {\n @if (!filter.filtered().length) {\n <p class=\"py-8 text-center text-sm text-(--font-primary-muted)\" data-testid=\"empty\">\n {{ ((store.workspaces().length || filter.query().trim()) ? \"documentWorkspaces.list.noMatch\" : \"documentWorkspaces.list.empty\") | transloco }}\n </p>\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 <checkbox\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (checkedChange)=\"selection.toggleAll()\" />\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 ",
5935
6064
  isInline: true,
5936
6065
  dependencies: [
5937
6066
  {
@@ -6262,7 +6391,7 @@ i0.ɵɵngDeclareClassMetadata({
6262
6391
  @default {
6263
6392
  @if (!filter.filtered().length) {
6264
6393
  <p class="py-8 text-center text-sm text-(--font-primary-muted)" data-testid="empty">
6265
- {{ (store.workspaces().length ? "documentWorkspaces.list.noMatch" : "documentWorkspaces.list.empty") | transloco }}
6394
+ {{ ((store.workspaces().length || filter.query().trim()) ? "documentWorkspaces.list.noMatch" : "documentWorkspaces.list.empty") | transloco }}
6266
6395
  </p>
6267
6396
  } @else if (viewMode() === "grid") {
6268
6397
  <div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] content-start gap-4 pb-8">
@@ -8203,24 +8332,43 @@ var ShareDialog = class ShareDialog {
8203
8332
  staged = signal([], ...ngDevMode ? [{ debugName: "staged" }] : /* istanbul ignore next */ []);
8204
8333
  adding = signal(false, ...ngDevMode ? [{ debugName: "adding" }] : /* istanbul ignore next */ []);
8205
8334
  busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
8335
+ loadingMembers = signal(false, ...ngDevMode ? [{ debugName: "loadingMembers" }] : /* istanbul ignore next */ []);
8206
8336
  error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
8207
8337
  constructor() {
8208
8338
  afterNextRender(() => {
8209
8339
  this.dialog().showModal();
8210
- this.directory.ensureResolved(this.memberIds());
8340
+ this.loadMembers();
8211
8341
  });
8212
8342
  }
8343
+ async loadMembers() {
8344
+ const workspace = this.workspace();
8345
+ if (workspace && !workspace.membersKnown) {
8346
+ this.error.set(null);
8347
+ this.loadingMembers.set(true);
8348
+ try {
8349
+ await this.store.update(workspace.id, {});
8350
+ } catch (error) {
8351
+ this.error.set(workspaceErrorMessage(error));
8352
+ } finally {
8353
+ this.loadingMembers.set(false);
8354
+ }
8355
+ }
8356
+ this.directory.ensureResolved(this.memberIds());
8357
+ }
8358
+ reloadMembers() {
8359
+ this.loadMembers();
8360
+ }
8213
8361
  async add() {
8214
8362
  const workspace = this.workspace();
8215
8363
  const picked = this.staged();
8216
- if (!workspace || !picked.length) return;
8364
+ if (!workspace?.membersKnown || !picked.length) return;
8217
8365
  this.adding.set(true);
8218
8366
  await this.save(() => withAdded(workspace, picked), () => this.staged.set([]));
8219
8367
  this.adding.set(false);
8220
8368
  }
8221
8369
  async change(id, role) {
8222
8370
  const workspace = this.workspace();
8223
- if (!workspace) return;
8371
+ if (!workspace?.membersKnown) return;
8224
8372
  await this.save(() => withRole(workspace, id, role));
8225
8373
  }
8226
8374
  async save(access, onSaved) {
@@ -8272,6 +8420,25 @@ var ShareDialog = class ShareDialog {
8272
8420
  @if (workspace(); as workspace) {
8273
8421
  <p class="text-(--font-primary-muted) mb-3 text-sm">{{ workspace.name }}</p>
8274
8422
 
8423
+ @if (!workspace.membersKnown) {
8424
+ @if (loadingMembers()) {
8425
+ <p class="text-(--font-primary-muted) text-sm" aria-live="polite" data-testid="members-loading">{{ "documentWorkspaces.share.membersLoading" | transloco }}</p>
8426
+ } @else {
8427
+ <div class="flex flex-col items-start gap-2">
8428
+ <p role="alert" class="text-(--font-primary-muted) text-sm" data-testid="members-unavailable">{{ "documentWorkspaces.share.membersUnavailable" | transloco }}</p>
8429
+ @if (error(); as message) {
8430
+ <p class="text-(--font-error-base) text-sm" data-testid="members-error">
8431
+ @if ("key" in message) {
8432
+ {{ message.key | transloco }}
8433
+ } @else {
8434
+ {{ message.text }}
8435
+ }
8436
+ </p>
8437
+ }
8438
+ <button variant="secondary" size="sm" (click)="reloadMembers()">{{ "documentWorkspaces.common.retry" | transloco }}</button>
8439
+ </div>
8440
+ }
8441
+ } @else {
8275
8442
  <div class="flex items-start gap-2">
8276
8443
  <principal-picker class="flex-1" [(entries)]="staged" [excludeIds]="memberIds()" />
8277
8444
  <button [disabled]="!staged().length || busy()" (click)="add()">
@@ -8297,6 +8464,7 @@ var ShareDialog = class ShareDialog {
8297
8464
  </div>
8298
8465
 
8299
8466
  <p class="text-(--font-primary-muted) text-xs">{{ "documentWorkspaces.share.accessNotice" | transloco }}</p>
8467
+ }
8300
8468
  }
8301
8469
  </DialogBody>
8302
8470
 
@@ -8422,6 +8590,25 @@ i0.ɵɵngDeclareClassMetadata({
8422
8590
  @if (workspace(); as workspace) {
8423
8591
  <p class="text-(--font-primary-muted) mb-3 text-sm">{{ workspace.name }}</p>
8424
8592
 
8593
+ @if (!workspace.membersKnown) {
8594
+ @if (loadingMembers()) {
8595
+ <p class="text-(--font-primary-muted) text-sm" aria-live="polite" data-testid="members-loading">{{ "documentWorkspaces.share.membersLoading" | transloco }}</p>
8596
+ } @else {
8597
+ <div class="flex flex-col items-start gap-2">
8598
+ <p role="alert" class="text-(--font-primary-muted) text-sm" data-testid="members-unavailable">{{ "documentWorkspaces.share.membersUnavailable" | transloco }}</p>
8599
+ @if (error(); as message) {
8600
+ <p class="text-(--font-error-base) text-sm" data-testid="members-error">
8601
+ @if ("key" in message) {
8602
+ {{ message.key | transloco }}
8603
+ } @else {
8604
+ {{ message.text }}
8605
+ }
8606
+ </p>
8607
+ }
8608
+ <button variant="secondary" size="sm" (click)="reloadMembers()">{{ "documentWorkspaces.common.retry" | transloco }}</button>
8609
+ </div>
8610
+ }
8611
+ } @else {
8425
8612
  <div class="flex items-start gap-2">
8426
8613
  <principal-picker class="flex-1" [(entries)]="staged" [excludeIds]="memberIds()" />
8427
8614
  <button [disabled]="!staged().length || busy()" (click)="add()">
@@ -8447,6 +8634,7 @@ i0.ɵɵngDeclareClassMetadata({
8447
8634
  </div>
8448
8635
 
8449
8636
  <p class="text-(--font-primary-muted) text-xs">{{ "documentWorkspaces.share.accessNotice" | transloco }}</p>
8637
+ }
8450
8638
  }
8451
8639
  </DialogBody>
8452
8640
 
@@ -9534,6 +9722,7 @@ i0.ɵɵngDeclareClassMetadata({
9534
9722
  }
9535
9723
  });
9536
9724
  const EditWorkspace = createCallable(WorkspaceEditDialog);
9725
+ const SEARCH_DEBOUNCE_MS = 300;
9537
9726
  var WorkspaceDetail = class WorkspaceDetail {
9538
9727
  workspaceId = input.required(...ngDevMode ? [{ debugName: "workspaceId" }] : /* istanbul ignore next */ []);
9539
9728
  uploadEndpoint = input("", ...ngDevMode ? [{ debugName: "uploadEndpoint" }] : /* istanbul ignore next */ []);
@@ -9565,10 +9754,7 @@ var WorkspaceDetail = class WorkspaceDetail {
9565
9754
  previewWidth = computed(() => this.userPreviewWidth() ?? Math.max(280, Math.round(this.hostWidth() * .4)), ...ngDevMode ? [{ debugName: "previewWidth" }] : /* istanbul ignore next */ []);
9566
9755
  maxPreviewWidth = computed(() => this.hostWidth() ? this.hostWidth() - 360 : void 0, ...ngDevMode ? [{ debugName: "maxPreviewWidth" }] : /* istanbul ignore next */ []);
9567
9756
  hasMore = computed(() => this.store.hasMoreDocuments(this.workspaceId()), ...ngDevMode ? [{ debugName: "hasMore" }] : /* istanbul ignore next */ []);
9568
- filtered = computed(() => {
9569
- const query = this.query().trim();
9570
- return query ? this.documents().filter((d) => matchesText(d.name, query)) : this.documents();
9571
- }, ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []);
9757
+ filtered = this.documents;
9572
9758
  writable = computed(() => canWrite(this.workspace()?.currentUserRole), ...ngDevMode ? [{ debugName: "writable" }] : /* istanbul ignore next */ []);
9573
9759
  selection = injectSelection({
9574
9760
  items: this.filtered,
@@ -9604,6 +9790,12 @@ var WorkspaceDetail = class WorkspaceDetail {
9604
9790
  this.workspaceId();
9605
9791
  untracked(() => void this.open());
9606
9792
  });
9793
+ effect((onCleanup) => {
9794
+ const value = this.query().trim();
9795
+ const id = untracked(() => this.workspaceId());
9796
+ const timer = setTimeout(() => untracked(() => void this.search(id, value)), value ? SEARCH_DEBOUNCE_MS : 0);
9797
+ onCleanup(() => clearTimeout(timer));
9798
+ });
9607
9799
  effect(() => {
9608
9800
  if (!this.writable()) untracked(() => this.selection.exit());
9609
9801
  });
@@ -9613,12 +9805,25 @@ var WorkspaceDetail = class WorkspaceDetail {
9613
9805
  observer.observe(host);
9614
9806
  inject(DestroyRef).onDestroy(() => observer.disconnect());
9615
9807
  }
9616
- inject(DestroyRef).onDestroy(() => this.uploads.watch(null));
9808
+ inject(DestroyRef).onDestroy(() => {
9809
+ this.uploads.watch(null);
9810
+ this.store.resetDocumentSearch(this.workspaceId());
9811
+ });
9812
+ }
9813
+ async search(id, value) {
9814
+ if (this.status() !== "ready" || this.workspaceId() !== id || value === (this.store.documentSearch()[id] ?? "")) return;
9815
+ this.refreshError.set(null);
9816
+ try {
9817
+ await this.store.searchDocuments(id, value);
9818
+ } catch (error) {
9819
+ if (this.workspaceId() === id) this.refreshError.set(this.translate(workspaceErrorMessage(error)));
9820
+ }
9617
9821
  }
9618
9822
  async open() {
9619
9823
  this.selection.exit();
9620
9824
  this.previewedDocumentId.set(null);
9621
9825
  this.query.set("");
9826
+ this.store.resetDocumentSearch(this.workspaceId());
9622
9827
  this.refreshError.set(null);
9623
9828
  this.loadMoreError.set(null);
9624
9829
  this.deletedElsewhere.set(false);
@@ -9780,7 +9985,7 @@ var WorkspaceDetail = class WorkspaceDetail {
9780
9985
  host: { classAttribute: "flex h-full min-h-0" },
9781
9986
  providers: [provideTranslocoScope("document-workspaces")],
9782
9987
  ngImport: i0,
9783
- 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\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'documentWorkspaces.detail.back' | transloco\"\n [tooltip]=\"'documentWorkspaces.detail.back' | transloco\"\n (click)=\"leave()\">\n <chevron-left-icon />\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\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\n class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\"\n [writerOnly]=\"workspace.currentUserRole\"\n (click)=\"confirmDeleteWorkspace(workspace)\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"tertiary\" scheme=\"neutral\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"share(workspace)\">\n <user-round-icon />\n {{ \"documentWorkspaces.share.title\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"tertiary\" scheme=\"neutral\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"edit(workspace)\">\n <pen-to-square-icon />\n {{ \"documentWorkspaces.common.edit\" | transloco }}\n </button>\n <button\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\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 <SearchInput\n role=\"search\"\n class=\"min-w-32 flex-[0_1_15rem]\"\n [formField]=\"searchForm\"\n [placeholder]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [aria-label]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [clearLabel]=\"'documentWorkspaces.common.clearSearch' | transloco\" />\n\n <button class=\"@max-md:px-2\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"upload(workspace)\">\n <upload-icon />\n <span class=\"@max-md:sr-only\">{{ \"documentWorkspaces.detail.upload\" | transloco }}</span>\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=\"tertiary\" scheme=\"neutral\" size=\"sm\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\" variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" 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=\"sm\" (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 <div class=\"mb-1 inline-flex size-14 shrink-0 items-center justify-center rounded-(--radius-xl) bg-(--teal-100,#E0F4F1) text-(--teal-400,#4EBFB0)\">\n <file-search-icon class=\"size-6\" />\n </div>\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 <svg class=\"mb-1 block size-14 shrink-0\" width=\"56\" height=\"56\" viewBox=\"0 0 38 38\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" data-testid=\"empty-icon\">\n <g opacity=\"0.8\">\n <rect class=\"fill-(--teal-100,#E0F4F1)\" width=\"38\" height=\"38\" rx=\"12\" />\n <path\n d=\"M11.2 24.8504H26.8C27.1575 24.8504 27.45 24.5579 27.45 24.2004V14.4504C27.45 14.0929 27.1575 13.8004 26.8 13.8004H20.7347C20.0319 13.8004 19.3453 13.5729 18.7847 13.1504L17.2247 11.9804C17.1109 11.8951 16.9769 11.8504 16.8347 11.8504H11.2C10.8425 11.8504 10.55 12.1429 10.55 12.5004V24.2004C10.55 24.5579 10.8425 24.8504 11.2 24.8504ZM26.8 26.8004H11.2C9.76591 26.8004 8.59998 25.6345 8.59998 24.2004V12.5004C8.59998 11.0663 9.76591 9.90039 11.2 9.90039H16.8347C17.3953 9.90039 17.9437 10.0832 18.3947 10.4204L19.9547 11.5904C20.1781 11.761 20.4544 11.8504 20.7347 11.8504H26.8C28.234 11.8504 29.4 13.0163 29.4 14.4504V24.2004C29.4 25.6345 28.234 26.8004 26.8 26.8004Z\"\n class=\"fill-(--teal-400,#4EBFB0)\" />\n </g>\n </svg>\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 <checkbox\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (checkedChange)=\"selection.toggleAll()\" />\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=\"sm\" [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 pointer-coarse: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 ",
9988
+ 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\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'documentWorkspaces.detail.back' | transloco\"\n [tooltip]=\"'documentWorkspaces.detail.back' | transloco\"\n (click)=\"leave()\">\n <chevron-left-icon />\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\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\n class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\"\n [writerOnly]=\"workspace.currentUserRole\"\n (click)=\"confirmDeleteWorkspace(workspace)\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"tertiary\" scheme=\"neutral\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"share(workspace)\">\n <user-round-icon />\n {{ \"documentWorkspaces.share.title\" | transloco }}\n </button>\n <button size=\"sm\" variant=\"tertiary\" scheme=\"neutral\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"edit(workspace)\">\n <pen-to-square-icon />\n {{ \"documentWorkspaces.common.edit\" | transloco }}\n </button>\n <button\n size=\"sm\"\n variant=\"tertiary\"\n scheme=\"neutral\"\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 <SearchInput\n role=\"search\"\n class=\"min-w-32 flex-[0_1_15rem]\"\n [formField]=\"searchForm\"\n [placeholder]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [aria-label]=\"'documentWorkspaces.detail.searchPlaceholder' | transloco\"\n [clearLabel]=\"'documentWorkspaces.common.clearSearch' | transloco\" />\n\n <button class=\"@max-md:px-2\" [writerOnly]=\"workspace.currentUserRole\" (click)=\"upload(workspace)\">\n <upload-icon />\n <span class=\"@max-md:sr-only\">{{ \"documentWorkspaces.detail.upload\" | transloco }}</span>\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=\"tertiary\" scheme=\"neutral\" size=\"sm\" (click)=\"selection.toggleAll()\">\n {{ (selection.allSelected() ? \"documentWorkspaces.common.unselectAll\" : \"documentWorkspaces.common.selectAll\") | transloco }}\n </button>\n <button class=\"text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest)\" variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" [disabled]=\"!selection.count()\" (click)=\"deleteSelected()\">\n <x-mark-icon />\n {{ \"documentWorkspaces.common.delete\" | transloco }}\n </button>\n </div>\n }\n <button variant=\"tertiary\" scheme=\"neutral\" size=\"sm\" 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 if (workspace.documentCount !== undefined) {\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=\"sm\" (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 <div class=\"mb-1 inline-flex size-14 shrink-0 items-center justify-center rounded-(--radius-xl) bg-(--teal-100,#E0F4F1) text-(--teal-400,#4EBFB0)\">\n <file-search-icon class=\"size-6\" />\n </div>\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 && !query().trim()) {\n <div class=\"flex flex-col items-center gap-1 py-8 text-center\" data-testid=\"empty\">\n <svg class=\"mb-1 block size-14 shrink-0\" width=\"56\" height=\"56\" viewBox=\"0 0 38 38\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" data-testid=\"empty-icon\">\n <g opacity=\"0.8\">\n <rect class=\"fill-(--teal-100,#E0F4F1)\" width=\"38\" height=\"38\" rx=\"12\" />\n <path\n d=\"M11.2 24.8504H26.8C27.1575 24.8504 27.45 24.5579 27.45 24.2004V14.4504C27.45 14.0929 27.1575 13.8004 26.8 13.8004H20.7347C20.0319 13.8004 19.3453 13.5729 18.7847 13.1504L17.2247 11.9804C17.1109 11.8951 16.9769 11.8504 16.8347 11.8504H11.2C10.8425 11.8504 10.55 12.1429 10.55 12.5004V24.2004C10.55 24.5579 10.8425 24.8504 11.2 24.8504ZM26.8 26.8004H11.2C9.76591 26.8004 8.59998 25.6345 8.59998 24.2004V12.5004C8.59998 11.0663 9.76591 9.90039 11.2 9.90039H16.8347C17.3953 9.90039 17.9437 10.0832 18.3947 10.4204L19.9547 11.5904C20.1781 11.761 20.4544 11.8504 20.7347 11.8504H26.8C28.234 11.8504 29.4 13.0163 29.4 14.4504V24.2004C29.4 25.6345 28.234 26.8004 26.8 26.8004Z\"\n class=\"fill-(--teal-400,#4EBFB0)\" />\n </g>\n </svg>\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 <checkbox\n [checked]=\"selection.allSelected()\"\n [indeterminate]=\"selection.partiallySelected()\"\n [aria-label]=\"'documentWorkspaces.common.selectAll' | transloco\"\n (checkedChange)=\"selection.toggleAll()\" />\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=\"sm\" [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 pointer-coarse: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 ",
9784
9989
  isInline: true,
9785
9990
  dependencies: [
9786
9991
  {
@@ -10145,7 +10350,7 @@ i0.ɵɵngDeclareClassMetadata({
10145
10350
  <p class="mb-6 text-sm text-(--font-primary-base)" aria-live="polite" data-testid="count">
10146
10351
  @if (selection.active()) {
10147
10352
  {{ "documentWorkspaces.detail.selectedDocumentCount" | transloco: { count: selection.count() } }}
10148
- } @else {
10353
+ } @else if (workspace.documentCount !== undefined) {
10149
10354
  {{ "documentWorkspaces.detail.fileCount" | transloco: { count: workspace.documentCount } }}
10150
10355
  }
10151
10356
  </p>
@@ -10184,7 +10389,7 @@ i0.ɵɵngDeclareClassMetadata({
10184
10389
  <button variant="secondary" size="sm" class="mt-2" (click)="leave()">{{ "documentWorkspaces.detail.back" | transloco }}</button>
10185
10390
  </div>
10186
10391
  } @else if (workspace(); as workspace) {
10187
- @if (!documents().length) {
10392
+ @if (!documents().length && !query().trim()) {
10188
10393
  <div class="flex flex-col items-center gap-1 py-8 text-center" data-testid="empty">
10189
10394
  <svg class="mb-1 block size-14 shrink-0" width="56" height="56" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-testid="empty-icon">
10190
10395
  <g opacity="0.8">
@@ -10682,7 +10887,9 @@ var WorkspaceMentionRows = class WorkspaceMentionRows {
10682
10887
  <workspace-icon size="md" [color]="item.color" />
10683
10888
  <span class="flex min-w-0 flex-col">
10684
10889
  <span class="truncate text-(length:--size-base) font-semibold text-(--font-neutral-black)">{{ item.name }}</span>
10685
- <span class="text-(length:--size-sm) text-(--font-primary-muted)">{{ "documentWorkspaces.mention.documentCount" | transloco: { count: item.documentCount } }}</span>
10890
+ @if (item.documentCount !== undefined) {
10891
+ <span class="text-(length:--size-sm) text-(--font-primary-muted)">{{ "documentWorkspaces.mention.documentCount" | transloco: { count: item.documentCount } }}</span>
10892
+ }
10686
10893
  </span>
10687
10894
  </span>
10688
10895
  </checkbox>
@@ -10766,7 +10973,9 @@ i0.ɵɵngDeclareClassMetadata({
10766
10973
  <workspace-icon size="md" [color]="item.color" />
10767
10974
  <span class="flex min-w-0 flex-col">
10768
10975
  <span class="truncate text-(length:--size-base) font-semibold text-(--font-neutral-black)">{{ item.name }}</span>
10769
- <span class="text-(length:--size-sm) text-(--font-primary-muted)">{{ "documentWorkspaces.mention.documentCount" | transloco: { count: item.documentCount } }}</span>
10976
+ @if (item.documentCount !== undefined) {
10977
+ <span class="text-(length:--size-sm) text-(--font-primary-muted)">{{ "documentWorkspaces.mention.documentCount" | transloco: { count: item.documentCount } }}</span>
10978
+ }
10770
10979
  </span>
10771
10980
  </span>
10772
10981
  </checkbox>