@firecms/core 3.0.0-canary.51 → 3.0.0-canary.53

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.
@@ -66,11 +66,7 @@ import {
66
66
  } from "../common";
67
67
  import { PopupFormField } from "../EntityCollectionTable/internal/popup_field/PopupFormField";
68
68
  import { GetPropertyForProps } from "../EntityCollectionTable/EntityCollectionTableProps";
69
- import {
70
- copyEntityAction,
71
- deleteEntityAction,
72
- editEntityAction
73
- } from "../EntityCollectionTable/internal/default_entity_actions";
69
+ import { copyEntityAction, deleteEntityAction, editEntityAction } from "../common/default_entity_actions";
74
70
  import { DeleteEntityDialog } from "../DeleteEntityDialog";
75
71
  import { useAnalyticsController } from "../../hooks/useAnalyticsController";
76
72
  import { useSelectionController } from "./useSelectionController";
@@ -134,560 +130,559 @@ export const EntityCollectionView = React.memo(
134
130
  }: EntityCollectionViewProps<M>
135
131
  ) {
136
132
 
137
- const context = useFireCMSContext();
138
- console.debug("EntityCollectionView context", context.customizationController.propertyConfigs);
139
- const fullPath = fullPathProp ?? collectionProp.path;
140
- const dataSource = useDataSource(collectionProp);
141
- const navigation = useNavigationController();
142
- const sideEntityController = useSideEntityController();
143
- const authController = useAuthController();
144
- const userConfigPersistence = useUserConfigurationPersistence();
145
- const analyticsController = useAnalyticsController();
146
- const customizationController = useCustomizationController();
147
-
148
- const containerRef = React.useRef<HTMLDivElement>(null);
149
-
150
- const collection = useMemo(() => {
151
- const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
152
- return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
153
- }, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
133
+ const context = useFireCMSContext();
134
+ const fullPath = fullPathProp ?? collectionProp.path;
135
+ const dataSource = useDataSource(collectionProp);
136
+ const navigation = useNavigationController();
137
+ const sideEntityController = useSideEntityController();
138
+ const authController = useAuthController();
139
+ const userConfigPersistence = useUserConfigurationPersistence();
140
+ const analyticsController = useAnalyticsController();
141
+ const customizationController = useCustomizationController();
142
+
143
+ const containerRef = React.useRef<HTMLDivElement>(null);
144
+
145
+ const collection = useMemo(() => {
146
+ const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
147
+ return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
148
+ }, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
149
+
150
+ const collectionRef = React.useRef(collection);
151
+ useEffect(() => {
152
+ collectionRef.current = collection;
153
+ }, [collection]);
154
+
155
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
156
+ const [selectedNavigationEntity, setSelectedNavigationEntity] = useState<Entity<M> | undefined>(undefined);
157
+ const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
158
+
159
+ const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
160
+
161
+ // number of entities in the collection
162
+ const [docsCount, setDocsCount] = useState<number>(0);
163
+
164
+ const unselectNavigatedEntity = useCallback(() => {
165
+ const currentSelection = selectedNavigationEntity;
166
+ setTimeout(() => {
167
+ if (currentSelection === selectedNavigationEntity)
168
+ setSelectedNavigationEntity(undefined);
169
+ }, 2400);
170
+ }, [selectedNavigationEntity]);
171
+
172
+ const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
173
+ const collection = collectionRef.current;
174
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
175
+ return false;
176
+ }
177
+ return collection.inlineEditing === undefined || collection.inlineEditing;
178
+ }, [authController, fullPath]);
154
179
 
155
- const collectionRef = React.useRef(collection);
156
- useEffect(() => {
157
- collectionRef.current = collection;
158
- }, [collection]);
159
-
160
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
161
- const [selectedNavigationEntity, setSelectedNavigationEntity] = useState<Entity<M> | undefined>(undefined);
162
- const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
163
-
164
- const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
165
-
166
- // number of entities in the collection
167
- const [docsCount, setDocsCount] = useState<number>(0);
168
-
169
- const unselectNavigatedEntity = useCallback(() => {
170
- const currentSelection = selectedNavigationEntity;
171
- setTimeout(() => {
172
- if (currentSelection === selectedNavigationEntity)
173
- setSelectedNavigationEntity(undefined);
174
- }, 2400);
175
- }, [selectedNavigationEntity]);
176
-
177
- const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
178
- const collection = collectionRef.current;
179
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
180
- return false;
181
- }
182
- return collection.inlineEditing === undefined || collection.inlineEditing;
183
- }, [authController, fullPath]);
180
+ const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
181
+ const hoverRow = !checkInlineEditing();
184
182
 
185
- const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
186
- const hoverRow = !checkInlineEditing();
183
+ const [popOverOpen, setPopOverOpen] = useState(false);
187
184
 
188
- const [popOverOpen, setPopOverOpen] = useState(false);
185
+ const selectionController = useSelectionController<M>();
186
+ const usedSelectionController = collection.selectionController ?? selectionController;
187
+ const {
188
+ selectedEntities,
189
+ isEntitySelected,
190
+ setSelectedEntities
191
+ } = usedSelectionController;
189
192
 
