@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.
- package/document-workspaces/i18n/de.json +3 -0
- package/document-workspaces/i18n/en.json +3 -0
- package/document-workspaces/i18n/fr.json +3 -0
- package/document-workspaces/i18n/ja.json +3 -0
- package/document-workspaces/index.d.ts +26 -4
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs +324 -53
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs.map +1 -1
- package/fesm2022/sinequa-atomic-angular.mjs +16 -19
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/index.d.ts +160 -160
- package/package.json +1 -1
|
@@ -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,
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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)="
|
|
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
|
-
|
|
5804
|
-
<
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
<
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
<
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
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 (
|
|
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 />
|