@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.
Files changed (91) hide show
  1. package/dist/ConfigControllerProvider.d.ts +6 -0
  2. package/dist/api/generateCollectionApi.d.ts +71 -0
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.es.js +9677 -5837
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +9653 -5813
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/types/collection_editor_controller.d.ts +14 -0
  10. package/dist/types/collection_inference.d.ts +8 -2
  11. package/dist/types/config_controller.d.ts +31 -1
  12. package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
  13. package/dist/ui/KanbanSetupAction.d.ts +10 -0
  14. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -0
  15. package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
  16. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
  17. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +20 -0
  18. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -1
  19. package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
  20. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
  21. package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
  22. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
  23. package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
  24. package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
  25. package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
  26. package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
  27. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
  28. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
  29. package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
  30. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
  31. package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
  32. package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
  33. package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
  34. package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
  35. package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
  36. package/dist/useCollectionEditorPlugin.d.ts +7 -1
  37. package/dist/utils/validateCollectionJson.d.ts +22 -0
  38. package/package.json +11 -11
  39. package/src/ConfigControllerProvider.tsx +81 -47
  40. package/src/api/generateCollectionApi.ts +119 -0
  41. package/src/api/index.ts +1 -0
  42. package/src/index.ts +28 -1
  43. package/src/types/collection_editor_controller.tsx +16 -3
  44. package/src/types/collection_inference.ts +15 -2
  45. package/src/types/config_controller.tsx +37 -1
  46. package/src/ui/AddKanbanColumnAction.tsx +203 -0
  47. package/src/ui/EditorCollectionActionStart.tsx +1 -2
  48. package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
  49. package/src/ui/KanbanSetupAction.tsx +38 -0
  50. package/src/ui/MissingReferenceWidget.tsx +1 -1
  51. package/src/ui/NewCollectionButton.tsx +1 -1
  52. package/src/ui/PropertyAddColumnComponent.tsx +1 -1
  53. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
  54. package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
  55. package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -258
  56. package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -173
  57. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
  58. package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
  59. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
  60. package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
  61. package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
  62. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
  63. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
  64. package/src/ui/collection_editor/EnumForm.tsx +147 -100
  65. package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
  66. package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
  67. package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
  68. package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
  69. package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
  70. package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
  72. package/src/ui/collection_editor/PropertyTree.tsx +130 -58
  73. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
  74. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
  75. package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
  76. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
  77. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
  78. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
  79. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
  80. package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
  81. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
  82. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +1 -5
  83. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +23 -2
  84. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
  85. package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
  86. package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
  87. package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
  88. package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
  89. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
  90. package/src/useCollectionEditorPlugin.tsx +32 -17
  91. 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={"outlined"}
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
- path,
14
- collection
15
- }: PluginHomePageActionsProps) {
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.configPermissions({
24
- user: authController.user,
25
- collection
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"} variant={"outlined"}
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
- variant={"outlined"}
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
+ }