190
- const selectionController = useSelectionController<M>();
191
- const usedSelectionController = collection.selectionController ?? selectionController;
192
- const {
193
- selectedEntities,
194
- isEntitySelected,
195
- setSelectedEntities
196
- } = usedSelectionController;
193
+ useEffect(() => {
194
+ setDeleteEntityClicked(undefined);
195
+ }, [selectedEntities]);
197
196
 
198
- useEffect(() => {
199
- setDeleteEntityClicked(undefined);
200
- }, [selectedEntities]);
201
-
202
- const tableController = useDataSourceEntityCollectionTableController<M>({
203
- fullPath,
204
- collection,
205
- lastDeleteTimestamp
206
- });
207
-
208
- const tableKey = React.useRef<string>(Math.random().toString(36));
209
- const popupCell = tableController.popupCell;
210
-
211
- const onPopupClose = useCallback(() => {
212
- tableController.setPopupCell?.(undefined);
213
- }, [tableController.setPopupCell]);
214
-
215
- const onEntityClick = useCallback((clickedEntity: Entity<M>) => {
216
- console.log("Entity clicked", clickedEntity)
217
- const collection = collectionRef.current;
218
- setSelectedNavigationEntity(clickedEntity);
219
- analyticsController.onAnalyticsEvent?.("edit_entity_clicked", {
220
- path: clickedEntity.path,
221
- entityId: clickedEntity.id
222
- });
223
- return sideEntityController.open({
224
- entityId: clickedEntity.id,
225
- path: clickedEntity.path,
197
+ const tableController = useDataSourceEntityCollectionTableController<M>({
198
+ fullPath,
226
199
  collection,
227
- updateUrl: true,
228
- onClose: unselectNavigatedEntity,
200
+ lastDeleteTimestamp
229
201
  });
230
- }, [unselectNavigatedEntity, sideEntityController]);
231
202
 
232
- const onNewClick = useCallback(() => {
233
-
234
- const collection = collectionRef.current;
235
- analyticsController.onAnalyticsEvent?.("new_entity_click", {
236
- path: fullPath
237
- });
238
- sideEntityController.open({
239
- path: fullPath,
240
- collection,
241
- updateUrl: true,
242
- onClose: unselectNavigatedEntity,
243
- });
244
- }, [fullPath, sideEntityController]);
203
+ const tableKey = React.useRef<string>(Math.random().toString(36));
204
+ const popupCell = tableController.popupCell;
205
+
206
+ const onPopupClose = useCallback(() => {
207
+ tableController.setPopupCell?.(undefined);
208
+ }, [tableController.setPopupCell]);
209
+
210
+ const onEntityClick = useCallback((clickedEntity: Entity<M>) => {
211
+ console.log("Entity clicked", clickedEntity)
212
+ const collection = collectionRef.current;
213
+ setSelectedNavigationEntity(clickedEntity);
214
+ analyticsController.onAnalyticsEvent?.("edit_entity_clicked", {
215
+ path: clickedEntity.path,
216
+ entityId: clickedEntity.id
217
+ });
218
+ return sideEntityController.open({
219
+ entityId: clickedEntity.id,
220
+ path: clickedEntity.path,
221
+ collection,
222
+ updateUrl: true,
223
+ onClose: unselectNavigatedEntity,
224
+ });
225
+ }, [unselectNavigatedEntity, sideEntityController]);
226
+
227
+ const onNewClick = useCallback(() => {
228
+
229
+ const collection = collectionRef.current;
230
+ analyticsController.onAnalyticsEvent?.("new_entity_click", {
231
+ path: fullPath
232
+ });
233
+ sideEntityController.open({
234
+ path: fullPath,
235
+ collection,
236
+ updateUrl: true,
237
+ onClose: unselectNavigatedEntity,
238
+ });
239
+ }, [fullPath, sideEntityController]);
240
+
241
+ const onMultipleDeleteClick = () => {
242
+ analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", {
243
+ path: fullPath
244
+ });
245
+ setDeleteEntityClicked(selectedEntities);
246
+ };
245
247
 
246
- const onMultipleDeleteClick = () => {
247
- analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", {
248
- path: fullPath
249
- });
250
- setDeleteEntityClicked(selectedEntities);
251
- };
248
+ const internalOnEntityDelete = (_path: string, entity: Entity<M>) => {
249
+ analyticsController.onAnalyticsEvent?.("single_entity_deleted", {
250
+ path: fullPath
251
+ });
252
+ setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
253
+ setLastDeleteTimestamp(Date.now());
254
+ };
252
255
 
253
- const internalOnEntityDelete = (_path: string, entity: Entity<M>) => {
254
- analyticsController.onAnalyticsEvent?.("single_entity_deleted", {
255
- path: fullPath
256
- });
257
- setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
258
- setLastDeleteTimestamp(Date.now());
259
- };
256
+ const internalOnMultipleEntitiesDelete = (_path: string, entities: Entity<M>[]) => {
257
+ analyticsController.onAnalyticsEvent?.("multiple_entities_deleted", {
258
+ path: fullPath
259
+ });
260
+ setSelectedEntities([]);
261
+ setDeleteEntityClicked(undefined);
262
+ setLastDeleteTimestamp(Date.now());
263
+ };
260
264
 
