@questpie/admin 3.5.1 → 3.5.3
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/dist/client/builder/types/collection-types.d.mts +9 -0
- package/dist/client/components/actions/action-dialog.mjs +5 -0
- package/dist/client/components/fields/rich-text-editor/bubble-menu.mjs +7 -0
- package/dist/client/components/fields/rich-text-editor/extensions.mjs +17 -1
- package/dist/client/components/fields/rich-text-editor/index.d.mts +2 -1
- package/dist/client/components/fields/rich-text-editor/index.mjs +35 -74
- package/dist/client/components/fields/rich-text-editor/slash-commands.mjs +30 -7
- package/dist/client/components/fields/rich-text-editor/toolbar.mjs +1 -312
- package/dist/client/components/fields/rich-text-editor/types.d.mts +4 -0
- package/dist/client/components/fields/rich-text-editor/types.mjs +1 -1
- package/dist/client/components/fields/rich-text-editor/utils.mjs +6 -12
- package/dist/client/components/filter-builder/filter-builder-sheet.mjs +75 -22
- package/dist/client/components/ui/dropdown-menu.mjs +1 -34
- package/dist/client/hooks/query-access.d.mts +9 -0
- package/dist/client/hooks/query-access.mjs +20 -0
- package/dist/client/hooks/typed-hooks.d.mts +4 -2
- package/dist/client/hooks/typed-hooks.mjs +30 -29
- package/dist/client/hooks/use-reactive-fields.d.mts +1 -0
- package/dist/client/hooks/use-reactive-fields.mjs +16 -1
- package/dist/client/hooks/use-server-actions.mjs +12 -1
- package/dist/client/hooks/use-view-state.mjs +15 -7
- package/dist/client/lib/view-filter-utils.mjs +30 -0
- package/dist/client/preview/block-scope-context.d.mts +2 -2
- package/dist/client/preview/preview-banner.d.mts +2 -2
- package/dist/client/preview/preview-field.d.mts +4 -4
- package/dist/client/scope/picker.d.mts +2 -2
- package/dist/client/scope/provider.d.mts +2 -2
- package/dist/client/styles/base.css +69 -77
- package/dist/client/utils/build-field-definitions-from-schema.mjs +1 -0
- package/dist/client/views/collection/auto-form-fields.mjs +3 -2
- package/dist/client/views/collection/cells/primitive-cells.mjs +9 -6
- package/dist/client/views/collection/columns/build-columns.mjs +3 -1
- package/dist/client/views/collection/field-renderer.mjs +11 -3
- package/dist/client/views/collection/form-view.mjs +207 -202
- package/dist/client/views/collection/list-view.mjs +581 -183
- package/dist/client/views/collection/outline.mjs +44 -19
- package/dist/client/views/collection/quick-filter-bar.mjs +45 -0
- package/dist/client/views/collection/table-view.mjs +60 -16
- package/dist/client/views/globals/global-form-view.mjs +12 -9
- package/dist/client/views/layout/admin-layout.mjs +1 -1
- package/dist/client/views/layout/admin-sidebar.mjs +20 -14
- package/dist/client/views/layout/admin-theme.mjs +5 -4
- package/dist/client.mjs +1 -1
- package/dist/components/rich-text/rich-text-renderer.d.mts +5 -5
- package/dist/components/rich-text/rich-text-renderer.mjs +5 -2
- package/dist/index.mjs +1 -1
- package/dist/modules/admin.d.mts +1 -1
- package/dist/server/augmentation/actions.d.mts +4 -3
- package/dist/server/augmentation/dashboard.d.mts +11 -11
- package/dist/server/augmentation/form-layout.d.mts +11 -6
- package/dist/server/augmentation/index.d.mts +7 -0
- package/dist/server/augmentation/sidebar.d.mts +8 -8
- package/dist/server/codegen/admin-client-template.mjs +55 -38
- package/dist/server/fields/index.d.mts +1 -1
- package/dist/server/fields/rich-text.d.mts +16 -17
- package/dist/server/fields/rich-text.mjs +18 -7
- package/dist/server/i18n/messages/cs.mjs +2 -0
- package/dist/server/i18n/messages/de.mjs +2 -0
- package/dist/server/i18n/messages/en.mjs +4 -0
- package/dist/server/i18n/messages/es.mjs +2 -0
- package/dist/server/i18n/messages/fr.mjs +2 -0
- package/dist/server/i18n/messages/pl.mjs +2 -0
- package/dist/server/i18n/messages/pt.mjs +2 -0
- package/dist/server/i18n/messages/sk.mjs +2 -0
- package/dist/server/modules/admin/block/block-builder.d.mts +0 -8
- package/dist/server/modules/admin/block/introspection.d.mts +2 -2
- package/dist/server/modules/admin/collections/account.d.mts +53 -52
- package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -56
- package/dist/server/modules/admin/collections/admin-preferences.d.mts +3 -2
- package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -49
- package/dist/server/modules/admin/collections/apikey.d.mts +72 -71
- package/dist/server/modules/admin/collections/assets.d.mts +42 -41
- package/dist/server/modules/admin/collections/session.d.mts +46 -45
- package/dist/server/modules/admin/collections/user.d.mts +67 -66
- package/dist/server/modules/admin/collections/verification.d.mts +39 -38
- package/dist/server/modules/admin/index.d.mts +3 -3
- package/dist/server/modules/admin/routes/admin-config.d.mts +2 -2
- package/dist/server/modules/admin/routes/admin-config.mjs +39 -23
- package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
- package/dist/server/modules/admin/routes/execute-action.mjs +28 -8
- package/dist/server/modules/admin/routes/locales.d.mts +2 -2
- package/dist/server/modules/admin/routes/reactive.mjs +2 -2
- package/dist/server/modules/admin/routes/route-helpers.d.mts +11 -7
- package/dist/server/modules/admin/routes/translations.d.mts +4 -4
- package/dist/server/modules/admin/routes/widget-data.mjs +12 -4
- package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +27 -27
- package/dist/server/modules/audit/.generated/module.d.mts +6 -6
- package/dist/server/modules/audit/collections/audit-log.d.mts +40 -39
- package/dist/server/plugin.mjs +3 -3
- package/dist/server.d.mts +1 -1
- package/dist/shared/types/index.d.mts +1 -0
- package/dist/shared/types/saved-views.types.d.mts +14 -7
- package/dist/shared.d.mts +3 -2
- package/package.json +4 -3
|
@@ -5,13 +5,16 @@ import { cn } from "../../lib/utils.mjs";
|
|
|
5
5
|
import { selectRealtime, useAdminStore } from "../../runtime/provider.mjs";
|
|
6
6
|
import { useSafeContentLocales } from "../../runtime/content-locales-provider.mjs";
|
|
7
7
|
import { useScopedLocale } from "../../runtime/locale-scope.mjs";
|
|
8
|
+
import { resolveIconElement } from "../../components/component-renderer.mjs";
|
|
8
9
|
import { flattenOptions } from "../../components/primitives/types.mjs";
|
|
9
10
|
import { Button } from "../../components/ui/button.mjs";
|
|
10
11
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select.mjs";
|
|
11
12
|
import { LocaleSwitcher } from "../../components/locale-switcher.mjs";
|
|
13
|
+
import { Badge } from "../../components/ui/badge.mjs";
|
|
12
14
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip.mjs";
|
|
13
15
|
import { Checkbox } from "../../components/ui/checkbox.mjs";
|
|
14
16
|
import { ActionButton } from "../../components/actions/action-button.mjs";
|
|
17
|
+
import { adminCollectionKey, getCollectionQueryApi } from "../../hooks/query-access.mjs";
|
|
15
18
|
import { useCollectionFields } from "../../hooks/use-collection-fields.mjs";
|
|
16
19
|
import { useSuspenseCollectionMeta } from "../../hooks/use-collection-meta.mjs";
|
|
17
20
|
import { ActionDialog } from "../../components/actions/action-dialog.mjs";
|
|
@@ -38,6 +41,7 @@ import { useDeleteSavedView, useSaveView, useSavedViews } from "../../hooks/use-
|
|
|
38
41
|
import { useViewState } from "../../hooks/use-view-state.mjs";
|
|
39
42
|
import { BulkActionToolbar } from "./bulk-action-toolbar.mjs";
|
|
40
43
|
import { buildOutlineRows } from "./outline.mjs";
|
|
44
|
+
import { QuickFilterBar } from "./quick-filter-bar.mjs";
|
|
41
45
|
import { UploadCollectionButton, mapListSchemaToConfig, stringifyGroupValue } from "./table-view.mjs";
|
|
42
46
|
import { Icon } from "@iconify/react";
|
|
43
47
|
import * as React from "react";
|
|
@@ -194,16 +198,30 @@ function SimpleValue({ value }) {
|
|
|
194
198
|
if (Array.isArray(value)) return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => stringifySimpleValue(item)).join(", ") });
|
|
195
199
|
return /* @__PURE__ */ jsx(Fragment, { children: stringifySimpleValue(value) });
|
|
196
200
|
}
|
|
201
|
+
function formatShortDate(date) {
|
|
202
|
+
const now = /* @__PURE__ */ new Date();
|
|
203
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
204
|
+
const month = date.toLocaleDateString(void 0, { month: "short" });
|
|
205
|
+
const day = date.getDate();
|
|
206
|
+
return sameYear ? `${month} ${day}` : `${month} ${day}, ${date.getFullYear()}`;
|
|
207
|
+
}
|
|
208
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
197
209
|
function stringifySimpleValue(value) {
|
|
198
210
|
if (value === null || value === void 0 || value === "") return "-";
|
|
199
211
|
if (typeof value === "object") {
|
|
212
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return formatShortDate(value);
|
|
200
213
|
const record = value;
|
|
201
214
|
return String(record.title ?? record.name ?? record.label ?? record.id ?? "-");
|
|
202
215
|
}
|
|
216
|
+
if (typeof value === "string" && ISO_DATE_RE.test(value)) {
|
|
217
|
+
const date = new Date(value);
|
|
218
|
+
if (!Number.isNaN(date.getTime())) return formatShortDate(date);
|
|
219
|
+
}
|
|
203
220
|
return String(value);
|
|
204
221
|
}
|
|
205
222
|
function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/admin", showSearch = true, showFilters = true, showToolbar = true, realtime, headerActions, actionsConfig }) {
|
|
206
223
|
"use no memo";
|
|
224
|
+
const collectionKey = adminCollectionKey(collection);
|
|
207
225
|
const globalRealtimeConfig = useAdminStore(selectRealtime);
|
|
208
226
|
const { fields: resolvedFields, schema } = useCollectionFields(collection, { fallbackFields: config?.fields });
|
|
209
227
|
const { collections: uploadCollections } = useUploadCollection();
|
|
@@ -248,7 +266,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
248
266
|
const [isSheetOpen, setIsSheetOpen] = useSidebarSearchParam("view-options", { legacyKey: "viewOptions" });
|
|
249
267
|
const [searchTerm, setSearchTerm] = React.useState("");
|
|
250
268
|
const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false);
|
|
251
|
-
const [
|
|
269
|
+
const [toggledOutlineKeys, setToggledOutlineKeys] = React.useState(() => /* @__PURE__ */ new Set());
|
|
252
270
|
const [rowSelection, setRowSelection] = React.useState({});
|
|
253
271
|
const defaultColumns = React.useMemo(() => computeDefaultColumns(resolvedFields, {
|
|
254
272
|
meta: collectionMeta,
|
|
@@ -260,14 +278,39 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
260
278
|
]);
|
|
261
279
|
const groupingConfig = resolvedListConfig?.grouping;
|
|
262
280
|
const defaultGroupBy = groupingConfig?.defaultField ?? null;
|
|
263
|
-
const
|
|
281
|
+
const defaultFilters = React.useMemo(() => resolvedListConfig?.defaultFilters ?? [], [resolvedListConfig?.defaultFilters]);
|
|
282
|
+
const viewState = useViewState(defaultColumns, React.useMemo(() => ({
|
|
264
283
|
realtime: resolvedRealtime,
|
|
265
|
-
groupBy: defaultGroupBy
|
|
266
|
-
|
|
284
|
+
groupBy: defaultGroupBy,
|
|
285
|
+
filters: defaultFilters
|
|
286
|
+
}), [
|
|
287
|
+
resolvedRealtime,
|
|
288
|
+
defaultGroupBy,
|
|
289
|
+
defaultFilters
|
|
290
|
+
]), collection, user?.id);
|
|
267
291
|
const effectiveRealtime = viewState.config.realtime ?? resolvedRealtime;
|
|
268
292
|
const visibleColumns = viewState.config.visibleColumns.length > 0 ? viewState.config.visibleColumns : defaultColumns;
|
|
293
|
+
const layout = resolvedListConfig?.layout;
|
|
294
|
+
const titleField = layout?.titleField ?? (collectionMeta?.title?.type === "field" ? collectionMeta.title.fieldName : void 0);
|
|
295
|
+
const subtitleField = layout?.subtitleField;
|
|
296
|
+
const leadingFields = normalizeFieldList(layout?.leadingFields);
|
|
297
|
+
const badgeFields = normalizeFieldList(layout?.badgeFields);
|
|
298
|
+
const metaFields = normalizeFieldList(layout?.metaFields);
|
|
299
|
+
const density = layout?.density ?? "compact";
|
|
269
300
|
const outlineRelationNames = React.useMemo(() => extractRelationNamesFromOutline(resolvedListConfig?.outline), [resolvedListConfig?.outline]);
|
|
270
|
-
const visibleColumnsForExpansion = React.useMemo(() => Array.from(new Set([
|
|
301
|
+
const visibleColumnsForExpansion = React.useMemo(() => Array.from(new Set([
|
|
302
|
+
...visibleColumns,
|
|
303
|
+
...outlineRelationNames,
|
|
304
|
+
...metaFields,
|
|
305
|
+
...leadingFields,
|
|
306
|
+
...badgeFields
|
|
307
|
+
])), [
|
|
308
|
+
visibleColumns,
|
|
309
|
+
outlineRelationNames,
|
|
310
|
+
metaFields,
|
|
311
|
+
leadingFields,
|
|
312
|
+
badgeFields
|
|
313
|
+
]);
|
|
271
314
|
const expandedFields = React.useMemo(() => autoExpandFields({
|
|
272
315
|
fields: resolvedFields,
|
|
273
316
|
list: resolvedListConfig,
|
|
@@ -279,7 +322,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
279
322
|
visibleColumnsForExpansion,
|
|
280
323
|
collectionMeta?.relations
|
|
281
324
|
]);
|
|
282
|
-
const isKnownSortField = React.useCallback((field) => !!field && (field === "_title" || !!resolvedFields?.[field]), [resolvedFields]);
|
|
325
|
+
const isKnownSortField = React.useCallback((field) => !!field && (field === "_title" || field === "createdAt" || field === "updatedAt" || !!resolvedFields?.[field]), [resolvedFields]);
|
|
283
326
|
const effectiveSort = React.useMemo(() => {
|
|
284
327
|
if (isKnownSortField(viewState.config.sortConfig?.field)) return viewState.config.sortConfig;
|
|
285
328
|
if (isKnownSortField(resolvedListConfig?.defaultSort?.field)) return resolvedListConfig.defaultSort;
|
|
@@ -292,7 +335,79 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
292
335
|
resolvedListConfig?.defaultSort,
|
|
293
336
|
isKnownSortField
|
|
294
337
|
]);
|
|
295
|
-
const
|
|
338
|
+
const availableFields = React.useMemo(() => getAllAvailableFields(resolvedFields, { meta: collectionMeta }), [resolvedFields, collectionMeta]);
|
|
339
|
+
const fieldByName = React.useMemo(() => new Map(availableFields.map((field) => [field.name, field])), [availableFields]);
|
|
340
|
+
const effectiveOutline = React.useMemo(() => {
|
|
341
|
+
const outline = resolvedListConfig?.outline;
|
|
342
|
+
const groupBy = viewState.config.groupBy;
|
|
343
|
+
if (!groupBy) return outline;
|
|
344
|
+
const levels = outline?.levels ?? [];
|
|
345
|
+
const first = levels[0];
|
|
346
|
+
if (first?.kind === "field" && first.field === groupBy) return outline;
|
|
347
|
+
return {
|
|
348
|
+
...outline,
|
|
349
|
+
defaultExpanded: outline?.defaultExpanded ?? true,
|
|
350
|
+
levels: [{
|
|
351
|
+
kind: "field",
|
|
352
|
+
field: groupBy
|
|
353
|
+
}, ...levels]
|
|
354
|
+
};
|
|
355
|
+
}, [resolvedListConfig?.outline, viewState.config.groupBy]);
|
|
356
|
+
const hasOutline = (resolvedListConfig?.outline?.levels?.length ?? 0) > 0;
|
|
357
|
+
const firstFieldLevel = React.useMemo(() => {
|
|
358
|
+
const first = effectiveOutline?.levels?.[0];
|
|
359
|
+
return first?.kind === "field" ? first : null;
|
|
360
|
+
}, [effectiveOutline]);
|
|
361
|
+
const groupValues = React.useMemo(() => {
|
|
362
|
+
if (!firstFieldLevel) return null;
|
|
363
|
+
const fieldDef = fieldByName.get(firstFieldLevel.field);
|
|
364
|
+
if (fieldDef?.type !== "select" || !fieldDef.options?.options) return null;
|
|
365
|
+
const flat = flattenOptions(fieldDef.options.options);
|
|
366
|
+
const order = firstFieldLevel.order;
|
|
367
|
+
if (Array.isArray(order)) return order.map((v) => flat.find((o) => String(o.value) === String(v)) ?? {
|
|
368
|
+
value: v,
|
|
369
|
+
label: { en: String(v) }
|
|
370
|
+
});
|
|
371
|
+
return flat;
|
|
372
|
+
}, [firstFieldLevel, fieldByName]);
|
|
373
|
+
const filteredGroupValues = React.useMemo(() => {
|
|
374
|
+
if (!groupValues || !firstFieldLevel) return groupValues;
|
|
375
|
+
const groupField = firstFieldLevel.field;
|
|
376
|
+
const relevant = viewState.config.filters.filter((f) => f.field === groupField);
|
|
377
|
+
if (relevant.length === 0) return groupValues;
|
|
378
|
+
return groupValues.filter((gv) => {
|
|
379
|
+
const val = String(gv.value);
|
|
380
|
+
for (const filter of relevant) switch (filter.operator) {
|
|
381
|
+
case "equals":
|
|
382
|
+
if (String(filter.value) !== val) return false;
|
|
383
|
+
break;
|
|
384
|
+
case "not_equals":
|
|
385
|
+
if (String(filter.value) === val) return false;
|
|
386
|
+
break;
|
|
387
|
+
case "in":
|
|
388
|
+
if (!Array.isArray(filter.value) || !filter.value.map(String).includes(val)) return false;
|
|
389
|
+
break;
|
|
390
|
+
case "not_in":
|
|
391
|
+
if (Array.isArray(filter.value) && filter.value.map(String).includes(val)) return false;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
});
|
|
396
|
+
}, [
|
|
397
|
+
groupValues,
|
|
398
|
+
firstFieldLevel,
|
|
399
|
+
viewState.config.filters
|
|
400
|
+
]);
|
|
401
|
+
const isGrouped = filteredGroupValues != null && filteredGroupValues.length > 0;
|
|
402
|
+
const [groupLimits, setGroupLimits] = React.useState({});
|
|
403
|
+
const DEFAULT_GROUP_LIMIT = 20;
|
|
404
|
+
const loadMoreInGroup = React.useCallback((groupKey) => {
|
|
405
|
+
setGroupLimits((prev) => ({
|
|
406
|
+
...prev,
|
|
407
|
+
[groupKey]: (prev[groupKey] ?? DEFAULT_GROUP_LIMIT) + DEFAULT_GROUP_LIMIT
|
|
408
|
+
}));
|
|
409
|
+
}, []);
|
|
410
|
+
const baseQueryOptions = React.useMemo(() => {
|
|
296
411
|
const options = {};
|
|
297
412
|
if (collectionMeta?.softDelete) options.includeDeleted = !!viewState.config.includeDeleted;
|
|
298
413
|
if (hasFieldsToExpand(expandedFields)) options.with = expandedFields;
|
|
@@ -302,28 +417,41 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
302
417
|
relationNames: collectionMeta?.relations ?? []
|
|
303
418
|
});
|
|
304
419
|
if (where) options.where = where;
|
|
305
|
-
const groupBy = viewState.config.groupBy;
|
|
306
420
|
const sortConfig = effectiveSort;
|
|
307
|
-
if (
|
|
308
|
-
else if (groupBy) options.orderBy = { [groupBy]: sortConfig?.direction ?? "asc" };
|
|
309
|
-
else if (sortConfig) options.orderBy = { [sortConfig.field]: sortConfig.direction };
|
|
310
|
-
const pageSize = viewState.config.pagination?.pageSize ?? 25;
|
|
311
|
-
const page = viewState.config.pagination?.page ?? 1;
|
|
312
|
-
options.limit = pageSize;
|
|
313
|
-
options.offset = (page - 1) * pageSize;
|
|
421
|
+
if (sortConfig) options.orderBy = { [sortConfig.field]: sortConfig.direction };
|
|
314
422
|
return options;
|
|
315
423
|
}, [
|
|
316
424
|
collectionMeta?.softDelete,
|
|
317
425
|
collectionMeta?.relations,
|
|
318
426
|
viewState.config.includeDeleted,
|
|
319
427
|
viewState.config.filters,
|
|
320
|
-
viewState.config.groupBy,
|
|
321
|
-
viewState.config.pagination?.page,
|
|
322
|
-
viewState.config.pagination?.pageSize,
|
|
323
428
|
expandedFields,
|
|
324
429
|
effectiveSort,
|
|
325
430
|
resolvedFields
|
|
326
431
|
]);
|
|
432
|
+
const flatQueryOptions = React.useMemo(() => {
|
|
433
|
+
if (isGrouped) return baseQueryOptions;
|
|
434
|
+
const options = { ...baseQueryOptions };
|
|
435
|
+
const groupBy = viewState.config.groupBy;
|
|
436
|
+
const sortConfig = effectiveSort;
|
|
437
|
+
if (groupBy && sortConfig?.field && sortConfig.field !== groupBy) options.orderBy = [{ [groupBy]: "asc" }, { [sortConfig.field]: sortConfig.direction }];
|
|
438
|
+
if (hasOutline && !isGrouped) options.limit = 500;
|
|
439
|
+
else {
|
|
440
|
+
const pageSize = viewState.config.pagination?.pageSize ?? 25;
|
|
441
|
+
const page = viewState.config.pagination?.page ?? 1;
|
|
442
|
+
options.limit = pageSize;
|
|
443
|
+
options.offset = (page - 1) * pageSize;
|
|
444
|
+
}
|
|
445
|
+
return options;
|
|
446
|
+
}, [
|
|
447
|
+
baseQueryOptions,
|
|
448
|
+
isGrouped,
|
|
449
|
+
hasOutline,
|
|
450
|
+
viewState.config.groupBy,
|
|
451
|
+
viewState.config.pagination?.page,
|
|
452
|
+
viewState.config.pagination?.pageSize,
|
|
453
|
+
effectiveSort
|
|
454
|
+
]);
|
|
327
455
|
const debouncedSearchTerm = useDebouncedValue(searchTerm, 300);
|
|
328
456
|
const isSearching = debouncedSearchTerm.trim().length > 0;
|
|
329
457
|
const { data: searchData, isLoading: searchLoading, isFetching: searchFetching } = useSearch({
|
|
@@ -332,13 +460,34 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
332
460
|
limit: 100,
|
|
333
461
|
highlights: true
|
|
334
462
|
}, { enabled: isSearching });
|
|
335
|
-
const
|
|
336
|
-
|
|
463
|
+
const groupQueries = useQueries({ queries: isGrouped && !isSearching ? filteredGroupValues.map((gv) => {
|
|
464
|
+
const limit = groupLimits[String(gv.value)] ?? DEFAULT_GROUP_LIMIT;
|
|
465
|
+
const groupWhere = {
|
|
466
|
+
...baseQueryOptions.where,
|
|
467
|
+
[firstFieldLevel.field]: gv.value
|
|
468
|
+
};
|
|
469
|
+
return getCollectionQueryApi(queryOpts, collectionKey).find({
|
|
470
|
+
...baseQueryOptions,
|
|
471
|
+
where: groupWhere,
|
|
472
|
+
limit,
|
|
473
|
+
offset: 0,
|
|
474
|
+
locale
|
|
475
|
+
}, { realtime: false });
|
|
476
|
+
}) : [] });
|
|
477
|
+
const { data: listData, isLoading: listLoading, error: listError } = useCollectionList(collectionKey, flatQueryOptions, { enabled: !isSearching && !isGrouped }, { realtime: effectiveRealtime });
|
|
478
|
+
const groupDataVersion = isGrouped ? groupQueries.map((q) => q.dataUpdatedAt).join(",") : "";
|
|
479
|
+
const items = React.useMemo(() => {
|
|
480
|
+
if (isSearching) return searchData?.docs ?? [];
|
|
481
|
+
if (isGrouped) return groupQueries.flatMap((q) => q.data?.docs ?? []);
|
|
482
|
+
return listData?.docs ?? [];
|
|
483
|
+
}, [
|
|
337
484
|
isSearching,
|
|
485
|
+
isGrouped,
|
|
338
486
|
searchData?.docs,
|
|
487
|
+
groupDataVersion,
|
|
339
488
|
listData?.docs
|
|
340
489
|
]);
|
|
341
|
-
const isLoading = isSearching ? searchLoading : listLoading;
|
|
490
|
+
const isLoading = isSearching ? searchLoading : isGrouped ? groupQueries.some((q) => q.isLoading) : listLoading;
|
|
342
491
|
const isSearchActive = isSearching && searchFetching;
|
|
343
492
|
const { isHighlighted } = useRealtimeHighlight(items, { enabled: effectiveRealtime && !isSearching });
|
|
344
493
|
const { getLock, isLocked: isDocLocked } = useLocks({
|
|
@@ -348,17 +497,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
348
497
|
});
|
|
349
498
|
const edgeLevels = React.useMemo(() => extractEdgeLevels(resolvedListConfig?.outline), [resolvedListConfig?.outline]);
|
|
350
499
|
const edgeQueries = useQueries({ queries: edgeLevels.map((level) => {
|
|
351
|
-
|
|
352
|
-
if (!collectionQueries?.find) return {
|
|
353
|
-
queryKey: [
|
|
354
|
-
"questpie",
|
|
355
|
-
"outline-edge-missing",
|
|
356
|
-
level.collection
|
|
357
|
-
],
|
|
358
|
-
queryFn: async () => ({ docs: [] }),
|
|
359
|
-
enabled: false
|
|
360
|
-
};
|
|
361
|
-
return collectionQueries.find({
|
|
500
|
+
return getCollectionQueryApi(queryOpts, adminCollectionKey(level.collection)).find({
|
|
362
501
|
where: level.where,
|
|
363
502
|
with: {
|
|
364
503
|
[level.parentField]: true,
|
|
@@ -376,39 +515,86 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
376
515
|
});
|
|
377
516
|
return map;
|
|
378
517
|
}, [edgeLevels, edgeQueries]);
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
518
|
+
const metaForValue = React.useCallback((value, field) => {
|
|
519
|
+
if (!field) return void 0;
|
|
520
|
+
const fieldDef = fieldByName.get(field);
|
|
521
|
+
if (fieldDef?.type !== "select") return void 0;
|
|
522
|
+
const options = fieldDef?.options?.options;
|
|
523
|
+
if (!Array.isArray(options)) return void 0;
|
|
524
|
+
const option = flattenOptions(options).find((opt) => String(opt.value) === String(value));
|
|
525
|
+
if (!option) return void 0;
|
|
526
|
+
if (!option.icon && !option.className) return void 0;
|
|
386
527
|
return {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
levels: [{
|
|
390
|
-
kind: "field",
|
|
391
|
-
field: groupBy
|
|
392
|
-
}, ...levels]
|
|
528
|
+
icon: option.icon,
|
|
529
|
+
className: option.className
|
|
393
530
|
};
|
|
394
|
-
}, [
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
531
|
+
}, [fieldByName]);
|
|
532
|
+
const remainingOutline = React.useMemo(() => {
|
|
533
|
+
if (!isGrouped || !effectiveOutline) return effectiveOutline;
|
|
534
|
+
return {
|
|
535
|
+
...effectiveOutline,
|
|
536
|
+
levels: effectiveOutline.levels.slice(1)
|
|
537
|
+
};
|
|
538
|
+
}, [isGrouped, effectiveOutline]);
|
|
539
|
+
const labelForValue = React.useCallback((value, field) => stringifyGroupValue(value, field ? fieldByName.get(field) : void 0, resolveText, t, uiLocale, t("common.noValue")), [
|
|
540
|
+
fieldByName,
|
|
541
|
+
resolveText,
|
|
542
|
+
t,
|
|
543
|
+
uiLocale
|
|
544
|
+
]);
|
|
545
|
+
const outlineRows = React.useMemo(() => isGrouped ? [] : buildOutlineRows({
|
|
398
546
|
docs: items,
|
|
399
547
|
outline: effectiveOutline,
|
|
400
548
|
edgesByCollection,
|
|
401
|
-
|
|
402
|
-
labelForValue
|
|
549
|
+
toggledKeys: toggledOutlineKeys,
|
|
550
|
+
labelForValue,
|
|
551
|
+
metaForValue
|
|
403
552
|
}), [
|
|
553
|
+
isGrouped,
|
|
404
554
|
items,
|
|
405
555
|
effectiveOutline,
|
|
406
556
|
edgesByCollection,
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
557
|
+
toggledOutlineKeys,
|
|
558
|
+
labelForValue,
|
|
559
|
+
metaForValue
|
|
560
|
+
]);
|
|
561
|
+
const allGroupDocs = React.useMemo(() => {
|
|
562
|
+
if (!isGrouped) return void 0;
|
|
563
|
+
return groupQueries.flatMap((q) => q.data?.docs ?? []);
|
|
564
|
+
}, [isGrouped, groupDataVersion]);
|
|
565
|
+
const groupOutlineRows = React.useMemo(() => {
|
|
566
|
+
if (!isGrouped) return [];
|
|
567
|
+
return filteredGroupValues.map((gv, i) => {
|
|
568
|
+
const docs = (groupQueries[i]?.data)?.docs ?? [];
|
|
569
|
+
if (docs.length === 0) return [];
|
|
570
|
+
const outline = remainingOutline;
|
|
571
|
+
if (!outline || outline.levels.length === 0) return docs.map((doc) => ({
|
|
572
|
+
kind: "record",
|
|
573
|
+
key: `record:${doc.id}`,
|
|
574
|
+
id: String(doc.id),
|
|
575
|
+
doc,
|
|
576
|
+
depth: 0
|
|
577
|
+
}));
|
|
578
|
+
return buildOutlineRows({
|
|
579
|
+
docs,
|
|
580
|
+
outline,
|
|
581
|
+
edgesByCollection,
|
|
582
|
+
toggledKeys: toggledOutlineKeys,
|
|
583
|
+
labelForValue,
|
|
584
|
+
metaForValue,
|
|
585
|
+
allDocs: allGroupDocs
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}, [
|
|
589
|
+
isGrouped,
|
|
590
|
+
filteredGroupValues,
|
|
591
|
+
groupDataVersion,
|
|
592
|
+
remainingOutline,
|
|
593
|
+
edgesByCollection,
|
|
594
|
+
toggledOutlineKeys,
|
|
595
|
+
labelForValue,
|
|
596
|
+
metaForValue,
|
|
597
|
+
allGroupDocs
|
|
412
598
|
]);
|
|
413
599
|
const table = useReactTable({
|
|
414
600
|
data: items,
|
|
@@ -419,33 +605,44 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
419
605
|
getRowId: (row) => String(row.id),
|
|
420
606
|
state: { rowSelection }
|
|
421
607
|
});
|
|
422
|
-
const rowsById = React.useMemo(() =>
|
|
423
|
-
|
|
424
|
-
|
|
608
|
+
const rowsById = React.useMemo(() => {
|
|
609
|
+
const map = /* @__PURE__ */ new Map();
|
|
610
|
+
for (const row of table.getRowModel().rows) {
|
|
611
|
+
map.set(String(row.id), row);
|
|
612
|
+
const originalId = row.original?.id;
|
|
613
|
+
if (originalId !== void 0 && originalId !== null) map.set(String(originalId), row);
|
|
614
|
+
}
|
|
615
|
+
return map;
|
|
616
|
+
}, [table]);
|
|
617
|
+
const deleteMutation = useCollectionDelete(collectionKey);
|
|
618
|
+
const restoreMutation = useCollectionRestore(collectionKey);
|
|
425
619
|
const { data: savedViewsData, isLoading: savedViewsLoading } = useSavedViews(collection, user?.id);
|
|
426
620
|
const saveViewMutation = useSaveView(collection, user?.id);
|
|
427
621
|
const deleteViewMutation = useDeleteSavedView(collection, user?.id);
|
|
428
622
|
const hasActiveFilters = viewState.config.filters.length > 0;
|
|
429
|
-
const hasViewOptionsState = hasActiveFilters || !!viewState.config.
|
|
623
|
+
const hasViewOptionsState = hasActiveFilters || !!viewState.config.sortConfig || !!viewState.config.groupBy || !!viewState.config.includeDeleted;
|
|
430
624
|
const groupableFields = React.useMemo(() => {
|
|
431
625
|
const groupableNames = groupingConfig?.fields ?? [];
|
|
432
626
|
if (groupableNames.length === 0) return [];
|
|
433
627
|
const groupableSet = new Set(groupableNames);
|
|
434
628
|
return availableFields.filter((field) => groupableSet.has(field.name));
|
|
435
629
|
}, [availableFields, groupingConfig?.fields]);
|
|
436
|
-
const layout = resolvedListConfig?.layout;
|
|
437
|
-
const titleField = layout?.titleField ?? (collectionMeta?.title?.type === "field" ? collectionMeta.title.fieldName : void 0);
|
|
438
|
-
const subtitleField = layout?.subtitleField;
|
|
439
|
-
const leadingFields = normalizeFieldList(layout?.leadingFields);
|
|
440
|
-
const badgeFields = normalizeFieldList(layout?.badgeFields);
|
|
441
|
-
const metaFields = normalizeFieldList(layout?.metaFields);
|
|
442
|
-
const density = layout?.density ?? "compact";
|
|
443
630
|
const clearFilters = React.useCallback(() => {
|
|
444
631
|
viewState.setConfig({
|
|
445
632
|
...viewState.config,
|
|
446
633
|
filters: []
|
|
447
634
|
});
|
|
448
635
|
}, [viewState]);
|
|
636
|
+
const applyQuickFilters = React.useCallback((filters) => {
|
|
637
|
+
viewState.setConfig((current) => ({
|
|
638
|
+
...current,
|
|
639
|
+
filters,
|
|
640
|
+
pagination: {
|
|
641
|
+
...current.pagination ?? { pageSize: 25 },
|
|
642
|
+
page: 1
|
|
643
|
+
}
|
|
644
|
+
}));
|
|
645
|
+
}, [viewState]);
|
|
449
646
|
const handleSaveView = React.useCallback((name, configuration) => {
|
|
450
647
|
saveViewMutation.mutate({
|
|
451
648
|
name,
|
|
@@ -469,7 +666,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
469
666
|
collection
|
|
470
667
|
]);
|
|
471
668
|
const toggleOutlineKey = React.useCallback((key) => {
|
|
472
|
-
|
|
669
|
+
setToggledOutlineKeys((current) => {
|
|
473
670
|
const next = new Set(current);
|
|
474
671
|
if (next.has(key)) next.delete(key);
|
|
475
672
|
else next.add(key);
|
|
@@ -484,6 +681,37 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
484
681
|
if (cell && column) return flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
485
682
|
return /* @__PURE__ */ jsx(SimpleValue, { value: fallback ?? getValueAtPath(row?.original, field) });
|
|
486
683
|
}, [columnsByKey]);
|
|
684
|
+
const resolveFieldValue = React.useCallback((item, field) => {
|
|
685
|
+
const value = getValueAtPath(item, field);
|
|
686
|
+
if (value != null && typeof value === "object") return value;
|
|
687
|
+
const fieldDef = fieldByName.get(field);
|
|
688
|
+
if (fieldDef?.type === "relation") {
|
|
689
|
+
const expanded = getValueAtPath(item, fieldDef.options?.relationName ?? field);
|
|
690
|
+
if (expanded && typeof expanded === "object") return expanded;
|
|
691
|
+
}
|
|
692
|
+
return value;
|
|
693
|
+
}, [fieldByName]);
|
|
694
|
+
const renderMetaField = React.useCallback((field, value) => {
|
|
695
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
|
|
696
|
+
className: "text-muted-foreground/40",
|
|
697
|
+
children: "-"
|
|
698
|
+
});
|
|
699
|
+
const fieldDef = fieldByName.get(field);
|
|
700
|
+
if (fieldDef?.type === "datetime" || fieldDef?.type === "date" || field === "updatedAt" || field === "createdAt") return /* @__PURE__ */ jsx("span", { children: typeof value === "string" && ISO_DATE_RE.test(value) ? formatShortDate(new Date(value)) : value instanceof Date ? formatShortDate(value) : String(value) });
|
|
701
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
702
|
+
variant: "secondary",
|
|
703
|
+
className: "max-w-full truncate",
|
|
704
|
+
children: typeof value === "object" ? String(value.title ?? value.name ?? value.label ?? value.id ?? "-") : String(value)
|
|
705
|
+
});
|
|
706
|
+
}, [fieldByName]);
|
|
707
|
+
const renderLeadingIcon = React.useCallback((field, value) => {
|
|
708
|
+
const fieldDef = fieldByName.get(field);
|
|
709
|
+
if (fieldDef?.type === "select" && fieldDef.options?.options) {
|
|
710
|
+
const option = flattenOptions(fieldDef.options.options).find((opt) => String(opt.value) === String(value));
|
|
711
|
+
if (option?.icon) return resolveIconElement(option.icon);
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}, [fieldByName]);
|
|
487
715
|
const handleRowClick = React.useCallback((item) => navigate(`${basePath}/collections/${collection}/${item.id}`), [
|
|
488
716
|
navigate,
|
|
489
717
|
basePath,
|
|
@@ -549,30 +777,6 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
549
777
|
align: "end",
|
|
550
778
|
children: t("viewOptions.title")
|
|
551
779
|
})] }),
|
|
552
|
-
/* @__PURE__ */ jsxs(Select, {
|
|
553
|
-
value: effectiveSort?.field ?? "",
|
|
554
|
-
onValueChange: (field) => viewState.setSort(field ? {
|
|
555
|
-
field,
|
|
556
|
-
direction: effectiveSort?.direction ?? "asc"
|
|
557
|
-
} : null),
|
|
558
|
-
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
559
|
-
className: "h-8 w-40",
|
|
560
|
-
children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort" })
|
|
561
|
-
}), /* @__PURE__ */ jsx(SelectContent, { children: availableFields.map((field) => /* @__PURE__ */ jsx(SelectItem, {
|
|
562
|
-
value: field.name,
|
|
563
|
-
children: resolveText(field.label, field.name)
|
|
564
|
-
}, field.name)) })]
|
|
565
|
-
}),
|
|
566
|
-
/* @__PURE__ */ jsx(Button, {
|
|
567
|
-
variant: "outline",
|
|
568
|
-
size: "icon-sm",
|
|
569
|
-
"aria-label": "Toggle sort direction",
|
|
570
|
-
onClick: () => viewState.setSort({
|
|
571
|
-
field: effectiveSort?.field ?? "createdAt",
|
|
572
|
-
direction: effectiveSort?.direction === "asc" ? "desc" : "asc"
|
|
573
|
-
}),
|
|
574
|
-
children: /* @__PURE__ */ jsx(Icon, { icon: effectiveSort?.direction === "asc" ? "ph:sort-ascending" : "ph:sort-descending" })
|
|
575
|
-
}),
|
|
576
780
|
canUploadToCollection && showToolbar && /* @__PURE__ */ jsx(UploadCollectionButton, {
|
|
577
781
|
collection,
|
|
578
782
|
onUploaded: () => actionHelpers.invalidateCollection(collection)
|
|
@@ -596,105 +800,121 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
596
800
|
onChange: (event) => setSearchTerm(event.target.value),
|
|
597
801
|
onClear: () => setSearchTerm(""),
|
|
598
802
|
placeholder: t("collectionSearch.placeholder"),
|
|
599
|
-
autoFocus: true,
|
|
600
803
|
isLoading: isSearchActive
|
|
601
804
|
})
|
|
602
805
|
}),
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
806
|
+
/* @__PURE__ */ jsx(QuickFilterBar, {
|
|
807
|
+
quickFilters: resolvedListConfig?.quickFilters,
|
|
808
|
+
currentFilters: viewState.config.filters,
|
|
809
|
+
onApply: applyQuickFilters
|
|
810
|
+
}),
|
|
811
|
+
(() => {
|
|
812
|
+
if (isGrouped ? groupOutlineRows.every((g) => g.length === 0) : outlineRows.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
|
|
813
|
+
title: emptyStateTitle,
|
|
814
|
+
description: emptyStateDescription,
|
|
815
|
+
height: "h-64",
|
|
816
|
+
action: isSearching || hasActiveFilters ? /* @__PURE__ */ jsxs("div", {
|
|
817
|
+
className: "flex gap-2",
|
|
818
|
+
children: [isSearching && /* @__PURE__ */ jsxs(Button, {
|
|
819
|
+
variant: "outline",
|
|
820
|
+
size: "sm",
|
|
821
|
+
className: "gap-2",
|
|
822
|
+
onClick: () => setSearchTerm(""),
|
|
823
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
824
|
+
icon: "ph:x",
|
|
825
|
+
className: "size-3.5"
|
|
826
|
+
}), t("common.clear")]
|
|
827
|
+
}), hasActiveFilters && /* @__PURE__ */ jsxs(Button, {
|
|
828
|
+
variant: "outline",
|
|
829
|
+
size: "sm",
|
|
830
|
+
className: "gap-2",
|
|
831
|
+
onClick: clearFilters,
|
|
832
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
833
|
+
icon: "ph:funnel-x",
|
|
834
|
+
className: "size-3.5"
|
|
835
|
+
}), t("viewOptions.clearFilters")]
|
|
836
|
+
})]
|
|
837
|
+
}) : void 0
|
|
838
|
+
});
|
|
839
|
+
const renderRecordRow = (outlineRow, groupContext) => {
|
|
637
840
|
const tableRow = rowsById.get(outlineRow.id);
|
|
638
|
-
|
|
639
|
-
const item = tableRow.original;
|
|
841
|
+
const item = tableRow?.original ?? outlineRow.doc;
|
|
640
842
|
const lock = getLock(item.id);
|
|
641
843
|
const locked = isDocLocked(item.id);
|
|
642
844
|
const lockUser = lock ? getLockUser(lock) : null;
|
|
643
|
-
const isSelected = tableRow
|
|
644
|
-
const titleValue = (titleField ? getValueAtPath(item, titleField) : item
|
|
845
|
+
const isSelected = tableRow?.getIsSelected() ?? false;
|
|
846
|
+
const titleValue = (titleField ? getValueAtPath(item, titleField) : item["_title"]) ?? item.title ?? item.name ?? item.id;
|
|
645
847
|
const subtitleValue = subtitleField ? getValueAtPath(item, subtitleField) : void 0;
|
|
848
|
+
const isGroupMismatch = groupContext != null && getValueAtPath(item, groupContext.field) !== groupContext.value;
|
|
646
849
|
return /* @__PURE__ */ jsxs("div", {
|
|
647
|
-
|
|
648
|
-
|
|
850
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
851
|
+
className: cn("group/list-row hover:bg-muted/40 data-[state=selected]:bg-muted/60 relative flex min-w-0 cursor-pointer items-center gap-2 rounded-md px-4 text-sm transition-colors", density === "compact" ? "min-h-9 py-1" : "min-h-11 py-2", isHighlighted(item.id) && "bg-info/10", isGroupMismatch && "opacity-45"),
|
|
649
852
|
children: [
|
|
853
|
+
outlineRow.depth > 0 && /* @__PURE__ */ jsx("span", {
|
|
854
|
+
className: "bg-border/40 absolute top-0 bottom-0 w-px",
|
|
855
|
+
style: { left: `${52 + (outlineRow.depth - 1) * 18}px` },
|
|
856
|
+
"aria-hidden": "true"
|
|
857
|
+
}),
|
|
650
858
|
/* @__PURE__ */ jsx("div", {
|
|
651
859
|
role: "presentation",
|
|
652
|
-
|
|
860
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
861
|
+
className: "shrink-0 opacity-0 transition-opacity duration-[var(--motion-duration-fast)] group-hover/list-row:opacity-100 data-[state=selected]:opacity-100",
|
|
653
862
|
onClick: (event) => event.stopPropagation(),
|
|
654
863
|
onKeyDown: (event) => event.stopPropagation(),
|
|
655
864
|
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
656
865
|
checked: isSelected,
|
|
657
|
-
disabled: !tableRow.getCanSelect(),
|
|
658
|
-
onCheckedChange: (checked) => tableRow
|
|
659
|
-
"aria-label": "
|
|
866
|
+
disabled: !tableRow || !tableRow.getCanSelect(),
|
|
867
|
+
onCheckedChange: (checked) => tableRow?.toggleSelected(!!checked),
|
|
868
|
+
"aria-label": t("table.selectRow")
|
|
660
869
|
})
|
|
661
870
|
}),
|
|
662
|
-
/* @__PURE__ */ jsx("button", {
|
|
871
|
+
outlineRow.expandable ? /* @__PURE__ */ jsx("button", {
|
|
663
872
|
type: "button",
|
|
664
|
-
className:
|
|
665
|
-
disabled: !outlineRow.expandable,
|
|
873
|
+
className: "text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring/40 -ml-1 flex size-7 shrink-0 items-center justify-center rounded-[var(--control-radius-inner)] transition-[background-color,color,box-shadow] focus-visible:ring-2 focus-visible:outline-none",
|
|
666
874
|
onClick: (event) => {
|
|
667
875
|
event.stopPropagation();
|
|
668
|
-
|
|
876
|
+
toggleOutlineKey(outlineRow.key);
|
|
669
877
|
},
|
|
670
|
-
"aria-label": outlineRow.collapsed ? "
|
|
671
|
-
children:
|
|
878
|
+
"aria-label": outlineRow.collapsed ? t("a11y.expand") : t("a11y.collapse"),
|
|
879
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
672
880
|
icon: "ph:caret-right-bold",
|
|
673
881
|
className: cn("size-3 transition-transform", !outlineRow.collapsed && "rotate-90")
|
|
674
|
-
})
|
|
675
|
-
}),
|
|
882
|
+
})
|
|
883
|
+
}) : null,
|
|
676
884
|
/* @__PURE__ */ jsxs("button", {
|
|
677
885
|
type: "button",
|
|
678
|
-
className: "min-w-0 flex-1 text-left",
|
|
886
|
+
className: "focus-visible:ring-ring/40 -my-1 min-w-0 flex-1 rounded-[var(--control-radius-inner)] py-1 text-left focus-visible:ring-2 focus-visible:outline-none",
|
|
887
|
+
style: outlineRow.depth > 0 ? { paddingLeft: `${outlineRow.depth * 18}px` } : void 0,
|
|
679
888
|
onClick: () => handleRowClick(item),
|
|
680
889
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
681
890
|
className: "flex min-w-0 items-center gap-2",
|
|
682
891
|
children: [
|
|
683
|
-
leadingFields.map((field) =>
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
892
|
+
leadingFields.map((field) => {
|
|
893
|
+
const fieldValue = getValueAtPath(item, field);
|
|
894
|
+
return /* @__PURE__ */ jsx("span", {
|
|
895
|
+
className: "flex size-4 shrink-0 items-center justify-center",
|
|
896
|
+
children: renderLeadingIcon(field, fieldValue) || renderField(tableRow, field, fieldValue)
|
|
897
|
+
}, field);
|
|
898
|
+
}),
|
|
687
899
|
/* @__PURE__ */ jsx("span", {
|
|
688
|
-
className: "text-foreground
|
|
900
|
+
className: cn("truncate", outlineRow.depth > 0 ? "text-muted-foreground font-normal" : "text-foreground font-medium"),
|
|
689
901
|
children: titleField ? renderField(tableRow, titleField, titleValue) : stringifySimpleValue(titleValue)
|
|
690
902
|
}),
|
|
691
|
-
badgeFields.map((field) =>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
903
|
+
badgeFields.map((field) => {
|
|
904
|
+
const fieldValue = getValueAtPath(item, field);
|
|
905
|
+
const icon = renderLeadingIcon(field, fieldValue);
|
|
906
|
+
if (icon) return /* @__PURE__ */ jsx("span", {
|
|
907
|
+
className: "flex size-4 shrink-0 items-center justify-center",
|
|
908
|
+
children: icon
|
|
909
|
+
}, field);
|
|
910
|
+
return /* @__PURE__ */ jsx("span", {
|
|
911
|
+
className: "text-muted-foreground inline-flex max-w-36 shrink-0 items-center text-xs",
|
|
912
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
913
|
+
className: "truncate",
|
|
914
|
+
children: renderField(tableRow, field, fieldValue)
|
|
915
|
+
})
|
|
916
|
+
}, field);
|
|
917
|
+
}),
|
|
698
918
|
locked && /* @__PURE__ */ jsxs("span", {
|
|
699
919
|
className: "text-warning inline-flex items-center gap-1 text-xs",
|
|
700
920
|
children: [/* @__PURE__ */ jsx(Icon, {
|
|
@@ -709,10 +929,10 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
709
929
|
})]
|
|
710
930
|
}),
|
|
711
931
|
metaFields.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
712
|
-
className: "text-muted-foreground hidden
|
|
932
|
+
className: "text-muted-foreground hidden shrink-0 items-center gap-2 text-xs md:flex",
|
|
713
933
|
children: metaFields.map((field) => /* @__PURE__ */ jsx("span", {
|
|
714
|
-
className: "min-w-0
|
|
715
|
-
children:
|
|
934
|
+
className: "flex w-28 min-w-0 items-center justify-end",
|
|
935
|
+
children: renderMetaField(field, resolveFieldValue(item, field))
|
|
716
936
|
}, field))
|
|
717
937
|
}),
|
|
718
938
|
actions.row.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
@@ -732,27 +952,198 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
732
952
|
})
|
|
733
953
|
]
|
|
734
954
|
}, outlineRow.key);
|
|
735
|
-
}
|
|
955
|
+
};
|
|
956
|
+
if (isGrouped) return /* @__PURE__ */ jsx("div", {
|
|
957
|
+
className: "flex flex-col gap-1.5 overflow-hidden",
|
|
958
|
+
children: filteredGroupValues.map((gv, i) => {
|
|
959
|
+
const rows = groupOutlineRows[i] ?? [];
|
|
960
|
+
const queryData = groupQueries[i]?.data;
|
|
961
|
+
const totalDocs = queryData?.totalDocs ?? 0;
|
|
962
|
+
const loadedCount = (queryData?.docs ?? []).length;
|
|
963
|
+
const groupKey = String(gv.value);
|
|
964
|
+
const groupLabel = labelForValue(gv.value, firstFieldLevel?.field);
|
|
965
|
+
const meta = metaForValue(gv.value, firstFieldLevel?.field);
|
|
966
|
+
const groupIcon = meta?.icon ? resolveIconElement(meta.icon) : null;
|
|
967
|
+
const isGroupLoading = groupQueries[i]?.isLoading ?? false;
|
|
968
|
+
if (totalDocs === 0 && !isGroupLoading) return null;
|
|
969
|
+
const groupToggleKey = `group:${groupKey}`;
|
|
970
|
+
const isGroupCollapsed = toggledOutlineKeys.has(groupToggleKey);
|
|
971
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
972
|
+
className: "flex flex-col",
|
|
973
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
974
|
+
type: "button",
|
|
975
|
+
className: "bg-muted/[0.06] hover:bg-muted/20 flex min-h-8 items-center gap-2 rounded-md px-3 py-1 text-left transition-colors",
|
|
976
|
+
onClick: () => toggleOutlineKey(groupToggleKey),
|
|
977
|
+
"aria-expanded": !isGroupCollapsed,
|
|
978
|
+
children: [
|
|
979
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
980
|
+
icon: "ph:caret-right-bold",
|
|
981
|
+
className: cn("text-muted-foreground size-3 shrink-0 transition-transform", !isGroupCollapsed && "rotate-90")
|
|
982
|
+
}),
|
|
983
|
+
groupIcon && /* @__PURE__ */ jsx("span", {
|
|
984
|
+
className: "size-4 shrink-0",
|
|
985
|
+
children: groupIcon
|
|
986
|
+
}),
|
|
987
|
+
/* @__PURE__ */ jsx("span", {
|
|
988
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
989
|
+
children: groupLabel
|
|
990
|
+
}),
|
|
991
|
+
/* @__PURE__ */ jsx("span", {
|
|
992
|
+
className: "text-muted-foreground/60 text-xs tabular-nums",
|
|
993
|
+
children: totalDocs
|
|
994
|
+
})
|
|
995
|
+
]
|
|
996
|
+
}), !isGroupCollapsed && (isGroupLoading ? /* @__PURE__ */ jsx("div", {
|
|
997
|
+
className: "flex items-center justify-center py-4",
|
|
998
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
999
|
+
icon: "ph:spinner-gap",
|
|
1000
|
+
className: "text-muted-foreground size-4 animate-spin"
|
|
1001
|
+
})
|
|
1002
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
1003
|
+
className: "flex flex-col gap-px",
|
|
1004
|
+
children: [rows.map((outlineRow) => {
|
|
1005
|
+
if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
|
|
1006
|
+
row: outlineRow,
|
|
1007
|
+
showCounts: resolvedListConfig?.outline?.showCounts ?? true,
|
|
1008
|
+
onToggle: toggleOutlineKey
|
|
1009
|
+
}, outlineRow.key);
|
|
1010
|
+
return renderRecordRow(outlineRow, {
|
|
1011
|
+
field: firstFieldLevel.field,
|
|
1012
|
+
value: gv.value
|
|
1013
|
+
});
|
|
1014
|
+
}), totalDocs > loadedCount && /* @__PURE__ */ jsxs("button", {
|
|
1015
|
+
type: "button",
|
|
1016
|
+
className: "text-muted-foreground hover:text-foreground hover:bg-muted/50 flex w-full items-center justify-center gap-2 rounded-md py-2 text-xs transition-colors",
|
|
1017
|
+
onClick: () => loadMoreInGroup(groupKey),
|
|
1018
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1019
|
+
icon: "ph:caret-down",
|
|
1020
|
+
className: "size-3"
|
|
1021
|
+
}), /* @__PURE__ */ jsxs("span", { children: [
|
|
1022
|
+
t("table.loadMore"),
|
|
1023
|
+
" (",
|
|
1024
|
+
totalDocs - loadedCount,
|
|
1025
|
+
")"
|
|
1026
|
+
] })]
|
|
1027
|
+
})]
|
|
1028
|
+
}))]
|
|
1029
|
+
}, groupKey);
|
|
1030
|
+
})
|
|
1031
|
+
});
|
|
1032
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1033
|
+
className: "flex flex-col gap-px overflow-hidden",
|
|
1034
|
+
children: outlineRows.map((outlineRow) => {
|
|
1035
|
+
if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
|
|
1036
|
+
row: outlineRow,
|
|
1037
|
+
showCounts: resolvedListConfig?.outline?.showCounts ?? true,
|
|
1038
|
+
onToggle: toggleOutlineKey
|
|
1039
|
+
}, outlineRow.key);
|
|
1040
|
+
return renderRecordRow(outlineRow);
|
|
1041
|
+
})
|
|
1042
|
+
});
|
|
1043
|
+
})(),
|
|
1044
|
+
!isSearching && (hasOutline || isGrouped) && /* @__PURE__ */ jsx("div", {
|
|
1045
|
+
className: "text-muted-foreground flex items-center gap-2 py-2 text-sm tabular-nums",
|
|
1046
|
+
"aria-live": "polite",
|
|
1047
|
+
"aria-atomic": "true",
|
|
1048
|
+
children: /* @__PURE__ */ jsxs("span", { children: [
|
|
1049
|
+
isGrouped ? groupQueries.reduce((sum, q) => sum + (q.data?.totalDocs ?? 0), 0) : items.length,
|
|
1050
|
+
" ",
|
|
1051
|
+
t("table.items")
|
|
1052
|
+
] })
|
|
736
1053
|
}),
|
|
737
|
-
/* @__PURE__ */ jsxs("div", {
|
|
738
|
-
className: "
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1054
|
+
!isSearching && !hasOutline && !isGrouped && /* @__PURE__ */ jsxs("div", {
|
|
1055
|
+
className: "qa-list-view__pagination flex items-center justify-between gap-4 py-2 tabular-nums",
|
|
1056
|
+
role: "navigation",
|
|
1057
|
+
"aria-label": t("table.pagination"),
|
|
1058
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1059
|
+
className: "text-muted-foreground flex items-center gap-4 text-sm",
|
|
1060
|
+
"aria-live": "polite",
|
|
1061
|
+
"aria-atomic": "true",
|
|
1062
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
1063
|
+
items.length > 0 ? `${((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + 1}-${Math.min(((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + items.length, listData?.totalDocs ?? items.length)}` : "0",
|
|
1064
|
+
" ",
|
|
1065
|
+
t("table.of"),
|
|
1066
|
+
" ",
|
|
1067
|
+
listData?.totalDocs ?? 0
|
|
1068
|
+
] }), /* @__PURE__ */ jsxs("div", {
|
|
1069
|
+
className: "flex items-center gap-2",
|
|
1070
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("table.show") }), /* @__PURE__ */ jsxs(Select, {
|
|
1071
|
+
value: String(viewState.config.pagination?.pageSize ?? 25),
|
|
1072
|
+
onValueChange: (value) => viewState.setPageSize(Number(value)),
|
|
1073
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1074
|
+
className: "h-8 w-[70px]",
|
|
1075
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
1076
|
+
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
1077
|
+
side: "top",
|
|
1078
|
+
children: [
|
|
1079
|
+
10,
|
|
1080
|
+
25,
|
|
1081
|
+
50,
|
|
1082
|
+
100
|
|
1083
|
+
].map((size) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1084
|
+
value: String(size),
|
|
1085
|
+
children: size
|
|
1086
|
+
}, size))
|
|
1087
|
+
})]
|
|
1088
|
+
})]
|
|
753
1089
|
})]
|
|
1090
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1091
|
+
className: "flex items-center gap-1",
|
|
1092
|
+
children: [
|
|
1093
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1094
|
+
variant: "ghost",
|
|
1095
|
+
size: "sm",
|
|
1096
|
+
className: "size-8 p-0",
|
|
1097
|
+
disabled: (viewState.config.pagination?.page ?? 1) <= 1,
|
|
1098
|
+
onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) - 1),
|
|
1099
|
+
"aria-label": t("table.previousPage"),
|
|
1100
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1101
|
+
icon: "ph:caret-left",
|
|
1102
|
+
className: "size-4"
|
|
1103
|
+
})
|
|
1104
|
+
}),
|
|
1105
|
+
Array.from({ length: Math.min(5, listData?.totalPages ?? 1) }, (_, i) => {
|
|
1106
|
+
const currentPage = viewState.config.pagination?.page ?? 1;
|
|
1107
|
+
const totalPages = listData?.totalPages ?? 1;
|
|
1108
|
+
let pageNum;
|
|
1109
|
+
if (totalPages <= 5) pageNum = i + 1;
|
|
1110
|
+
else if (currentPage <= 3) pageNum = i + 1;
|
|
1111
|
+
else if (currentPage >= totalPages - 2) pageNum = totalPages - 4 + i;
|
|
1112
|
+
else pageNum = currentPage - 2 + i;
|
|
1113
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1114
|
+
variant: currentPage === pageNum ? "secondary" : "ghost",
|
|
1115
|
+
size: "sm",
|
|
1116
|
+
className: "size-8 min-w-8 p-0 tabular-nums",
|
|
1117
|
+
onClick: () => viewState.setPage(pageNum),
|
|
1118
|
+
"aria-label": t("table.page", { page: pageNum }),
|
|
1119
|
+
"aria-current": currentPage === pageNum ? "page" : void 0,
|
|
1120
|
+
children: pageNum
|
|
1121
|
+
}, pageNum);
|
|
1122
|
+
}),
|
|
1123
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1124
|
+
variant: "ghost",
|
|
1125
|
+
size: "sm",
|
|
1126
|
+
className: "size-8 p-0",
|
|
1127
|
+
disabled: (viewState.config.pagination?.page ?? 1) >= (listData?.totalPages ?? 1),
|
|
1128
|
+
onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) + 1),
|
|
1129
|
+
"aria-label": t("table.nextPage"),
|
|
1130
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1131
|
+
icon: "ph:caret-right",
|
|
1132
|
+
className: "size-4"
|
|
1133
|
+
})
|
|
1134
|
+
})
|
|
1135
|
+
]
|
|
754
1136
|
})]
|
|
755
1137
|
}),
|
|
1138
|
+
isSearching && /* @__PURE__ */ jsxs("div", {
|
|
1139
|
+
className: "text-muted-foreground flex items-center gap-2 py-2 text-sm tabular-nums",
|
|
1140
|
+
"aria-live": "polite",
|
|
1141
|
+
"aria-atomic": "true",
|
|
1142
|
+
children: [isSearchActive && /* @__PURE__ */ jsx(Icon, {
|
|
1143
|
+
icon: "ph:spinner-gap",
|
|
1144
|
+
className: "size-3 animate-spin"
|
|
1145
|
+
}), t("cell.item", { count: items.length })]
|
|
1146
|
+
}),
|
|
756
1147
|
/* @__PURE__ */ jsx(BulkActionToolbar, {
|
|
757
1148
|
table,
|
|
758
1149
|
actions: actions.bulk,
|
|
@@ -781,7 +1172,9 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
781
1172
|
onDeleteView: (viewId) => deleteViewMutation.mutate(viewId),
|
|
782
1173
|
supportsSoftDelete: !!collectionMeta?.softDelete,
|
|
783
1174
|
groupableFields,
|
|
784
|
-
defaultGroupBy
|
|
1175
|
+
defaultGroupBy,
|
|
1176
|
+
defaultFilters,
|
|
1177
|
+
panels: { columns: false }
|
|
785
1178
|
}),
|
|
786
1179
|
dialogAction && /* @__PURE__ */ jsx(ActionDialog, {
|
|
787
1180
|
open: !!dialogAction,
|
|
@@ -796,10 +1189,10 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
|
|
|
796
1189
|
});
|
|
797
1190
|
}
|
|
798
1191
|
function OutlineHeaderRow({ row, showCounts, onToggle }) {
|
|
1192
|
+
const groupIcon = row.kind === "group" && "icon" in row ? resolveIconElement(row.icon) : null;
|
|
799
1193
|
return /* @__PURE__ */ jsxs("button", {
|
|
800
1194
|
type: "button",
|
|
801
|
-
className: "bg-
|
|
802
|
-
style: { paddingLeft: `${12 + row.depth * 18}px` },
|
|
1195
|
+
className: "hover:bg-muted/50 focus-visible:ring-ring/40 flex min-h-8 w-full items-center gap-2 px-3 py-1 text-left transition-[background-color,color,box-shadow] focus-visible:ring-2 focus-visible:outline-none",
|
|
803
1196
|
onClick: () => onToggle(row.key),
|
|
804
1197
|
"aria-expanded": !row.collapsed,
|
|
805
1198
|
children: [
|
|
@@ -811,12 +1204,17 @@ function OutlineHeaderRow({ row, showCounts, onToggle }) {
|
|
|
811
1204
|
icon: "ph:folder-simple",
|
|
812
1205
|
className: "text-muted-foreground size-4"
|
|
813
1206
|
}),
|
|
1207
|
+
groupIcon && /* @__PURE__ */ jsx("span", {
|
|
1208
|
+
className: "size-4 shrink-0",
|
|
1209
|
+
children: groupIcon
|
|
1210
|
+
}),
|
|
814
1211
|
/* @__PURE__ */ jsx("span", {
|
|
815
|
-
className: "
|
|
1212
|
+
className: "text-muted-foreground truncate text-xs font-medium",
|
|
1213
|
+
style: row.depth > 0 ? { paddingLeft: `${row.depth * 18}px` } : void 0,
|
|
816
1214
|
children: row.label
|
|
817
1215
|
}),
|
|
818
1216
|
showCounts && /* @__PURE__ */ jsx("span", {
|
|
819
|
-
className: "
|
|
1217
|
+
className: "text-muted-foreground/70 ml-1 text-xs tabular-nums",
|
|
820
1218
|
children: row.count
|
|
821
1219
|
})
|
|
822
1220
|
]
|