@firecms/core 3.0.1 → 3.1.0-canary.768c91f
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/README.md +1 -1
- package/dist/components/AIIcon.d.ts +16 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
- package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
- package/dist/components/EntityCollectionView/Board.d.ts +2 -0
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
- package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
- package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
- package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
- package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
- package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/VirtualTable/types.d.ts +2 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/contexts/index.d.ts +10 -0
- package/dist/core/DrawerNavigationGroup.d.ts +45 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/form/components/ErrorFocus.d.ts +1 -1
- package/dist/form/validation.d.ts +3 -2
- package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
- package/dist/hooks/useCollapsedGroups.d.ts +4 -1
- package/dist/index.es.js +5266 -1578
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5260 -1573
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useRestoreScroll.d.ts +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +5 -0
- package/dist/preview/components/DatePreview.d.ts +13 -3
- package/dist/preview/components/ImagePreview.d.ts +5 -1
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +50 -2
- package/dist/types/datasource.d.ts +0 -1
- package/dist/types/plugins.d.ts +62 -1
- package/dist/types/properties.d.ts +259 -4
- package/dist/util/__tests__/conditions.test.d.ts +1 -0
- package/dist/util/__tests__/objects.test.d.ts +1 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/entities.d.ts +2 -3
- package/dist/util/index.d.ts +2 -1
- package/dist/util/property_utils.d.ts +2 -1
- package/dist/util/resolutions.d.ts +3 -3
- package/package.json +14 -11
- package/src/app/Scaffold.tsx +14 -15
- package/src/components/AIIcon.tsx +39 -0
- package/src/components/ArrayContainer.tsx +1 -4
- package/src/components/ClearFilterSortButton.tsx +19 -16
- package/src/components/ConfirmationDialog.tsx +0 -2
- package/src/components/DeleteEntityDialog.tsx +2 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
- package/src/components/EntityCollectionView/Board.tsx +324 -0
- package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
- package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
- package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
- package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
- package/src/components/EntityCollectionView/board_types.ts +113 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
- package/src/components/ErrorTooltip.tsx +2 -1
- package/src/components/HomePage/DefaultHomePage.tsx +47 -10
- package/src/components/HomePage/HomePageDnD.tsx +56 -41
- package/src/components/HomePage/NavigationCard.tsx +20 -18
- package/src/components/HomePage/NavigationGroup.tsx +17 -16
- package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
- package/src/components/ReferenceWidget.tsx +2 -4
- package/src/components/SelectableTable/SelectableTable.tsx +75 -67
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
- package/src/components/UnsavedChangesDialog.tsx +0 -2
- package/src/components/UserDisplay.tsx +4 -4
- package/src/components/VirtualTable/VirtualTable.tsx +272 -118
- package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
- package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
- package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
- package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
- package/src/components/VirtualTable/types.tsx +2 -0
- package/src/components/common/useColumnsIds.tsx +95 -3
- package/src/components/index.tsx +4 -0
- package/src/contexts/BreacrumbsContext.tsx +15 -8
- package/src/contexts/index.ts +10 -0
- package/src/core/DefaultAppBar.tsx +40 -27
- package/src/core/DefaultDrawer.tsx +42 -56
- package/src/core/DrawerNavigationGroup.tsx +118 -0
- package/src/core/DrawerNavigationItem.tsx +4 -3
- package/src/core/EntityEditView.tsx +41 -43
- package/src/core/EntitySidePanel.tsx +28 -26
- package/src/core/SideDialogs.tsx +4 -2
- package/src/core/field_configs.tsx +14 -9
- package/src/core/index.tsx +1 -0
- package/src/form/EntityForm.tsx +69 -60
- package/src/form/PropertyFieldBinding.tsx +61 -46
- package/src/form/components/ErrorFocus.tsx +3 -3
- package/src/form/components/StorageItemPreview.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
- package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
- package/src/form/validation.ts +245 -160
- package/src/hooks/useBreadcrumbsController.tsx +18 -0
- package/src/hooks/useBuildNavigationController.tsx +46 -23
- package/src/hooks/useCollapsedGroups.ts +12 -4
- package/src/hooks/useValidateAuthenticator.tsx +1 -1
- package/src/internal/useBuildDataSource.ts +68 -34
- package/src/internal/useBuildSideDialogsController.tsx +11 -8
- package/src/internal/useBuildSideEntityController.tsx +2 -4
- package/src/internal/useRestoreScroll.tsx +26 -14
- package/src/preview/PropertyPreview.tsx +41 -32
- package/src/preview/PropertyPreviewProps.tsx +6 -0
- package/src/preview/components/DatePreview.tsx +72 -4
- package/src/preview/components/EmptyValue.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +37 -21
- package/src/preview/components/StorageThumbnail.tsx +16 -12
- package/src/preview/components/UrlComponentPreview.tsx +28 -25
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
- package/src/routes/CustomCMSRoute.tsx +1 -0
- package/src/routes/FireCMSRoute.tsx +26 -13
- package/src/types/analytics.ts +10 -0
- package/src/types/collections.ts +57 -3
- package/src/types/datasource.ts +54 -56
- package/src/types/plugins.tsx +69 -1
- package/src/types/properties.ts +347 -27
- package/src/util/__tests__/conditions.test.ts +506 -0
- package/src/util/__tests__/objects.test.ts +196 -0
- package/src/util/callbacks.ts +6 -3
- package/src/util/collections.ts +51 -6
- package/src/util/conditions.ts +339 -0
- package/src/util/entities.ts +29 -30
- package/src/util/entity_cache.ts +2 -1
- package/src/util/index.ts +2 -1
- package/src/util/join_collections.ts +10 -8
- package/src/util/objects.ts +31 -13
- package/src/util/{references.ts → previews.ts} +16 -2
- package/src/util/property_utils.tsx +37 -11
- package/src/util/resolutions.ts +62 -58
- /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
PartialEntityCollection,
|
|
15
15
|
PropertyOrBuilder,
|
|
16
16
|
ResolvedProperty,
|
|
17
|
-
SaveEntityProps
|
|
17
|
+
SaveEntityProps,
|
|
18
|
+
ViewMode
|
|
18
19
|
} from "../../types";
|
|
19
20
|
import {
|
|
20
21
|
EntityCollectionRowActions,
|
|
21
22
|
EntityCollectionTable,
|
|
22
23
|
useDataSourceTableController
|
|
23
24
|
} from "../EntityCollectionTable";
|
|
25
|
+
import { CollectionTableToolbar } from "../EntityCollectionTable/internal/CollectionTableToolbar";
|
|
24
26
|
|
|
25
27
|
import {
|
|
26
28
|
canCreateEntity,
|
|
@@ -45,8 +47,12 @@ import {
|
|
|
45
47
|
useNavigationController,
|
|
46
48
|
useSideEntityController
|
|
47
49
|
} from "../../hooks";
|
|
50
|
+
import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
|
|
48
51
|
import { useUserConfigurationPersistence } from "../../hooks/useUserConfigurationPersistence";
|
|
49
52
|
import { EntityCollectionViewActions } from "./EntityCollectionViewActions";
|
|
53
|
+
import { EntityCollectionCardView } from "./EntityCollectionCardView";
|
|
54
|
+
import { EntityCollectionBoardView } from "./EntityCollectionBoardView";
|
|
55
|
+
import { ViewModeToggle, KanbanPropertyOption } from "./ViewModeToggle";
|
|
50
56
|
import {
|
|
51
57
|
AddIcon,
|
|
52
58
|
Button,
|
|
@@ -142,18 +148,19 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
142
148
|
*/
|
|
143
149
|
export const EntityCollectionView = React.memo(
|
|
144
150
|
function EntityCollectionView<M extends Record<string, any>>({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
fullPath: fullPathProp,
|
|
152
|
+
fullIdPath,
|
|
153
|
+
parentCollectionIds,
|
|
154
|
+
isSubCollection,
|
|
155
|
+
className,
|
|
156
|
+
updateUrl,
|
|
157
|
+
...collectionProp
|
|
158
|
+
}: EntityCollectionViewProps<M>
|
|
153
159
|
) {
|
|
154
160
|
|
|
155
161
|
const context = useFireCMSContext();
|
|
156
162
|
const navigation = useNavigationController();
|
|
163
|
+
const breadcrumbs = useBreadcrumbsController();
|
|
157
164
|
const fullPath = fullPathProp ?? collectionProp.path;
|
|
158
165
|
const dataSource = useDataSource(collectionProp);
|
|
159
166
|
const sideEntityController = useSideEntityController();
|
|
@@ -184,8 +191,19 @@ export const EntityCollectionView = React.memo(
|
|
|
184
191
|
|
|
185
192
|
const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
|
|
186
193
|
|
|
187
|
-
//
|
|
188
|
-
const [
|
|
194
|
+
// Track recently deleted entities for optimistic Kanban count updates
|
|
195
|
+
const [deletedEntities, setDeletedEntities] = React.useState<Entity<M>[]>([]);
|
|
196
|
+
|
|
197
|
+
// number of entities in the collection (undefined = loading)
|
|
198
|
+
const [docsCount, setDocsCount] = useState<number | undefined>(undefined);
|
|
199
|
+
|
|
200
|
+
// Optimistic state for column order to prevent UI flickering during persistence
|
|
201
|
+
const [localPropertiesOrder, setLocalPropertiesOrder] = useState<string[] | undefined>(collection.propertiesOrder);
|
|
202
|
+
|
|
203
|
+
// Sync local state with collection's propertiesOrder when it changes from external sources
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setLocalPropertiesOrder(collection.propertiesOrder);
|
|
206
|
+
}, [collection.propertiesOrder]);
|
|
189
207
|
|
|
190
208
|
const unselectNavigatedEntity = useCallback(() => {
|
|
191
209
|
const currentSelection = highlightedEntity;
|
|
@@ -208,6 +226,83 @@ export const EntityCollectionView = React.memo(
|
|
|
208
226
|
|
|
209
227
|
const [popOverOpen, setPopOverOpen] = useState(false);
|
|
210
228
|
|
|
229
|
+
// View mode priority: URL > saved user config > collection.defaultViewMode
|
|
230
|
+
const defaultViewMode = collection.defaultViewMode ?? "table";
|
|
231
|
+
|
|
232
|
+
// Parse view from URL
|
|
233
|
+
const getViewFromUrl = useCallback((): ViewMode | null => {
|
|
234
|
+
const params = new URLSearchParams(window.location.search);
|
|
235
|
+
const urlView = params.get("__view");
|
|
236
|
+
if (urlView && ["table", "kanban", "cards"].includes(urlView)) {
|
|
237
|
+
return urlView as ViewMode;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
// Get saved view from local persistence
|
|
243
|
+
const getSavedView = useCallback((): ViewMode | null => {
|
|
244
|
+
const saved = userConfigPersistence?.getCollectionConfig<M>(fullPath)?.defaultViewMode;
|
|
245
|
+
return (saved as ViewMode) ?? null;
|
|
246
|
+
}, [userConfigPersistence, fullPath]);
|
|
247
|
+
|
|
248
|
+
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
|
|
249
|
+
// Priority: URL > saved config > collection default
|
|
250
|
+
const urlView = getViewFromUrl();
|
|
251
|
+
if (urlView) return urlView;
|
|
252
|
+
const savedView = getSavedView();
|
|
253
|
+
if (savedView) return savedView;
|
|
254
|
+
return defaultViewMode;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Sync URL with current view on init (if view came from saved config)
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const urlView = getViewFromUrl();
|
|
260
|
+
if (!urlView && viewMode !== "table") {
|
|
261
|
+
// View came from saved config but URL doesn't have it - update URL without push
|
|
262
|
+
const url = new URL(window.location.href);
|
|
263
|
+
url.searchParams.set("__view", viewMode);
|
|
264
|
+
window.history.replaceState({}, "", url.toString());
|
|
265
|
+
}
|
|
266
|
+
}, []); // Only on mount
|
|
267
|
+
|
|
268
|
+
// Update URL when view mode changes (user action)
|
|
269
|
+
const setViewMode = useCallback((newMode: ViewMode) => {
|
|
270
|
+
setViewModeState(newMode);
|
|
271
|
+
|
|
272
|
+
// Update URL with __view param
|
|
273
|
+
const url = new URL(window.location.href);
|
|
274
|
+
if (newMode === "table") {
|
|
275
|
+
url.searchParams.delete("__view");
|
|
276
|
+
} else {
|
|
277
|
+
url.searchParams.set("__view", newMode);
|
|
278
|
+
}
|
|
279
|
+
window.history.pushState({}, "", url.toString());
|
|
280
|
+
}, []);
|
|
281
|
+
|
|
282
|
+
// Listen for browser back/forward
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const handlePopState = () => {
|
|
285
|
+
const urlView = getViewFromUrl();
|
|
286
|
+
if (urlView) {
|
|
287
|
+
// URL has explicit view - use it
|
|
288
|
+
setViewModeState(urlView);
|
|
289
|
+
} else {
|
|
290
|
+
// No URL param - fallback to saved config or collection default
|
|
291
|
+
const savedView = getSavedView();
|
|
292
|
+
setViewModeState(savedView ?? defaultViewMode);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
window.addEventListener("popstate", handlePopState);
|
|
297
|
+
return () => window.removeEventListener("popstate", handlePopState);
|
|
298
|
+
}, [getViewFromUrl, getSavedView, defaultViewMode]);
|
|
299
|
+
|
|
300
|
+
// Card view size state - controls the grid column count
|
|
301
|
+
const [cardSize, setCardSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
302
|
+
|
|
303
|
+
// Table view size state - controls row height
|
|
304
|
+
const [tableSize, setTableSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
305
|
+
|
|
211
306
|
const selectionController = useSelectionController<M>();
|
|
212
307
|
const usedSelectionController = collection.selectionController ?? selectionController;
|
|
213
308
|
const {
|
|
@@ -284,6 +379,7 @@ export const EntityCollectionView = React.memo(
|
|
|
284
379
|
path: fullPath
|
|
285
380
|
});
|
|
286
381
|
setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
|
|
382
|
+
setDeletedEntities(prev => [...prev, entity]);
|
|
287
383
|
setLastDeleteTimestamp(Date.now());
|
|
288
384
|
};
|
|
289
385
|
|
|
@@ -293,6 +389,7 @@ export const EntityCollectionView = React.memo(
|
|
|
293
389
|
});
|
|
294
390
|
setSelectedEntities([]);
|
|
295
391
|
setDeleteEntityClicked(undefined);
|
|
392
|
+
setDeletedEntities(prev => [...prev, ...entities]);
|
|
296
393
|
setLastDeleteTimestamp(Date.now());
|
|
297
394
|
};
|
|
298
395
|
|
|
@@ -317,9 +414,9 @@ export const EntityCollectionView = React.memo(
|
|
|
317
414
|
}, [userConfigPersistence]);
|
|
318
415
|
|
|
319
416
|
const onColumnResize = useCallback(({
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
417
|
+
width,
|
|
418
|
+
key
|
|
419
|
+
}: OnColumnResizeParams) => {
|
|
323
420
|
|
|
324
421
|
const collection = collectionRef.current;
|
|
325
422
|
// Only for property columns
|
|
@@ -328,29 +425,44 @@ export const EntityCollectionView = React.memo(
|
|
|
328
425
|
onCollectionModifiedForUser(fullPath, localCollection);
|
|
329
426
|
}, [onCollectionModifiedForUser, fullPath]);
|
|
330
427
|
|
|
331
|
-
const
|
|
428
|
+
const onTableSizeChanged = useCallback((size: CollectionSize) => {
|
|
429
|
+
setTableSize(size);
|
|
332
430
|
if (userConfigPersistence)
|
|
333
431
|
onCollectionModifiedForUser(fullPath, { defaultSize: size })
|
|
334
432
|
}, [onCollectionModifiedForUser, fullPath, userConfigPersistence]);
|
|
335
433
|
|
|
434
|
+
// View mode change: update URL + save to local persistence
|
|
435
|
+
const onViewModeChange = useCallback((mode: ViewMode) => {
|
|
436
|
+
analyticsController.onAnalyticsEvent?.("view_mode_changed", {
|
|
437
|
+
path: fullPath,
|
|
438
|
+
from: viewMode,
|
|
439
|
+
to: mode
|
|
440
|
+
});
|
|
441
|
+
setViewMode(mode);
|
|
442
|
+
// Save to local persistence for next visit
|
|
443
|
+
if (userConfigPersistence) {
|
|
444
|
+
onCollectionModifiedForUser(fullPath, { defaultViewMode: mode } as PartialEntityCollection<M>);
|
|
445
|
+
}
|
|
446
|
+
}, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
|
|
447
|
+
|
|
336
448
|
const createEnabled = canCreateEntity(collection, authController, fullPath, null);
|
|
337
449
|
|
|
338
450
|
const uniqueFieldValidator: UniqueFieldValidator = useCallback(
|
|
339
451
|
({
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
452
|
+
name,
|
|
453
|
+
value,
|
|
454
|
+
property,
|
|
455
|
+
entityId
|
|
456
|
+
}) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
|
|
345
457
|
[fullPath]);
|
|
346
458
|
|
|
347
459
|
const onValueChange: OnCellValueChange<any, any> = ({
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
460
|
+
value,
|
|
461
|
+
propertyKey,
|
|
462
|
+
onValueUpdated,
|
|
463
|
+
setError,
|
|
464
|
+
data: entity,
|
|
465
|
+
}) => {
|
|
354
466
|
|
|
355
467
|
const updatedValues = setIn({ ...entity.values }, propertyKey, value);
|
|
356
468
|
|
|
@@ -394,10 +506,88 @@ export const EntityCollectionView = React.memo(
|
|
|
394
506
|
authController,
|
|
395
507
|
}), [collection, fullPath]);
|
|
396
508
|
|
|
509
|
+
// Check if Kanban view is possible (collection has at least one string enum property)
|
|
510
|
+
const hasEnumProperty = useMemo(() => {
|
|
511
|
+
const properties = resolvedCollection.properties;
|
|
512
|
+
return Object.values(properties).some((prop: any) =>
|
|
513
|
+
prop && prop.dataType === "string" && prop.enumValues
|
|
514
|
+
);
|
|
515
|
+
}, [resolvedCollection.properties]);
|
|
516
|
+
|
|
517
|
+
// Compute the effective enabled views:
|
|
518
|
+
// - Start from collection.enabledViews (defaults to all three)
|
|
519
|
+
// - Filter out kanban if no enum properties exist
|
|
520
|
+
const enabledViews: ViewMode[] = useMemo(() => {
|
|
521
|
+
const configured = collection.enabledViews ?? ["table", "cards", "kanban"];
|
|
522
|
+
if (!hasEnumProperty) {
|
|
523
|
+
return configured.filter(v => v !== "kanban");
|
|
524
|
+
}
|
|
525
|
+
return configured;
|
|
526
|
+
}, [collection.enabledViews, hasEnumProperty]);
|
|
527
|
+
|
|
528
|
+
// Compute available enum properties for kanban column selection
|
|
529
|
+
const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
|
|
530
|
+
const options: KanbanPropertyOption[] = [];
|
|
531
|
+
const properties = resolvedCollection.properties;
|
|
532
|
+
|
|
533
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
534
|
+
const prop = property as any;
|
|
535
|
+
if (prop && prop.dataType === "string" && prop.enumValues) {
|
|
536
|
+
options.push({
|
|
537
|
+
key,
|
|
538
|
+
label: prop.name || key
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return options;
|
|
544
|
+
}, [resolvedCollection.properties]);
|
|
545
|
+
|
|
546
|
+
// Get saved kanban property from user config
|
|
547
|
+
const getSavedKanbanProperty = useCallback((): string | undefined => {
|
|
548
|
+
const saved = userConfigPersistence?.getCollectionConfig<M>(fullPath);
|
|
549
|
+
return (saved as any)?.kanbanColumnProperty;
|
|
550
|
+
}, [userConfigPersistence, fullPath]);
|
|
551
|
+
|
|
552
|
+
// Selected kanban property state - priority: saved config > collection default > first available
|
|
553
|
+
const [selectedKanbanProperty, setSelectedKanbanProperty] = useState<string>(() => {
|
|
554
|
+
const saved = getSavedKanbanProperty();
|
|
555
|
+
if (saved && kanbanPropertyOptions.some(o => o.key === saved)) return saved;
|
|
556
|
+
if (collection.kanban?.columnProperty) return collection.kanban.columnProperty;
|
|
557
|
+
return kanbanPropertyOptions[0]?.key ?? "";
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Update selected property if options change and current selection is no longer valid
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
if (kanbanPropertyOptions.length > 0 && !kanbanPropertyOptions.some(o => o.key === selectedKanbanProperty)) {
|
|
563
|
+
const saved = getSavedKanbanProperty();
|
|
564
|
+
if (saved && kanbanPropertyOptions.some(o => o.key === saved)) {
|
|
565
|
+
setSelectedKanbanProperty(saved);
|
|
566
|
+
} else if (collection.kanban?.columnProperty && kanbanPropertyOptions.some(o => o.key === collection.kanban?.columnProperty)) {
|
|
567
|
+
setSelectedKanbanProperty(collection.kanban.columnProperty);
|
|
568
|
+
} else {
|
|
569
|
+
setSelectedKanbanProperty(kanbanPropertyOptions[0]?.key ?? "");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}, [kanbanPropertyOptions, selectedKanbanProperty, getSavedKanbanProperty, collection.kanban?.columnProperty]);
|
|
573
|
+
|
|
574
|
+
// Handle kanban property change
|
|
575
|
+
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
576
|
+
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
577
|
+
path: fullPath,
|
|
578
|
+
property
|
|
579
|
+
});
|
|
580
|
+
setSelectedKanbanProperty(property);
|
|
581
|
+
// Save to local persistence
|
|
582
|
+
if (userConfigPersistence) {
|
|
583
|
+
onCollectionModifiedForUser(fullPath, { kanbanColumnProperty: property } as any);
|
|
584
|
+
}
|
|
585
|
+
}, [userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController]);
|
|
586
|
+
|
|
397
587
|
const getPropertyFor = useCallback(({
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
588
|
+
propertyKey,
|
|
589
|
+
entity
|
|
590
|
+
}: GetPropertyForProps<M>) => {
|
|
401
591
|
let propertyOrBuilder: PropertyOrBuilder<any, M> | undefined = getPropertyInPath<M>(collection.properties, propertyKey);
|
|
402
592
|
|
|
403
593
|
// we might not find the property in the collection if combining property builders and map spread
|
|
@@ -417,7 +607,18 @@ export const EntityCollectionView = React.memo(
|
|
|
417
607
|
});
|
|
418
608
|
}, [collection.properties, customizationController.propertyConfigs, resolvedCollection.properties]);
|
|
419
609
|
|
|
420
|
-
|
|
610
|
+
// Use a collection with local propertiesOrder for optimistic UI updates
|
|
611
|
+
const collectionWithLocalOrder = useMemo(() => {
|
|
612
|
+
if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) {
|
|
613
|
+
return {
|
|
614
|
+
...resolvedCollection,
|
|
615
|
+
propertiesOrder: localPropertiesOrder
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return resolvedCollection;
|
|
619
|
+
}, [resolvedCollection, localPropertiesOrder]);
|
|
620
|
+
|
|
621
|
+
const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
|
|
421
622
|
|
|
422
623
|
const additionalFields = useMemo(() => {
|
|
423
624
|
const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = collection.subcollections?.map((subcollection) => {
|
|
@@ -427,23 +628,22 @@ export const EntityCollectionView = React.memo(
|
|
|
427
628
|
width: 200,
|
|
428
629
|
dependencies: [],
|
|
429
630
|
Builder: ({ entity }) => (
|
|
430
|
-
<Button
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}}>
|
|
631
|
+
<Button
|
|
632
|
+
className={"max-w-full truncate justify-start"}
|
|
633
|
+
startIcon={<KeyboardTabIcon size={"small"} />}
|
|
634
|
+
onClick={(event: any) => {
|
|
635
|
+
event.stopPropagation();
|
|
636
|
+
navigateToEntity({
|
|
637
|
+
openEntityMode,
|
|
638
|
+
collection,
|
|
639
|
+
entityId: entity.id,
|
|
640
|
+
selectedTab: subcollection.id ?? subcollection.path,
|
|
641
|
+
path: fullPath,
|
|
642
|
+
fullIdPath,
|
|
643
|
+
navigation,
|
|
644
|
+
sideEntityController
|
|
645
|
+
})
|
|
646
|
+
}}>
|
|
447
647
|
{subcollection.name}
|
|
448
648
|
</Button>
|
|
449
649
|
)
|
|
@@ -465,7 +665,7 @@ export const EntityCollectionView = React.memo(
|
|
|
465
665
|
<ReferencePreview
|
|
466
666
|
key={reference.path + "/" + reference.id}
|
|
467
667
|
reference={reference}
|
|
468
|
-
size={"small"}/>
|
|
668
|
+
size={"small"} />
|
|
469
669
|
);
|
|
470
670
|
})}
|
|
471
671
|
</div>
|
|
@@ -488,9 +688,9 @@ export const EntityCollectionView = React.memo(
|
|
|
488
688
|
const largeLayout = useLargeLayout();
|
|
489
689
|
|
|
490
690
|
const getActionsForEntity = ({
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
691
|
+
entity,
|
|
692
|
+
customEntityActions
|
|
693
|
+
}: {
|
|
494
694
|
entity?: Entity<M>,
|
|
495
695
|
customEntityActions?: EntityAction[]
|
|
496
696
|
}): EntityAction[] => {
|
|
@@ -514,11 +714,11 @@ export const EntityCollectionView = React.memo(
|
|
|
514
714
|
};
|
|
515
715
|
|
|
516
716
|
const tableRowActionsBuilder = useCallback(({
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
717
|
+
entity,
|
|
718
|
+
size,
|
|
719
|
+
width,
|
|
720
|
+
frozen
|
|
721
|
+
}: {
|
|
522
722
|
entity: Entity<any>,
|
|
523
723
|
size: CollectionSize,
|
|
524
724
|
width: number,
|
|
@@ -558,45 +758,28 @@ export const EntityCollectionView = React.memo(
|
|
|
558
758
|
|
|
559
759
|
}, [updateLastDeleteTimestamp, usedSelectionController]);
|
|
560
760
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
variant={"subtitle1"}
|
|
568
|
-
className={`leading-none truncate max-w-[160px] lg:max-w-[240px] ${collection.description ? "cursor-pointer" : "cursor-auto"}`}
|
|
569
|
-
onClick={collection.description
|
|
570
|
-
? (e) => {
|
|
571
|
-
setPopOverOpen(true);
|
|
572
|
-
e.stopPropagation();
|
|
573
|
-
}
|
|
574
|
-
: undefined}>
|
|
575
|
-
{`${collection.name}`}
|
|
576
|
-
</Typography>
|
|
577
|
-
|
|
578
|
-
<EntitiesCount
|
|
579
|
-
fullPath={fullPath}
|
|
580
|
-
collection={collection}
|
|
581
|
-
filter={tableController.filterValues}
|
|
582
|
-
sortBy={tableController.sortBy}
|
|
583
|
-
onCountChange={setDocsCount}
|
|
584
|
-
/>
|
|
585
|
-
|
|
586
|
-
</div>}
|
|
587
|
-
>
|
|
761
|
+
// Update breadcrumb count when count changes (only if loaded)
|
|
762
|
+
useEffect(() => {
|
|
763
|
+
if (docsCount !== undefined) {
|
|
764
|
+
breadcrumbs.updateCount(fullPath, docsCount);
|
|
765
|
+
}
|
|
766
|
+
}, [docsCount, fullPath, breadcrumbs.updateCount]);
|
|
588
767
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
768
|
+
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
769
|
+
const countFetcher = <EntitiesCount
|
|
770
|
+
fullPath={fullPath}
|
|
771
|
+
collection={collection}
|
|
772
|
+
filter={tableController.filterValues}
|
|
773
|
+
sortBy={tableController.sortBy}
|
|
774
|
+
onCountChange={setDocsCount}
|
|
775
|
+
/>;
|
|
592
776
|
|
|
593
|
-
</Popover>;
|
|
594
777
|
|
|
595
778
|
const buildAdditionalHeaderWidget = useCallback(({
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
779
|
+
property,
|
|
780
|
+
propertyKey,
|
|
781
|
+
onHover
|
|
782
|
+
}: {
|
|
600
783
|
property: ResolvedProperty,
|
|
601
784
|
propertyKey: string,
|
|
602
785
|
onHover: boolean
|
|
@@ -616,7 +799,7 @@ export const EntityCollectionView = React.memo(
|
|
|
616
799
|
fullPath={fullPath}
|
|
617
800
|
collection={collection}
|
|
618
801
|
tableController={tableController}
|
|
619
|
-
parentCollectionIds={parentCollectionIds ?? []}/>;
|
|
802
|
+
parentCollectionIds={parentCollectionIds ?? []} />;
|
|
620
803
|
})}
|
|
621
804
|
</>;
|
|
622
805
|
}, [customizationController.plugins, fullPath, parentCollectionIds]);
|
|
@@ -625,9 +808,9 @@ export const EntityCollectionView = React.memo(
|
|
|
625
808
|
? function () {
|
|
626
809
|
if (typeof AddColumnComponent === "function")
|
|
627
810
|
return <AddColumnComponent fullPath={fullPath}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
811
|
+
parentCollectionIds={parentCollectionIds ?? []}
|
|
812
|
+
collection={collection}
|
|
813
|
+
tableController={tableController} />;
|
|
631
814
|
return null;
|
|
632
815
|
}
|
|
633
816
|
: undefined;
|
|
@@ -643,32 +826,55 @@ export const EntityCollectionView = React.memo(
|
|
|
643
826
|
parentCollectionIds
|
|
644
827
|
});
|
|
645
828
|
|
|
829
|
+
// Popover open state managed at parent level to prevent closing when view changes
|
|
830
|
+
const [viewModePopoverOpen, setViewModePopoverOpen] = useState(false);
|
|
831
|
+
|
|
832
|
+
// Create ViewModeToggle once to prevent remounting when view changes
|
|
833
|
+
const viewModeToggleElement = (
|
|
834
|
+
<ViewModeToggle
|
|
835
|
+
viewMode={viewMode}
|
|
836
|
+
onViewModeChange={onViewModeChange}
|
|
837
|
+
enabledViews={enabledViews}
|
|
838
|
+
size={viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
|
|
839
|
+
onSizeChanged={viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
|
|
840
|
+
open={viewModePopoverOpen}
|
|
841
|
+
onOpenChange={setViewModePopoverOpen}
|
|
842
|
+
kanbanPropertyOptions={kanbanPropertyOptions}
|
|
843
|
+
selectedKanbanProperty={selectedKanbanProperty}
|
|
844
|
+
onKanbanPropertyChange={onKanbanPropertyChange}
|
|
845
|
+
/>
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
// Compute plugin-provided error view for collection loading errors
|
|
849
|
+
const pluginErrorView = useMemo(() => {
|
|
850
|
+
const error = tableController.dataLoadingError;
|
|
851
|
+
if (!error || !customizationController.plugins) return null;
|
|
852
|
+
for (const plugin of customizationController.plugins) {
|
|
853
|
+
if (plugin.collectionView?.CollectionError) {
|
|
854
|
+
const CollectionError = plugin.collectionView.CollectionError;
|
|
855
|
+
return <CollectionError
|
|
856
|
+
path={fullPath}
|
|
857
|
+
collection={collection}
|
|
858
|
+
parentCollectionIds={parentCollectionIds}
|
|
859
|
+
error={error}
|
|
860
|
+
/>;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
}, [tableController.dataLoadingError, customizationController.plugins, fullPath, collection, parentCollectionIds]);
|
|
865
|
+
|
|
646
866
|
return (
|
|
647
|
-
<div className={cls("overflow-hidden h-full w-full rounded-md", className)}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
onEntityClick={onEntityClick}
|
|
657
|
-
onColumnResize={onColumnResize}
|
|
658
|
-
onValueChange={onValueChange}
|
|
659
|
-
tableRowActionsBuilder={tableRowActionsBuilder}
|
|
660
|
-
uniqueFieldValidator={uniqueFieldValidator}
|
|
661
|
-
title={title}
|
|
662
|
-
selectionController={usedSelectionController}
|
|
663
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
664
|
-
defaultSize={collection.defaultSize}
|
|
665
|
-
properties={resolvedCollection.properties}
|
|
666
|
-
getPropertyFor={getPropertyFor}
|
|
667
|
-
onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
|
|
668
|
-
onScroll={tableController.onScroll}
|
|
669
|
-
initialScroll={tableController.initialScroll}
|
|
867
|
+
<div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col", className)}
|
|
868
|
+
ref={containerRef}>
|
|
869
|
+
|
|
870
|
+
{/* Unified toolbar - rendered once, outside view conditionals */}
|
|
871
|
+
{countFetcher}
|
|
872
|
+
<CollectionTableToolbar
|
|
873
|
+
loading={tableController.dataLoading}
|
|
874
|
+
onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
|
|
875
|
+
onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
|
|
670
876
|
textSearchLoading={textSearchLoading}
|
|
671
|
-
|
|
877
|
+
viewModeToggle={viewModeToggleElement}
|
|
672
878
|
actionsStart={<EntityCollectionViewStartActions
|
|
673
879
|
parentCollectionIds={parentCollectionIds ?? []}
|
|
674
880
|
collection={collection}
|
|
@@ -676,7 +882,8 @@ export const EntityCollectionView = React.memo(
|
|
|
676
882
|
path={fullPath}
|
|
677
883
|
relativePath={collection.path}
|
|
678
884
|
selectionController={usedSelectionController}
|
|
679
|
-
collectionEntitiesCount={docsCount}
|
|
885
|
+
collectionEntitiesCount={docsCount}
|
|
886
|
+
resolvedProperties={resolvedCollection.properties} />}
|
|
680
887
|
actions={<EntityCollectionViewActions
|
|
681
888
|
parentCollectionIds={parentCollectionIds ?? []}
|
|
682
889
|
collection={collection}
|
|
@@ -689,33 +896,145 @@ export const EntityCollectionView = React.memo(
|
|
|
689
896
|
selectionEnabled={selectionEnabled}
|
|
690
897
|
collectionEntitiesCount={docsCount}
|
|
691
898
|
/>}
|
|
692
|
-
emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
|
|
693
|
-
? <div className="flex flex-col items-center justify-center">
|
|
694
|
-
<Typography variant={"subtitle2"}>So empty...</Typography>
|
|
695
|
-
<Button
|
|
696
|
-
color={"primary"}
|
|
697
|
-
variant={"outlined"}
|
|
698
|
-
onClick={onNewClick}
|
|
699
|
-
className="mt-4"
|
|
700
|
-
>
|
|
701
|
-
<AddIcon/>
|
|
702
|
-
Create your first entry
|
|
703
|
-
</Button>
|
|
704
|
-
</div>
|
|
705
|
-
: <Typography variant={"label"}>No results with the applied filter/sort</Typography>
|
|
706
|
-
}
|
|
707
|
-
hoverRow={hoverRow}
|
|
708
|
-
inlineEditing={checkInlineEditing()}
|
|
709
|
-
AdditionalHeaderWidget={buildAdditionalHeaderWidget}
|
|
710
|
-
AddColumnComponent={addColumnComponentInternal}
|
|
711
|
-
getIdColumnWidth={getIdColumnWidth}
|
|
712
|
-
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
713
|
-
path={fullPath}
|
|
714
|
-
fullIdPath={fullIdPath ?? fullPath}
|
|
715
|
-
collection={collection}/>}
|
|
716
|
-
openEntityMode={openEntityMode}
|
|
717
899
|
/>
|
|
718
900
|
|
|
901
|
+
{/* View content - only the view-specific content changes */}
|
|
902
|
+
{tableController.dataLoadingError && pluginErrorView
|
|
903
|
+
? pluginErrorView
|
|
904
|
+
: viewMode === "kanban" && enabledViews.includes("kanban") ? (
|
|
905
|
+
<EntityCollectionBoardView
|
|
906
|
+
key={`kanban-view-${fullPath}-${selectedKanbanProperty}`}
|
|
907
|
+
collection={collection}
|
|
908
|
+
tableController={tableController}
|
|
909
|
+
fullPath={fullPath}
|
|
910
|
+
parentCollectionIds={parentCollectionIds}
|
|
911
|
+
columnProperty={selectedKanbanProperty}
|
|
912
|
+
onEntityClick={onEntityClick}
|
|
913
|
+
selectionController={usedSelectionController}
|
|
914
|
+
selectionEnabled={selectionEnabled}
|
|
915
|
+
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
916
|
+
deletedEntities={deletedEntities}
|
|
917
|
+
emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
|
|
918
|
+
? <div className="flex flex-col items-center justify-center">
|
|
919
|
+
<Typography variant={"subtitle2"}>So empty...</Typography>
|
|
920
|
+
<Button
|
|
921
|
+
onClick={onNewClick}
|
|
922
|
+
className="mt-4"
|
|
923
|
+
>
|
|
924
|
+
<AddIcon />
|
|
925
|
+
Create your first entry
|
|
926
|
+
</Button>
|
|
927
|
+
</div>
|
|
928
|
+
: <Typography variant={"label"}>No results with the applied filter/sort</Typography>
|
|
929
|
+
}
|
|
930
|
+
/>
|
|
931
|
+
) : viewMode === "cards" ? (
|
|
932
|
+
<EntityCollectionCardView
|
|
933
|
+
key={`cards-view-${fullPath}`}
|
|
934
|
+
collection={collection}
|
|
935
|
+
tableController={tableController}
|
|
936
|
+
onEntityClick={onEntityClick}
|
|
937
|
+
selectionController={usedSelectionController}
|
|
938
|
+
selectionEnabled={selectionEnabled}
|
|
939
|
+
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
940
|
+
onScroll={tableController.onScroll}
|
|
941
|
+
initialScroll={tableController.initialScroll}
|
|
942
|
+
size={cardSize}
|
|
943
|
+
emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
|
|
944
|
+
? <div className="flex flex-col items-center justify-center">
|
|
945
|
+
<Typography variant={"subtitle2"}>So empty...</Typography>
|
|
946
|
+
<Button
|
|
947
|
+
onClick={onNewClick}
|
|
948
|
+
className="mt-4"
|
|
949
|
+
>
|
|
950
|
+
<AddIcon />
|
|
951
|
+
Create your first entry
|
|
952
|
+
</Button>
|
|
953
|
+
</div>
|
|
954
|
+
: <Typography variant={"label"}>No results with the applied filter/sort</Typography>
|
|
955
|
+
}
|
|
956
|
+
/>
|
|
957
|
+
) : (
|
|
958
|
+
<EntityCollectionTable
|
|
959
|
+
key={`collection_table_${fullPath}`}
|
|
960
|
+
hideToolbar={true}
|
|
961
|
+
additionalFields={additionalFields}
|
|
962
|
+
tableController={tableController}
|
|
963
|
+
enablePopupIcon={true}
|
|
964
|
+
displayedColumnIds={displayedColumnIds}
|
|
965
|
+
onSizeChanged={onTableSizeChanged}
|
|
966
|
+
onEntityClick={onEntityClick}
|
|
967
|
+
onColumnResize={onColumnResize}
|
|
968
|
+
onValueChange={onValueChange}
|
|
969
|
+
tableRowActionsBuilder={tableRowActionsBuilder}
|
|
970
|
+
uniqueFieldValidator={uniqueFieldValidator}
|
|
971
|
+
selectionController={usedSelectionController}
|
|
972
|
+
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
973
|
+
defaultSize={tableSize}
|
|
974
|
+
properties={resolvedCollection.properties}
|
|
975
|
+
getPropertyFor={getPropertyFor}
|
|
976
|
+
onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
|
|
977
|
+
onScroll={tableController.onScroll}
|
|
978
|
+
initialScroll={tableController.initialScroll}
|
|
979
|
+
textSearchLoading={textSearchLoading}
|
|
980
|
+
textSearchEnabled={textSearchEnabled}
|
|
981
|
+
emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
|
|
982
|
+
? <div className="flex flex-col items-center justify-center">
|
|
983
|
+
<Typography variant={"subtitle2"}>So empty...</Typography>
|
|
984
|
+
<Button
|
|
985
|
+
onClick={onNewClick}
|
|
986
|
+
className="mt-4"
|
|
987
|
+
>
|
|
988
|
+
<AddIcon />
|
|
989
|
+
Create your first entry
|
|
990
|
+
</Button>
|
|
991
|
+
</div>
|
|
992
|
+
: <Typography variant={"label"}>No results with the applied filter/sort</Typography>
|
|
993
|
+
}
|
|
994
|
+
hoverRow={hoverRow}
|
|
995
|
+
inlineEditing={checkInlineEditing()}
|
|
996
|
+
AdditionalHeaderWidget={buildAdditionalHeaderWidget}
|
|
997
|
+
AddColumnComponent={addColumnComponentInternal}
|
|
998
|
+
getIdColumnWidth={getIdColumnWidth}
|
|
999
|
+
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1000
|
+
path={fullPath}
|
|
1001
|
+
fullIdPath={fullIdPath ?? fullPath}
|
|
1002
|
+
collection={collection} />}
|
|
1003
|
+
openEntityMode={openEntityMode}
|
|
1004
|
+
onColumnsOrderChange={(newColumns) => {
|
|
1005
|
+
// Extract property keys from the new column order
|
|
1006
|
+
// Filter to only include actual property columns (not frozen columns, not additional fields, etc.)
|
|
1007
|
+
// Deduplicate to clean up any previously duplicated keys
|
|
1008
|
+
const seenKeys = new Set<string>();
|
|
1009
|
+
const newPropertiesOrder = newColumns
|
|
1010
|
+
.filter(col => !col.frozen && getPropertyInPath(collection.properties, col.key))
|
|
1011
|
+
.map(col => col.key)
|
|
1012
|
+
.filter(key => {
|
|
1013
|
+
if (seenKeys.has(key)) return false;
|
|
1014
|
+
seenKeys.add(key);
|
|
1015
|
+
return true;
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Optimistically update local state to prevent UI flickering
|
|
1019
|
+
setLocalPropertiesOrder(newPropertiesOrder);
|
|
1020
|
+
|
|
1021
|
+
// Call each plugin's onColumnsReorder callback
|
|
1022
|
+
if (customizationController?.plugins) {
|
|
1023
|
+
customizationController.plugins
|
|
1024
|
+
.filter(plugin => plugin.collectionView?.onColumnsReorder)
|
|
1025
|
+
.forEach(plugin => {
|
|
1026
|
+
plugin.collectionView!.onColumnsReorder!({
|
|
1027
|
+
fullPath,
|
|
1028
|
+
parentCollectionIds: parentCollectionIds ?? [],
|
|
1029
|
+
collection,
|
|
1030
|
+
newPropertiesOrder
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}}
|
|
1035
|
+
/>
|
|
1036
|
+
)}
|
|
1037
|
+
|
|
719
1038
|
{popupCell && <PopupFormField
|
|
720
1039
|
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
721
1040
|
open={Boolean(popupCell)}
|
|
@@ -728,7 +1047,7 @@ export const EntityCollectionView = React.memo(
|
|
|
728
1047
|
customFieldValidator={uniqueFieldValidator}
|
|
729
1048
|
path={resolvedFullPath}
|
|
730
1049
|
onCellValueChange={onValueChange}
|
|
731
|
-
container={containerRef.current}/>}
|
|
1050
|
+
container={containerRef.current} />}
|
|
732
1051
|
|
|
733
1052
|
{deleteEntityClicked &&
|
|
734
1053
|
<DeleteEntityDialog
|
|
@@ -739,7 +1058,7 @@ export const EntityCollectionView = React.memo(
|
|
|
739
1058
|
open={Boolean(deleteEntityClicked)}
|
|
740
1059
|
onEntityDelete={internalOnEntityDelete}
|
|
741
1060
|
onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
|
|
742
|
-
onClose={() => setDeleteEntityClicked(undefined)}/>}
|
|
1061
|
+
onClose={() => setDeleteEntityClicked(undefined)} />}
|
|
743
1062
|
|
|
744
1063
|
</div>
|
|
745
1064
|
);
|
|
@@ -769,12 +1088,12 @@ export const EntityCollectionView = React.memo(
|
|
|
769
1088
|
}) as React.FunctionComponent<EntityCollectionViewProps<any>>
|
|
770
1089
|
|
|
771
1090
|
function EntitiesCount({
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1091
|
+
fullPath,
|
|
1092
|
+
collection,
|
|
1093
|
+
filter,
|
|
1094
|
+
sortBy,
|
|
1095
|
+
onCountChange
|
|
1096
|
+
}: {
|
|
778
1097
|
fullPath: string,
|
|
779
1098
|
collection: EntityCollection,
|
|
780
1099
|
filter?: FilterValues<any>,
|
|
@@ -803,9 +1122,9 @@ function EntitiesCount({
|
|
|
803
1122
|
}, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
|
|
804
1123
|
|
|
805
1124
|
useEffect(() => {
|
|
806
|
-
if (onCountChange) {
|
|
1125
|
+
if (onCountChange && count !== undefined) {
|
|
807
1126
|
setError(undefined);
|
|
808
|
-
onCountChange(count
|
|
1127
|
+
onCountChange(count);
|
|
809
1128
|
}
|
|
810
1129
|
}, [onCountChange, count]);
|
|
811
1130
|
|
|
@@ -813,14 +1132,11 @@ function EntitiesCount({
|
|
|
813
1132
|
return null;
|
|
814
1133
|
}
|
|
815
1134
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
variant={"caption"}
|
|
819
|
-
color={"secondary"}>
|
|
820
|
-
{count !== undefined ? `${count} entities` : <Skeleton className={"w-full max-w-[80px] mt-1"}/>}
|
|
821
|
-
</Typography>;
|
|
1135
|
+
// Count is now displayed in the breadcrumb bar, this component only fetches and reports
|
|
1136
|
+
return null;
|
|
822
1137
|
}
|
|
823
1138
|
|
|
1139
|
+
|
|
824
1140
|
function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityCollection {
|
|
825
1141
|
if (key.includes(".")) {
|
|
826
1142
|
const [parentKey, ...childKey] = key.split(".");
|
|
@@ -830,10 +1146,10 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
|
|
|
830
1146
|
}
|
|
831
1147
|
|
|
832
1148
|
function EntityIdHeaderWidget({
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1149
|
+
collection,
|
|
1150
|
+
path,
|
|
1151
|
+
fullIdPath
|
|
1152
|
+
}: {
|
|
837
1153
|
collection: EntityCollection,
|
|
838
1154
|
path: string,
|
|
839
1155
|
fullIdPath: string
|
|
@@ -857,29 +1173,29 @@ function EntityIdHeaderWidget({
|
|
|
857
1173
|
alignOffset={-117}
|
|
858
1174
|
trigger={
|
|
859
1175
|
<IconButton size={"small"}>
|
|
860
|
-
<SearchIcon size={"small"}/>
|
|
1176
|
+
<SearchIcon size={"small"} />
|
|
861
1177
|
</IconButton>
|
|
862
1178
|
}>
|
|
863
1179
|
<div
|
|
864
1180
|
className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-950 text-surface-900 dark:text-white")}>
|
|
865
1181
|
<form noValidate={true}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1182
|
+
onSubmit={(e) => {
|
|
1183
|
+
e.preventDefault();
|
|
1184
|
+
if (!searchString) return;
|
|
1185
|
+
setOpenPopup(false);
|
|
1186
|
+
const entityId = searchString.trim();
|
|
1187
|
+
setRecentIds(addRecentId(collection.id, entityId));
|
|
1188
|
+
navigateToEntity({
|
|
1189
|
+
openEntityMode,
|
|
1190
|
+
collection,
|
|
1191
|
+
entityId,
|
|
1192
|
+
path,
|
|
1193
|
+
fullIdPath,
|
|
1194
|
+
sideEntityController,
|
|
1195
|
+
navigation
|
|
1196
|
+
})
|
|
1197
|
+
}}
|
|
1198
|
+
className={"w-96 max-w-full"}>
|
|
883
1199
|
|
|
884
1200
|
<div className="flex p-2 w-full gap-2">
|
|
885
1201
|
<input
|
|
@@ -890,32 +1206,32 @@ function EntityIdHeaderWidget({
|
|
|
890
1206
|
setSearchString(e.target.value);
|
|
891
1207
|
}}
|
|
892
1208
|
value={searchString}
|
|
893
|
-
className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled}/>
|
|
1209
|
+
className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled} />
|
|
894
1210
|
<Button variant={"text"}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
><KeyboardTabIcon/></Button>
|
|
1211
|
+
disabled={!(searchString.trim())}
|
|
1212
|
+
type={"submit"}
|
|
1213
|
+
><KeyboardTabIcon /></Button>
|
|
898
1214
|
</div>
|
|
899
1215
|
</form>
|
|
900
1216
|
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
901
1217
|
{recentIds.map(id => (
|
|
902
1218
|
<ReferencePreview reference={new EntityReference(id, path)}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1219
|
+
key={id}
|
|
1220
|
+
hover={true}
|
|
1221
|
+
onClick={() => {
|
|
1222
|
+
setOpenPopup(false);
|
|
1223
|
+
navigateToEntity({
|
|
1224
|
+
openEntityMode,
|
|
1225
|
+
collection,
|
|
1226
|
+
entityId: id,
|
|
1227
|
+
path,
|
|
1228
|
+
fullIdPath,
|
|
1229
|
+
sideEntityController,
|
|
1230
|
+
navigation
|
|
1231
|
+
})
|
|
1232
|
+
}}
|
|
1233
|
+
includeEntityLink={false}
|
|
1234
|
+
size={"small"} />
|
|
919
1235
|
))}
|
|
920
1236
|
</div>}
|
|
921
1237
|
</div>
|