@firecms/core 3.0.0-alpha.20 → 3.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/components/TextareaAutosize.d.ts +2 -0
  2. package/dist/components/index.d.ts +2 -0
  3. package/dist/core/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +7 -1
  4. package/dist/core/components/EntityCollectionTable/fields/TableReferenceField.d.ts +4 -2
  5. package/dist/core/components/EntityCollectionTable/useEntityCollectionTableController.d.ts +1 -1
  6. package/dist/core/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
  7. package/dist/core/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  8. package/dist/core/components/VirtualTable/VirtualTableProps.d.ts +12 -0
  9. package/dist/core/components/VirtualTable/types.d.ts +2 -0
  10. package/dist/core/form_field_configs.d.ts +1 -1
  11. package/dist/core/util/index.d.ts +1 -0
  12. package/dist/core/util/make_properties_editable.d.ts +3 -0
  13. package/dist/core/util/property_utils.d.ts +2 -1
  14. package/dist/index.es.js +8605 -8489
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/index.umd.js +105 -105
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/types/collections.d.ts +1 -0
  19. package/dist/types/firecms.d.ts +14 -5
  20. package/dist/types/firecms_context.d.ts +8 -0
  21. package/dist/types/plugins.d.ts +22 -0
  22. package/package.json +137 -136
  23. package/src/components/BooleanSwitch.tsx +1 -1
  24. package/src/components/BooleanSwitchWithLabel.tsx +2 -2
  25. package/src/components/CircularProgress.tsx +1 -1
  26. package/src/components/Select.tsx +0 -12
  27. package/src/components/TextField.tsx +5 -6
  28. package/src/components/TextareaAutosize.tsx +8 -4
  29. package/src/components/index.tsx +2 -0
  30. package/src/core/FireCMS.tsx +5 -3
  31. package/src/core/components/EntityCollectionTable/EntityCollectionTable.tsx +11 -3
  32. package/src/core/components/EntityCollectionTable/EntityCollectionTableProps.tsx +11 -1
  33. package/src/core/components/EntityCollectionTable/fields/TableReferenceField.tsx +25 -12
  34. package/src/core/components/EntityCollectionTable/internal/PropertyTableCell.tsx +4 -2
  35. package/src/core/components/EntityCollectionTable/useEntityCollectionTableController.tsx +1 -1
  36. package/src/core/components/EntityCollectionView/EntityCollectionView.tsx +55 -3
  37. package/src/core/components/VirtualTable/VirtualTable.tsx +19 -7
  38. package/src/core/components/VirtualTable/VirtualTableHeader.tsx +26 -24
  39. package/src/core/components/VirtualTable/VirtualTableHeaderRow.tsx +7 -2
  40. package/src/core/components/VirtualTable/VirtualTableProps.tsx +14 -0
  41. package/src/core/components/VirtualTable/VirtualTableRow.tsx +1 -1
  42. package/src/core/components/VirtualTable/types.tsx +2 -0
  43. package/src/core/util/index.ts +1 -0
  44. package/src/core/util/make_properties_editable.ts +28 -0
  45. package/src/core/util/property_utils.tsx +6 -1
  46. package/src/form/components/LabelWithIcon.tsx +2 -3
  47. package/src/form/field_bindings/TextFieldBinding.tsx +1 -2
  48. package/src/preview/components/ReferencePreview.tsx +23 -3
  49. package/src/types/collections.ts +2 -0
  50. package/src/types/firecms.tsx +18 -6
  51. package/src/types/firecms_context.tsx +9 -0
  52. package/src/types/plugins.tsx +24 -1
@@ -1,14 +1,14 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
  import { ReferencePreview, ReferencePreviewContainer } from "../../../../preview";
3
- import { CollectionSize, Entity, EntityReference, FilterValues } from "../../../../types";
3
+ import { CollectionSize, Entity, EntityCollection, EntityReference, FilterValues } from "../../../../types";
4
4
 
