@firecms/core 3.0.0-canary.289 → 3.0.0-canary.290
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/dist/components/HomePage/HomePageDnD.d.ts +2 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/index.es.js +328 -193
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +329 -194
- package/dist/index.umd.js.map +1 -1
- package/dist/types/fields.d.ts +8 -0
- package/dist/types/properties.d.ts +32 -6
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +15 -15
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -0
- package/src/components/HomePage/DefaultHomePage.tsx +13 -9
- package/src/components/HomePage/HomePageDnD.tsx +140 -38
- package/src/components/PropertyCollectionView.tsx +5 -5
- package/src/components/SelectableTable/SelectableTable.tsx +0 -12
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
- package/src/form/EntityForm.tsx +3 -3
- package/src/form/PropertyFieldBinding.tsx +4 -4
- package/src/form/components/LocalChangesMenu.tsx +10 -6
- package/src/form/field_bindings/BlockFieldBinding.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +101 -29
- package/src/types/fields.tsx +10 -0
- package/src/types/properties.ts +35 -6
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/useStorageUploadController.tsx +71 -34
package/dist/types/fields.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export interface FieldProps<T extends CMSType = any, CustomProps = any, M extend
|
|
|
67
67
|
* Is this field part of an array
|
|
68
68
|
*/
|
|
69
69
|
partOfArray?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Is this field part of a block
|
|
72
|
+
*/
|
|
73
|
+
partOfBlock?: boolean;
|
|
70
74
|
/**
|
|
71
75
|
* Display the child properties directly, without being wrapped in an
|
|
72
76
|
* extendable panel. Note that this will also hide the title of this property.
|
|
@@ -178,6 +182,10 @@ export interface PropertyFieldBindingProps<T extends CMSType, M extends Record<s
|
|
|
178
182
|
* Is this field part of an array
|
|
179
183
|
*/
|
|
180
184
|
partOfArray?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Is this field part of a block
|
|
187
|
+
*/
|
|
188
|
+
partOfBlock?: boolean;
|
|
181
189
|
/**
|
|
182
190
|
* Display the child properties directly, without being wrapped in an
|
|
183
191
|
* extendable panel. Note that this will also hide the title of this property.
|
|
@@ -102,6 +102,10 @@ export interface BaseProperty<T extends CMSType, CustomProps = any> {
|
|
|
102
102
|
/**
|
|
103
103
|
* Should this property be editable. If set to true, the user will be able to modify the property and
|
|
104
104
|
* save the new config. The saved config will then become the source of truth.
|
|
105
|
+
* Defaults to `true.
|
|
106
|
+
* This props is only useful when you are using the collection editor to modify collection
|
|
107
|
+
* configurations from the CMS itself. You can also use the `editable` prop in the
|
|
108
|
+
* `EntityCollection` interface to disable the edition of all properties in a collection.
|
|
105
109
|
*/
|
|
106
110
|
editable?: boolean;
|
|
107
111
|
/**
|
|
@@ -617,8 +621,15 @@ export type StorageConfig = {
|
|
|
617
621
|
/**
|
|
618
622
|
* Use client side image compression and resizing
|
|
619
623
|
* Will only be applied to these MIME types: image/jpeg, image/png and image/webp
|
|
624
|
+
* @deprecated Use `imageResize` instead
|
|
620
625
|
*/
|
|
621
|
-
imageCompression?:
|
|
626
|
+
imageCompression?: ImageResize;
|
|
627
|
+
/**
|
|
628
|
+
* Advanced image resizing and cropping configuration.
|
|
629
|
+
* Applied before upload to optimize storage and bandwidth.
|
|
630
|
+
* Only applies to image MIME types: image/jpeg, image/png, image/webp
|
|
631
|
+
*/
|
|
632
|
+
imageResize?: ImageResize;
|
|
622
633
|
/**
|
|
623
634
|
* Specific metadata set in your uploaded file.
|
|
624
635
|
* For the default Firebase implementation, the values passed here are of type
|
|
@@ -730,17 +741,32 @@ export type PreviewType = "image" | "video" | "audio" | "file";
|
|
|
730
741
|
* @group Entity properties
|
|
731
742
|
*/
|
|
732
743
|
export type FileType = "image/*" | "video/*" | "audio/*" | "application/*" | "text/*" | "font/*" | string;
|
|
733
|
-
export interface
|
|
744
|
+
export interface ImageResize {
|
|
745
|
+
/**
|
|
746
|
+
* Maximum width in pixels. Image will be scaled down proportionally if wider.
|
|
747
|
+
*/
|
|
748
|
+
maxWidth?: number;
|
|
734
749
|
/**
|
|
735
|
-
*
|
|
750
|
+
* Maximum height in pixels. Image will be scaled down proportionally if taller.
|
|
736
751
|
*/
|
|
737
752
|
maxHeight?: number;
|
|
738
753
|
/**
|
|
739
|
-
*
|
|
754
|
+
* Resize mode determines how the image fits within maxWidth/maxHeight bounds.
|
|
755
|
+
* - `contain`: Scale down to fit within bounds, preserving aspect ratio (default)
|
|
756
|
+
* - `cover`: Scale to fill bounds, preserving aspect ratio (may crop)
|
|
740
757
|
*/
|
|
741
|
-
|
|
758
|
+
mode?: 'contain' | 'cover';
|
|
759
|
+
/**
|
|
760
|
+
* Output format for the resized image.
|
|
761
|
+
* - `original`: Keep the original format (default)
|
|
762
|
+
* - `jpeg`: Convert to JPEG
|
|
763
|
+
* - `png`: Convert to PNG
|
|
764
|
+
* - `webp`: Convert to WebP
|
|
765
|
+
*/
|
|
766
|
+
format?: 'original' | 'jpeg' | 'png' | 'webp';
|
|
742
767
|
/**
|
|
743
|
-
*
|
|
768
|
+
* Quality for lossy formats (JPEG, WebP). Number between 0 and 100.
|
|
769
|
+
* Higher is better quality but larger file size. Defaults to 80.
|
|
744
770
|
*/
|
|
745
771
|
quality?: number;
|
|
746
772
|
}
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { Properties
|
|
1
|
+
import { Properties } from "../types";
|
|
2
2
|
export declare function makePropertiesEditable(properties: Properties): Properties<any>;
|
|
3
|
-
export declare function makePropertiesNonEditable(properties: PropertiesOrBuilders): PropertiesOrBuilders;
|
|
@@ -31,6 +31,7 @@ export declare function useStorageUploadController<M extends object>({ entityId,
|
|
|
31
31
|
fileNameBuilder: (file: File) => Promise<string>;
|
|
32
32
|
storagePathBuilder: (file: File) => string;
|
|
33
33
|
onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?: any) => Promise<void>;
|
|
34
|
+
onFileUploadError: (entry: StorageFieldItem) => void;
|
|
34
35
|
onFilesAdded: (acceptedFiles: File[]) => Promise<void>;
|
|
35
36
|
multipleFilesSupported: boolean;
|
|
36
37
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.0-canary.
|
|
4
|
+
"version": "3.0.0-canary.290",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,11 +53,12 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/editor": "^3.0.0-canary.
|
|
57
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
59
|
-
"@radix-ui/react-portal": "^1.1.
|
|
56
|
+
"@firecms/editor": "^3.0.0-canary.290",
|
|
57
|
+
"@firecms/formex": "^3.0.0-canary.290",
|
|
58
|
+
"@firecms/ui": "^3.0.0-canary.290",
|
|
59
|
+
"@radix-ui/react-portal": "^1.1.10",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
|
+
"compressorjs": "^1.2.1",
|
|
61
62
|
"date-fns": "^3.6.0",
|
|
62
63
|
"fuse.js": "^7.1.0",
|
|
63
64
|
"history": "^5.3.0",
|
|
@@ -67,11 +68,10 @@
|
|
|
67
68
|
"prism-react-renderer": "^2.4.1",
|
|
68
69
|
"react-dropzone": "^14.3.8",
|
|
69
70
|
"react-fast-compare": "^3.2.2",
|
|
70
|
-
"react-image-file-resizer": "^0.4.8",
|
|
71
71
|
"react-transition-group": "^4.4.5",
|
|
72
72
|
"react-use-measure": "^2.1.7",
|
|
73
73
|
"react-window": "^1.8.11",
|
|
74
|
-
"vite-plugin-static-copy": "3.1.
|
|
74
|
+
"vite-plugin-static-copy": "3.1.4",
|
|
75
75
|
"yup": "^0.32.11"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"react-router-dom": "^6.28.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@jest/globals": "^30.
|
|
84
|
+
"@jest/globals": "^30.2.0",
|
|
85
85
|
"@testing-library/react": "^16.3.0",
|
|
86
86
|
"@testing-library/user-event": "^14.6.1",
|
|
87
87
|
"@types/jest": "^29.5.14",
|
|
@@ -93,22 +93,22 @@
|
|
|
93
93
|
"@vitejs/plugin-react": "^4.7.0",
|
|
94
94
|
"babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
|
|
95
95
|
"cross-env": "^7.0.3",
|
|
96
|
-
"eslint-plugin-react-compiler": "^19.
|
|
96
|
+
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
|
97
97
|
"jest": "^29.7.0",
|
|
98
98
|
"npm-run-all": "^4.1.5",
|
|
99
|
-
"react-router": "^6.30.
|
|
100
|
-
"react-router-dom": "^6.30.
|
|
101
|
-
"ts-jest": "^29.4.
|
|
99
|
+
"react-router": "^6.30.2",
|
|
100
|
+
"react-router-dom": "^6.30.2",
|
|
101
|
+
"ts-jest": "^29.4.5",
|
|
102
102
|
"ts-node": "^10.9.2",
|
|
103
103
|
"tsd": "^0.31.2",
|
|
104
|
-
"typescript": "^5.9.
|
|
105
|
-
"vite": "^7.
|
|
104
|
+
"typescript": "^5.9.3",
|
|
105
|
+
"vite": "^7.2.4"
|
|
106
106
|
},
|
|
107
107
|
"files": [
|
|
108
108
|
"dist",
|
|
109
109
|
"src"
|
|
110
110
|
],
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "80538231810a133e5b3cba8a8d4b47ec6e5619f8",
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
@@ -376,6 +376,11 @@ export const EntityCollectionView = React.memo(
|
|
|
376
376
|
console.error("Save failure");
|
|
377
377
|
console.error(e);
|
|
378
378
|
setError(e);
|
|
379
|
+
},
|
|
380
|
+
onPreSaveHookError: (e: Error) => {
|
|
381
|
+
console.error("Pre-save hook error");
|
|
382
|
+
console.error(e);
|
|
383
|
+
setError(e);
|
|
379
384
|
}
|
|
380
385
|
});
|
|
381
386
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import Fuse from "fuse.js";
|
|
3
3
|
import { Container, SearchBar } from "@firecms/ui";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
useCollapsedGroups,
|
|
6
|
+
useCustomizationController,
|
|
7
|
+
useFireCMSContext,
|
|
8
|
+
useNavigationController
|
|
9
|
+
} from "../../hooks";
|
|
6
10
|
import {
|
|
7
11
|
CMSAnalyticsEvent,
|
|
8
12
|
NavigationEntry,
|
|
@@ -143,7 +147,6 @@ export function DefaultHomePage({
|
|
|
143
147
|
allProcessed = allProcessed.filter(
|
|
144
148
|
(g) =>
|
|
145
149
|
g.entries.length ||
|
|
146
|
-
groupOrderFromNavController.includes(g.name) ||
|
|
147
150
|
(g.name === DEFAULT_GROUP_NAME && hasPluginAdditionalCards)
|
|
148
151
|
);
|
|
149
152
|
}
|
|
@@ -177,6 +180,7 @@ export function DefaultHomePage({
|
|
|
177
180
|
const persistNavigationGroups = (
|
|
178
181
|
latest: { name: string; entries: NavigationEntry[] }[]
|
|
179
182
|
) => {
|
|
183
|
+
// Map ALL groups including "Views"
|
|
180
184
|
const draggable: NavigationGroupMapping[] = latest.map((g) => ({
|
|
181
185
|
name: g.name,
|
|
182
186
|
entries: g.entries.map((e) => e.path)
|
|
@@ -221,13 +225,14 @@ export function DefaultHomePage({
|
|
|
221
225
|
dialogOpenForGroup,
|
|
222
226
|
setDialogOpenForGroup,
|
|
223
227
|
handleRenameGroup,
|
|
228
|
+
handleDialogClose,
|
|
224
229
|
isHoveringNewGroupDropZone,
|
|
225
230
|
setIsHoveringNewGroupDropZone
|
|
226
231
|
} = useHomePageDnd({
|
|
227
232
|
items,
|
|
228
233
|
setItems: updateItems,
|
|
229
234
|
disabled: !allowDragAndDrop || performingSearch,
|
|
230
|
-
onPersist: persistNavigationGroups,
|
|
235
|
+
onPersist: persistNavigationGroups,
|
|
231
236
|
onGroupMoved: (g) =>
|
|
232
237
|
context.analyticsController?.onAnalyticsEvent?.("home_move_group", {
|
|
233
238
|
name: g
|
|
@@ -355,7 +360,7 @@ export function DefaultHomePage({
|
|
|
355
360
|
items={containers}
|
|
356
361
|
strategy={verticalListSortingStrategy}
|
|
357
362
|
>
|
|
358
|
-
{items.map((groupData) => {
|
|
363
|
+
{items.map((groupData, groupIndex) => {
|
|
359
364
|
const groupKey = groupData.name;
|
|
360
365
|
const entriesInGroup = groupData.entries;
|
|
361
366
|
|
|
@@ -378,14 +383,13 @@ export function DefaultHomePage({
|
|
|
378
383
|
|
|
379
384
|
if (
|
|
380
385
|
entriesInGroup.length === 0 &&
|
|
381
|
-
(AdditionalCards.length === 0 || performingSearch)
|
|
382
|
-
!groupOrderFromNavController.includes(groupKey)
|
|
386
|
+
(AdditionalCards.length === 0 || performingSearch)
|
|
383
387
|
)
|
|
384
388
|
return null;
|
|
385
389
|
|
|
386
390
|
return (
|
|
387
391
|
<SortableNavigationGroup
|
|
388
|
-
key={
|
|
392
|
+
key={`group-${groupIndex}`}
|
|
389
393
|
groupName={groupKey}
|
|
390
394
|
disabled={dndDisabled}
|
|
391
395
|
>
|
|
@@ -557,7 +561,7 @@ export function DefaultHomePage({
|
|
|
557
561
|
existingGroupNames={items
|
|
558
562
|
.map((g) => g.name)
|
|
559
563
|
.filter((n) => n !== dialogOpenForGroup)}
|
|
560
|
-
onClose={
|
|
564
|
+
onClose={handleDialogClose}
|
|
561
565
|
onRename={(newName) => {
|
|
562
566
|
handleRenameGroup(dialogOpenForGroup, newName);
|
|
563
567
|
}}
|
|
@@ -163,13 +163,9 @@ export function SortableNavigationGroup({
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
167
|
-
/* Main DnD hook */
|
|
168
|
-
|
|
169
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
170
166
|
export function useHomePageDnd({
|
|
171
|
-
items
|
|
172
|
-
setItems
|
|
167
|
+
items,
|
|
168
|
+
setItems,
|
|
173
169
|
disabled,
|
|
174
170
|
onCardMovedBetweenGroups,
|
|
175
171
|
onGroupMoved,
|
|
@@ -191,6 +187,9 @@ export function useHomePageDnd({
|
|
|
191
187
|
onPersist?: (latest: { name: string; entries: NavigationEntry[] }[]) => void;
|
|
192
188
|
}) {
|
|
193
189
|
/* ---------------- local state ---------------- */
|
|
190
|
+
const dndItems = items;
|
|
191
|
+
const setDndItems = setItems;
|
|
192
|
+
|
|
194
193
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
|
195
194
|
const [activeIsGroup, setActiveIsGroup] = useState(false);
|
|
196
195
|
const [currentDraggingGroupId, setCurrentDraggingGroupId] =
|
|
@@ -200,6 +199,15 @@ export function useHomePageDnd({
|
|
|
200
199
|
const [dialogOpenForGroup, setDialogOpenForGroup] = useState<string | null>(null);
|
|
201
200
|
const [isHoveringNewGroupDropZone, setIsHoveringNewGroupDropZone] =
|
|
202
201
|
useState(false);
|
|
202
|
+
const [pendingNewGroupName, setPendingNewGroupName] = useState<string | null>(null);
|
|
203
|
+
const [stateBeforeNewGroup, setStateBeforeNewGroup] = useState<
|
|
204
|
+
{ name: string; entries: NavigationEntry[] }[] | null
|
|
205
|
+
>(null);
|
|
206
|
+
|
|
207
|
+
/* store the original state before any drag modifications */
|
|
208
|
+
const preDragItemsRef = useRef<
|
|
209
|
+
{ name: string; entries: NavigationEntry[] }[] | null
|
|
210
|
+
>(null);
|
|
203
211
|
|
|
204
212
|
/* store interim state for cross-group moves */
|
|
205
213
|
const interimItemsRef = useRef<
|
|
@@ -321,6 +329,9 @@ export function useHomePageDnd({
|
|
|
321
329
|
setDndKitActiveNode(active);
|
|
322
330
|
if (disabled) return;
|
|
323
331
|
|
|
332
|
+
// Capture the original state before any drag modifications
|
|
333
|
+
preDragItemsRef.current = cloneItemsForDnd(dndItems);
|
|
334
|
+
|
|
324
335
|
const isGroup = dndItems.some((g) => g.name === active.id);
|
|
325
336
|
if (!active.data.current) active.data.current = {};
|
|
326
337
|
active.data.current.type = isGroup ? "group" : "item";
|
|
@@ -349,18 +360,35 @@ export function useHomePageDnd({
|
|
|
349
360
|
|
|
350
361
|
if (overCont && activeCont !== overCont) {
|
|
351
362
|
recentlyMovedToNewContainer.current = true;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
363
|
+
lastOverId.current = overIdNow;
|
|
364
|
+
|
|
365
|
+
// Update state for visual feedback during drag
|
|
366
|
+
setDndItems((current) => {
|
|
367
|
+
const newState = cloneItemsForDnd(current);
|
|
368
|
+
const srcIdx = newState.findIndex((g) => g.name === activeCont);
|
|
369
|
+
const tgtIdx = newState.findIndex((g) => g.name === overCont);
|
|
370
|
+
if (srcIdx === -1 || tgtIdx === -1) return current;
|
|
371
|
+
const src = newState[srcIdx];
|
|
372
|
+
const tgt = newState[tgtIdx];
|
|
373
|
+
const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
|
|
374
|
+
if (idxInSrc === -1) return current;
|
|
375
|
+
const [moved] = src.entries.splice(idxInSrc, 1);
|
|
376
|
+
|
|
377
|
+
// Calculate insertion position - SAME logic as handleDragEnd
|
|
378
|
+
const overIsContainer = overIdNow === overCont;
|
|
379
|
+
if (overIsContainer) {
|
|
380
|
+
tgt.entries.push(moved);
|
|
381
|
+
} else {
|
|
382
|
+
const overIdx = tgt.entries.findIndex((e) => e.url === overIdNow);
|
|
383
|
+
if (overIdx !== -1) {
|
|
384
|
+
tgt.entries.splice(overIdx, 0, moved);
|
|
385
|
+
} else {
|
|
386
|
+
tgt.entries.push(moved);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return newState;
|
|
391
|
+
});
|
|
364
392
|
} else if (activeCont === overCont) {
|
|
365
393
|
recentlyMovedToNewContainer.current = false;
|
|
366
394
|
}
|
|
@@ -396,11 +424,26 @@ export function useHomePageDnd({
|
|
|
396
424
|
}
|
|
397
425
|
/* ─── card move ─── */
|
|
398
426
|
else {
|
|
399
|
-
|
|
427
|
+
// CRITICAL: Find source container from ORIGINAL pre-drag state, not current (potentially stale) dndItems
|
|
428
|
+
const findContainerInState = (id: string, state: { name: string; entries: NavigationEntry[] }[]): string | undefined => {
|
|
429
|
+
const group = state.find((g) => g.name === id);
|
|
430
|
+
if (group) return group.name;
|
|
431
|
+
for (const g of state) {
|
|
432
|
+
if (g.entries.some((e) => e.url === id)) return g.name;
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const sourceState = preDragItemsRef.current || dndItems;
|
|
438
|
+
const activeCont = findContainerInState(activeIdNow as string, sourceState);
|
|
439
|
+
const overCont = findDndContainer(overIdNow);
|
|
400
440
|
|
|
401
441
|
/* drop on new-group zone */
|
|
402
442
|
if (overIdNow === "new-group-drop-zone") {
|
|
403
443
|
if (activeCont) {
|
|
444
|
+
// Save current state before making changes
|
|
445
|
+
setStateBeforeNewGroup(cloneItemsForDnd(dndItems));
|
|
446
|
+
|
|
404
447
|
const newState = cloneItemsForDnd(dndItems);
|
|
405
448
|
const srcIdx = newState.findIndex((g) => g.name === activeCont);
|
|
406
449
|
if (srcIdx !== -1) {
|
|
@@ -421,8 +464,10 @@ export function useHomePageDnd({
|
|
|
421
464
|
name: tentative,
|
|
422
465
|
entries: [dragged]
|
|
423
466
|
});
|
|
467
|
+
|
|
468
|
+
// Update local state but DON'T persist yet
|
|
424
469
|
setDndItems(newState);
|
|
425
|
-
|
|
470
|
+
setPendingNewGroupName(tentative);
|
|
426
471
|
setDialogOpenForGroup(tentative);
|
|
427
472
|
onNewGroupDrop?.();
|
|
428
473
|
}
|
|
@@ -459,18 +504,59 @@ export function useHomePageDnd({
|
|
|
459
504
|
onPersist?.(newState);
|
|
460
505
|
}
|
|
461
506
|
}
|
|
462
|
-
} else if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
) {
|
|
466
|
-
onPersist?.(interimItemsRef.current);
|
|
467
|
-
}
|
|
507
|
+
} else if (overCont && activeCont !== overCont) {
|
|
508
|
+
// Card moved between different groups - use CLEAN pre-drag state
|
|
509
|
+
const finalState = cloneItemsForDnd(sourceState);
|
|
468
510
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
511
|
+
// Find target container from clean state too
|
|
512
|
+
const finalOverId = lastOverId.current || overIdNow;
|
|
513
|
+
const cleanOverCont = findContainerInState(finalOverId as string, sourceState) || overCont;
|
|
514
|
+
|
|
515
|
+
const srcIdx = finalState.findIndex((g) => g.name === activeCont);
|
|
516
|
+
const tgtIdx = finalState.findIndex((g) => g.name === cleanOverCont);
|
|
517
|
+
|
|
518
|
+
if (srcIdx !== -1 && tgtIdx !== -1) {
|
|
519
|
+
const src = finalState[srcIdx];
|
|
520
|
+
const tgt = finalState[tgtIdx];
|
|
521
|
+
const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
|
|
522
|
+
|
|
523
|
+
if (idxInSrc !== -1) {
|
|
524
|
+
// Remove from source
|
|
525
|
+
const [moved] = src.entries.splice(idxInSrc, 1);
|
|
526
|
+
|
|
527
|
+
// Calculate insertion position in target
|
|
528
|
+
const overIsContainer = finalOverId === cleanOverCont;
|
|
529
|
+
if (overIsContainer) {
|
|
530
|
+
tgt.entries.push(moved);
|
|
531
|
+
} else {
|
|
532
|
+
const overIdx = tgt.entries.findIndex((e) => e.url === finalOverId);
|
|
533
|
+
if (overIdx !== -1) {
|
|
534
|
+
tgt.entries.splice(overIdx, 0, moved);
|
|
535
|
+
} else {
|
|
536
|
+
tgt.entries.push(moved);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Remove empty source group if needed
|
|
541
|
+
if (src.entries.length === 0) {
|
|
542
|
+
finalState.splice(srcIdx, 1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
setDndItems(finalState);
|
|
546
|
+
onPersist?.(finalState);
|
|
547
|
+
|
|
548
|
+
onCardMovedBetweenGroups?.(moved);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else if (recentlyMovedToNewContainer.current) {
|
|
552
|
+
// This shouldn't happen but log it for debugging
|
|
553
|
+
console.error("Move between containers detected but conditions not met", {
|
|
554
|
+
activeCont,
|
|
555
|
+
overCont,
|
|
556
|
+
activeIdNow,
|
|
557
|
+
overIdNow
|
|
558
|
+
});
|
|
559
|
+
}
|
|
474
560
|
}
|
|
475
561
|
}
|
|
476
562
|
|
|
@@ -501,9 +587,29 @@ export function useHomePageDnd({
|
|
|
501
587
|
...updated[idx],
|
|
502
588
|
name: newName
|
|
503
589
|
};
|
|
504
|
-
|
|
590
|
+
|
|
591
|
+
// Persist after successful rename
|
|
592
|
+
onPersist?.(updated);
|
|
505
593
|
return updated;
|
|
506
594
|
});
|
|
595
|
+
|
|
596
|
+
// Clear all pending state
|
|
597
|
+
setPendingNewGroupName(null);
|
|
598
|
+
setStateBeforeNewGroup(null);
|
|
599
|
+
setDialogOpenForGroup(null);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/* Handle dialog close without renaming */
|
|
603
|
+
const handleDialogClose = () => {
|
|
604
|
+
// If there's a pending new group that wasn't renamed, restore previous state
|
|
605
|
+
if (pendingNewGroupName && dialogOpenForGroup === pendingNewGroupName && stateBeforeNewGroup) {
|
|
606
|
+
// Restore the state from before the new group was created
|
|
607
|
+
setDndItems(stateBeforeNewGroup);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Clear all pending state
|
|
611
|
+
setPendingNewGroupName(null);
|
|
612
|
+
setStateBeforeNewGroup(null);
|
|
507
613
|
setDialogOpenForGroup(null);
|
|
508
614
|
};
|
|
509
615
|
|
|
@@ -535,15 +641,12 @@ export function useHomePageDnd({
|
|
|
535
641
|
dialogOpenForGroup,
|
|
536
642
|
setDialogOpenForGroup,
|
|
537
643
|
handleRenameGroup,
|
|
644
|
+
handleDialogClose,
|
|
538
645
|
isHoveringNewGroupDropZone,
|
|
539
646
|
setIsHoveringNewGroupDropZone
|
|
540
647
|
};
|
|
541
648
|
}
|
|
542
649
|
|
|
543
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
544
|
-
/* New-group drop-zone component */
|
|
545
|
-
|
|
546
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
547
650
|
export function NewGroupDropZone({
|
|
548
651
|
disabled,
|
|
549
652
|
setIsHovering
|
|
@@ -588,8 +691,7 @@ export function NewGroupDropZone({
|
|
|
588
691
|
isOver
|
|
589
692
|
? "bg-surface-accent-100 dark:bg-surface-accent-800 border-surface-300 dark:border-surface-600"
|
|
590
693
|
: "bg-surface-50 dark:bg-surface-900 border-surface-200 dark:border-surface-700"
|
|
591
|
-
)}
|
|
592
|
-
>
|
|
694
|
+
)}>
|
|
593
695
|
<div className="text-center p-4">
|
|
594
696
|
<span className="block font-medium text-sm">
|
|
595
697
|
Drop here to create a new group
|
|
@@ -131,7 +131,7 @@ export const PropertyCollectionView = ({
|
|
|
131
131
|
if (baseKey && property && isArrayOfPrimitives) {
|
|
132
132
|
return (
|
|
133
133
|
<div
|
|
134
|
-
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
134
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
135
135
|
<div className="col-span-4 pr-2">
|
|
136
136
|
<Typography variant="caption"
|
|
137
137
|
color={"secondary"}
|
|
@@ -152,7 +152,7 @@ export const PropertyCollectionView = ({
|
|
|
152
152
|
|
|
153
153
|
// Array of maps or unknown -> array header + combined item header (MapName [index]) then content
|
|
154
154
|
return (
|
|
155
|
-
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
155
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
156
156
|
{baseKey && arrayLabel && !suppressHeader && (
|
|
157
157
|
<Typography variant="caption"
|
|
158
158
|
color={"secondary"}
|
|
@@ -211,7 +211,7 @@ export const PropertyCollectionView = ({
|
|
|
211
211
|
if (!property) return null;
|
|
212
212
|
return (
|
|
213
213
|
<div
|
|
214
|
-
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
214
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
215
215
|
<div className="col-span-4 pr-2">
|
|
216
216
|
<Typography variant="caption"
|
|
217
217
|
color={"secondary"}
|
|
@@ -240,7 +240,7 @@ export const PropertyCollectionView = ({
|
|
|
240
240
|
const headerText = property?.name || label;
|
|
241
241
|
|
|
242
242
|
return (
|
|
243
|
-
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
243
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
244
244
|
{showMapHeader && (
|
|
245
245
|
<Typography variant="caption"
|
|
246
246
|
color={"secondary"}
|
|
@@ -277,7 +277,7 @@ export const PropertyCollectionView = ({
|
|
|
277
277
|
if (!property) return null;
|
|
278
278
|
return (
|
|
279
279
|
<div
|
|
280
|
-
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin}` : ""}`}>
|
|
280
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
281
281
|
<div className="col-span-4 pr-2">
|
|
282
282
|
<Typography variant="caption"
|
|
283
283
|
color={"secondary"}
|
|
@@ -327,15 +327,3 @@ function createFilterField({
|
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
function filterableProperty(property: ResolvedProperty, partOfArray = false): boolean {
|
|
331
|
-
if (partOfArray) {
|
|
332
|
-
return ["string", "number", "date", "reference"].includes(property.dataType);
|
|
333
|
-
}
|
|
334
|
-
if (property.dataType === "array") {
|
|
335
|
-
if (property.of)
|
|
336
|
-
return filterableProperty(property.of, true);
|
|
337
|
-
else
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
return ["string", "number", "boolean", "date", "reference", "array"].includes(property.dataType);
|
|
341
|
-
}
|
|
@@ -55,7 +55,7 @@ export function DateTimeFilterField({
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
setOperation(op);
|
|
58
|
-
setInternalValue(newValue
|
|
58
|
+
setInternalValue(newValue);
|
|
59
59
|
|
|
60
60
|
const hasNewValue = newValue !== null && Array.isArray(newValue)
|
|
61
61
|
? newValue.length > 0
|
|
@@ -96,6 +96,7 @@ export function DateTimeFilterField({
|
|
|
96
96
|
mode={mode}
|
|
97
97
|
size={"large"}
|
|
98
98
|
locale={locale}
|
|
99
|
+
disabled={internalValue === null}
|
|
99
100
|
value={internalValue ?? undefined}
|
|
100
101
|
onChange={(dateValue: Date | null) => {
|
|
101
102
|
updateFilter(operation, dateValue === null ? undefined : dateValue);
|
|
@@ -139,7 +139,6 @@ export function StringNumberFilterField({
|
|
|
139
139
|
updateFilter(operation, dataType === "number" ? parseInt(value as string) : value as string)
|
|
140
140
|
}}
|
|
141
141
|
endAdornment={internalValue && <IconButton
|
|
142
|
-
className="absolute right-2 top-3"
|
|
143
142
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
144
143
|
<CloseIcon/>
|
|
145
144
|
</IconButton>}
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -857,13 +857,13 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
857
857
|
/>}
|
|
858
858
|
|
|
859
859
|
{formex.dirty
|
|
860
|
-
? <Tooltip title={"
|
|
861
|
-
<Chip size={"small"} colorScheme={"orangeDarker"}>
|
|
860
|
+
? <Tooltip title={"This form has been modified"}>
|
|
861
|
+
<Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
|
|
862
862
|
<EditIcon size={"smallest"}/>
|
|
863
863
|
</Chip>
|
|
864
864
|
</Tooltip>
|
|
865
865
|
: <Tooltip title={"The current form is in sync with the database"}>
|
|
866
|
-
<Chip size={"small"}>
|
|
866
|
+
<Chip size={"small"} className={"py-1"} >
|
|
867
867
|
<CheckIcon size={"smallest"}/>
|
|
868
868
|
</Chip>
|
|
869
869
|
</Tooltip>}
|