@firecms/collection_editor 3.0.0 → 3.1.0-canary.1df3b2c
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/ConfigControllerProvider.d.ts +6 -0
- package/dist/api/generateCollectionApi.d.ts +71 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.es.js +9677 -5837
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9653 -5813
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collection_editor_controller.d.ts +14 -0
- package/dist/types/collection_inference.d.ts +8 -2
- package/dist/types/config_controller.d.ts +31 -1
- package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
- package/dist/ui/KanbanSetupAction.d.ts +10 -0
- package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -0
- package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -1
- package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
- package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
- package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
- package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
- package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
- package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
- package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
- package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
- package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
- package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
- package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
- package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
- package/dist/useCollectionEditorPlugin.d.ts +7 -1
- package/dist/utils/validateCollectionJson.d.ts +22 -0
- package/package.json +11 -11
- package/src/ConfigControllerProvider.tsx +81 -47
- package/src/api/generateCollectionApi.ts +119 -0
- package/src/api/index.ts +1 -0
- package/src/index.ts +28 -1
- package/src/types/collection_editor_controller.tsx +16 -3
- package/src/types/collection_inference.ts +15 -2
- package/src/types/config_controller.tsx +37 -1
- package/src/ui/AddKanbanColumnAction.tsx +203 -0
- package/src/ui/EditorCollectionActionStart.tsx +1 -2
- package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
- package/src/ui/KanbanSetupAction.tsx +38 -0
- package/src/ui/MissingReferenceWidget.tsx +1 -1
- package/src/ui/NewCollectionButton.tsx +1 -1
- package/src/ui/PropertyAddColumnComponent.tsx +1 -1
- package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -258
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -173
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
- package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
- package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
- package/src/ui/collection_editor/EnumForm.tsx +147 -100
- package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
- package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
- package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
- package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
- package/src/ui/collection_editor/PropertyTree.tsx +130 -58
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
- package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +1 -5
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +23 -2
- package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
- package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
- package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
- package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
- package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
- package/src/useCollectionEditorPlugin.tsx +32 -17
- package/src/utils/validateCollectionJson.ts +380 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
EntityCollection,
|
|
4
|
+
getPropertyInPath,
|
|
5
|
+
ResolvedStringProperty,
|
|
6
|
+
resolveCollection,
|
|
7
|
+
resolveEnumValues,
|
|
8
|
+
toSnakeCase,
|
|
9
|
+
useAuthController,
|
|
10
|
+
useCustomizationController
|
|
11
|
+
} from "@firecms/core";
|
|
12
|
+
import {
|
|
13
|
+
AddIcon,
|
|
14
|
+
Button,
|
|
15
|
+
cls,
|
|
16
|
+
defaultBorderMixin,
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogActions,
|
|
19
|
+
DialogContent,
|
|
20
|
+
IconButton,
|
|
21
|
+
TextField,
|
|
22
|
+
Typography
|
|
23
|
+
} from "@firecms/ui";
|
|
24
|
+
import { useCollectionsConfigController } from "../useCollectionsConfigController";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Component rendered at the end of the Kanban board to add new columns (enum values).
|
|
28
|
+
* Opens a dialog to input a new enum value for the column property.
|
|
29
|
+
*/
|
|
30
|
+
export function AddKanbanColumnAction({
|
|
31
|
+
collection,
|
|
32
|
+
fullPath,
|
|
33
|
+
parentCollectionIds,
|
|
34
|
+
columnProperty
|
|
35
|
+
}: {
|
|
36
|
+
collection: EntityCollection;
|
|
37
|
+
fullPath: string;
|
|
38
|
+
parentCollectionIds: string[];
|
|
39
|
+
columnProperty: string;
|
|
40
|
+
}) {
|
|
41
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
42
|
+
const [newValueLabel, setNewValueLabel] = useState("");
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
|
|
45
|
+
const configController = useCollectionsConfigController();
|
|
46
|
+
const authController = useAuthController();
|
|
47
|
+
const customizationController = useCustomizationController();
|
|
48
|
+
|
|
49
|
+
const resolvedCollection = useMemo(() => resolveCollection({
|
|
50
|
+
collection,
|
|
51
|
+
path: fullPath,
|
|
52
|
+
propertyConfigs: customizationController.propertyConfigs,
|
|
53
|
+
authController
|
|
54
|
+
}), [collection, fullPath, customizationController.propertyConfigs, authController]);
|
|
55
|
+
|
|
56
|
+
// Get current enum values
|
|
57
|
+
const currentEnumValues = useMemo(() => {
|
|
58
|
+
const property = getPropertyInPath(resolvedCollection.properties, columnProperty);
|
|
59
|
+
if (!property || !('dataType' in property) || property.dataType !== "string") {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const stringProperty = property as ResolvedStringProperty;
|
|
63
|
+
if (!stringProperty.enumValues) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return resolveEnumValues(stringProperty.enumValues) ?? [];
|
|
67
|
+
}, [resolvedCollection, columnProperty]);
|
|
68
|
+
|
|
69
|
+
const handleAddColumn = useCallback(async () => {
|
|
70
|
+
if (!newValueLabel.trim() || !configController) return;
|
|
71
|
+
|
|
72
|
+
setSaving(true);
|
|
73
|
+
try {
|
|
74
|
+
// Check for property in persisted collection first, then resolved collection
|
|
75
|
+
// This handles code-defined properties that aren't in the persisted config
|
|
76
|
+
let property = collection?.properties?.[columnProperty];
|
|
77
|
+
let isCodeDefinedProperty = false;
|
|
78
|
+
|
|
79
|
+
if (!property || typeof property === 'function') {
|
|
80
|
+
// Property not in persisted config - check resolved collection
|
|
81
|
+
property = resolvedCollection.properties?.[columnProperty];
|
|
82
|
+
isCodeDefinedProperty = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Type guard: property must be an object with dataType === "string"
|
|
86
|
+
if (!property || typeof property === 'function' || !('dataType' in property) || property.dataType !== "string") {
|
|
87
|
+
console.error("Column property not found or not a string. Property:", property);
|
|
88
|
+
setSaving(false);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Now we know property is a StringProperty
|
|
93
|
+
const stringProperty = property as { dataType: "string"; enumValues?: any; name?: string };
|
|
94
|
+
|
|
95
|
+
// Create new enum value
|
|
96
|
+
const newId = toSnakeCase(newValueLabel.trim());
|
|
97
|
+
const newEnumValue = {
|
|
98
|
+
id: newId,
|
|
99
|
+
label: newValueLabel.trim()
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Get existing enum values from the resolved property (current runtime values)
|
|
103
|
+
// Use currentEnumValues which is already computed from resolvedCollection
|
|
104
|
+
const existingEnumValues = currentEnumValues.map(ev => ({
|
|
105
|
+
id: ev.id,
|
|
106
|
+
label: ev.label
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// Add new enum value
|
|
110
|
+
const updatedEnumValues = [...existingEnumValues, newEnumValue];
|
|
111
|
+
|
|
112
|
+
// Build the property to save
|
|
113
|
+
// If it's code-defined, we create a minimal override with just enumValues
|
|
114
|
+
const updatedProperty = isCodeDefinedProperty
|
|
115
|
+
? {
|
|
116
|
+
dataType: "string" as const,
|
|
117
|
+
name: stringProperty.name || columnProperty,
|
|
118
|
+
enumValues: updatedEnumValues
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
...property,
|
|
122
|
+
enumValues: updatedEnumValues
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Save the updated property
|
|
126
|
+
await configController.saveProperty({
|
|
127
|
+
path: fullPath,
|
|
128
|
+
propertyKey: columnProperty,
|
|
129
|
+
property: updatedProperty as any,
|
|
130
|
+
parentCollectionIds
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
setNewValueLabel("");
|
|
134
|
+
setDialogOpen(false);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("Error adding new column:", error);
|
|
137
|
+
} finally {
|
|
138
|
+
setSaving(false);
|
|
139
|
+
}
|
|
140
|
+
}, [newValueLabel, configController, collection, columnProperty, fullPath, parentCollectionIds]);
|
|
141
|
+
|
|
142
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
143
|
+
if (e.key === "Enter" && newValueLabel.trim()) {
|
|
144
|
+
handleAddColumn();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<div
|
|
151
|
+
className={cls(
|
|
152
|
+
"border h-full w-80 min-w-80 mx-2 flex flex-col items-center justify-center rounded-md",
|
|
153
|
+
"bg-surface-50 dark:bg-surface-950 hover:bg-surface-100 dark:hover:bg-surface-900",
|
|
154
|
+
"cursor-pointer transition-colors duration-200 ease-in-out",
|
|
155
|
+
defaultBorderMixin
|
|
156
|
+
)}
|
|
157
|
+
onClick={() => setDialogOpen(true)}
|
|
158
|
+
>
|
|
159
|
+
<IconButton size="large" className="opacity-60 hover:opacity-100">
|
|
160
|
+
<AddIcon size="large" />
|
|
161
|
+
</IconButton>
|
|
162
|
+
<Typography variant="caption" color="secondary" className="mt-2">
|
|
163
|
+
Add Column
|
|
164
|
+
</Typography>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
168
|
+
<DialogContent className="max-w-md">
|
|
169
|
+
<Typography variant="h6" className="mb-4">
|
|
170
|
+
Add New Column
|
|
171
|
+
</Typography>
|
|
172
|
+
<Typography variant="body2" color="secondary" className="mb-4">
|
|
173
|
+
Add a new option to the "{columnProperty}" property.
|
|
174
|
+
This will create a new column in the Kanban board.
|
|
175
|
+
</Typography>
|
|
176
|
+
<TextField
|
|
177
|
+
label="Column Name"
|
|
178
|
+
value={newValueLabel}
|
|
179
|
+
onChange={(e) => setNewValueLabel(e.target.value)}
|
|
180
|
+
onKeyDown={handleKeyDown}
|
|
181
|
+
autoFocus
|
|
182
|
+
disabled={saving}
|
|
183
|
+
/>
|
|
184
|
+
</DialogContent>
|
|
185
|
+
<DialogActions>
|
|
186
|
+
<Button
|
|
187
|
+
variant="text"
|
|
188
|
+
onClick={() => setDialogOpen(false)}
|
|
189
|
+
disabled={saving}
|
|
190
|
+
>
|
|
191
|
+
Cancel
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
onClick={handleAddColumn}
|
|
195
|
+
disabled={saving || !newValueLabel.trim()}
|
|
196
|
+
>
|
|
197
|
+
{saving ? "Adding..." : "Add Column"}
|
|
198
|
+
</Button>
|
|
199
|
+
</DialogActions>
|
|
200
|
+
</Dialog>
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -35,7 +35,7 @@ export function EditorCollectionActionStart({
|
|
|
35
35
|
title={tableController.sortBy || tableController.filterValues ? "Save default filter and sort" : "Clear default filter and sort"}>
|
|
36
36
|
<Button
|
|
37
37
|
size={"small"}
|
|
38
|
-
variant={"
|
|
38
|
+
variant={"text"}
|
|
39
39
|
onClick={() => configController
|
|
40
40
|
?.saveCollection({
|
|
41
41
|
id: collection.id,
|
|
@@ -58,7 +58,6 @@ export function EditorCollectionActionStart({
|
|
|
58
58
|
{(collection.initialFilter || collection.initialSort) && <Tooltip
|
|
59
59
|
title={"Reset to default filter and sort"}>
|
|
60
60
|
<Button
|
|
61
|
-
color={"primary"}
|
|
62
61
|
size={"small"}
|
|
63
62
|
variant={"text"}
|
|
64
63
|
onClick={() => {
|
|
@@ -4,26 +4,31 @@ import {
|
|
|
4
4
|
useAuthController,
|
|
5
5
|
useSnackbarController
|
|
6
6
|
} from "@firecms/core";
|
|
7
|
-
import { DeleteIcon, IconButton, Menu, MenuItem, MoreVertIcon, SettingsIcon, } from "@firecms/ui";
|
|
7
|
+
import { ContentCopyIcon, DeleteIcon, IconButton, Menu, MenuItem, MoreVertIcon, SettingsIcon, } from "@firecms/ui";
|
|
8
8
|
import { useCollectionEditorController } from "../useCollectionEditorController";
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { useCollectionsConfigController } from "../useCollectionsConfigController";
|
|
11
11
|
|
|
12
12
|
export function HomePageEditorCollectionAction({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
path,
|
|
14
|
+
collection
|
|
15
|
+
}: PluginHomePageActionsProps) {
|
|
17
16
|
|
|
18
17
|
const snackbarController = useSnackbarController();
|
|
19
18
|
const authController = useAuthController();
|
|
20
19
|
const configController = useCollectionsConfigController();
|
|
21
20
|
const collectionEditorController = useCollectionEditorController();
|
|
22
21
|
|
|
23
|
-
const permissions = collectionEditorController
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const permissions = collectionEditorController?.configPermissions
|
|
23
|
+
? collectionEditorController.configPermissions({
|
|
24
|
+
user: authController.user,
|
|
25
|
+
collection
|
|
26
|
+
})
|
|
27
|
+
: {
|
|
28
|
+
createCollections: false,
|
|
29
|
+
editCollections: false,
|
|
30
|
+
deleteCollections: false
|
|
31
|
+
};
|
|
27
32
|
|
|
28
33
|
const onEditCollectionClicked = () => {
|
|
29
34
|
collectionEditorController?.editCollection({
|
|
@@ -32,6 +37,17 @@ export function HomePageEditorCollectionAction({
|
|
|
32
37
|
});
|
|
33
38
|
};
|
|
34
39
|
|
|
40
|
+
const onDuplicateCollectionClicked = () => {
|
|
41
|
+
// Use copyFrom to duplicate the collection with all properties
|
|
42
|
+
// The editor will handle clearing name, path, and id
|
|
43
|
+
collectionEditorController?.createCollection({
|
|
44
|
+
copyFrom: collection,
|
|
45
|
+
parentCollectionIds: [],
|
|
46
|
+
redirect: true,
|
|
47
|
+
sourceClick: "home_page_duplicate"
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
35
51
|
const [deleteRequested, setDeleteRequested] = useState(false);
|
|
36
52
|
|
|
37
53
|
const deleteCollection = () => {
|
|
@@ -50,9 +66,21 @@ export function HomePageEditorCollectionAction({
|
|
|
50
66
|
{permissions.deleteCollections &&
|
|
51
67
|
<Menu
|
|
52
68
|
trigger={<IconButton size={"small"}>
|
|
53
|
-
<MoreVertIcon size={"small"}/>
|
|
69
|
+
<MoreVertIcon size={"small"} />
|
|
54
70
|
</IconButton>}
|
|
55
71
|
>
|
|
72
|
+
{permissions.createCollections &&
|
|
73
|
+
<MenuItem
|
|
74
|
+
dense={true}
|
|
75
|
+
onClick={(event) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
onDuplicateCollectionClicked();
|
|
79
|
+
}}>
|
|
80
|
+
<ContentCopyIcon />
|
|
81
|
+
Duplicate
|
|
82
|
+
</MenuItem>
|
|
83
|
+
}
|
|
56
84
|
<MenuItem
|
|
57
85
|
dense={true}
|
|
58
86
|
onClick={(event) => {
|
|
@@ -60,7 +88,7 @@ export function HomePageEditorCollectionAction({
|
|
|
60
88
|
event.stopPropagation();
|
|
61
89
|
setDeleteRequested(true);
|
|
62
90
|
}}>
|
|
63
|
-
<DeleteIcon/>
|
|
91
|
+
<DeleteIcon />
|
|
64
92
|
Delete
|
|
65
93
|
</MenuItem>
|
|
66
94
|
|
|
@@ -74,7 +102,7 @@ export function HomePageEditorCollectionAction({
|
|
|
74
102
|
onClick={(event) => {
|
|
75
103
|
onEditCollectionClicked();
|
|
76
104
|
}}>
|
|
77
|
-
<SettingsIcon size={"small"}/>
|
|
105
|
+
<SettingsIcon size={"small"} />
|
|
78
106
|
</IconButton>}
|
|
79
107
|
</div>
|
|
80
108
|
|
|
@@ -85,7 +113,7 @@ export function HomePageEditorCollectionAction({
|
|
|
85
113
|
title={<>Delete this collection?</>}
|
|
86
114
|
body={<> This will <b>not
|
|
87
115
|
delete any data</b>, only
|
|
88
|
-
the collection in the CMS</>}/>
|
|
116
|
+
the collection in the CMS</>} />
|
|
89
117
|
</>;
|
|
90
118
|
|
|
91
119
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { EntityCollection, useNavigationController } from "@firecms/core";
|
|
3
|
+
import { Button } from "@firecms/ui";
|
|
4
|
+
import { useCollectionEditorController } from "../useCollectionEditorController";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Component rendered when Kanban view is missing orderProperty configuration.
|
|
8
|
+
* Provides a CTA button to open the collection editor to configure Kanban.
|
|
9
|
+
*/
|
|
10
|
+
export function KanbanSetupAction({
|
|
11
|
+
collection,
|
|
12
|
+
fullPath,
|
|
13
|
+
parentCollectionIds
|
|
14
|
+
}: {
|
|
15
|
+
collection: EntityCollection;
|
|
16
|
+
fullPath: string;
|
|
17
|
+
parentCollectionIds: string[];
|
|
18
|
+
}) {
|
|
19
|
+
const collectionEditorController = useCollectionEditorController();
|
|
20
|
+
|
|
21
|
+
const handleConfigureClick = () => {
|
|
22
|
+
collectionEditorController.editCollection({
|
|
23
|
+
id: collection.id,
|
|
24
|
+
parentCollectionIds,
|
|
25
|
+
initialView: "display",
|
|
26
|
+
expandKanban: true
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Button
|
|
32
|
+
variant="outlined"
|
|
33
|
+
onClick={handleConfigureClick}
|
|
34
|
+
>
|
|
35
|
+
Configure Kanban
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -11,7 +11,7 @@ export function MissingReferenceWidget({ path: pathProp }: {
|
|
|
11
11
|
const collectionEditor = useCollectionEditorController();
|
|
12
12
|
return <div className={"p-1 flex flex-col items-center"}>
|
|
13
13
|
<ErrorView error={"No collection for path: " + path}/>
|
|
14
|
-
<Button className={"mx-2"}
|
|
14
|
+
<Button className={"mx-2"}
|
|
15
15
|
size={"small"}
|
|
16
16
|
onClick={() => {
|
|
17
17
|
collectionEditor.createCollection({
|
|
@@ -5,7 +5,7 @@ export function NewCollectionButton() {
|
|
|
5
5
|
const collectionEditorController = useCollectionEditorController();
|
|
6
6
|
return <div className={"bg-surface-50 dark:bg-surface-900 min-w-fit rounded"}>
|
|
7
7
|
<Button className={"min-w-fit"}
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
onClick={() => collectionEditorController.createCollection({
|
|
10
10
|
parentCollectionIds: [],
|
|
11
11
|
redirect: true,
|
|
@@ -29,7 +29,7 @@ export function PropertyAddColumnComponent({
|
|
|
29
29
|
asChild={true}
|
|
30
30
|
title={canEditCollection ? "Add new property" : "You don't have permission to add new properties"}>
|
|
31
31
|
<div
|
|
32
|
-
className={"p-0.5 w-20 h-full flex items-center justify-center cursor-pointer bg-surface-100 bg-opacity-40 hover:bg-surface-100 dark:bg-surface-950 dark:bg-opacity-40 dark:hover:bg-surface-950"}
|
|
32
|
+
className={"p-0.5 w-20 h-full flex items-center justify-center cursor-pointer bg-surface-100 bg-opacity-40 bg-surface-100/40 hover:bg-surface-100 dark:bg-surface-950 dark:bg-opacity-40 dark:bg-surface-950/40 dark:hover:bg-surface-950"}
|
|
33
33
|
// className={onHover ? "bg-white dark:bg-surface-950" : undefined}
|
|
34
34
|
onClick={() => {
|
|
35
35
|
collectionEditorController.editProperty({
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { EntityCollection, useNavigationController, useSnackbarController, AIIcon } from "@firecms/core";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
CircularProgress,
|
|
6
|
+
IconButton,
|
|
7
|
+
Menu,
|
|
8
|
+
SendIcon,
|
|
9
|
+
TextField,
|
|
10
|
+
Typography
|
|
11
|
+
} from "@firecms/ui";
|
|
12
|
+
import {
|
|
13
|
+
CollectionGenerationCallback,
|
|
14
|
+
CollectionGenerationApiError,
|
|
15
|
+
CollectionOperation
|
|
16
|
+
} from "../../api/generateCollectionApi";
|
|
17
|
+
import { PersistedCollection } from "../../types/persisted_collection";
|
|
18
|
+
|
|
19
|
+
export interface AICollectionGeneratorPopoverProps {
|
|
20
|
+
/**
|
|
21
|
+
* Current collection being edited (if modifying an existing collection)
|
|
22
|
+
*/
|
|
23
|
+
existingCollection?: PersistedCollection;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback when a collection is generated or modified.
|
|
27
|
+
* Includes the collection and optionally the operations that were applied.
|
|
28
|
+
*/
|
|
29
|
+
onGenerated: (collection: EntityCollection, operations?: CollectionOperation[]) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback function for generating/modifying collections.
|
|
33
|
+
* The plugin is API-agnostic - the consumer provides the implementation.
|
|
34
|
+
*/
|
|
35
|
+
generateCollection: CollectionGenerationCallback;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional custom trigger button. If not provided, a default AI button is used.
|
|
39
|
+
*/
|
|
40
|
+
trigger?: React.ReactNode;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Size of the button
|
|
44
|
+
*/
|
|
45
|
+
size?: "small" | "medium" | "large";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether to show the label text
|
|
49
|
+
*/
|
|
50
|
+
showLabel?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function AICollectionGeneratorPopover({
|
|
54
|
+
existingCollection,
|
|
55
|
+
onGenerated,
|
|
56
|
+
generateCollection,
|
|
57
|
+
trigger,
|
|
58
|
+
size = "small",
|
|
59
|
+
showLabel = true
|
|
60
|
+
}: AICollectionGeneratorPopoverProps) {
|
|
61
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
62
|
+
const [prompt, setPrompt] = useState("");
|
|
63
|
+
const [loading, setLoading] = useState(false);
|
|
64
|
+
const [error, setError] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
const navigation = useNavigationController();
|
|
67
|
+
const snackbarController = useSnackbarController();
|
|
68
|
+
|
|
69
|
+
const existingCollections = navigation.collections ?? [];
|
|
70
|
+
|
|
71
|
+
const handleGenerate = async () => {
|
|
72
|
+
if (!prompt.trim()) return;
|
|
73
|
+
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const collectionsContext = existingCollections.map(c => ({
|
|
79
|
+
path: c.path,
|
|
80
|
+
id: c.id,
|
|
81
|
+
name: c.name,
|
|
82
|
+
properties: c.properties,
|
|
83
|
+
propertiesOrder: c.propertiesOrder
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const result = await generateCollection({
|
|
87
|
+
prompt: prompt.trim(),
|
|
88
|
+
existingCollections: collectionsContext.slice(0, 30),
|
|
89
|
+
...(existingCollection && {
|
|
90
|
+
existingCollection: {
|
|
91
|
+
path: existingCollection.path,
|
|
92
|
+
id: existingCollection.id,
|
|
93
|
+
name: existingCollection.name,
|
|
94
|
+
properties: existingCollection.properties,
|
|
95
|
+
propertiesOrder: existingCollection.propertiesOrder
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
onGenerated(result.collection, result.operations);
|
|
101
|
+
setMenuOpen(false);
|
|
102
|
+
setPrompt("");
|
|
103
|
+
snackbarController.open({
|
|
104
|
+
type: "success",
|
|
105
|
+
message: existingCollection
|
|
106
|
+
? "Collection updated with AI suggestions"
|
|
107
|
+
: "Collection generated successfully"
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error("Error generating collection:", e);
|
|
111
|
+
const errorMessage = e instanceof CollectionGenerationApiError
|
|
112
|
+
? e.message
|
|
113
|
+
: "Failed to generate collection. Please try again.";
|
|
114
|
+
setError(errorMessage);
|
|
115
|
+
snackbarController.open({
|
|
116
|
+
type: "error",
|
|
117
|
+
message: errorMessage
|
|
118
|
+
});
|
|
119
|
+
} finally {
|
|
120
|
+
setLoading(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
125
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
handleGenerate();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const defaultTrigger = showLabel ? (
|
|
132
|
+
<Button
|
|
133
|
+
variant="text"
|
|
134
|
+
size={size}
|
|
135
|
+
disabled={loading}
|
|
136
|
+
startIcon={loading
|
|
137
|
+
? <CircularProgress size="smallest" />
|
|
138
|
+
: <AIIcon size="small" />
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
AI Assist
|
|
142
|
+
</Button>
|
|
143
|
+
) : (
|
|
144
|
+
<IconButton
|
|
145
|
+
size={size}
|
|
146
|
+
disabled={loading}
|
|
147
|
+
aria-label="AI Assist"
|
|
148
|
+
>
|
|
149
|
+
{loading
|
|
150
|
+
? <CircularProgress size="smallest" />
|
|
151
|
+
: <AIIcon size="small" />
|
|
152
|
+
}
|
|
153
|
+
</IconButton>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Menu
|
|
158
|
+
open={menuOpen}
|
|
159
|
+
onOpenChange={(open) => {
|
|
160
|
+
setMenuOpen(open);
|
|
161
|
+
if (!open) {
|
|
162
|
+
setError(null);
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
165
|
+
trigger={trigger ?? defaultTrigger}
|
|
166
|
+
>
|
|
167
|
+
<div className="p-4 flex flex-col gap-3 min-w-[360px] max-w-[480px]">
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<AIIcon size="small" />
|
|
170
|
+
<Typography variant="subtitle2">
|
|
171
|
+
{existingCollection ? "Modify Collection with AI" : "Generate Collection with AI"}
|
|
172
|
+
</Typography>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<Typography variant="caption" color="secondary">
|
|
176
|
+
{existingCollection
|
|
177
|
+
? "Describe the changes you want to make to this collection."
|
|
178
|
+
: "Describe the collection you want to create."
|
|
179
|
+
}
|
|
180
|
+
</Typography>
|
|
181
|
+
|
|
182
|
+
<TextField
|
|
183
|
+
size="small"
|
|
184
|
+
multiline
|
|
185
|
+
autoFocus
|
|
186
|
+
className="w-full text-text-primary dark:text-text-primary-dark"
|
|
187
|
+
value={prompt}
|
|
188
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
189
|
+
onKeyDown={handleKeyDown}
|
|
190
|
+
placeholder={existingCollection
|
|
191
|
+
? "e.g., Add a thumbnail image field with storage, make price required..."
|
|
192
|
+
: "e.g., Create a products collection with name, price, description, and category..."
|
|
193
|
+
}
|
|
194
|
+
disabled={loading}
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
{error && (
|
|
198
|
+
<Typography variant="caption" className="text-red-500">
|
|
199
|
+
{error}
|
|
200
|
+
</Typography>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
<div className="flex justify-end gap-2">
|
|
204
|
+
<Button
|
|
205
|
+
variant="text"
|
|
206
|
+
size="small"
|
|
207
|
+
onClick={() => setMenuOpen(false)}
|
|
208
|
+
disabled={loading}
|
|
209
|
+
>
|
|
210
|
+
Cancel
|
|
211
|
+
</Button>
|
|
212
|
+
<Button
|
|
213
|
+
variant="filled"
|
|
214
|
+
size="small"
|
|
215
|
+
onClick={handleGenerate}
|
|
216
|
+
disabled={!prompt.trim() || loading}
|
|
217
|
+
startIcon={loading ? <CircularProgress size="smallest" /> : undefined}
|
|
218
|
+
>
|
|
219
|
+
{loading ? "Generating..." : "Generate"}
|
|
220
|
+
</Button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</Menu>
|
|
224
|
+
);
|
|
225
|
+
}
|