@firecms/collection_editor 3.0.0-beta.13 → 3.0.0-beta.14

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.
@@ -4,14 +4,30 @@ import equal from "react-fast-compare"
4
4
  import {
5
5
  AdditionalFieldDelegate,
6
6
  CMSType,
7
- ErrorBoundary,
8
7
  isPropertyBuilder,
9
8
  PropertiesOrBuilders,
10
9
  PropertyOrBuilder
11
10
  } from "@firecms/core";
12
11
  import { AutorenewIcon, defaultBorderMixin, DragHandleIcon, IconButton, RemoveIcon, Tooltip } from "@firecms/ui";
13
12
  import { NonEditablePropertyPreview, PropertyFieldPreview } from "./PropertyFieldPreview";
14
- import { DragDropContext, Draggable, DraggableProvided, Droppable } from "@hello-pangea/dnd";
13
+ import {
14
+ closestCenter,
15
+ DndContext,
16
+ DragEndEvent,
17
+ KeyboardSensor,
18
+ PointerSensor,
19
+ useSensor,
20
+ useSensors
21
+ } from "@dnd-kit/core";
22
+ import {
23
+ SortableContext,
24
+ sortableKeyboardCoordinates,
25
+ useSortable,
26
+ verticalListSortingStrategy
27
+ } from "@dnd-kit/sortable";
28
+ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
29
+
30
+ import { CSS } from "@dnd-kit/utilities";
15
31
  import { getFullId, getFullIdPath } from "./util";
16
32
  import { editableProperty } from "../../utils/entities";
17
33
 