5
5
  import { getPreviewSizeFrom } from "../../../../preview/util";
6
6
  import { getReferenceFrom } from "../../../util";
7
- import { useNavigationContext, useReferenceDialog } from "../../../../hooks";
7
+ import { useFireCMSContext, useNavigationContext, useReferenceDialog } from "../../../../hooks";
8
8
  import { ErrorView } from "../../ErrorView";
9
9
  import { Button } from "../../../../components/Button";
10
10
 
11
- export function TableReferenceField(props: {
11
+ type TableReferenceFieldProps = {
12
12
  name: string;
13
13
  disabled: boolean;
14
14
  internalValue: EntityReference | EntityReference[] | undefined | null;
@@ -19,8 +19,27 @@ export function TableReferenceField(props: {
19
19
  title?: string;
20
20
  path: string;
21
21
  forceFilter?: FilterValues<string>;
22
- }) {
22
+ };
23
+
24
+ export function TableReferenceField(props: TableReferenceFieldProps) {
25
+
26
+ const context = useFireCMSContext();
27
+ const navigationContext = useNavigationContext();
28
+ const { path } = props;
29
+ const collection = navigationContext.getCollection<EntityCollection>(path);
30
+ if (!collection) {
31
+ if (context.components?.missingReference) {
32
+ return <context.components.missingReference path={path}/>;
33
+ } else {
34
+ throw Error(`Couldn't find the corresponding collection view for the path: ${path}`);
35
+ }
36
+ }
37
+ return <TableReferenceFieldSuccess {...props} collection={collection}/>;
38
+ }
23
39
 
40
+ function TableReferenceFieldSuccess(props: TableReferenceFieldProps & {
41
+ collection: EntityCollection;
42
+ }) {
24
43
  const {
25
44
  name,
26
45
  internalValue,
@@ -31,7 +50,8 @@ export function TableReferenceField(props: {
31
50
  previewProperties,
32
51
  title,
33
52
  disabled,
34
- forceFilter
53
+ forceFilter,
54
+ collection
35
55
  } = props;
36
56
 
37
57
  const [onHover, setOnHover] = useState(false);
@@ -39,13 +59,6 @@ export function TableReferenceField(props: {
39
59
  const hoverTrue = useCallback(() => setOnHover(true), []);
40
60
  const hoverFalse = useCallback(() => setOnHover(false), []);
41
61
 
42
- const navigationContext = useNavigationContext();
43
-
44
- const collection = navigationContext.getCollection(path);
45
- if (!collection) {
46
- throw Error(`Couldn't find the corresponding collection view for the path: ${path}`);
47
- }
48
-
49
62
  const onSingleEntitySelected = useCallback((entity: Entity<any>) => {
50
63
  updateValue(entity ? getReferenceFrom(entity) : null);
51
64
  }, [updateValue]);
@@ -128,11 +128,14 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any, any>>(
128
128
  );
129
129
 
130
130
  const saveValues = (value: any) => {
131
+ if (equal(value, internalValueRef.current))
132
+ return;
131
133
  setSaved(false);
132
134
  validation
133
135
  .validate(value)
134
136
  .then(() => {
135
137
  setError(undefined);
138
+ internalValueRef.current = value;
136
139
  if (onValueChange) {
137
140
  onValueChange({
138
141
  value,
@@ -167,7 +170,6 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any, any>>(
167
170
  } else {
168
171
  updatedValue = newValue;
169
172
  }
170
- internalValueRef.current = updatedValue;
171
173
  setInternalValue(updatedValue);
172
174
  saveValues(updatedValue);
173
175
  };
@@ -234,7 +236,7 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any, any>>(
234
236
  propertyKey={propertyKey}
235
237
  property={property}
236
238
  entity={entity}
237
- value={value}
239
+ value={internalValue}
238
240
  size={getPreviewSizeFrom(size)}
239
241
  />
240
242
  </EntityTableCell>;
@@ -33,7 +33,7 @@ export function useEntityCollectionTableController<M extends Record<string, any>
33
33
  collection,
34
34
  entitiesDisplayedFirst,
35
35
  lastDeleteTimestamp,
36
- forceFilter: forceFilterFromProps
36
+ forceFilter: forceFilterFromProps,
37
37
  }: EntityCollectionTableControllerProps<M>)
38
38
  : TableController<M> {
39
39
 
@@ -10,6 +10,7 @@ import {
10
10
  FilterValues,
11
11
  PartialEntityCollection,
12
12
  PropertyOrBuilder,
13
+ ResolvedProperty,
13
14
  SaveEntityProps,
14
15
  SelectionController
15
16
  } from "../../../types";
@@ -228,6 +229,17 @@ export const EntityCollectionView = React.memo(
228
229
  setLastDeleteTimestamp(Date.now());
229
230
  }, [setSelectedEntities]);
230
231
 
232
+ let AddColumnComponent: React.ComponentType<{
233
+ fullPath: string,
234
+ parentPathSegments: string[],
235
+ collection: EntityCollection;
236
+ }> | undefined
237
+
238
+ // we are only using the first plugin that implements this
239
+ if (context?.plugins) {
240
+ AddColumnComponent = context.plugins.find(plugin => plugin.collectionView?.AddColumnComponent)?.collectionView?.AddColumnComponent;
241
+ }
242
+
231
243
  const onCollectionModifiedForUser = useCallback((path: string, partialCollection: PartialEntityCollection<M>) => {
232
244
  if (userConfigPersistence) {
233
245
  const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
@@ -305,7 +317,11 @@ export const EntityCollectionView = React.memo(
305
317
  fields: context.fields
306
318
  }), [collection, fullPath]);
307
319
 
308
- const getPropertyFor = useCallback(({ propertyKey, propertyValue, entity }: GetPropertyForProps<M>) => {
320
+ const getPropertyFor = useCallback(({
321
+ propertyKey,
322
+ propertyValue,
323
+ entity
324
+ }: GetPropertyForProps<M>) => {
309
325
  let propertyOrBuilder: PropertyOrBuilder<any, M> | undefined = getPropertyInPath<M>(collection.properties, propertyKey);
310
326
 
311
327
  // we might not find the property in the collection if combining property builders and map spread
@@ -485,6 +501,42 @@ export const EntityCollectionView = React.memo(
485
501
 
486
502
  </Popover>;
487
503
 
504
+ function buildAdditionalHeaderWidget({
505
+ property,
506
+ propertyKey,
507
+ onHover
508
+ }: {
509
+ property: ResolvedProperty,
510
+ propertyKey: string,
511
+ onHover: boolean
512
+ }) {
513
+ if (!context.plugins)
514
+ return null;
515
+ return <>
516
+ {context.plugins.filter(plugin => plugin.collectionView?.HeaderAction)
517
+ .map((plugin, i) => {
518
+ const HeaderAction = plugin.collectionView!.HeaderAction!;
519
+ return <HeaderAction
520
+ key={`plugin_header_action_${i}`}
521
+ propertyKey={propertyKey}
522
+ property={property}
523
+ onHover={onHover}
524
+ fullPath={fullPath}
525
+ parentPathSegments={parentPathSegments ?? []}/>;
526
+ })}
527
+ </>;
528
+ }
529
+
530
+ const addColumnComponentInternal = AddColumnComponent
531
+ ? function () {
532
+ if (typeof AddColumnComponent === "function")
533
+ return <AddColumnComponent fullPath={fullPath}
534
+ parentPathSegments={parentPathSegments ?? []}
535
+ collection={collection}/>;
536
+ return null;
537
+ }
538
+ : undefined;
539
+
488
540
  return (
489
541
  <div className={cn("overflow-hidden h-full w-full", className)}
490
542
  ref={containerRef}>
@@ -519,6 +571,8 @@ export const EntityCollectionView = React.memo(
519
571
  />}
520
572
  hoverRow={hoverRow}
521
573
  inlineEditing={checkInlineEditing()}
574
+ additionalHeaderWidget={buildAdditionalHeaderWidget}
575
+ AddColumnComponent={addColumnComponentInternal}
522
576
  />
523
577
 
524
578
  <PopupFormField
@@ -632,5 +686,3 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
632
686
  }
633
687
  return { properties: { [key]: { columnWidth: width } } } as PartialEntityCollection;
634
688
  }
635
-
636
-
@@ -26,7 +26,10 @@ import { AssignmentIcon } from "../../../icons";
26
26
  const VirtualListContext = createContext<VirtualTableContextProps<any>>({} as any);
27
27
  VirtualListContext.displayName = "VirtualListContext";
28
28
 
29
- type InnerElementProps = { children: React.ReactNode, style: any };
29
+ type InnerElementProps = {
30
+ children: React.ReactNode,
31
+ style: any
32
+ };
30
33
 
31
34
  // eslint-disable-next-line react/display-name
32
35
  const innerElementType = forwardRef<HTMLDivElement, InnerElementProps>(({
@@ -107,7 +110,8 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
107
110
  createFilterField,
108
111
  rowClassName,
109
112
  className,
110
- endAdornment
113
+ endAdornment,
114
+ AddColumnComponent
111
115
  }: VirtualTableProps<T>) {
112
116
 
113
117
  const sortByProperty: string | undefined = sortBy ? sortBy[0] : undefined;
@@ -186,11 +190,11 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
186
190
 
187
191
  const maxScroll = Math.max((data?.length ?? 0) * getRowHeight(size) - bounds.height, 0);
188
192
  const onEndReachedInternal = useCallback((scrollOffset: number) => {
189
- if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + 500) {
193
+ if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + 600) {
190
194
  endReachCallbackThreshold.current = scrollOffset;
191
195
  onEndReached();
192
196
  }
193
- }, [data, onEndReached]);
197
+ }, [data?.length, onEndReached]);
194
198
 
195
199
  const onScroll = useCallback(({
196
200
  scrollOffset,
@@ -200,7 +204,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
200
204
  scrollOffset: number,
201
205
  scrollUpdateWasRequested: boolean;
202
206
  }) => {
203
- if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - 500))
207
+ if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - 600))
204
208
  onEndReachedInternal(scrollOffset);
205
209
  }, [maxScroll, onEndReachedInternal]);
206
210
 
@@ -278,7 +282,8 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
278
282
  hoverRow: hoverRow ?? false,
279
283
  createFilterField,
280
284
  rowClassName,
281
- endAdornment
285
+ endAdornment,
286
+ AddColumnComponent
282
287
  }}>
283
288
 
284
289
  <MemoizedList
@@ -288,6 +293,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
288
293
  height={bounds.height}
289
294
  itemCount={(data?.length ?? 0) + (endAdornment ? 1 : 0)}
290
295
  onScroll={onScroll}
296
+ includeAddColumn={Boolean(AddColumnComponent)}
291
297
  itemSize={getRowHeight(size)}/>
292
298
 
293
299
  </VirtualListContext.Provider>
@@ -303,7 +309,8 @@ function MemoizedList({
303
309
  height,
304
310
  itemCount,
305
311
  onScroll,
306
- itemSize
312
+ itemSize,
313
+ includeAddColumn
307
314
  }: {
308
315
  outerRef: RefObject<HTMLDivElement>;
309
316
  width: number;
@@ -315,6 +322,7 @@ function MemoizedList({
315
322
  scrollUpdateWasRequested: boolean;
316
323
  }) => void;
317
324
  itemSize: number;
325
+ includeAddColumn?: boolean;
318
326
  }) {
319
327
 
320
328
  const Row = useCallback(({
@@ -360,6 +368,7 @@ function MemoizedList({
360
368
  top: `calc(${style.top}px + 48px)`
361
369
  }}
362
370
  size={size}>
371
+
363
372
  {columns.map((column: VirtualTableColumn, columnIndex: number) => {
364
373
  const cellData = rowData && rowData[column.key];
365
374
  return <VirtualTableCell
@@ -373,6 +382,9 @@ function MemoizedList({
373
382
  rowIndex={index}
374
383
  columnIndex={columnIndex}/>;
375
384
  })}
385
+
386
+ {includeAddColumn && <div className={"w-20"}/>}
387
+
376
388
  </VirtualTableRow>
377
389
  );
378
390
  }}
@@ -3,12 +3,9 @@ import equal from "react-fast-compare";
3
3
 
4
4
  import { VirtualTableColumn, VirtualTableSort, VirtualTableWhereFilterOp } from "./VirtualTableProps";
5
5
  import { ErrorBoundary } from "../ErrorBoundary";
6
- import { Button, IconButton } from "../../../components";
6
+ import { Badge, Button, cn, IconButton, Popover } from "../../../components";
7
7
  import { defaultBorderMixin } from "../../../styles";
8
- import { ArrowUpwardIcon, ExpandMoreIcon } from "../../../icons";
9
- import { Popover } from "../../../components/Popover";
10
- import { Badge } from "../../../components/Badge";
11
- import { cn } from "../../../components/util/cn";
8
+ import { ArrowUpwardIcon, ExpandMoreIcon, FilterIcon, FilterListIcon } from "../../../icons";
12
9
 
13
10
  interface FilterFormProps<T> {
14
11
  column: VirtualTableColumn<T>;
@@ -40,6 +37,7 @@ type VirtualTableHeaderProps<M extends Record<string, any>> = {
40
37
  onFilterUpdate: (column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, any]) => void;
41
38
  onClickResizeColumn?: (columnIndex: number, column: VirtualTableColumn) => void;
42
39
  createFilterField?: (props: FilterFormFieldProps<any>) => React.ReactNode;
40
+ additionalHeaderWidget?: (onHover: boolean) => React.ReactNode;
43
41
  };
44
42
 
45
43
  export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
@@ -53,7 +51,8 @@ export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
53
51
  filter,
54
52
  column,
55
53
  onClickResizeColumn,
56
- createFilterField
54
+ createFilterField,
55
+ additionalHeaderWidget
57
56
  }: VirtualTableHeaderProps<M>) {
58
57
 
59
58
  const [onHover, setOnHover] = useState(false);
@@ -115,23 +114,26 @@ export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
115
114
  </div>
116
115
 
117
116
  {column.sortable && (sort || hovered || openFilter) &&
118
- <Badge color="secondary"
119
- invisible={!sort}>
120
- <IconButton
121
- size={"small"}
122
- className={onHover || openFilter ? "bg-white dark:bg-gray-950" : undefined}
123
- onClick={() => {
124
- onColumnSort(column.key as Extract<keyof M, string>);
125
- }}
126
- >
127
- {!sort &&
128
- <ArrowUpwardIcon/>}
129
- {sort === "asc" &&
130
- <ArrowUpwardIcon/>}
131
- {sort === "desc" &&
132
- <ArrowUpwardIcon className={"rotate-180"}/>}
133
- </IconButton>
134
- </Badge>
117
+ <>
118
+ {additionalHeaderWidget && additionalHeaderWidget(hovered || openFilter)}
119
+ <Badge color="secondary"
120
+ invisible={!sort}>
121
+ <IconButton
122
+ size={"small"}
123
+ className={onHover || openFilter ? "bg-white dark:bg-gray-950" : undefined}
124
+ onClick={() => {
125
+ onColumnSort(column.key as Extract<keyof M, string>);
126
+ }}
127
+ >
128
+ {!sort &&
129
+ <ArrowUpwardIcon/>}
130
+ {sort === "asc" &&
131
+ <ArrowUpwardIcon/>}
132
+ {sort === "desc" &&
133
+ <ArrowUpwardIcon className={"rotate-180"}/>}
134
+ </IconButton>
135
+ </Badge>
136
+ </>
135
137
  }
136
138
 
137
139
  {column.filter && createFilterField && <div>
@@ -146,7 +148,7 @@ export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
146
148
  className={onHover || openFilter ? "bg-white dark:bg-gray-950" : undefined}
147
149
  size={"small"}
148
150
  onClick={handleSettingsClick}>
149
- <ExpandMoreIcon/>
151
+ <FilterListIcon size={"small"}/>
150
152
  </IconButton>}
151
153
  >
152
154
  {column.filter &&
@@ -16,7 +16,8 @@ export const VirtualTableHeaderRow = ({
16
16
  filter,
17
17
  onColumnResize,
18
18
  onColumnResizeEnd,
19
- createFilterField
19
+ createFilterField,
20
+ AddColumnComponent
20
21
  }: VirtualTableContextProps<any>) => {
21
22
 
22
23
  const columnRefs = columns.map(() => createRef<HTMLDivElement>());
@@ -119,9 +120,13 @@ export const VirtualTableHeaderRow = ({
119
120
  onColumnSort={onColumnSort}
120
121
  onClickResizeColumn={onClickResizeColumn}
121
122
  column={column}
122
- createFilterField={createFilterField}/>
123
+ createFilterField={createFilterField}
124
+ additionalHeaderWidget={column.additionalHeaderWidget}/>
123
125
  </ErrorBoundary>;
124
126
  })}
127
+
128
+ {AddColumnComponent && <AddColumnComponent/>}
129
+
125
130
  </div>
126
131
  );
127
132
  };
@@ -136,6 +136,12 @@ export interface VirtualTableProps<T extends Record<string, any>> {
136
136
  */
137
137
  endAdornment?: React.ReactNode;
138
138
 
139
+ /**
140
+ * If adding this callback, a button to add a new column is displayed.
141
+ * @param column
142
+ */
143
+ AddColumnComponent?: React.ComponentType;
144
+
139
145
  }
140
146
 
141
147
  export type CellRendererParams<T extends any> = {
@@ -206,7 +212,15 @@ export interface VirtualTableColumn<CustomProps extends any = any> {
206
212
  */
207
213
  resizable?: boolean;
208
214
 
215
+ /**
216
+ * Custom props passed to the cell renderer
217
+ */
209
218
  custom?: CustomProps;
219
+
220
+ /**
221
+ * Additional children to be rendered in the header when hovering
222
+ */
223
+ additionalHeaderWidget?: (onHover:boolean) => React.ReactNode;
210
224
  }
211
225
 
212
226
  /**
@@ -15,7 +15,7 @@ export const VirtualTableRow = React.memo<VirtualTableRowProps<any>>(
15
15
  size,
16
16
  style,
17
17
  hoverRow,
18
- rowClassName
18
+ rowClassName,
19
19
  }: VirtualTableRowProps<T>) {
20
20
 
21
21
  const onClick = useCallback((event: React.SyntheticEvent) => onRowClick
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import {
2
3
  CellRendererParams,
3
4
  OnRowClickParams,
@@ -39,4 +40,5 @@ export type VirtualTableContextProps<T extends any> = {
39
40
  createFilterField?: (props: FilterFormFieldProps<any>) => React.ReactNode;
40
41
  rowClassName?: (rowData: T) => string | undefined;
41
42
  endAdornment?: React.ReactNode;
43
+ AddColumnComponent?: React.ComponentType;
42
44
  };
@@ -17,3 +17,4 @@ export * from "./plurals";
17
17
  export * from "./references";
18
18
  export * from "./debounce";
19
19
  export * from "./flatten_object";
20
+ export * from "./make_properties_editable";
@@ -0,0 +1,28 @@
1
+ import { Properties, PropertiesOrBuilders } from "../../types";
2
+ import { isPropertyBuilder } from "./entities";
3
+
4
+ export function makePropertiesEditable(properties: Properties) {
5
+ Object.keys(properties).forEach((key) => {
6
+ const property = properties[key];
7
+ property.editable = true;
8
+ if (property.dataType === "map" && property.properties) {
9
+ makePropertiesEditable(property.properties as Properties);
10
+ }
11
+ });
12
+ }
13
+
14
+ export function makePropertiesNonEditable(properties: PropertiesOrBuilders): PropertiesOrBuilders {
15
+ return Object.entries(properties).reduce((acc, [key, property]) => {
16
+ if (!isPropertyBuilder(property) && property.dataType === "map" && property.properties) {
17
+ const updated = { ...property, properties: makePropertiesNonEditable(property.properties as PropertiesOrBuilders) };
18
+ acc[key] = updated;
19
+ }
20
+ if (isPropertyBuilder(property)) {
21
+ acc[key] = property;
22
+ } else {
23
+ acc[key] = { ...property, editable: false };
24
+ }
25
+ return acc;
26
+ }, {} as PropertiesOrBuilders);
27
+
28
+ }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { FieldConfig, PropertiesOrBuilders, PropertyOrBuilder, ResolvedProperty } from "../../types";
3
+ import { EntityCollection, FieldConfig, PropertiesOrBuilders, PropertyOrBuilder, ResolvedProperty } from "../../types";
4
4
  import { getFieldConfig } from "../form_field_configs";
5
5
  import { isPropertyBuilder } from "./entities";
6
6
  import { resolveProperty } from "./resolutions";
@@ -123,3 +123,8 @@ export function getPropertiesWithPropertiesOrder<M extends Record<string, any>>(
123
123
  });
124
124
  return result;
125
125
  }
126
+
127
+ export function getDefaultPropertiesOrder(collection: EntityCollection<any>): string[] {
128
+ if (collection.propertiesOrder) return collection.propertiesOrder;
129
+ return [...Object.keys(collection.properties), ...(collection.additionalFields ?? []).map(field => field.id)];
130
+ }
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import { Typography } from "../../components/Typography";
3
2
 
4
3
  interface LabelWithIconProps {
5
4
  icon: React.ReactNode;
@@ -27,8 +26,8 @@ export function LabelWithIcon({
27
26
 
28
27
  {icon}
29
28
 
30
- <Typography component={"span"}
31
- className={`font-medium text-${small ? "base" : "sm"} origin-top-left transform ${small ? "translate-x-2 scale-75" : ""}`}>{(title ?? "") + (required ? " *" : "")}</Typography>
29
+ <span
30
+ className={`font-medium text-${small ? "base" : "sm"} origin-top-left transform ${small ? "translate-x-2 scale-75" : ""}`}>{(title ?? "") + (required ? " *" : "")}</span>
32
31
 
33
32
  </span>
34
33
  );
@@ -4,9 +4,8 @@ import { FieldProps, PreviewType } from "../../types";
4
4
  import { FieldHelperText, LabelWithIcon } from "../components";
5
5
  import { useClearRestoreValue } from "../../hooks";
6
6
  import { getIconForProperty } from "../../core";
7
- import { IconButton, TextField } from "../../components";
7
+ import { Collapse, IconButton, TextField } from "../../components";
8
8
  import { PropertyPreview } from "../../preview";
9
- import { Collapse } from "../../components/Collapse";
10
9
  import { ClearIcon } from "../../icons";
11
10
 
12
11
  interface TextFieldProps<T extends string | number> extends FieldProps<T> {
@@ -61,15 +61,35 @@ function ReferencePreviewInternal<M extends Record<string, any>>({
61
61
  }: ReferencePreviewProps) {
62
62
 
63
63
  const context = useFireCMSContext();
64
-
65
64
  const navigationContext = useNavigationContext();
66
- const sideEntityController = useSideEntityController();
67
65
 
68
66
  const collection = navigationContext.getCollection<EntityCollection<M>>(reference.path);
69
67
  if (!collection) {
70
- throw Error(`Couldn't find the corresponding collection view for the path: ${reference.path}`);
68
+ if (context.components?.missingReference) {
69
+ return <context.components.missingReference path={reference.path}/>;
70
+ } else {
71
+ throw Error(`Couldn't find the corresponding collection view for the path: ${reference.path}`);
72
+ }
71
73
  }
72
74
 
75
+ return <ReferencePreviewExisting
76
+ reference={reference}
77
+ collection={collection}
78
+ previewProperties={previewProperties}
79
+ size={size}
80
+ disabled={disabled}
81
+ allowEntityNavigation={allowEntityNavigation}
82
+ onClick={onClick}
83
+ onHover={onHover}/>
84
+ }
85
+
86
+ function ReferencePreviewExisting<M extends Record<string, any> = any>({ reference, collection, previewProperties, size, disabled, allowEntityNavigation, onClick, onHover }: ReferencePreviewProps & {
87
+ collection: EntityCollection<M>
88
+ }) {
89
+
90
+ const context = useFireCMSContext();
91
+ const sideEntityController = useSideEntityController();
92
+
73
93
  const {
74
94
  entity,
75
95
  dataLoading,
@@ -477,6 +477,8 @@ export type TableController<M extends Record<string, any> = any> = {
477
477
  sortBy?: [string, "asc" | "desc"]) => boolean;
478
478
  popupCell?: SelectedCellProps<M>;
479
479
  setPopupCell?: (popupCell?: SelectedCellProps<M>) => void;
480
+
481
+ onAddColumn?: (column: string) => void;
480
482
  }
481
483
 
482
484
  export type SelectedCellProps<M extends Record<string, any>> = {
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import { User } from "./user";
2
3
  import { AuthController } from "./auth";
3
4
  import { DataSource } from "./datasource";
@@ -123,12 +124,6 @@ export type FireCMSProps<UserType extends User> = {
123
124
  */
124
125
  authController: AuthController<UserType>;
125
126
 
126
- /**
127
- * Optional link builder you can add to generate a button in your entity forms.
128
- * The function must return a URL that gets opened when the button is clicked
129
- */
130
- entityLinkBuilder?: EntityLinkBuilder;
131
-
132
127
  /**
133
128
  * Path under the navigation routes of the CMS will be created. Defaults to `/`.
134
129
  * Internally FireCMS uses `react-router` to create the routes, the base path is attached to the
@@ -160,4 +155,21 @@ export type FireCMSProps<UserType extends User> = {
160
155
  */
161
156
  onAnalyticsEvent?: (event: CMSAnalyticsEvent, data?: object) => void;
162
157
 
158
+ /**
159
+ * Optional link builder you can add to generate a button in your entity forms.
160
+ * The function must return a URL that gets opened when the button is clicked
161
+ */
162
+ entityLinkBuilder?: EntityLinkBuilder;
163
+
164
+ components?: {
165
+
166
+ /**
167
+ * Component to render when a reference is missing
168
+ */
169
+ missingReference?: React.ComponentType<{
170
+ path: string,
171
+ }>;
172
+
173
+ }
174
+
163
175
  };
@@ -108,4 +108,13 @@ export type FireCMSContext<UserType extends User = User, AuthControllerType exte
108
108
  * You can also define an entity view from the UI.
109
109
  */
110
110
  entityViews?: EntityCustomView[];
111
+
112
+ components?: {
113
+
114
+ /**
115
+ * Component to render when a reference is missing
116
+ */
117
+ missingReference?: React.ComponentType<{ path: string }>;
118
+
119
+ }
111
120
  };