@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.
- package/LICENSE +21 -6
- package/dist/{CollectionEditorDialog-ywdxhs1L.js → CollectionEditorDialog-CmGXXSY9.js} +42 -209
- package/dist/CollectionEditorDialog-CmGXXSY9.js.map +1 -0
- package/dist/{CollectionsStudioView-BDzMFzqH.js → CollectionsStudioView-DcLHT5bU.js} +6 -8
- package/dist/CollectionsStudioView-DcLHT5bU.js.map +1 -0
- package/dist/{ContentHomePage-0tHuEIm_.js → ContentHomePage-C7vFqKSe.js} +5 -7
- package/dist/ContentHomePage-C7vFqKSe.js.map +1 -0
- package/dist/{ExportCollectionAction-BIrq92To.js → ExportCollectionAction-BfN34eWX.js} +36 -38
- package/dist/ExportCollectionAction-BfN34eWX.js.map +1 -0
- package/dist/{ImportCollectionAction-h8yg_To8.js → ImportCollectionAction-SZrInjhx.js} +5 -7
- package/dist/ImportCollectionAction-SZrInjhx.js.map +1 -0
- package/dist/{PropertyEditView-BuZrNnBN.js → PropertyEditView-Cvryrb3B.js} +563 -489
- package/dist/PropertyEditView-Cvryrb3B.js.map +1 -0
- package/dist/{RolesView-CMPsaIXo.js → RolesView-BCb7qwWs.js} +22 -11
- package/dist/RolesView-BCb7qwWs.js.map +1 -0
- package/dist/{UsersView-BkeblMVT.js → UsersView-Cex24r8H.js} +7 -71
- package/dist/UsersView-Cex24r8H.js.map +1 -0
- package/dist/collection_editor/ui/collection_editor/LayoutModeSwitch.d.ts +2 -2
- package/dist/collection_editor/ui/collection_editor/properties/RelationPropertyField.d.ts +1 -7
- package/dist/collection_editor/ui/collection_editor/properties/VectorPropertyField.d.ts +3 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +1 -1
- package/dist/components/EntityCollectionView/EntityCollectionListView.d.ts +18 -2
- package/dist/components/EntityCollectionView/FilterPresetsButton.d.ts +21 -0
- package/dist/components/EntityDetailView.d.ts +31 -0
- package/dist/components/EntityEditView.d.ts +3 -2
- package/dist/components/ReferenceTable/EntitySelectionTable.d.ts +1 -1
- package/dist/components/admin/CreationResultDialog.d.ts +5 -0
- package/dist/components/admin/RolesFilterSelect.d.ts +2 -0
- package/dist/components/admin/UserRolesSelectField.d.ts +2 -0
- package/dist/components/common/default_entity_actions.d.ts +7 -1
- package/dist/components/field_configs.d.ts +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/data_import/utils/data.d.ts +1 -1
- package/dist/data_import/utils/file_headers.d.ts +6 -1
- package/dist/data_import/utils/file_to_json.d.ts +1 -11
- package/dist/data_import/utils/transforms.d.ts +11 -0
- package/dist/editor.js +2 -4
- package/dist/editor.js.map +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/field_bindings/RelationFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +11 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/navigation/useResolvedViews.d.ts +2 -1
- package/dist/{index-eRJbMvHi.js → index-DjduZG1T.js} +3 -3
- package/dist/index-DjduZG1T.js.map +1 -0
- package/dist/{index-BuZaHcyc.js → index-MKPc70-v.js} +3 -3
- package/dist/index-MKPc70-v.js.map +1 -0
- package/dist/{index-CS6uJ7oW.js → index-PLIQXpTt.js} +4 -6
- package/dist/index-PLIQXpTt.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +352 -148
- package/dist/index.js.map +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
- package/dist/types/components/EntityFormProps.d.ts +2 -1
- package/dist/types/fields.d.ts +3 -3
- package/dist/util/navigation_utils.d.ts +1 -1
- package/dist/{util-zfU1zOCX.js → util-DbWax_sV.js} +5453 -2641
- package/dist/util-DbWax_sV.js.map +1 -0
- package/package.json +46 -39
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/ui/AddKanbanColumnAction.tsx +12 -2
- package/src/collection_editor/ui/CollectionViewHeaderAction.tsx +1 -2
- package/src/collection_editor/ui/EditorCollectionAction.tsx +1 -2
- package/src/collection_editor/ui/EditorCollectionActionStart.tsx +1 -2
- package/src/collection_editor/ui/EditorEntityAction.tsx +1 -2
- package/src/collection_editor/ui/HomePageEditorCollectionAction.tsx +1 -2
- package/src/collection_editor/ui/NewCollectionButton.tsx +1 -2
- package/src/collection_editor/ui/NewCollectionCard.tsx +4 -6
- package/src/collection_editor/ui/PropertyAddColumnComponent.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +10 -2
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +18 -2
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +23 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.tsx +16 -2
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +19 -9
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +13 -2
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +24 -2
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +22 -3
- package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +11 -2
- package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +12 -2
- package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +16 -3
- package/src/collection_editor/ui/collection_editor/EnumForm.tsx +17 -2
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +18 -2
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/KanbanConfigSection.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/LayoutModeSwitch.tsx +17 -5
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +32 -6
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +7 -7
- package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +14 -2
- package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +16 -2
- package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +9 -2
- package/src/collection_editor/ui/collection_editor/properties/BlockPropertyField.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/properties/MarkdownPropertyField.tsx +9 -2
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +37 -57
- package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +11 -2
- package/src/collection_editor/ui/collection_editor/properties/VectorPropertyField.tsx +34 -0
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +15 -7
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +1 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +15 -3
- package/src/collection_editor/ui/collection_editor/properties/conditions/property_paths.ts +1 -1
- package/src/collection_editor/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -2
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +0 -2
- package/src/collection_editor/validateCollectionJson.ts +97 -10
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/ArrayContainer.tsx +19 -15
- package/src/components/ClearFilterSortButton.tsx +1 -2
- package/src/components/CollectionEditorDialogs.tsx +1 -1
- package/src/components/DefaultAppBar.tsx +15 -3
- package/src/components/DefaultDrawer.tsx +3 -3
- package/src/components/DrawerNavigationGroup.tsx +1 -2
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +14 -6
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -1
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
- package/src/components/EntityCollectionTable/fields/TableMultipleRelationField.tsx +1 -2
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -2
- package/src/components/EntityCollectionTable/fields/TableRelationField.tsx +1 -2
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +1 -2
- package/src/components/EntityCollectionTable/fields/VirtualTableSelect.tsx +0 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +15 -27
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +1 -2
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -5
- package/src/components/EntityCollectionTable/table_bindings.tsx +51 -45
- package/src/components/EntityCollectionView/Board.tsx +1 -2
- package/src/components/EntityCollectionView/BoardColumn.tsx +9 -2
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +5 -4
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +18 -16
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +16 -17
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +87 -18
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +20 -11
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -7
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +14 -5
- package/src/components/EntityCollectionView/FilterPresetsButton.tsx +292 -0
- package/src/components/EntityCollectionView/FiltersDialog.tsx +1 -2
- package/src/components/EntityCollectionView/SplitListView.tsx +76 -25
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +20 -7
- package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +1 -1
- package/src/components/EntityCollectionView/useBoardDataController.tsx +74 -6
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +1 -1
- package/src/components/EntityDetailView.tsx +619 -0
- package/src/components/EntityEditView.tsx +29 -10
- package/src/components/EntityEditViewFormActions.tsx +20 -7
- package/src/components/EntityPreview.tsx +14 -5
- package/src/components/EntitySidePanel.tsx +116 -62
- package/src/components/EntityView.tsx +1 -2
- package/src/components/HomePage/ContentHomePage.tsx +1 -1
- package/src/components/HomePage/FavouritesView.tsx +1 -2
- package/src/components/HomePage/NavigationCard.tsx +1 -2
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -2
- package/src/components/HomePage/NavigationGroup.tsx +1 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -2
- package/src/components/PropertyIdCopyTooltip.tsx +1 -2
- package/src/components/RebaseAuthGate.tsx +2 -2
- package/src/components/RebaseNavigation.tsx +9 -7
- package/src/components/ReferenceTable/EntitySelectionTable.tsx +12 -8
- package/src/components/RelationSelector.tsx +34 -6
- package/src/components/SearchIconsView.tsx +10 -2
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
- package/src/components/SideDialogs.tsx +63 -38
- package/src/components/UserSelector.tsx +30 -6
- package/src/components/admin/CreationResultDialog.tsx +135 -0
- package/src/components/admin/RolesFilterSelect.tsx +45 -0
- package/src/components/admin/RolesView.tsx +53 -14
- package/src/components/admin/UserRolesSelectField.tsx +50 -0
- package/src/components/admin/UsersView.tsx +41 -124
- package/src/components/app/Scaffold.tsx +1 -2
- package/src/components/common/default_entity_actions.tsx +119 -12
- package/src/components/field_configs.tsx +39 -3
- package/src/components/history/EntityHistoryEntry.tsx +1 -2
- package/src/components/history/EntityHistoryView.tsx +1 -2
- package/src/components/index.ts +2 -0
- package/src/data_export/export/BasicExportAction.tsx +35 -38
- package/src/data_export/export/ExportCollectionAction.tsx +39 -40
- package/src/data_import/components/DataNewPropertiesMapping.tsx +15 -2
- package/src/data_import/components/ImportFileUpload.tsx +1 -2
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -2
- package/src/data_import/import/ImportCollectionAction.tsx +21 -8
- package/src/data_import/utils/data.ts +23 -5
- package/src/data_import/utils/file_headers.ts +13 -89
- package/src/data_import/utils/file_to_json.ts +43 -68
- package/src/data_import/utils/transforms.ts +47 -0
- package/src/editor/components/SlashCommandMenu.tsx +17 -2
- package/src/editor/components/editor-bubble-item.tsx +1 -1
- package/src/editor/extensions/Image/index.ts +1 -1
- package/src/editor/extensions/Image.ts +1 -1
- package/src/editor/selectors/color-selector.tsx +1 -2
- package/src/editor/selectors/link-selector.tsx +1 -2
- package/src/editor/selectors/node-selector.tsx +16 -2
- package/src/editor/selectors/text-buttons.tsx +1 -2
- package/src/editor/utils/prosemirror-utils.ts +1 -1
- package/src/form/EntityForm.tsx +16 -6
- package/src/form/EntityFormActions.tsx +11 -3
- package/src/form/PropertyFieldBinding.tsx +5 -12
- package/src/form/components/FieldHelperText.tsx +1 -2
- package/src/form/components/LocalChangesMenu.tsx +17 -2
- package/src/form/components/StorageItemPreview.tsx +1 -2
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -2
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +17 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -1
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +10 -3
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +1 -2
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +7 -7
- package/src/form/field_bindings/RelationFieldBinding.tsx +150 -147
- package/src/form/field_bindings/RepeatFieldBinding.tsx +1 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +1 -2
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +202 -0
- package/src/form/index.tsx +1 -0
- package/src/form/validation.ts +54 -2
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -1
- package/src/hooks/navigation/useResolvedViews.tsx +30 -15
- package/src/hooks/navigation/useTopLevelNavigation.ts +1 -1
- package/src/index.ts +6 -0
- package/src/preview/PropertyPreview.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +1 -1
- package/src/preview/components/UrlComponentPreview.tsx +1 -2
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +2 -2
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +23 -24
- package/src/routes/RebaseRoute.tsx +64 -35
- package/src/types/components/EntityFormActionsProps.tsx +1 -1
- package/src/types/components/EntityFormProps.tsx +3 -1
- package/src/types/fields.tsx +4 -3
- package/src/util/navigation_utils.ts +4 -3
- package/src/util/previews.ts +1 -1
- package/src/util/property_utils.tsx +22 -6
- package/src/util/resolutions.ts +2 -2
- package/dist/CollectionEditorDialog-ywdxhs1L.js.map +0 -1
- package/dist/CollectionsStudioView-BDzMFzqH.js.map +0 -1
- package/dist/ContentHomePage-0tHuEIm_.js.map +0 -1
- package/dist/ExportCollectionAction-BIrq92To.js.map +0 -1
- package/dist/ImportCollectionAction-h8yg_To8.js.map +0 -1
- package/dist/PropertyEditView-BuZrNnBN.js.map +0 -1
- package/dist/RolesView-CMPsaIXo.js.map +0 -1
- package/dist/UsersView-BkeblMVT.js.map +0 -1
- package/dist/index-BuZaHcyc.js.map +0 -1
- package/dist/index-CS6uJ7oW.js.map +0 -1
- package/dist/index-eRJbMvHi.js.map +0 -1
- 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 "
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 {
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
312
|
-
//
|
|
313
|
-
if (
|
|
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
|
|
355
|
+
const relation = val as EntityRelation;
|
|
356
356
|
const displayName = resolveRelationDisplayName(relation, prop);
|
|
357
357
|
totalCount = 1;
|
|
358
358
|
if (displayName) {
|