@@ -48,103 +64,109 @@ export const PropertyTree = React.memo(
48
64
 
49
65
  const propertiesOrder = propertiesOrderProp ?? Object.keys(properties);
50
66
 
51
- const onDragEnd = (result: any) => {
52
- // dropped outside the list
53
- if (!result.destination) {
67
+ const sensors = useSensors(
68
+ useSensor(PointerSensor, {
69
+ activationConstraint: {
70
+ distance: 5,
71
+ }
72
+ }),
73
+ useSensor(KeyboardSensor, {
74
+ coordinateGetter: sortableKeyboardCoordinates,
75
+ })
76
+ );
77
+
78
+ const handleDragEnd = (event: DragEndEvent) => {
79
+ const {
80
+ active,
81
+ over
82
+ } = event;
83
+
84
+ if (!over || active.id === over.id) {
54
85
  return;
55
86
  }
56
- const startIndex = result.source.index;
57
- const endIndex = result.destination.index;
58
-
59
- const newPropertiesOrder = Array.from(propertiesOrder);
60
- const [removed] = newPropertiesOrder.splice(startIndex, 1);
61
- newPropertiesOrder.splice(endIndex, 0, removed);
62
- if (onPropertyMove)
63
- onPropertyMove(newPropertiesOrder, namespace);
64
- }
87
+
88
+ const activeId = String(active.id);
89
+ const overId = String(over.id);
90
+
91
+ // Extract property keys from the full IDs
92
+ const activeKey = activeId.includes(".") ? activeId.split(".").pop() : activeId;
93
+ const overKey = overId.includes(".") ? overId.split(".").pop() : overId;
94
+
95
+ if (!activeKey || !overKey) return;
96
+
97
+ const oldIndex = propertiesOrder.indexOf(activeKey);
98
+ const newIndex = propertiesOrder.indexOf(overKey);
99
+
100
+ if (oldIndex !== -1 && newIndex !== -1) {
101
+ const newPropertiesOrder = [...propertiesOrder];
102
+ const [removed] = newPropertiesOrder.splice(oldIndex, 1);
103
+ newPropertiesOrder.splice(newIndex, 0, removed);
104
+
105
+ if (onPropertyMove) {
106
+ onPropertyMove(newPropertiesOrder, namespace);
107
+ }
108
+ }
109
+ };
110
+
111
+ const items = propertiesOrder.map(key => getFullId(key, namespace));
65
112
 
66
113
  return (
67
- <>
68
-
69
- <DragDropContext onDragEnd={onDragEnd}>
70
- <Droppable droppableId={`droppable_${namespace}`}>
71
- {(droppableProvided, droppableSnapshot) => (
72
- <div
73
- {...droppableProvided.droppableProps}
74
- ref={droppableProvided.innerRef}
75
- className={className}>
76
- {propertiesOrder && propertiesOrder
77
- .map((propertyKey: string, index: number) => {
78
- const property = properties[propertyKey] as PropertyOrBuilder;
79
- const additionalField = additionalFields?.find(field => field.key === propertyKey);
80
-
81
- if (!property && !additionalField) {
82
- console.warn(`Property ${propertyKey} not found in properties or additionalFields`);
83
- return null;
84
- }
85
- return (
86
- <Draggable
87
- key={`array_field_${namespace}_${propertyKey}}`}
88
- draggableId={`array_field_${namespace}_${propertyKey}}`}
89
- index={index}>
90
- {(provided, snapshot) => {
91
- return (
92
- <ErrorBoundary>
93
- <PropertyTreeEntry
94
- propertyKey={propertyKey as string}
95
- propertyOrBuilder={property}
96
- additionalField={additionalField}
97
- provided={provided}
98
- errors={errors}
99
- namespace={namespace}
100
- inferredPropertyKeys={inferredPropertyKeys}
101
- onPropertyMove={onPropertyMove}
102
- onPropertyRemove={onPropertyRemove}
103
- onPropertyClick={snapshot.isDragging ? undefined : onPropertyClick}
104
- selectedPropertyKey={selectedPropertyKey}
105
- collectionEditable={collectionEditable}
106
- />
107
- </ErrorBoundary>
108
- );
109
- }}
110
- </Draggable>);
111
- }).filter(Boolean)}
112
-
113
- {droppableProvided.placeholder}
114
-
115
- </div>
116
- )}
117
- </Droppable>
118
- </DragDropContext>
119
-
120
- </>
114
+ <DndContext
115
+ sensors={sensors}
116
+ collisionDetection={closestCenter}
117
+ onDragEnd={handleDragEnd}
118
+ modifiers={[restrictToVerticalAxis]}
119
+ >
120
+ <SortableContext
121
+ items={items}
122
+ strategy={verticalListSortingStrategy}
123
+ >
124
+ <div className={className}>
125
+
126
+ {propertiesOrder && propertiesOrder
127
+ .map((propertyKey: string, index: number) => {
128
+ const property = properties[propertyKey] as PropertyOrBuilder;
129
+ const additionalField = additionalFields?.find(field => field.key === propertyKey);
130
+
131
+ if (!property && !additionalField) {
132
+ console.warn(`Property ${propertyKey} not found in properties or additionalFields`);
133
+ return null;
134
+ }
135
+
136
+ const id = getFullId(propertyKey, namespace);
137
+
138
+ return (
139
+ <PropertyTreeEntry
140
+ key={id}
141
+ id={id}
142
+ propertyKey={propertyKey}
143
+ propertyOrBuilder={property}
144
+ additionalField={additionalField}
145
+ errors={errors}
146
+ namespace={namespace}
147
+ inferredPropertyKeys={inferredPropertyKeys}
148
+ onPropertyMove={onPropertyMove}
149
+ onPropertyRemove={onPropertyRemove}
150
+ onPropertyClick={onPropertyClick}
151
+ selectedPropertyKey={selectedPropertyKey}
152
+ collectionEditable={collectionEditable}
153
+ />
154
+ );
155
+ }).filter(Boolean)}
156
+ </div>
157
+ </SortableContext>
158
+ </DndContext>
121
159
  );
122
160
  },
123
- (prevProps, nextProps) => {
124
-
125
- const isSelected = nextProps.selectedPropertyKey?.startsWith(nextProps.namespace ?? "");
126
- const wasSelected = prevProps.selectedPropertyKey?.startsWith(prevProps.namespace ?? "");
127
- if (isSelected || wasSelected)
128
- return false;
129
-
130
- return equal(prevProps.properties, nextProps.properties) &&
131
- prevProps.propertiesOrder === nextProps.propertiesOrder &&
132
- equal(prevProps.additionalFields, nextProps.additionalFields) &&
133
- equal(prevProps.errors, nextProps.errors) &&
134
- equal(prevProps.onPropertyClick, nextProps.onPropertyClick) &&
135
- // equal(prevProps.onPropertyMove, nextProps.onPropertyMove) &&
136
- // equal(prevProps.onPropertyRemove, nextProps.onPropertyRemove) &&
137
- prevProps.namespace === nextProps.namespace &&
138
- prevProps.collectionEditable === nextProps.collectionEditable;
139
- }
161
+ equal
140
162
  );
141
163
 
