@rebasepro/admin 0.1.2 → 0.2.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 (243) hide show
  1. package/LICENSE +21 -6
  2. package/dist/{CollectionEditorDialog-ywdxhs1L.js → CollectionEditorDialog-CmGXXSY9.js} +42 -209
  3. package/dist/CollectionEditorDialog-CmGXXSY9.js.map +1 -0
  4. package/dist/{CollectionsStudioView-BDzMFzqH.js → CollectionsStudioView-DcLHT5bU.js} +6 -8
  5. package/dist/CollectionsStudioView-DcLHT5bU.js.map +1 -0
  6. package/dist/{ContentHomePage-0tHuEIm_.js → ContentHomePage-C7vFqKSe.js} +5 -7
  7. package/dist/ContentHomePage-C7vFqKSe.js.map +1 -0
  8. package/dist/{ExportCollectionAction-BIrq92To.js → ExportCollectionAction-BfN34eWX.js} +36 -38
  9. package/dist/ExportCollectionAction-BfN34eWX.js.map +1 -0
  10. package/dist/{ImportCollectionAction-h8yg_To8.js → ImportCollectionAction-SZrInjhx.js} +5 -7
  11. package/dist/ImportCollectionAction-SZrInjhx.js.map +1 -0
  12. package/dist/{PropertyEditView-BuZrNnBN.js → PropertyEditView-Cvryrb3B.js} +563 -489
  13. package/dist/PropertyEditView-Cvryrb3B.js.map +1 -0
  14. package/dist/{RolesView-CMPsaIXo.js → RolesView-BCb7qwWs.js} +22 -11
  15. package/dist/RolesView-BCb7qwWs.js.map +1 -0
  16. package/dist/{UsersView-BkeblMVT.js → UsersView-Cex24r8H.js} +7 -71
  17. package/dist/UsersView-Cex24r8H.js.map +1 -0
  18. package/dist/collection_editor/ui/collection_editor/LayoutModeSwitch.d.ts +2 -2
  19. package/dist/collection_editor/ui/collection_editor/properties/RelationPropertyField.d.ts +1 -7
  20. package/dist/collection_editor/ui/collection_editor/properties/VectorPropertyField.d.ts +3 -0
  21. package/dist/collection_editor_ui.js +5 -5
  22. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
  23. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +1 -1
  24. package/dist/components/EntityCollectionView/EntityCollectionListView.d.ts +18 -2
  25. package/dist/components/EntityCollectionView/FilterPresetsButton.d.ts +21 -0
  26. package/dist/components/EntityDetailView.d.ts +31 -0
  27. package/dist/components/EntityEditView.d.ts +3 -2
  28. package/dist/components/ReferenceTable/EntitySelectionTable.d.ts +1 -1
  29. package/dist/components/admin/CreationResultDialog.d.ts +5 -0
  30. package/dist/components/admin/RolesFilterSelect.d.ts +2 -0
  31. package/dist/components/admin/UserRolesSelectField.d.ts +2 -0
  32. package/dist/components/common/default_entity_actions.d.ts +7 -1
  33. package/dist/components/field_configs.d.ts +1 -1
  34. package/dist/components/index.d.ts +1 -0
  35. package/dist/data_import/utils/data.d.ts +1 -1
  36. package/dist/data_import/utils/file_headers.d.ts +6 -1
  37. package/dist/data_import/utils/file_to_json.d.ts +1 -11
  38. package/dist/data_import/utils/transforms.d.ts +11 -0
  39. package/dist/editor.js +2 -4
  40. package/dist/editor.js.map +1 -1
  41. package/dist/form/EntityForm.d.ts +1 -1
  42. package/dist/form/field_bindings/RelationFieldBinding.d.ts +1 -1
  43. package/dist/form/field_bindings/VectorFieldBinding.d.ts +11 -0
  44. package/dist/form/index.d.ts +1 -0
  45. package/dist/hooks/navigation/useResolvedViews.d.ts +2 -1
  46. package/dist/{index-eRJbMvHi.js → index-DjduZG1T.js} +3 -3
  47. package/dist/index-DjduZG1T.js.map +1 -0
  48. package/dist/{index-BuZaHcyc.js → index-MKPc70-v.js} +3 -3
  49. package/dist/index-MKPc70-v.js.map +1 -0
  50. package/dist/{index-CS6uJ7oW.js → index-PLIQXpTt.js} +4 -6
  51. package/dist/index-PLIQXpTt.js.map +1 -0
  52. package/dist/index.d.ts +4 -1
  53. package/dist/index.js +352 -148
  54. package/dist/index.js.map +1 -1
  55. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  56. package/dist/types/components/EntityFormProps.d.ts +2 -1
  57. package/dist/types/fields.d.ts +3 -3
  58. package/dist/util/navigation_utils.d.ts +1 -1
  59. package/dist/{util-zfU1zOCX.js → util-DbWax_sV.js} +5453 -2641
  60. package/dist/util-DbWax_sV.js.map +1 -0
  61. package/package.json +46 -39
  62. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  63. package/src/collection_editor/ui/AddKanbanColumnAction.tsx +12 -2
  64. package/src/collection_editor/ui/CollectionViewHeaderAction.tsx +1 -2
  65. package/src/collection_editor/ui/EditorCollectionAction.tsx +1 -2
  66. package/src/collection_editor/ui/EditorCollectionActionStart.tsx +1 -2
  67. package/src/collection_editor/ui/EditorEntityAction.tsx +1 -2
  68. package/src/collection_editor/ui/HomePageEditorCollectionAction.tsx +1 -2
  69. package/src/collection_editor/ui/NewCollectionButton.tsx +1 -2
  70. package/src/collection_editor/ui/NewCollectionCard.tsx +4 -6
  71. package/src/collection_editor/ui/PropertyAddColumnComponent.tsx +1 -2
  72. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +10 -2
  73. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +18 -2
  74. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +23 -17
  75. package/src/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.tsx +16 -2
  76. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +19 -9
  77. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +13 -2
  78. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +24 -2
  79. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +22 -3
  80. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +1 -2
  81. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +11 -2
  82. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +12 -2
  83. package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +16 -3
  84. package/src/collection_editor/ui/collection_editor/EnumForm.tsx +17 -2
  85. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +18 -2
  86. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +1 -2
  87. package/src/collection_editor/ui/collection_editor/KanbanConfigSection.tsx +1 -2
  88. package/src/collection_editor/ui/collection_editor/LayoutModeSwitch.tsx +17 -5
  89. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +32 -6
  90. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +7 -7
  91. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +14 -2
  92. package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +16 -2
  93. package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +9 -2
  94. package/src/collection_editor/ui/collection_editor/properties/BlockPropertyField.tsx +1 -2
  95. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -2
  96. package/src/collection_editor/ui/collection_editor/properties/MarkdownPropertyField.tsx +9 -2
  97. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +37 -57
  98. package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +11 -2
  99. package/src/collection_editor/ui/collection_editor/properties/VectorPropertyField.tsx +34 -0
  100. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +15 -7
  101. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +1 -2
  102. package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +15 -3
  103. package/src/collection_editor/ui/collection_editor/properties/conditions/property_paths.ts +1 -1
  104. package/src/collection_editor/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -2
  105. package/src/collection_editor/useLocalCollectionsConfigController.tsx +0 -2
  106. package/src/collection_editor/validateCollectionJson.ts +97 -10
  107. package/src/components/AdminModeSyncer.tsx +1 -1
  108. package/src/components/ArrayContainer.tsx +19 -15
  109. package/src/components/ClearFilterSortButton.tsx +1 -2
  110. package/src/components/CollectionEditorDialogs.tsx +1 -1
  111. package/src/components/DefaultAppBar.tsx +15 -3
  112. package/src/components/DefaultDrawer.tsx +3 -3
  113. package/src/components/DrawerNavigationGroup.tsx +1 -2
  114. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +14 -6
  115. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -1
  116. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
  117. package/src/components/EntityCollectionTable/fields/TableMultipleRelationField.tsx +1 -2
  118. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -2
  119. package/src/components/EntityCollectionTable/fields/TableRelationField.tsx +1 -2
  120. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +1 -2
  121. package/src/components/EntityCollectionTable/fields/VirtualTableSelect.tsx +0 -1
  122. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +15 -27
  123. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +1 -2
  124. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -2
  125. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -5
  126. package/src/components/EntityCollectionTable/table_bindings.tsx +51 -45
  127. package/src/components/EntityCollectionView/Board.tsx +1 -2
  128. package/src/components/EntityCollectionView/BoardColumn.tsx +9 -2
  129. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +5 -4
  130. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +18 -16
  131. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +16 -17
  132. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +87 -18
  133. package/src/components/EntityCollectionView/EntityCollectionView.tsx +20 -11
  134. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -7
  135. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +14 -5
  136. package/src/components/EntityCollectionView/FilterPresetsButton.tsx +292 -0
  137. package/src/components/EntityCollectionView/FiltersDialog.tsx +1 -2
  138. package/src/components/EntityCollectionView/SplitListView.tsx +76 -25
  139. package/src/components/EntityCollectionView/ViewModeToggle.tsx +20 -7
  140. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +1 -1
  141. package/src/components/EntityCollectionView/useBoardDataController.tsx +74 -6
  142. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +1 -1
  143. package/src/components/EntityDetailView.tsx +619 -0
  144. package/src/components/EntityEditView.tsx +29 -10
  145. package/src/components/EntityEditViewFormActions.tsx +20 -7
  146. package/src/components/EntityPreview.tsx +14 -5
  147. package/src/components/EntitySidePanel.tsx +116 -62
  148. package/src/components/EntityView.tsx +1 -2
  149. package/src/components/HomePage/ContentHomePage.tsx +1 -1
  150. package/src/components/HomePage/FavouritesView.tsx +1 -2
  151. package/src/components/HomePage/NavigationCard.tsx +1 -2
  152. package/src/components/HomePage/NavigationCardBinding.tsx +1 -2
  153. package/src/components/HomePage/NavigationGroup.tsx +1 -2
  154. package/src/components/HomePage/SmallNavigationCard.tsx +1 -2
  155. package/src/components/PropertyIdCopyTooltip.tsx +1 -2
  156. package/src/components/RebaseAuthGate.tsx +2 -2
  157. package/src/components/RebaseNavigation.tsx +9 -7
  158. package/src/components/ReferenceTable/EntitySelectionTable.tsx +12 -8
  159. package/src/components/RelationSelector.tsx +34 -6
  160. package/src/components/SearchIconsView.tsx +10 -2
  161. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  162. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
  163. package/src/components/SideDialogs.tsx +63 -38
  164. package/src/components/UserSelector.tsx +30 -6
  165. package/src/components/admin/CreationResultDialog.tsx +135 -0
  166. package/src/components/admin/RolesFilterSelect.tsx +45 -0
  167. package/src/components/admin/RolesView.tsx +53 -14
  168. package/src/components/admin/UserRolesSelectField.tsx +50 -0
  169. package/src/components/admin/UsersView.tsx +41 -124
  170. package/src/components/app/Scaffold.tsx +1 -2
  171. package/src/components/common/default_entity_actions.tsx +119 -12
  172. package/src/components/field_configs.tsx +39 -3
  173. package/src/components/history/EntityHistoryEntry.tsx +1 -2
  174. package/src/components/history/EntityHistoryView.tsx +1 -2
  175. package/src/components/index.ts +2 -0
  176. package/src/data_export/export/BasicExportAction.tsx +35 -38
  177. package/src/data_export/export/ExportCollectionAction.tsx +39 -40
  178. package/src/data_import/components/DataNewPropertiesMapping.tsx +15 -2
  179. package/src/data_import/components/ImportFileUpload.tsx +1 -2
  180. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -2
  181. package/src/data_import/import/ImportCollectionAction.tsx +21 -8
  182. package/src/data_import/utils/data.ts +23 -5
  183. package/src/data_import/utils/file_headers.ts +13 -89
  184. package/src/data_import/utils/file_to_json.ts +43 -68
  185. package/src/data_import/utils/transforms.ts +47 -0
  186. package/src/editor/components/SlashCommandMenu.tsx +17 -2
  187. package/src/editor/components/editor-bubble-item.tsx +1 -1
  188. package/src/editor/extensions/Image/index.ts +1 -1
  189. package/src/editor/extensions/Image.ts +1 -1
  190. package/src/editor/selectors/color-selector.tsx +1 -2
  191. package/src/editor/selectors/link-selector.tsx +1 -2
  192. package/src/editor/selectors/node-selector.tsx +16 -2
  193. package/src/editor/selectors/text-buttons.tsx +1 -2
  194. package/src/editor/utils/prosemirror-utils.ts +1 -1
  195. package/src/form/EntityForm.tsx +16 -6
  196. package/src/form/EntityFormActions.tsx +11 -3
  197. package/src/form/PropertyFieldBinding.tsx +5 -12
  198. package/src/form/components/FieldHelperText.tsx +1 -2
  199. package/src/form/components/LocalChangesMenu.tsx +17 -2
  200. package/src/form/components/StorageItemPreview.tsx +1 -2
  201. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -2
  202. package/src/form/field_bindings/KeyValueFieldBinding.tsx +17 -2
  203. package/src/form/field_bindings/MapFieldBinding.tsx +1 -1
  204. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +10 -3
  205. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -2
  206. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +1 -2
  207. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +7 -7
  208. package/src/form/field_bindings/RelationFieldBinding.tsx +150 -147
  209. package/src/form/field_bindings/RepeatFieldBinding.tsx +1 -1
  210. package/src/form/field_bindings/SelectFieldBinding.tsx +1 -2
  211. package/src/form/field_bindings/TextFieldBinding.tsx +10 -2
  212. package/src/form/field_bindings/VectorFieldBinding.tsx +202 -0
  213. package/src/form/index.tsx +1 -0
  214. package/src/form/validation.ts +54 -2
  215. package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -1
  216. package/src/hooks/navigation/useResolvedViews.tsx +30 -15
  217. package/src/hooks/navigation/useTopLevelNavigation.ts +1 -1
  218. package/src/index.ts +6 -0
  219. package/src/preview/PropertyPreview.tsx +1 -1
  220. package/src/preview/components/ImagePreview.tsx +1 -1
  221. package/src/preview/components/UrlComponentPreview.tsx +1 -2
  222. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +2 -2
  223. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +23 -24
  224. package/src/routes/RebaseRoute.tsx +64 -35
  225. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  226. package/src/types/components/EntityFormProps.tsx +3 -1
  227. package/src/types/fields.tsx +4 -3
  228. package/src/util/navigation_utils.ts +4 -3
  229. package/src/util/previews.ts +1 -1
  230. package/src/util/property_utils.tsx +22 -6
  231. package/src/util/resolutions.ts +2 -2
  232. package/dist/CollectionEditorDialog-ywdxhs1L.js.map +0 -1
  233. package/dist/CollectionsStudioView-BDzMFzqH.js.map +0 -1
  234. package/dist/ContentHomePage-0tHuEIm_.js.map +0 -1
  235. package/dist/ExportCollectionAction-BIrq92To.js.map +0 -1
  236. package/dist/ImportCollectionAction-h8yg_To8.js.map +0 -1
  237. package/dist/PropertyEditView-BuZrNnBN.js.map +0 -1
  238. package/dist/RolesView-CMPsaIXo.js.map +0 -1
  239. package/dist/UsersView-BkeblMVT.js.map +0 -1
  240. package/dist/index-BuZaHcyc.js.map +0 -1
  241. package/dist/index-CS6uJ7oW.js.map +0 -1
  242. package/dist/index-eRJbMvHi.js.map +0 -1
  243. package/dist/util-zfU1zOCX.js.map +0 -1