261
- const internalOnMultipleEntitiesDelete = (_path: string, entities: Entity<M>[]) => {
262
- analyticsController.onAnalyticsEvent?.("multiple_entities_deleted", {
263
- path: fullPath
264
- });
265
- setSelectedEntities([]);
266
- setDeleteEntityClicked(undefined);
267
- setLastDeleteTimestamp(Date.now());
268
- };
269
-
270
- let AddColumnComponent: React.ComponentType<{
271
- fullPath: string,
272
- parentCollectionIds: string[],
273
- collection: EntityCollection;
274
- }> | undefined
275
-
276
- // we are only using the first plugin that implements this
277
- if (customizationController?.plugins) {
278
- AddColumnComponent = customizationController.plugins.find(plugin => plugin.collectionView?.AddColumnComponent)?.collectionView?.AddColumnComponent;
279
- }
265
+ let AddColumnComponent: React.ComponentType<{
266
+ fullPath: string,
267
+ parentCollectionIds: string[],
268
+ collection: EntityCollection;
269
+ }> | undefined
280
270
 
281
- const onCollectionModifiedForUser = useCallback((path: string, partialCollection: PartialEntityCollection<M>) => {
282
- if (userConfigPersistence) {
283
- const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
284
- const updatedConfig = mergeDeep(currentStoredConfig, partialCollection);
285
- userConfigPersistence.onCollectionModified(path, updatedConfig);
271
+ // we are only using the first plugin that implements this
272
+ if (customizationController?.plugins) {
273
+ AddColumnComponent = customizationController.plugins.find(plugin => plugin.collectionView?.AddColumnComponent)?.collectionView?.AddColumnComponent;
286
274
  }
287
- }, [userConfigPersistence]);
288
-
289
- const onColumnResize = useCallback(({
290
- width,
291
- key
292
- }: OnColumnResizeParams) => {
293
-
294
- const collection = collectionRef.current;
295
- // Only for property columns
296
- if (!getPropertyInPath(collection.properties, key)) return;
297
- const localCollection = buildPropertyWidthOverwrite(key, width);
298
- onCollectionModifiedForUser(fullPath, localCollection);
299
- }, [onCollectionModifiedForUser, fullPath]);
300
-
301
- const onSizeChanged = useCallback((size: CollectionSize) => {
302
- if (userConfigPersistence)
303
- onCollectionModifiedForUser(fullPath, { defaultSize: size })
304
- }, [onCollectionModifiedForUser, fullPath, userConfigPersistence]);
305
-
306
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
307
-
308
- const uniqueFieldValidator: UniqueFieldValidator = useCallback(
309
- ({
310
- name,
311
- value,
312
- property,
313
- entityId
314
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId),
315
- [fullPath]);
316
-
317
- const onValueChange: OnCellValueChange<any, any> = ({
318
- value,
319
- propertyKey,
320
- onValueUpdated,
321
- setError,
322
- data: entity,
323
- }) => {
324
-
325
- const updatedValues = setIn({ ...entity.values }, propertyKey, value);
326
-
327
- const saveProps: SaveEntityProps = {
328
- path: fullPath,
329
- entityId: entity.id,
330
- values: updatedValues,
331
- previousValues: entity.values,
332
- collection,
333
- status: "existing"
334
- };
335
275
 
336
- return saveEntityWithCallbacks({
337
- ...saveProps,
338
- collection,
339
- dataSource,
340
- context,
341
- onSaveSuccess: () => {
342
- setError(undefined);
343
- onValueUpdated();
344
- },
345
- onSaveFailure: (e: Error) => {
346
- console.error("Save failure");
347
- console.error(e);
348
- setError(e);
276
+ const onCollectionModifiedForUser = useCallback((path: string, partialCollection: PartialEntityCollection<M>) => {
277
+ if (userConfigPersistence) {
278
+ const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
279
+ const updatedConfig = mergeDeep(currentStoredConfig, partialCollection);
280
+ userConfigPersistence.onCollectionModified(path, updatedConfig);
349
281
  }
350
- });
282
+ }, [userConfigPersistence]);
283
+
284
+ const onColumnResize = useCallback(({
285
+ width,
286
+ key
287
+ }: OnColumnResizeParams) => {
288
+
289
+ const collection = collectionRef.current;
290
+ // Only for property columns
291
+ if (!getPropertyInPath(collection.properties, key)) return;
292
+ const localCollection = buildPropertyWidthOverwrite(key, width);
293
+ onCollectionModifiedForUser(fullPath, localCollection);
294
+ }, [onCollectionModifiedForUser, fullPath]);
295
+
296
+ const onSizeChanged = useCallback((size: CollectionSize) => {
297
+ if (userConfigPersistence)
298
+ onCollectionModifiedForUser(fullPath, { defaultSize: size })
299
+ }, [onCollectionModifiedForUser, fullPath, userConfigPersistence]);
300
+
301
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null);
302
+
303
+ const uniqueFieldValidator: UniqueFieldValidator = useCallback(
304
+ ({
305
+ name,
306
+ value,
307
+ property,
308
+ entityId
309
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId),
310
+ [fullPath]);
311
+
312
+ const onValueChange: OnCellValueChange<any, any> = ({
313
+ value,
314
+ propertyKey,
315
+ onValueUpdated,
316
+ setError,
317
+ data: entity,
318
+ }) => {
319
+
320
+ const updatedValues = setIn({ ...entity.values }, propertyKey, value);
321
+
322
+ const saveProps: SaveEntityProps = {
323
+ path: fullPath,
324
+ entityId: entity.id,
325
+ values: updatedValues,
326
+ previousValues: entity.values,
327
+ collection,
328
+ status: "existing"
329
+ };
351
330
 
352
- };
353
-
354
- const resolvedFullPath = navigation.resolveAliasesFrom(fullPath);
355
- const resolvedCollection = useMemo(() => resolveCollection<M>({
356
- collection,
357
- path: fullPath,
358
- fields: customizationController.propertyConfigs
359
- }), [collection, fullPath]);
360
-
361
- const getPropertyFor = useCallback(({
362
- propertyKey,
363
- entity
364
- }: GetPropertyForProps<M>) => {
365
- let propertyOrBuilder: PropertyOrBuilder<any, M> | undefined = getPropertyInPath<M>(collection.properties, propertyKey);
366
-
367
- // we might not find the property in the collection if combining property builders and map spread
368
- if (!propertyOrBuilder) {
369
- // these 2 properties are coming from the resolved collection with default values
370
- propertyOrBuilder = getPropertyInPath<M>(resolvedCollection.properties, propertyKey);
371
- }
331
+ return saveEntityWithCallbacks({
332
+ ...saveProps,
333
+ collection,
334
+ dataSource,
335
+ context,
336
+ onSaveSuccess: () => {
337
+ setError(undefined);
338
+ onValueUpdated();
339
+ },
340
+ onSaveFailure: (e: Error) => {
341
+ console.error("Save failure");
342
+ console.error(e);
343
+ setError(e);
344
+ }
345
+ });
372
346
 
373
- return resolveProperty({
374
- propertyKey,
375
- propertyOrBuilder,
347
+ };
348
+
349
+ const resolvedFullPath = navigation.resolveAliasesFrom(fullPath);
350
+ const resolvedCollection = useMemo(() => resolveCollection<M>({
351
+ collection,
376
352
  path: fullPath,
377
- values: entity.values,
378
- entityId: entity.id,
379
353
  fields: customizationController.propertyConfigs
380
- });
381
- }, [collection.properties, customizationController.propertyConfigs, fullPath, resolvedCollection.properties]);
382
-
383
- const displayedColumnIds = useColumnIds(resolvedCollection, true);
384
-
385
- const additionalFields = useMemo(() => {
386
- const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = collection.subcollections?.map((subcollection) => {
387
- return {
388
- key: getSubcollectionColumnId(subcollection),
389
- name: subcollection.name,
390
- width: 200,
391
- dependencies: [],
392
- Builder: ({ entity }) => (
393
- <Button color={"primary"}
394
- variant={"outlined"}
395
- startIcon={<KeyboardTabIcon size={"small"}/>}
396
- onClick={(event: any) => {
397
- event.stopPropagation();
398
- sideEntityController.open({
399
- path: fullPath,
400
- entityId: entity.id,
401
- selectedSubPath: subcollection.id ?? subcollection.path,
402
- collection,
403
- updateUrl: true,
404
- });
405
- }}>
406
- {subcollection.name}
407
- </Button>
408
- )
409
- };
410
- }) ?? [];
411
-
412
- const collectionGroupParentCollections: AdditionalFieldDelegate<M, any>[] = collection.collectionGroup
413
- ? [{
414
- key: COLLECTION_GROUP_PARENT_ID,
415
- name: "Parent entities",
416
- width: 260,
417
- dependencies: [],
418
- Builder: ({ entity }) => {
419
- const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
420
- return (
421
- <>
422
- {collectionsWithPath.map((reference) => {
423
- return (
424
- <ReferencePreview
425
- key={reference.path + "/" + reference.id}
426
- reference={reference}
427
- size={"tiny"}/>
428
- );
429
- })}
430
- </>
431
- );
432
- }
433
- }]
434
- : [];
435
-
436
- return [
437
- ...(collection.additionalFields ?? []),
438
- ...subcollectionColumns,
439
- ...collectionGroupParentCollections
440
- ];
441
- }, [collection, fullPath, sideEntityController]);
442
-
443
- const updateLastDeleteTimestamp = useCallback(() => {
444
- setLastDeleteTimestamp(Date.now());
445
- }, []);
446
-
447
- const largeLayout = useLargeLayout();
448
-
449
- const getActionsForEntity = ({
450
- entity,
451
- customEntityActions
452
- }: {
453
- entity?: Entity<M>,
454
- customEntityActions?: EntityAction[]
455
- }): EntityAction[] => {
456
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
457
- const actions: EntityAction[] = [editEntityAction];
458
- if (createEnabled)
459
- actions.push(copyEntityAction);
460
- if (deleteEnabled)
461
- actions.push(deleteEntityAction);
462
- if (customEntityActions)
463
- actions.push(...customEntityActions);
464
- return actions;
465
- };
466
-
467
- const getIdColumnWidth = () => {
468
- const entityActions = getActionsForEntity({});
469
- const collapsedActions = entityActions.filter(a => a.collapsed !== false);
470
- const uncollapsedActions = entityActions.filter(a => a.collapsed === false);
471
- const actionsWidth = uncollapsedActions.length * (largeLayout ? 40 : 30);
472
- return (largeLayout ? (80 + actionsWidth) : (70 + actionsWidth)) + (collapsedActions.length > 0 ? (largeLayout ? 40 : 30) : 0);
473
- };
474
-
475
- const tableRowActionsBuilder = ({
476
- entity,
477
- size,
478
- width,
479
- frozen
480
- }: {
481
- entity: Entity<any>,
482
- size: CollectionSize,
483
- width: number,
484
- frozen?: boolean
485
- }) => {
486
-
487
- const isSelected = isEntitySelected(entity);
488
-
489
- const actions = getActionsForEntity({
490
- entity,
491
- customEntityActions: collection.entityActions
492
- });
493
-
494
- return (
495
- <EntityCollectionRowActions
496
- entity={entity}
497
- width={width}
498
- frozen={frozen}
499
- isSelected={isSelected}
500
- selectionEnabled={selectionEnabled}
501
- size={size}
502
- highlightEntity={setSelectedNavigationEntity}
503
- unhighlightEntity={unselectNavigatedEntity}
504
- collection={collection}
505
- fullPath={fullPath}
506
- actions={actions}
507
- hideId={collection?.hideIdFromCollection}
508
- onCollectionChange={updateLastDeleteTimestamp}
509
- selectionController={usedSelectionController}
510
- />
511
- );
354
+ }), [collection, fullPath]);
355
+
356
+ const getPropertyFor = useCallback(({
357
+ propertyKey,
358
+ entity
359
+ }: GetPropertyForProps<M>) => {
360
+ let propertyOrBuilder: PropertyOrBuilder<any, M> | undefined = getPropertyInPath<M>(collection.properties, propertyKey);
361
+
362
+ // we might not find the property in the collection if combining property builders and map spread
363
+ if (!propertyOrBuilder) {
364
+ // these 2 properties are coming from the resolved collection with default values
365
+ propertyOrBuilder = getPropertyInPath<M>(resolvedCollection.properties, propertyKey);
366
+ }
512
367
 
513
- };
514
-
515
- const title = <Popover
516
- open={popOverOpen}
517
- onOpenChange={setPopOverOpen}
518
- enabled={Boolean(collection.description)}
519
- trigger={<div className="flex flex-col items-start">
520
- <Typography
521
- variant={"subtitle1"}
522
- className={`leading-none truncate max-w-[160px] lg:max-w-[240px] ${collection.description ? "cursor-pointer" : "cursor-auto"}`}
523
- onClick={collection.description
524
- ? (e) => {
525
- setPopOverOpen(true);
526
- e.stopPropagation();
368
+ return resolveProperty({
369
+ propertyKey,
370
+ propertyOrBuilder,
371
+ path: fullPath,
372
+ values: entity.values,
373
+ entityId: entity.id,
374
+ fields: customizationController.propertyConfigs
375
+ });
376
+ }, [collection.properties, customizationController.propertyConfigs, fullPath, resolvedCollection.properties]);
377
+
378
+ const displayedColumnIds = useColumnIds(resolvedCollection, true);
379
+
380
+ const additionalFields = useMemo(() => {
381
+ const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = collection.subcollections?.map((subcollection) => {
382
+ return {
383
+ key: getSubcollectionColumnId(subcollection),
384
+ name: subcollection.name,
385
+ width: 200,
386
+ dependencies: [],
387
+ Builder: ({ entity }) => (
388
+ <Button color={"primary"}
389
+ variant={"outlined"}
390
+ startIcon={<KeyboardTabIcon size={"small"}/>}
391
+ onClick={(event: any) => {
392
+ event.stopPropagation();
393
+ sideEntityController.open({
394
+ path: fullPath,
395
+ entityId: entity.id,
396
+ selectedSubPath: subcollection.id ?? subcollection.path,
397
+ collection,
398
+ updateUrl: true,
399
+ });
400
+ }}>
401
+ {subcollection.name}
402
+ </Button>
403
+ )
404
+ };
405
+ }) ?? [];
406
+
407
+ const collectionGroupParentCollections: AdditionalFieldDelegate<M, any>[] = collection.collectionGroup
408
+ ? [{
409
+ key: COLLECTION_GROUP_PARENT_ID,
410
+ name: "Parent entities",
411
+ width: 260,
412
+ dependencies: [],
413
+ Builder: ({ entity }) => {
414
+ const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
415
+ return (
416
+ <div className={"flex flex-col gap-2 w-full"}>
417
+ {collectionsWithPath.map((reference) => {
418
+ return (
419
+ <ReferencePreview
420
+ key={reference.path + "/" + reference.id}
421
+ reference={reference}
422
+ size={"tiny"}/>
423
+ );
424
+ })}
425
+ </div>
426
+ );
527
427
  }
528
- : undefined}>
529
- {`${collection.name}`}
530
- </Typography>
531
-
532
- <EntitiesCount
533
- fullPath={fullPath}
534
- collection={collection}
535
- filter={tableController.filterValues}
536
- sortBy={tableController.sortBy}
537
- onCountChange={setDocsCount}
538
- />
539
-
540
- </div>}
541
- >
542
-
543
- {collection.description && <div className="m-4 text-gray-900 dark:text-white">
544
- <Markdown source={collection.description}/>
545
- </div>}
546
-
547
- </Popover>;
548
-
549
- const buildAdditionalHeaderWidget = useCallback(({
550
- property,
551
- propertyKey,
552
- onHover
553
- }: {
554
- property: ResolvedProperty,
555
- propertyKey: string,
556
- onHover: boolean
557
- }) => {
558
- const collection = collectionRef.current;
559
- if (!customizationController.plugins)
560
- return null;
561
- return <>
562
- {customizationController.plugins.filter(plugin => plugin.collectionView?.HeaderAction)
563
- .map((plugin, i) => {
564
- const HeaderAction = plugin.collectionView!.HeaderAction!;
565
- return <HeaderAction
566
- onHover={onHover}
567
- key={`plugin_header_action_${i}`}
568
- propertyKey={propertyKey}
569
- property={property}
570
- fullPath={fullPath}
571
- collection={collection}
572
- parentCollectionIds={parentCollectionIds ?? []}/>;
573
- })}
574
- </>;
575
- }, [customizationController.plugins, fullPath, parentCollectionIds]);
576
-
577
- const addColumnComponentInternal = AddColumnComponent
578
- ? function () {
579
- if (typeof AddColumnComponent === "function")
580
- return <AddColumnComponent fullPath={fullPath}
581
- parentCollectionIds={parentCollectionIds ?? []}
582
- collection={collection}/>;
583
- return null;
584
- }
585
- : undefined;
586
-
587
- const {
588
- textSearchLoading,
589
- textSearchInitialised,
590
- onTextSearchClick,
591
- textSearchEnabled
592
- } = useTableSearchHelper({
593
- collection,
594
- fullPath: resolvedFullPath,
595
- parentCollectionIds
596
- });
428
+ }]
429
+ : [];
430
+
431
+ return [
432
+ ...(collection.additionalFields ?? []),
433
+ ...subcollectionColumns,
434
+ ...collectionGroupParentCollections
435
+ ];
436
+ }, [collection, fullPath, sideEntityController]);
437
+
438
+ const updateLastDeleteTimestamp = useCallback(() => {
439
+ setLastDeleteTimestamp(Date.now());
440
+ }, []);
441
+
442
+ const largeLayout = useLargeLayout();
443
+
444
+ const getActionsForEntity = ({
445
+ entity,
446
+ customEntityActions
447
+ }: {
448
+ entity?: Entity<M>,
449
+ customEntityActions?: EntityAction[]
450
+ }): EntityAction[] => {
451
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
452
+ const actions: EntityAction[] = [editEntityAction];
453
+ if (createEnabled)
454
+ actions.push(copyEntityAction);
455
+ if (deleteEnabled)
456
+ actions.push(deleteEntityAction);
457
+ if (customEntityActions)
458
+ actions.push(...customEntityActions);
459
+ return actions;
460
+ };
597
461
 
598
- return (
599
- <div className={cn("overflow-hidden h-full w-full rounded-md", className)}
600
- ref={containerRef}>
601
- <EntityCollectionTable
602
- key={`collection_table_${fullPath}`}
603
- additionalFields={additionalFields}
604
- tableController={tableController}
605
- enablePopupIcon={true}
606
- displayedColumnIds={displayedColumnIds}
607
- onSizeChanged={onSizeChanged}
608
- onEntityClick={onEntityClick}
609
- onColumnResize={onColumnResize}
610
- onValueChange={onValueChange}
611
- tableRowActionsBuilder={tableRowActionsBuilder}
612
- uniqueFieldValidator={uniqueFieldValidator}
613
- title={title}
614
- selectionController={usedSelectionController}
615
- highlightedEntities={selectedNavigationEntity ? [selectedNavigationEntity] : []}
616
- defaultSize={collection.defaultSize}
617
- properties={resolvedCollection.properties}
618
- getPropertyFor={getPropertyFor}
619
- onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
620
- textSearchLoading={textSearchLoading}
621
- textSearchEnabled={textSearchEnabled}
622
- actionsStart={<EntityCollectionViewStartActions
623
- parentCollectionIds={parentCollectionIds ?? []}
462
+ const getIdColumnWidth = () => {
463
+ const entityActions = getActionsForEntity({});
464
+ const collapsedActions = entityActions.filter(a => a.collapsed !== false);
465
+ const uncollapsedActions = entityActions.filter(a => a.collapsed === false);
466
+ const actionsWidth = uncollapsedActions.length * (largeLayout ? 40 : 30);
467
+ return (largeLayout ? (80 + actionsWidth) : (70 + actionsWidth)) + (collapsedActions.length > 0 ? (largeLayout ? 40 : 30) : 0);
468
+ };
469
+
470
+ const tableRowActionsBuilder = ({
471
+ entity,
472
+ size,
473
+ width,
474
+ frozen
475
+ }: {
476
+ entity: Entity<any>,
477
+ size: CollectionSize,
478
+ width: number,
479
+ frozen?: boolean
480
+ }) => {
481
+
482
+ const isSelected = isEntitySelected(entity);
483
+
484
+ const actions = getActionsForEntity({
485
+ entity,
486
+ customEntityActions: collection.entityActions
487
+ });
488
+
489
+ return (
490
+ <EntityCollectionRowActions
491
+ entity={entity}
492
+ width={width}
493
+ frozen={frozen}
494
+ isSelected={isSelected}
495
+ selectionEnabled={selectionEnabled}
496
+ size={size}
497
+ highlightEntity={setSelectedNavigationEntity}
498
+ unhighlightEntity={unselectNavigatedEntity}
624
499
  collection={collection}
625
- tableController={tableController}
626
- path={fullPath}
627
- relativePath={collection.path}
500
+ fullPath={fullPath}
501
+ actions={actions}
502
+ hideId={collection?.hideIdFromCollection}
503
+ onCollectionChange={updateLastDeleteTimestamp}
628
504
  selectionController={usedSelectionController}
629
- collectionEntitiesCount={docsCount}/>}
630
- actions={<EntityCollectionViewActions
631
- parentCollectionIds={parentCollectionIds ?? []}
505
+ />
506
+ );
507
+
508
+ };
509
+
510
+ const title = <Popover
511
+ open={popOverOpen}
512
+ onOpenChange={setPopOverOpen}
513
+ enabled={Boolean(collection.description)}
514
+ trigger={<div className="flex flex-col items-start">
515
+ <Typography
516
+ variant={"subtitle1"}
517
+ className={`leading-none truncate max-w-[160px] lg:max-w-[240px] ${collection.description ? "cursor-pointer" : "cursor-auto"}`}
518
+ onClick={collection.description
519
+ ? (e) => {
520
+ setPopOverOpen(true);
521
+ e.stopPropagation();
522
+ }
523
+ : undefined}>
524
+ {`${collection.name}`}
525
+ </Typography>
526
+
527
+ <EntitiesCount
528
+ fullPath={fullPath}
632
529
  collection={collection}
530
+ filter={tableController.filterValues}
531
+ sortBy={tableController.sortBy}
532
+ onCountChange={setDocsCount}
533
+ />
534
+
535
+ </div>}
536
+ >
537
+
538
+ {collection.description && <div className="m-4 text-gray-900 dark:text-white">
539
+ <Markdown source={collection.description}/>
540
+ </div>}
541
+
542
+ </Popover>;
543
+
544
+ const buildAdditionalHeaderWidget = useCallback(({
545
+ property,
546
+ propertyKey,
547
+ onHover
548
+ }: {
549
+ property: ResolvedProperty,
550
+ propertyKey: string,
551
+ onHover: boolean
552
+ }) => {
553
+ const collection = collectionRef.current;
554
+ if (!customizationController.plugins)
555
+ return null;
556
+ return <>
557
+ {customizationController.plugins.filter(plugin => plugin.collectionView?.HeaderAction)
558
+ .map((plugin, i) => {
559
+ const HeaderAction = plugin.collectionView!.HeaderAction!;
560
+ return <HeaderAction
561
+ onHover={onHover}
562
+ key={`plugin_header_action_${i}`}
563
+ propertyKey={propertyKey}
564
+ property={property}
565
+ fullPath={fullPath}
566
+ collection={collection}
567
+ parentCollectionIds={parentCollectionIds ?? []}/>;
568
+ })}
569
+ </>;
570
+ }, [customizationController.plugins, fullPath, parentCollectionIds]);
571
+
572
+ const addColumnComponentInternal = AddColumnComponent
573
+ ? function () {
574
+ if (typeof AddColumnComponent === "function")
575
+ return <AddColumnComponent fullPath={fullPath}
576
+ parentCollectionIds={parentCollectionIds ?? []}
577
+ collection={collection}/>;
578
+ return null;
579
+ }
580
+ : undefined;
581
+
582
+ const {
583
+ textSearchLoading,
584
+ textSearchInitialised,
585
+ onTextSearchClick,
586
+ textSearchEnabled
587
+ } = useTableSearchHelper({
588
+ collection,
589
+ fullPath: resolvedFullPath,
590
+ parentCollectionIds
591
+ });
592
+
593
+ return (
594
+ <div className={cn("overflow-hidden h-full w-full rounded-md", className)}
595
+ ref={containerRef}>
596
+ <EntityCollectionTable
597
+ key={`collection_table_${fullPath}`}
598
+ additionalFields={additionalFields}
633
599
  tableController={tableController}
634
- onMultipleDeleteClick={onMultipleDeleteClick}
635
- onNewClick={onNewClick}
636
- path={fullPath}
637
- relativePath={collection.path}
600
+ enablePopupIcon={true}
601
+ displayedColumnIds={displayedColumnIds}
602
+ onSizeChanged={onSizeChanged}
603
+ onEntityClick={onEntityClick}
604
+ onColumnResize={onColumnResize}
605
+ onValueChange={onValueChange}
606
+ tableRowActionsBuilder={tableRowActionsBuilder}
607
+ uniqueFieldValidator={uniqueFieldValidator}
608
+ title={title}
638
609
  selectionController={usedSelectionController}
639
- selectionEnabled={selectionEnabled}
640
- collectionEntitiesCount={docsCount}
641
- />}
642
- emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
643
- ? <div className="flex flex-col items-center justify-center">
644
- <Typography variant={"subtitle2"}>So empty...</Typography>
645
- <Button
646
- color={"primary"}
647
- variant={"outlined"}
648
- onClick={onNewClick}
649
- className="mt-4"
650
- >
651
- <AddIcon/>
652
- Create your first entity
653
- </Button>
654
- </div>
655
- : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
656
- }
657
- hoverRow={hoverRow}
658
- inlineEditing={checkInlineEditing()}
659
- AdditionalHeaderWidget={buildAdditionalHeaderWidget}
660
- AddColumnComponent={addColumnComponentInternal}
661
- getIdColumnWidth={getIdColumnWidth}
662
- additionalIDHeaderWidget={<EntityIdHeaderWidget
663
- path={fullPath}
664
- collection={collection}/>}
665
- />
666
-
667
- <PopupFormField
668
- key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entity?.id}`}
669
- open={Boolean(popupCell)}
670
- onClose={onPopupClose}
671
- cellRect={popupCell?.cellRect}
672
- propertyKey={popupCell?.propertyKey}
673
- collection={collection}
674
- entity={popupCell?.entity}
675
- tableKey={tableKey.current}
676
- customFieldValidator={uniqueFieldValidator}
677
- path={resolvedFullPath}
678
- onCellValueChange={onValueChange}
679
- container={containerRef.current}/>
680
-
681
- {deleteEntityClicked &&
682
- <DeleteEntityDialog
683
- entityOrEntitiesToDelete={deleteEntityClicked}
684
- path={fullPath}
610
+ highlightedEntities={selectedNavigationEntity ? [selectedNavigationEntity] : []}
611
+ defaultSize={collection.defaultSize}
612
+ properties={resolvedCollection.properties}
613
+ getPropertyFor={getPropertyFor}
614
+ onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
615
+ textSearchLoading={textSearchLoading}
616
+ textSearchEnabled={textSearchEnabled}
617
+ actionsStart={<EntityCollectionViewStartActions
618
+ parentCollectionIds={parentCollectionIds ?? []}
619
+ collection={collection}
620
+ tableController={tableController}
621
+ path={fullPath}
622
+ relativePath={collection.path}
623
+ selectionController={usedSelectionController}
624
+ collectionEntitiesCount={docsCount}/>}
625
+ actions={<EntityCollectionViewActions
626
+ parentCollectionIds={parentCollectionIds ?? []}
627
+ collection={collection}
628
+ tableController={tableController}
629
+ onMultipleDeleteClick={onMultipleDeleteClick}
630
+ onNewClick={onNewClick}
631
+ path={fullPath}
632
+ relativePath={collection.path}
633
+ selectionController={usedSelectionController}
634
+ selectionEnabled={selectionEnabled}
635
+ collectionEntitiesCount={docsCount}
636
+ />}
637
+ emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
638
+ ? <div className="flex flex-col items-center justify-center">
639
+ <Typography variant={"subtitle2"}>So empty...</Typography>
640
+ <Button
641
+ color={"primary"}
642
+ variant={"outlined"}
643
+ onClick={onNewClick}
644
+ className="mt-4"
645
+ >
646
+ <AddIcon/>
647
+ Create your first entity
648
+ </Button>
649
+ </div>
650
+ : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
651
+ }
652
+ hoverRow={hoverRow}
653
+ inlineEditing={checkInlineEditing()}
654
+ AdditionalHeaderWidget={buildAdditionalHeaderWidget}
655
+ AddColumnComponent={addColumnComponentInternal}
656
+ getIdColumnWidth={getIdColumnWidth}
657
+ additionalIDHeaderWidget={<EntityIdHeaderWidget
658
+ path={fullPath}
659
+ collection={collection}/>}
660
+ />
661
+
662
+ <PopupFormField
663
+ key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entity?.id}`}
664
+ open={Boolean(popupCell)}
665
+ onClose={onPopupClose}
666
+ cellRect={popupCell?.cellRect}
667
+ propertyKey={popupCell?.propertyKey}
685
668
  collection={collection}
686
- callbacks={collection.callbacks}
687
- open={Boolean(deleteEntityClicked)}
688
- onEntityDelete={internalOnEntityDelete}
689
- onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
690
- onClose={() => setDeleteEntityClicked(undefined)}/>}
669
+ entity={popupCell?.entity}
670
+ tableKey={tableKey.current}
671
+ customFieldValidator={uniqueFieldValidator}
672
+ path={resolvedFullPath}
673
+ onCellValueChange={onValueChange}
674
+ container={containerRef.current}/>
675
+
676
+ {deleteEntityClicked &&
677
+ <DeleteEntityDialog
678
+ entityOrEntitiesToDelete={deleteEntityClicked}
679
+ path={fullPath}
680
+ collection={collection}
681
+ callbacks={collection.callbacks}
682
+ open={Boolean(deleteEntityClicked)}
683
+ onEntityDelete={internalOnEntityDelete}
684
+ onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
685
+ onClose={() => setDeleteEntityClicked(undefined)}/>}
691
686
 
692
687
  </div>
693
688
  );