142
164
  export function PropertyTreeEntry({
165
+ id,
143
166
  propertyKey,
144
167
  namespace,
145
168
  propertyOrBuilder,
146
169
  additionalField,
147
- provided,
148
170
  selectedPropertyKey,
149
171
  errors,
150
172
  onPropertyClick,
@@ -153,12 +175,12 @@ export function PropertyTreeEntry({
153
175
  inferredPropertyKeys,
154
176
  collectionEditable
155
177
  }: {
178
+ id: string;
156
179
  propertyKey: string;
157
180
  namespace?: string;
158
181
  propertyOrBuilder: PropertyOrBuilder;
159
182
  additionalField?: AdditionalFieldDelegate<any>;
160
183
  selectedPropertyKey?: string;
161
- provided: DraggableProvided;
162
184
  errors: Record<string, any>;
163
185
  onPropertyClick?: (propertyKey: string, namespace?: string) => void;
164
186
  onPropertyMove?: (propertiesOrder: string[], namespace?: string) => void;
@@ -167,8 +189,27 @@ export function PropertyTreeEntry({
167
189
  collectionEditable: boolean;
168
190
  }) {
169
191
 
192
+ const {
193
+ attributes,
194
+ listeners,
195
+ setNodeRef,
196
+ transform,
197
+ transition,
198
+ isDragging
199
+ } = useSortable({
200
+ id
201
+ });
202
+
203
+ const style = {
204
+ // Key change: use Translate instead of Transform to prevent stretching
205
+ transform: CSS.Translate.toString(transform),
206
+ transition,
207
+ zIndex: isDragging ? 10 : undefined,
208
+ position: "relative" as const,
209
+ };
210
+
170
211
  const isPropertyInferred = inferredPropertyKeys?.includes(namespace ? `${namespace}.${propertyKey}` : propertyKey);
171
- const fullId = getFullId(propertyKey, namespace);
212
+ const fullId = id;
172
213
  const fullIdPath = getFullIdPath(propertyKey, namespace);
173
214
  const hasError = fullIdPath in errors;
174
215
 
@@ -190,65 +231,68 @@ export function PropertyTreeEntry({
190
231
  }
191
232
  }
192
233
 
193
- // const hasError = fullId ? getIn(errors, idToPropertiesPath(fullId)) : false;
194
234
  const selected = selectedPropertyKey === fullId;
195
235
  const editable = propertyOrBuilder && ((collectionEditable && !isPropertyBuilder(propertyOrBuilder)) || editableProperty(propertyOrBuilder));
196
236
 
197
237
  return (
198
238
  <div
199
- ref={provided.innerRef}
200
- {...provided.draggableProps}
201
- {...provided.dragHandleProps}
239
+ ref={setNodeRef}
240
+ style={style}
202
241
  className="relative -ml-8"
203
242
  >
204
- {subtree && <div
205
- className={"absolute border-l " + defaultBorderMixin}
206
- style={{
207
- left: "32px",
208
- top: "64px",
209
- bottom: "16px"
210
- }}/>}
211
-
212
- {!isPropertyBuilder(propertyOrBuilder) && !additionalField && editable
213
- ? <PropertyFieldPreview
214
- property={propertyOrBuilder}
215
- onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
216
- includeName={true}
217
- selected={selected}
218
- hasError={hasError}/>
219
- : <NonEditablePropertyPreview name={propertyKey}
220
- property={propertyOrBuilder}
221
- onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
222
- selected={selected}/>}
223
-
224
- <div className="absolute top-2 right-2 flex flex-row ">
225
-
226
- {isPropertyInferred && <Tooltip title={"Inferred property"}>
227
- <AutorenewIcon size="small" className={"p-2"}/>
228
- </Tooltip>}
229
-
230
- {onPropertyRemove && <Tooltip title={"Remove"}
231
- asChild={true}>
232
- <IconButton size="small"
233
- color="inherit"
234
- onClick={() => onPropertyRemove(propertyKey, namespace)}>
235
- <RemoveIcon size={"small"}/>
236
- </IconButton>
237
- </Tooltip>}
238
-
239
- {onPropertyMove && <Tooltip title={"Move"}
240
- asChild={true}>
241
- <IconButton
242
- component={"span"}
243
- size="small"
244
- >
245
- <DragHandleIcon size={"small"}/>
246
- </IconButton>
247
- </Tooltip>}
248
- </div>
243
+ <div className="relative">
244
+ {subtree && <div
245
+ className={"absolute border-l " + defaultBorderMixin}
246
+ style={{
247
+ left: "32px",
248
+ top: "64px",
249
+ bottom: "16px"
250
+ }}/>}
249
251
 
250
- {subtree && <div className={"ml-16"}>{subtree}</div>}
252
+ <div>
253
+ {!isPropertyBuilder(propertyOrBuilder) && !additionalField && editable
254
+ ? <PropertyFieldPreview
255
+ property={propertyOrBuilder}
256
+ onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
257
+ includeName={true}
258
+ selected={selected}
259
+ hasError={hasError}/>
260
+ : <NonEditablePropertyPreview name={propertyKey}
261
+ property={propertyOrBuilder}
262
+ onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
263
+ selected={selected}/>}
264
+ </div>
265
+
266
+ <div className="absolute top-2 right-2 flex flex-row">
267
+ {isPropertyInferred && <Tooltip title={"Inferred property"}>
268
+ <AutorenewIcon size="small" className={"p-2"}/>
269
+ </Tooltip>}
270
+
271
+ {onPropertyRemove && !isPropertyInferred && <Tooltip title={"Remove"}
272
+ asChild={true}>
273
+ <IconButton size="small"
274
+ color="inherit"
275
+ onClick={() => onPropertyRemove(propertyKey, namespace)}>
276
+ <RemoveIcon size={"small"}/>
277
+ </IconButton>
278
+ </Tooltip>}
279
+
280
+ {onPropertyMove && <Tooltip title={"Move"}
281
+ asChild={true}>
282
+ <IconButton
283
+ component={"span"}
284
+ size="small"
285
+ {...attributes}
286
+ {...listeners}
287
+ >
288
+ <DragHandleIcon size={"small"}/>
289
+ </IconButton>
290
+ </Tooltip>}
291
+
292
+ </div>
293
+
294
+ {subtree && <div className={"ml-16"}>{subtree}</div>}
295
+ </div>
251
296
  </div>
252
297
  );
253
-
254
298
  }
@@ -107,7 +107,7 @@ export function MapPropertyField({ disabled, getData, allowDataInference, proper
107
107
  <div className={"col-span-12"}>
108
108
  <BooleanSwitchWithLabel
109
109
  position={"start"}
110
- size={"small"}
110
+ size={"medium"}
111
111
  label="Spread children as columns"
112
112
  onValueChange={(v) => setFieldValue("spreadChildren", v)}
113
113
  value={values.spreadChildren ?? false}
@@ -19,6 +19,7 @@ export function AdvancedPropertyValidation({ disabled }: {
19
19
  {({ field, form }: FormexFieldProps) => {
20
20
  return <SwitchControl
21
21
  label={"Hide from collection"}
22
+ size={"medium"}
22
23
  disabled={disabled}
23
24
  form={form}
24
25
  tooltip={"Hide this field from the collection view. It will still be visible in the form view"}
@@ -33,6 +34,7 @@ export function AdvancedPropertyValidation({ disabled }: {
33
34
  {({ field, form }: FormexFieldProps) => {
34
35
  return <SwitchControl
35
36
  label={"Read only"}
37
+ size={"medium"}
36
38
  disabled={disabled}
37
39
  tooltip={"Is this a read only field. Display only as a preview"}
38
40
  form={form}
@@ -41,8 +41,6 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
41
41
  icon: React.ReactNode
42
42
  };
43
43
 
44
- getPathSuggestions?: (path?: string) => Promise<string[]>;
45
-
46
44
  collectionInference?: CollectionInference;
47
45
 
48
46
  getData?: (path: string, parentPaths: string[]) => Promise<object[]>;
@@ -51,6 +49,8 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
51
49
 
52
50
  onAnalyticsEvent?: (event: string, params?: object) => void;
53
51
 
52
+ includeIntroView?: boolean;
53
+
54
54
  }
55
55
 
56
56
  /**
@@ -70,11 +70,11 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
70
70
  configPermissions,
71
71
  reservedGroups,
72
72
  extraView,
73
- getPathSuggestions,
74
73
  getUser,
75
74
  collectionInference,
76
75
  getData,
77
76
  onAnalyticsEvent,
77
+ includeIntroView = true
78
78
  }: CollectionConfigControllerProps<EC, USER>): FireCMSPlugin<any, any, PersistedCollection> {
79
79
 
80
80
  return {
@@ -88,7 +88,6 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
88
88
  collectionInference,
89
89
  reservedGroups,
90
90
  extraView,
91
- getPathSuggestions,
92
91
  getUser,
93
92
  getData,
94
93
  onAnalyticsEvent,
@@ -96,7 +95,7 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
96
95
  },
97
96
  homePage: {
98
97
  additionalActions: <NewCollectionButton/>,
99
- additionalChildrenStart: <IntroWidget/>,
98
+ additionalChildrenStart: includeIntroView ? <IntroWidget/> : undefined,
100
99
  // additionalChildrenEnd: <RootCollectionSuggestions introMode={introMode}/>,
101
100
  CollectionActions: HomePageEditorCollectionAction,
102
101
  AdditionalCards: NewCollectionCard,
@@ -110,7 +109,7 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
110
109
  };
111
110
  }
112
111
 
113
- export function IntroWidget({}: {}) {
112
+ export function IntroWidget() {
114
113
 
115
114
  const navigation = useNavigationController();
116
115
  if (!navigation.topLevelNavigation)
@@ -1,4 +1,5 @@
1
1
  import {
2
+ EntityCallbacks,
2
3
  EntityCollection,
3
4
  joinCollectionLists,
4
5
  makePropertiesEditable,