@@ -0,0 +1,292 @@
1
+ import React, { useCallback, useMemo } from "react";
2
+ import { CheckIcon, ChevronsUpDownIcon, cls, FilterChip, Menu, MenuItem, Tooltip } from "@rebasepro/ui";
3
+ import type { EntityTableController, FilterValues, FilterPreset } from "@rebasepro/types";
4
+
5
+ export interface FilterPresetsButtonProps<M extends Record<string, unknown>> {
6
+ filterPresets: FilterPreset<Extract<keyof M, string> | (string & {})>[];
7
+ tableController: EntityTableController<M>;
8
+ compact?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Maximum number of presets shown as inline toggle chips before the
13
+ * rest are collapsed into an overflow menu.
14
+ */
15
+ const MAX_VISIBLE_PRESETS = 4;
16
+
17
+ /** Max characters for a chip label before truncation. */
18
+ const MAX_LABEL_LENGTH = 22;
19
+
20
+ // ─── Helpers ────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Deep equality without JSON.stringify.
24
+ * Handles primitives, arrays, Dates, and plain objects recursively.
25
+ */
26
+ function deepEqual(a: unknown, b: unknown): boolean {
27
+ if (a === b) return true;
28
+ if (a == null || b == null) return false;
29
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
30
+ if (Array.isArray(a) && Array.isArray(b)) {
31
+ if (a.length !== b.length) return false;
32
+ return a.every((v, i) => deepEqual(v, b[i]));
33
+ }
34
+ if (typeof a === "object" && typeof b === "object") {
35
+ const aKeys = Object.keys(a as Record<string, unknown>);
36
+ const bKeys = Object.keys(b as Record<string, unknown>);
37
+ if (aKeys.length !== bKeys.length) return false;
38
+ return aKeys.every(k =>
39
+ deepEqual(
40
+ (a as Record<string, unknown>)[k],
41
+ (b as Record<string, unknown>)[k]
42
+ )
43
+ );
44
+ }
45
+ return false;
46
+ }
47
+
48
+ /**
49
+ * Check if a preset's filters are a subset of the controller's
50
+ * current filter values (key-by-key deep comparison on the tuples).
51
+ */
52
+ function isPresetActive(
53
+ preset: FilterPreset<string>,
54
+ controllerFilters: Record<string, unknown> | undefined
55
+ ): boolean {
56
+ if (!controllerFilters) return false;
57
+ const entries = Object.entries(preset.filterValues);
58
+ if (entries.length === 0) return false;
59
+ for (const [key, presetTuple] of entries) {
60
+ const controllerTuple = controllerFilters[key];
61
+ if (!controllerTuple || !presetTuple) return false;
62
+ if (!deepEqual(presetTuple, controllerTuple)) return false;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Generate a human-readable label from filter keys when no explicit label is provided.
69
+ */
70
+ function generateLabel(filterValues: FilterValues<string>): string {
71
+ const keys = Object.keys(filterValues);
72
+ if (keys.length === 0) return "Filter";
73
+ if (keys.length === 1) return keys[0].replace(/_/g, " ");
74
+ if (keys.length === 2) return keys.map(k => k.replace(/_/g, " ")).join(" + ");
75
+ return `${keys[0].replace(/_/g, " ")} +${keys.length - 1}`;
76
+ }
77
+
78
+ /**
79
+ * Truncate a label if it exceeds MAX_LABEL_LENGTH.
80
+ */
81
+ function truncateLabel(label: string): { display: string; truncated: boolean } {
82
+ if (label.length <= MAX_LABEL_LENGTH) return { display: label, truncated: false };
83
+ return { display: label.slice(0, MAX_LABEL_LENGTH - 1) + "…", truncated: true };
84
+ }
85
+
86
+ // ─── Overflow Menu ──────────────────────────────────────────────────
87
+
88
+ interface OverflowMenuProps {
89
+ presets: { preset: FilterPreset<string>; originalIndex: number }[];
90
+ activeSet: Set<number>;
91
+ onToggle: (index: number) => void;
92
+ }
93
+
94
+ function OverflowMenu({ presets, activeSet, onToggle }: OverflowMenuProps) {
95
+ const activeCount = presets.filter(p => activeSet.has(p.originalIndex)).length;
96
+
97
+ const trigger = (
98
+ <FilterChip
99
+ active={activeCount > 0}
100
+ icon={<ChevronsUpDownIcon size={12} />}
101
+ >
102
+ +{presets.length}
103
+ </FilterChip>
104
+ );
105
+
106
+ return (
107
+ <Menu trigger={trigger} side="bottom" align="start">
108
+ <div className="min-w-[180px] max-w-[300px] max-h-[320px] overflow-y-auto">
109
+ {presets.map(({ preset, originalIndex }) => {
110
+ const rawLabel = preset.label ?? generateLabel(preset.filterValues as FilterValues<string>);
111
+ const active = activeSet.has(originalIndex);
112
+
113
+ return (
114
+ <MenuItem
115
+ key={originalIndex}
116
+ dense
117
+ onClick={() => onToggle(originalIndex)}
118
+ className={cls(active && "bg-primary/10 dark:bg-primary/20")}
119
+ >
120
+ <span className="flex items-center gap-2 w-full min-w-0">
121
+ {active && <CheckIcon size={14} className="text-primary shrink-0" />}
122
+ <span className={cls("truncate", active ? "text-primary font-medium" : "")}>
123
+ {rawLabel}
124
+ </span>
125
+ </span>
126
+ </MenuItem>
127
+ );
128
+ })}
129
+ </div>
130
+ </Menu>
131
+ );
132
+ }
133
+
134
+ // ─── Main Component ─────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Filter Presets — displayed as inline toggle chips in the collection toolbar.
138
+ *
139
+ * Active state is **derived** from the controller's actual filter values,
140
+ * not tracked locally. A chip is "active" when ALL of its filter entries
141
+ * match the current controller state (deep value comparison, no JSON.stringify).
142
+ *
143
+ * This means:
144
+ * - Toggling a preset on adds its filters to the controller.
145
+ * - Toggling it off removes its filter keys.
146
+ * - Changing filters via the dialog automatically deactivates unmatched chips.
147
+ * - Clearing all filters deactivates all chips.
148
+ * - Multiple presets can be active simultaneously.
149
+ */
150
+ export function FilterPresetsButton<M extends Record<string, unknown>>({
151
+ filterPresets,
152
+ tableController,
153
+ compact
154
+ }: FilterPresetsButtonProps<M>) {
155
+
156
+ // ── Derive active state from controller ─────────────────────────
157
+ const activeSet = useMemo(() => {
158
+ const set = new Set<number>();
159
+ if (!filterPresets.length || !tableController.setFilterValues) return set;
160
+ for (let i = 0; i < filterPresets.length; i++) {
161
+ if (isPresetActive(
162
+ filterPresets[i] as FilterPreset<string>,
163
+ tableController.filterValues as Record<string, unknown> | undefined
164
+ )) {
165
+ set.add(i);
166
+ }
167
+ }
168
+ return set;
169
+ }, [filterPresets, tableController.filterValues, tableController.setFilterValues]);
170
+
171
+ // ── Toggle handler ──────────────────────────────────────────────
172
+ const handleToggle = useCallback((index: number) => {
173
+ const preset = filterPresets[index] as FilterPreset<string>;
174
+ const currentFilters = { ...(tableController.filterValues ?? {}) } as Record<string, [string, unknown]>;
175
+ const wasActive = isPresetActive(preset, currentFilters);
176
+
177
+ if (wasActive) {
178
+ // Toggle OFF: remove this preset's filter keys
179
+ for (const key of Object.keys(preset.filterValues)) {
180
+ delete currentFilters[key];
181
+ }
182
+ // Re-apply overlapping keys from other still-active presets
183
+ for (let i = 0; i < filterPresets.length; i++) {
184
+ if (i === index) continue;
185
+ const other = filterPresets[i] as FilterPreset<string>;
186
+ if (isPresetActive(other, currentFilters)) {
187
+ Object.assign(currentFilters, other.filterValues);
188
+ }
189
+ }
190
+ } else {
191
+ // Toggle ON: merge this preset's filters on top
192
+ Object.assign(currentFilters, preset.filterValues);
193
+ }
194
+
195
+ // Apply
196
+ if (Object.keys(currentFilters).length === 0) {
197
+ if (tableController.clearFilter) {
198
+ tableController.clearFilter();
199
+ } else {
200
+ tableController.setFilterValues?.(
201
+ {} as FilterValues<Extract<keyof M, string> | (string & {})>
202
+ );
203
+ }
204
+ tableController.setSortBy?.(undefined);
205
+ } else {
206
+ tableController.setFilterValues?.(
207
+ currentFilters as FilterValues<Extract<keyof M, string> | (string & {})>
208
+ );
209
+
210
+ // Resolve sort: use the toggled preset's sort if toggling on,
211
+ // otherwise find sort from remaining active presets
212
+ if (!wasActive && preset.sort) {
213
+ tableController.setSortBy?.(
214
+ preset.sort as [Extract<keyof M, string> | (string & {}), "asc" | "desc"]
215
+ );
216
+ } else if (wasActive) {
217
+ let remainingSort: [string, "asc" | "desc"] | undefined;
218
+ for (let i = 0; i < filterPresets.length; i++) {
219
+ if (i === index) continue;
220
+ const other = filterPresets[i] as FilterPreset<string>;
221
+ if (isPresetActive(other, currentFilters) && other.sort) {
222
+ remainingSort = other.sort;
223
+ }
224
+ }
225
+ tableController.setSortBy?.(
226
+ remainingSort as [Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined
227
+ );
228
+ }
229
+ }
230
+ }, [filterPresets, tableController]);
231
+
232
+ // ── Guard (after hooks to preserve Rules of Hooks) ──────────────
233
+ if (!filterPresets.length || !tableController.setFilterValues) return null;
234
+
235
+ // ── Split visible vs overflow ───────────────────────────────────
236
+ const maxVisible = compact ? 2 : MAX_VISIBLE_PRESETS;
237
+ const needsOverflow = filterPresets.length > maxVisible;
238
+ const visibleCount = needsOverflow ? maxVisible - 1 : filterPresets.length;
239
+
240
+ const visiblePresets = filterPresets.slice(0, visibleCount);
241
+ const overflowPresets = needsOverflow
242
+ ? filterPresets.slice(visibleCount).map((preset, i) => ({
243
+ preset: preset as FilterPreset<string>,
244
+ originalIndex: visibleCount + i
245
+ }))
246
+ : [];
247
+
248
+ const hasOverflow = overflowPresets.length > 0;
249
+
250
+ return (
251
+ <div className="flex items-center gap-1 min-w-0 overflow-hidden">
252
+ {/* Scrollable chip area — on narrow screens chips scroll,
253
+ on desktop there's enough space so no scroll occurs.
254
+ py-0.5 gives the inset shadow breathing room inside the clip area. */}
255
+ <div
256
+ className="flex items-center gap-1 min-w-0 overflow-x-auto py-0.5"
257
+ style={{ scrollbarWidth: "none" }}
258
+ >
259
+ {visiblePresets.map((preset, index) => {
260
+ const rawLabel = preset.label ?? generateLabel(preset.filterValues as FilterValues<string>);
261
+ const { display, truncated } = truncateLabel(rawLabel);
262
+ const active = activeSet.has(index);
263
+
264
+ const chip = (
265
+ <FilterChip
266
+ key={index}
267
+ active={active}
268
+ onClick={() => handleToggle(index)}
269
+ size="small"
270
+ >
271
+ {display}
272
+ </FilterChip>
273
+ );
274
+
275
+ if (truncated) {
276
+ return <Tooltip key={index} title={rawLabel}>{chip}</Tooltip>;
277
+ }
278
+ return chip;
279
+ })}
280
+ </div>
281
+
282
+ {/* Overflow button — always pinned outside the scroll area */}
283
+ {hasOverflow && (
284
+ <OverflowMenu
285
+ presets={overflowPresets}
286
+ activeSet={activeSet}
287
+ onToggle={handleToggle}
288
+ />
289
+ )}
290
+ </div>
291
+ );
292
+ }
@@ -3,12 +3,11 @@ import type { Property } from "@rebasepro/types";
3
3
  import React, { useCallback, useMemo, useState } from "react";
4
4
  import { FilterValues, WhereFilterOp } from "@rebasepro/types";
5
5
  import { Button, cls, defaultBorderMixin, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@rebasepro/ui";
6
- import { FilterIcon } from "lucide-react";
6
+ import { FilterIcon, VirtualTableWhereFilterOp } from "@rebasepro/ui";
7
7
  import { StringNumberFilterField } from "../SelectableTable/filters/StringNumberFilterField";
8
8
  import { BooleanFilterField } from "../SelectableTable/filters/BooleanFilterField";
9
9
  import { DateTimeFilterField } from "../SelectableTable/filters/DateTimeFilterField";
10
10
  import { ReferenceFilterField } from "../SelectableTable/filters/ReferenceFilterField";
11
- import { VirtualTableWhereFilterOp } from "@rebasepro/ui";
12
11
  import { enumToObjectEntries } from "@rebasepro/common";
13
12
  import { useTranslation } from "@rebasepro/core";
14
13
 
@@ -2,6 +2,7 @@ import type { EntityCollection } from "@rebasepro/types";
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { CollectionSize, Entity, EntityTableController, SelectionController } from "@rebasepro/types";
4
4
  import { EntityEditView } from "../EntityEditView";
5
+ import { EntityDetailView } from "../EntityDetailView";
5
6
  import {
6
7
  cls,
7
8
  defaultBorderMixin,
@@ -9,7 +10,7 @@ import {
9
10
  } from "@rebasepro/ui";
10
11
  import { useLargeLayout } from "@rebasepro/core";
11
12
  import { useCollectionRegistryController } from "../../index";
12
- import { useNavigate } from "react-router-dom";
13
+ import { useNavigate, useLocation } from "react-router-dom";
13
14
  import { useUrlController } from "../../index";
14
15
  import { ErrorBoundary } from "@rebasepro/ui";
15
16
 
@@ -107,6 +108,8 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
107
108
  }: SplitListViewProps<M>) {
108
109
  const largeLayout = useLargeLayout();
109
110
  const collectionRegistryController = useCollectionRegistryController();
111
+ const location = useLocation();
112
+ const isEditMode = location.pathname.endsWith("/edit") || location.pathname.split("/").pop() === "edit";
110
113
  const navigate = useNavigate();
111
114
  const urlController = useUrlController();
112
115
 
@@ -288,30 +291,78 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
288
291
  style={{ transitionDuration: `${TRANSITION_DURATION}ms` }}
289
292
  >
290
293
  <ErrorBoundary>
291
- <EntityEditView
292
- key={String(renderedEntityId)}
293
- path={path}
294
- collection={collection as EntityCollection<Record<string, unknown>>}
295
- entityId={renderedEntityId}
296
- parentCollectionSlugs={usedParentCollectionIds}
297
- parentEntityIds={usedParentEntityIds}
298
- selectedTab={selectedTab}
299
- layout="split"
300
- onTabChange={(params) => {
301
- const newSelectedTab = params.selectedTab;
302
- let entityUrl = urlController.buildUrlCollectionPath(
303
- newSelectedTab
304
- ? `${path}/${renderedEntityId}/${newSelectedTab}`
305
- : `${path}/${renderedEntityId}`
306
- );
307
- // Preserve the __view query param
308
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
309
- if (currentViewParam) {
310
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
311
- }
312
- navigate(entityUrl);
313
- }}
314
- />
294
+ {collection.defaultEntityAction === "view" && !isEditMode
295
+ ? <EntityDetailView
296
+ key={String(renderedEntityId)}
297
+ path={path}
298
+ collection={collection as EntityCollection<Record<string, unknown>>}
299
+ entityId={renderedEntityId}
300
+ parentCollectionSlugs={usedParentCollectionIds}
301
+ parentEntityIds={usedParentEntityIds}
302
+ selectedTab={selectedTab}
303
+ layout="split"
304
+ onEditClick={() => {
305
+ let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}/edit`);
306
+ const currentViewParam = new URLSearchParams(window.location.search).get("__view");
307
+ if (currentViewParam) {
308
+ entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
309
+ }
310
+ navigate(entityUrl);
311
+ }}
312
+ onTabChange={(params) => {
313
+ const newSelectedTab = params.selectedTab;
314
+ let entityUrl = urlController.buildUrlCollectionPath(
315
+ newSelectedTab
316
+ ? `${path}/${renderedEntityId}/${newSelectedTab}`
317
+ : `${path}/${renderedEntityId}`
318
+ );
319
+ const currentViewParam = new URLSearchParams(window.location.search).get("__view");
320
+ if (currentViewParam) {
321
+ entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
322
+ }
323
+ navigate(entityUrl);
324
+ }}
325
+ />
326
+ : <EntityEditView
327
+ key={String(renderedEntityId)}
328
+ path={path}
329
+ collection={collection as EntityCollection<Record<string, unknown>>}
330
+ entityId={renderedEntityId}
331
+ parentCollectionSlugs={usedParentCollectionIds}
332
+ parentEntityIds={usedParentEntityIds}
333
+ selectedTab={selectedTab}
334
+ layout="split"
335
+ onSaved={(params) => {
336
+ let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}`);
337
+ const currentViewParam = new URLSearchParams(window.location.search).get("__view");
338
+ if (currentViewParam) {
339
+ entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
340
+ }
341
+ navigate(entityUrl, { replace: true });
342
+ }}
343
+ navigateBack={() => {
344
+ let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}`);
345
+ const currentViewParam = new URLSearchParams(window.location.search).get("__view");
346
+ if (currentViewParam) {
347
+ entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
348
+ }
349
+ navigate(entityUrl);
350
+ }}
351
+ onTabChange={(params) => {
352
+ const newSelectedTab = params.selectedTab;
353
+ let entityUrl = urlController.buildUrlCollectionPath(
354
+ newSelectedTab
355
+ ? `${path}/${renderedEntityId}/${newSelectedTab}`
356
+ : `${path}/${renderedEntityId}`
357
+ );
358
+ const currentViewParam = new URLSearchParams(window.location.search).get("__view");
359
+ if (currentViewParam) {
360
+ entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
361
+ }
362
+ navigate(entityUrl);
363
+ }}
364
+ />
365
+ }
315
366
  </ErrorBoundary>
316
367
  </div>
317
368
  ) : <></>;
@@ -1,8 +1,20 @@
1
1
 
2
2
  import React, { useMemo } from "react";
3
3
  import { CollectionSize, ViewMode } from "@rebasepro/types";
4
- import { Button, Popover, Select, SelectItem, ToggleButtonGroup, ToggleButtonOption , iconSize } from "@rebasepro/ui";
5
- import { LayoutGridIcon, ListIcon, TableIcon, ColumnsIcon, KanbanIcon } from "lucide-react";
4
+ import {
5
+ Button,
6
+ ColumnsIcon,
7
+ iconSize,
8
+ KanbanIcon,
9
+ LayoutGridIcon,
10
+ ListIcon,
11
+ Popover,
12
+ Select,
13
+ SelectItem,
14
+ TableIcon,
15
+ ToggleButtonGroup,
16
+ ToggleButtonOption
17
+ } from "@rebasepro/ui";
6
18
  import { useTranslation } from "@rebasepro/core";
7
19
 
8
20
  export type KanbanPropertyOption = {
@@ -66,9 +78,7 @@ export function ViewModeToggle({
66
78
 
67
79
  const { t } = useTranslation();
68
80
 
69
- if (!onViewModeChange) {
70
- return null;
71
- }
81
+
72
82
 
73
83
  // Get icon for current view mode
74
84
  const getViewModeIcon = () => {
@@ -117,9 +127,12 @@ export function ViewModeToggle({
117
127
  ];
118
128
 
119
129
  return allOptions;
120
- }, []);
121
-
130
+ }, [t]);
122
131
 
132
+ // ── Guard (after hooks to preserve Rules of Hooks) ──────────────
133
+ if (!onViewModeChange) {
134
+ return null;
135
+ }
123
136
 
124
137
  return (
125
138
  <div className="overflow-visible">
@@ -70,7 +70,7 @@ export function useKanbanDragAndDrop<M extends Record<string, unknown>>({
70
70
  : null;
71
71
 
72
72
  try {
73
- let a = prevKey;
73
+ const a = prevKey;
74
74
  let b = nextKey;
75
75
  if (a !== null && b !== null && a >= b) {
76
76
  // Handle duplicate or out-of-order keys to prevent fractional-indexing crash
@@ -5,6 +5,26 @@ import { useData, useRebaseContext } from "@rebasepro/core";
5
5
 
6
6
  const DEFAULT_PAGE_SIZE = 20;
7
7
 
8
+ /**
9
+ * Shallow equality for entity value records.
10
+ * Handles primitives and reference equality for nested values.
11
+ * Avoids JSON.stringify allocation on every comparison.
12
+ */
13
+ function shallowEqualValues(
14
+ a: Record<string, unknown> | undefined,
15
+ b: Record<string, unknown> | undefined
16
+ ): boolean {
17
+ if (a === b) return true;
18
+ if (!a || !b) return false;
19
+ const keysA = Object.keys(a);
20
+ const keysB = Object.keys(b);
21
+ if (keysA.length !== keysB.length) return false;
22
+ for (const key of keysA) {
23
+ if (a[key] !== b[key]) return false;
24
+ }
25
+ return true;
26
+ }
27
+
8
28
  /**
9
29
  * Data state for a single board column
10
30
  */
@@ -159,6 +179,34 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
159
179
  };
160
180
  }, []);
161
181
 
182
+ /**
183
+ * Check if a column value is excluded by the active filter on the columnProperty.
184
+ * E.g. if the user sets "Status IN (Open, In Progress)", this returns true
185
+ * for "Waiting on Customer" since that column should show as empty.
186
+ */
187
+ const isColumnExcludedByFilter = useCallback((column: string, filterValues?: FilterValues<string>, colProperty?: string): boolean => {
188
+ if (!filterValues || !colProperty) return false;
189
+ const filterForColumn = filterValues[colProperty as keyof typeof filterValues];
190
+ if (!filterForColumn || !Array.isArray(filterForColumn)) return false;
191
+
192
+ const [op, val] = filterForColumn as [string, unknown];
193
+
194
+ if (op === "==" && typeof val === "string") {
195
+ return column !== val;
196
+ }
197
+ if (op === "!=" && typeof val === "string") {
198
+ return column === val;
199
+ }
200
+ if (op === "in" && Array.isArray(val)) {
201
+ return !val.map(String).includes(column);
202
+ }
203
+ if (op === "not-in" && Array.isArray(val)) {
204
+ return val.map(String).includes(column);
205
+ }
206
+
207
+ return false;
208
+ }, []);
209
+
162
210
  // Helper function to subscribe to a single column - uses refs to avoid dependency issues
163
211
  const subscribeToColumn = useCallback((column: string, itemCount: number) => {
164
212
  // Skip if we're in the middle of cleanup
@@ -173,6 +221,23 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
173
221
  const currentSearchString = searchStringRef.current;
174
222
  const currentResolvedPath = resolvedPathRef.current;
175
223
 
224
+ // If the column is excluded by the active filter on the column property,
225
+ // set it as empty immediately without querying.
226
+ const excluded = isColumnExcludedByFilter(column, currentFilterValues, currentColumnProperty);
227
+ if (excluded) {
228
+ setColumnData(prev => ({
229
+ ...prev,
230
+ [column]: {
231
+ entities: [],
232
+ loading: false,
233
+ hasMore: false,
234
+ error: undefined,
235
+ totalCount: 0
236
+ }
237
+ }));
238
+ return;
239
+ }
240
+
176
241
  // Build where map for this column
177
242
  const whereMap: Record<string, string> = {};
178
243
  if (currentFilterValues) {
@@ -293,8 +358,6 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
293
358
 
294
359
  const newHasMore = entities.length >= itemCount;
295
360
 
296
- console.log(`[useBoardDataController] Listener update for col ${column}. Length: ${processed.length}. Entities:`, processed.map(e => e.id));
297
-
298
361
  // Compare with current state — skip update if identical to avoid UI flash
299
362
  setColumnData(prev => {
300
363
  const existing = prev[column];
@@ -308,9 +371,9 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
308
371
  identical = false;
309
372
  break;
310
373
  }
311
- // Deep-compare values by JSON serialization
312
- // This covers order key, column assignment, and all other fields
313
- if (JSON.stringify(a.values) !== JSON.stringify(b.values)) {
374
+ // Shallow-compare values to avoid JSON.stringify allocations
375
+ // entity.values are flat records from the DB, so shallow suffices
376
+ if (!shallowEqualValues(a.values, b.values)) {
314
377
  identical = false;
315
378
  break;
316
379
  }
@@ -404,6 +467,12 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
404
467
  const itemCount = currentColumnItemCounts[column] ?? pageSize;
405
468
  subscribeToColumn(column, itemCount);
406
469
 
470
+ // Skip count query for columns excluded by the filter — subscribeToColumn
471
+ // already set them to totalCount: 0.
472
+ if (isColumnExcludedByFilter(column, currentFilterValues, currentColumnProperty)) {
473
+ return;
474
+ }
475
+
407
476
  // Count query for column (for display in column header)
408
477
  const accessor = currentDataClient.collection(currentResolvedPath);
409
478
  if (accessor.count) {
@@ -602,7 +671,6 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
602
671
  };
603
672
  }
604
673
 
605
- console.log(`[useBoardDataController] moveItemOptimistically: ${itemId} from ${sourceColumn} (${updated[sourceColumn].entities.length}) to ${targetColumn} (${updated[targetColumn].entities.length})`);
606
674
  } else if (sourceColumn !== targetColumn) {
607
675
  // If item not found locally but counts need update
608
676
  if (updated[sourceColumn]?.totalCount !== undefined) {
@@ -352,7 +352,7 @@ id });
352
352
  // cardinality:"one" → single EntityRelation
353
353
  const isRelation = "__type" in val && (val as Record<string, unknown>).__type === "relation";
354
354
  if (isRelation) {
355
- const relation = val as unknown as EntityRelation;
355
+ const relation = val as EntityRelation;
356
356
  const displayName = resolveRelationDisplayName(relation, prop);
357
357
  totalCount = 1;
358
358
  if (displayName) {