@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.
Files changed (94) hide show
  1. package/dist/client/builder/types/collection-types.d.mts +9 -0
  2. package/dist/client/components/actions/action-dialog.mjs +5 -0
  3. package/dist/client/components/fields/rich-text-editor/bubble-menu.mjs +7 -0
  4. package/dist/client/components/fields/rich-text-editor/extensions.mjs +17 -1
  5. package/dist/client/components/fields/rich-text-editor/index.d.mts +2 -1
  6. package/dist/client/components/fields/rich-text-editor/index.mjs +35 -74
  7. package/dist/client/components/fields/rich-text-editor/slash-commands.mjs +30 -7
  8. package/dist/client/components/fields/rich-text-editor/toolbar.mjs +1 -312
  9. package/dist/client/components/fields/rich-text-editor/types.d.mts +4 -0
  10. package/dist/client/components/fields/rich-text-editor/types.mjs +1 -1
  11. package/dist/client/components/fields/rich-text-editor/utils.mjs +6 -12
  12. package/dist/client/components/filter-builder/filter-builder-sheet.mjs +75 -22
  13. package/dist/client/components/ui/dropdown-menu.mjs +1 -34
  14. package/dist/client/hooks/query-access.d.mts +9 -0
  15. package/dist/client/hooks/query-access.mjs +20 -0
  16. package/dist/client/hooks/typed-hooks.d.mts +4 -2
  17. package/dist/client/hooks/typed-hooks.mjs +30 -29
  18. package/dist/client/hooks/use-reactive-fields.d.mts +1 -0
  19. package/dist/client/hooks/use-reactive-fields.mjs +16 -1
  20. package/dist/client/hooks/use-server-actions.mjs +12 -1
  21. package/dist/client/hooks/use-view-state.mjs +15 -7
  22. package/dist/client/lib/view-filter-utils.mjs +30 -0
  23. package/dist/client/preview/block-scope-context.d.mts +2 -2
  24. package/dist/client/preview/preview-banner.d.mts +2 -2
  25. package/dist/client/preview/preview-field.d.mts +4 -4
  26. package/dist/client/scope/picker.d.mts +2 -2
  27. package/dist/client/scope/provider.d.mts +2 -2
  28. package/dist/client/styles/base.css +69 -77
  29. package/dist/client/utils/build-field-definitions-from-schema.mjs +1 -0
  30. package/dist/client/views/collection/auto-form-fields.mjs +3 -2
  31. package/dist/client/views/collection/cells/primitive-cells.mjs +9 -6
  32. package/dist/client/views/collection/columns/build-columns.mjs +3 -1
  33. package/dist/client/views/collection/field-renderer.mjs +11 -3
  34. package/dist/client/views/collection/form-view.mjs +207 -202
  35. package/dist/client/views/collection/list-view.mjs +581 -183
  36. package/dist/client/views/collection/outline.mjs +44 -19
  37. package/dist/client/views/collection/quick-filter-bar.mjs +45 -0
  38. package/dist/client/views/collection/table-view.mjs +60 -16
  39. package/dist/client/views/globals/global-form-view.mjs +12 -9
  40. package/dist/client/views/layout/admin-layout.mjs +1 -1
  41. package/dist/client/views/layout/admin-sidebar.mjs +20 -14
  42. package/dist/client/views/layout/admin-theme.mjs +5 -4
  43. package/dist/client.mjs +1 -1
  44. package/dist/components/rich-text/rich-text-renderer.d.mts +5 -5
  45. package/dist/components/rich-text/rich-text-renderer.mjs +5 -2
  46. package/dist/index.mjs +1 -1
  47. package/dist/modules/admin.d.mts +1 -1
  48. package/dist/server/augmentation/actions.d.mts +4 -3
  49. package/dist/server/augmentation/dashboard.d.mts +11 -11
  50. package/dist/server/augmentation/form-layout.d.mts +11 -6
  51. package/dist/server/augmentation/index.d.mts +7 -0
  52. package/dist/server/augmentation/sidebar.d.mts +8 -8
  53. package/dist/server/codegen/admin-client-template.mjs +55 -38
  54. package/dist/server/fields/index.d.mts +1 -1
  55. package/dist/server/fields/rich-text.d.mts +16 -17
  56. package/dist/server/fields/rich-text.mjs +18 -7
  57. package/dist/server/i18n/messages/cs.mjs +2 -0
  58. package/dist/server/i18n/messages/de.mjs +2 -0
  59. package/dist/server/i18n/messages/en.mjs +4 -0
  60. package/dist/server/i18n/messages/es.mjs +2 -0
  61. package/dist/server/i18n/messages/fr.mjs +2 -0
  62. package/dist/server/i18n/messages/pl.mjs +2 -0
  63. package/dist/server/i18n/messages/pt.mjs +2 -0
  64. package/dist/server/i18n/messages/sk.mjs +2 -0
  65. package/dist/server/modules/admin/block/block-builder.d.mts +0 -8
  66. package/dist/server/modules/admin/block/introspection.d.mts +2 -2
  67. package/dist/server/modules/admin/collections/account.d.mts +53 -52
  68. package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -56
  69. package/dist/server/modules/admin/collections/admin-preferences.d.mts +3 -2
  70. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -49
  71. package/dist/server/modules/admin/collections/apikey.d.mts +72 -71
  72. package/dist/server/modules/admin/collections/assets.d.mts +42 -41
  73. package/dist/server/modules/admin/collections/session.d.mts +46 -45
  74. package/dist/server/modules/admin/collections/user.d.mts +67 -66
  75. package/dist/server/modules/admin/collections/verification.d.mts +39 -38
  76. package/dist/server/modules/admin/index.d.mts +3 -3
  77. package/dist/server/modules/admin/routes/admin-config.d.mts +2 -2
  78. package/dist/server/modules/admin/routes/admin-config.mjs +39 -23
  79. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  80. package/dist/server/modules/admin/routes/execute-action.mjs +28 -8
  81. package/dist/server/modules/admin/routes/locales.d.mts +2 -2
  82. package/dist/server/modules/admin/routes/reactive.mjs +2 -2
  83. package/dist/server/modules/admin/routes/route-helpers.d.mts +11 -7
  84. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  85. package/dist/server/modules/admin/routes/widget-data.mjs +12 -4
  86. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +27 -27
  87. package/dist/server/modules/audit/.generated/module.d.mts +6 -6
  88. package/dist/server/modules/audit/collections/audit-log.d.mts +40 -39
  89. package/dist/server/plugin.mjs +3 -3
  90. package/dist/server.d.mts +1 -1
  91. package/dist/shared/types/index.d.mts +1 -0
  92. package/dist/shared/types/saved-views.types.d.mts +14 -7
  93. package/dist/shared.d.mts +3 -2
  94. 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 [collapsedOutlineKeys, setCollapsedOutlineKeys] = React.useState(() => /* @__PURE__ */ new Set());
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 viewState = useViewState(defaultColumns, {
281
+ const defaultFilters = React.useMemo(() => resolvedListConfig?.defaultFilters ?? [], [resolvedListConfig?.defaultFilters]);
282
+ const viewState = useViewState(defaultColumns, React.useMemo(() => ({
264
283
  realtime: resolvedRealtime,
265
- groupBy: defaultGroupBy
266
- }, collection, user?.id);
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([...visibleColumns, ...outlineRelationNames])), [visibleColumns, outlineRelationNames]);
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 queryOptions = React.useMemo(() => {
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 (groupBy && sortConfig?.field && sortConfig.field !== groupBy) options.orderBy = [{ [groupBy]: "asc" }, { [sortConfig.field]: sortConfig.direction }];
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 { data: listData, isLoading: listLoading, error: listError } = useCollectionList(collection, queryOptions, { enabled: !isSearching }, { realtime: effectiveRealtime });
336
- const items = React.useMemo(() => isSearching ? searchData?.docs ?? [] : listData?.docs ?? [], [
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
- const collectionQueries = queryOpts.collections?.[level.collection];
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 effectiveOutline = React.useMemo(() => {
380
- const outline = resolvedListConfig?.outline;
381
- const groupBy = viewState.config.groupBy;
382
- if (!groupBy) return outline;
383
- const levels = outline?.levels ?? [];
384
- const first = levels[0];
385
- if (first?.kind === "field" && first.field === groupBy) return outline;
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
- ...outline,
388
- defaultExpanded: outline?.defaultExpanded ?? true,
389
- levels: [{
390
- kind: "field",
391
- field: groupBy
392
- }, ...levels]
528
+ icon: option.icon,
529
+ className: option.className
393
530
  };
394
- }, [resolvedListConfig?.outline, viewState.config.groupBy]);
395
- const availableFields = React.useMemo(() => getAllAvailableFields(resolvedFields, { meta: collectionMeta }), [resolvedFields, collectionMeta]);
396
- const fieldByName = React.useMemo(() => new Map(availableFields.map((field) => [field.name, field])), [availableFields]);
397
- const outlineRows = React.useMemo(() => buildOutlineRows({
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
- collapsedKeys: collapsedOutlineKeys,
402
- labelForValue: (value, field) => stringifyGroupValue(value, field ? fieldByName.get(field) : void 0, resolveText, t, uiLocale, t("common.noValue"))
549
+ toggledKeys: toggledOutlineKeys,
550
+ labelForValue,
551
+ metaForValue
403
552
  }), [
553
+ isGrouped,
404
554
  items,
405
555
  effectiveOutline,
406
556
  edgesByCollection,
407
- collapsedOutlineKeys,
408
- fieldByName,
409
- resolveText,
410
- t,
411
- uiLocale
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(() => new Map(table.getRowModel().rows.map((row) => [String(row.id), row])), [table, items]);
423
- const deleteMutation = useCollectionDelete(collection);
424
- const restoreMutation = useCollectionRestore(collection);
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.groupBy || viewState.config.visibleColumns.length !== defaultColumns.length || !!viewState.config.includeDeleted;
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
- setCollapsedOutlineKeys((current) => {
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
- outlineRows.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
604
- title: emptyStateTitle,
605
- description: emptyStateDescription,
606
- height: "h-64",
607
- action: isSearching || hasActiveFilters ? /* @__PURE__ */ jsxs("div", {
608
- className: "flex gap-2",
609
- children: [isSearching && /* @__PURE__ */ jsxs(Button, {
610
- variant: "outline",
611
- size: "sm",
612
- className: "gap-2",
613
- onClick: () => setSearchTerm(""),
614
- children: [/* @__PURE__ */ jsx(Icon, {
615
- icon: "ph:x",
616
- className: "size-3.5"
617
- }), t("common.clear")]
618
- }), hasActiveFilters && /* @__PURE__ */ jsxs(Button, {
619
- variant: "outline",
620
- size: "sm",
621
- className: "gap-2",
622
- onClick: clearFilters,
623
- children: [/* @__PURE__ */ jsx(Icon, {
624
- icon: "ph:funnel-x",
625
- className: "size-3.5"
626
- }), t("viewOptions.clearFilters")]
627
- })]
628
- }) : void 0
629
- }) : /* @__PURE__ */ jsx("div", {
630
- className: "border-border-subtle overflow-hidden rounded-md border",
631
- children: outlineRows.map((outlineRow) => {
632
- if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
633
- row: outlineRow,
634
- showCounts: resolvedListConfig?.outline?.showCounts ?? true,
635
- onToggle: toggleOutlineKey
636
- }, outlineRow.key);
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
- if (!tableRow) return null;
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.getIsSelected();
644
- const titleValue = (titleField ? getValueAtPath(item, titleField) : item._title) ?? item.title ?? item.name ?? item.id;
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
- className: cn("group/list-row border-border-subtle hover:bg-muted/35 flex min-w-0 items-start gap-2 border-b px-3 text-sm transition-colors last:border-b-0", density === "compact" ? "py-2" : "py-3", isHighlighted(item.id) && "bg-info/10", isSelected && "bg-muted/50"),
648
- style: { paddingLeft: `${12 + outlineRow.depth * 18}px` },
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
- className: "mt-0.5 shrink-0",
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.toggleSelected(!!checked),
659
- "aria-label": "Select row"
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: cn("text-muted-foreground mt-0.5 flex size-5 shrink-0 items-center justify-center rounded transition-colors", outlineRow.expandable && "hover:bg-muted hover:text-foreground"),
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
- if (outlineRow.expandable) toggleOutlineKey(outlineRow.key);
876
+ toggleOutlineKey(outlineRow.key);
669
877
  },
670
- "aria-label": outlineRow.collapsed ? "Expand row" : "Collapse row",
671
- children: outlineRow.expandable ? /* @__PURE__ */ jsx(Icon, {
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
- }) : null
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) => /* @__PURE__ */ jsx("span", {
684
- className: "shrink-0",
685
- children: renderField(tableRow, field, getValueAtPath(item, field))
686
- }, field)),
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 truncate font-medium",
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) => /* @__PURE__ */ jsx("span", {
692
- className: "bg-muted text-muted-foreground inline-flex h-5 max-w-36 shrink-0 items-center rounded px-1.5 font-mono text-[11px]",
693
- children: /* @__PURE__ */ jsx("span", {
694
- className: "truncate",
695
- children: renderField(tableRow, field, getValueAtPath(item, field))
696
- })
697
- }, field)),
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 max-w-[42%] shrink-0 items-center justify-end gap-3 overflow-hidden text-right text-xs md:flex",
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 truncate",
715
- children: renderField(tableRow, field, getValueAtPath(item, field))
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: "text-muted-foreground flex flex-col gap-2 text-xs sm:flex-row sm:items-center sm:justify-between",
739
- children: [/* @__PURE__ */ jsx("span", { children: isSearching ? `${items.length} item${items.length === 1 ? "" : "s"}` : `${items.length > 0 ? ((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + 1 : 0}-${Math.min(((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + items.length, listData?.totalDocs ?? items.length)} ${t("table.of")} ${listData?.totalDocs ?? 0}` }), !isSearching && (listData?.totalPages ?? 1) > 1 && /* @__PURE__ */ jsxs("div", {
740
- className: "flex items-center gap-2",
741
- children: [/* @__PURE__ */ jsx(Button, {
742
- variant: "outline",
743
- size: "sm",
744
- disabled: (viewState.config.pagination?.page ?? 1) <= 1,
745
- onClick: () => viewState.setPage(Math.max(1, (viewState.config.pagination?.page ?? 1) - 1)),
746
- children: t("common.previous")
747
- }), /* @__PURE__ */ jsx(Button, {
748
- variant: "outline",
749
- size: "sm",
750
- disabled: (viewState.config.pagination?.page ?? 1) >= (listData?.totalPages ?? 1),
751
- onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) + 1),
752
- children: t("common.next")
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-background/95 border-border-subtle sticky top-0 z-10 flex w-full items-center gap-2 border-b px-3 py-2 text-left backdrop-blur",
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: "font-chrome chrome-meta text-muted-foreground truncate text-[11px] font-semibold tracking-[0.12em] uppercase",
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: "font-chrome chrome-meta text-muted-foreground text-[11px] tabular-nums",
1217
+ className: "text-muted-foreground/70 ml-1 text-xs tabular-nums",
820
1218
  children: row.count
821
1219
  })
822
1220
  ]