@firecms/collection_editor 3.0.0-alpha.17 → 3.0.0-alpha.19

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 (64) hide show
  1. package/package.json +4 -3
  2. package/src/ConfigControllerProvider.tsx +177 -0
  3. package/src/components/EditorCollectionAction.tsx +95 -0
  4. package/src/components/HomePageEditorCollectionAction.tsx +81 -0
  5. package/src/components/NewCollectionCard.tsx +45 -0
  6. package/src/components/RootCollectionSuggestions.tsx +53 -0
  7. package/src/components/collection_editor/CollectionDetailsForm.tsx +312 -0
  8. package/src/components/collection_editor/CollectionEditorDialog.tsx +640 -0
  9. package/src/components/collection_editor/CollectionEditorWelcomeView.tsx +212 -0
  10. package/src/components/collection_editor/CollectionPropertiesEditorForm.tsx +450 -0
  11. package/src/components/collection_editor/CollectionYupValidation.tsx +6 -0
  12. package/src/components/collection_editor/EntityCustomViewsSelectDialog.tsx +29 -0
  13. package/src/components/collection_editor/EnumForm.tsx +354 -0
  14. package/src/components/collection_editor/PropertyEditView.tsx +535 -0
  15. package/src/components/collection_editor/PropertyFieldPreview.tsx +205 -0
  16. package/src/components/collection_editor/PropertySelectItem.tsx +31 -0
  17. package/src/components/collection_editor/PropertyTree.tsx +228 -0
  18. package/src/components/collection_editor/SelectIcons.tsx +72 -0
  19. package/src/components/collection_editor/SubcollectionsEditTab.tsx +239 -0
  20. package/src/components/collection_editor/UnsavedChangesDialog.tsx +47 -0
  21. package/src/components/collection_editor/import/CollectionEditorImportDataPreview.tsx +37 -0
  22. package/src/components/collection_editor/import/CollectionEditorImportMapping.tsx +236 -0
  23. package/src/components/collection_editor/import/clean_import_data.ts +53 -0
  24. package/src/components/collection_editor/properties/BlockPropertyField.tsx +131 -0
  25. package/src/components/collection_editor/properties/BooleanPropertyField.tsx +36 -0
  26. package/src/components/collection_editor/properties/CommonPropertyFields.tsx +112 -0
  27. package/src/components/collection_editor/properties/DateTimePropertyField.tsx +86 -0
  28. package/src/components/collection_editor/properties/EnumPropertyField.tsx +116 -0
  29. package/src/components/collection_editor/properties/FieldHelperView.tsx +13 -0
  30. package/src/components/collection_editor/properties/KeyValuePropertyField.tsx +20 -0
  31. package/src/components/collection_editor/properties/MapPropertyField.tsx +154 -0
  32. package/src/components/collection_editor/properties/NumberPropertyField.tsx +38 -0
  33. package/src/components/collection_editor/properties/ReferencePropertyField.tsx +184 -0
  34. package/src/components/collection_editor/properties/RepeatPropertyField.tsx +115 -0
  35. package/src/components/collection_editor/properties/StoragePropertyField.tsx +194 -0
  36. package/src/components/collection_editor/properties/StringPropertyField.tsx +85 -0
  37. package/src/components/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +36 -0
  38. package/src/components/collection_editor/properties/validation/ArrayPropertyValidation.tsx +50 -0
  39. package/src/components/collection_editor/properties/validation/GeneralPropertyValidation.tsx +49 -0
  40. package/src/components/collection_editor/properties/validation/NumberPropertyValidation.tsx +99 -0
  41. package/src/components/collection_editor/properties/validation/StringPropertyValidation.tsx +131 -0
  42. package/src/components/collection_editor/properties/validation/ValidationPanel.tsx +28 -0
  43. package/src/components/collection_editor/templates/blog_template.ts +115 -0
  44. package/src/components/collection_editor/templates/products_template.ts +89 -0
  45. package/src/components/collection_editor/templates/users_template.ts +34 -0
  46. package/src/components/collection_editor/util.ts +21 -0
  47. package/src/components/collection_editor/utils/supported_fields.tsx +28 -0
  48. package/src/components/collection_editor/utils/update_property_for_widget.ts +258 -0
  49. package/src/components/collection_editor/utils/useTraceUpdate.tsx +23 -0
  50. package/src/index.ts +31 -0
  51. package/src/types/collection_editor_controller.tsx +31 -0
  52. package/src/types/collection_inference.ts +3 -0
  53. package/src/types/config_controller.tsx +30 -0
  54. package/src/types/config_permissions.ts +20 -0
  55. package/src/types/persisted_collection.ts +7 -0
  56. package/src/useCollectionEditorController.tsx +9 -0
  57. package/src/useCollectionEditorPlugin.tsx +103 -0
  58. package/src/useCollectionsConfigController.tsx +9 -0
  59. package/src/utils/arrays.ts +3 -0
  60. package/src/utils/entities.ts +38 -0
  61. package/src/utils/icons.ts +17 -0
  62. package/src/utils/join_collections.ts +144 -0
  63. package/src/utils/synonyms.ts +1952 -0
  64. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,205 @@
1
+ import {
2
+ cardClickableMixin,
3
+ cardMixin,
4
+ cardSelectedMixin,
5
+ cn,
6
+ ErrorBoundary,
7
+ FieldConfigBadge,
8
+ FunctionsIcon,
9
+ getFieldConfig,
10
+ isPropertyBuilder,
11
+ Paper,
12
+ Property,
13
+ PropertyOrBuilder,
14
+ RemoveCircleIcon,
15
+ Typography,
16
+ useFireCMSContext
17
+ } from "@firecms/core";
18
+
19
+ import { editableProperty } from "../../utils/entities";
20
+
21
+ export function PropertyFieldPreview({
22
+ property,
23
+ onClick,
24
+ hasError,
25
+ includeName,
26
+ includeEditButton,
27
+ selected
28
+ }: {
29
+ property: Property,
30
+ hasError?: boolean,
31
+ selected?: boolean,
32
+ includeName?: boolean,
33
+ includeEditButton?: boolean;
34
+ onClick?: () => void
35
+ }) {
36
+
37
+ const { fields } = useFireCMSContext();
38
+
39
+ const fieldConfig = getFieldConfig(property, fields);
40
+ const disabled = !editableProperty(property);
41
+
42
+ const borderColorClass = hasError
43
+ ? "border-red-500"
44
+ : (selected ? "border-blue-500" : "border-transparent");
45
+
46
+ return <ErrorBoundary>
47
+ <div
48
+ onClick={onClick}
49
+ className="flex flex-row w-full cursor-pointer">
50
+ <div className={"m-4"}>
51
+ <FieldConfigBadge fieldConfig={fieldConfig}/>
52
+ </div>
53
+ <Paper
54
+ className={cn(
55
+ "pl-2 w-full flex flex-row gap-4 items-center",
56
+ cardMixin,
57
+ onClick ? cardClickableMixin : "",
58
+ selected ? cardSelectedMixin : "",
59
+ "flex-grow p-4 border transition-colors duration-200",
60
+ borderColorClass
61
+ )}
62
+ >
63
+
64
+ <div className="w-full flex flex-col">
65
+
66
+ {includeName &&
67
+ <ErrorBoundary>
68
+ <Typography variant="body1"
69
+ component="span"
70
+ className="flex-grow pr-2">
71
+ {property.name
72
+ ? property.name
73
+ : "\u00a0"
74
+ }
75
+ </Typography>
76
+ </ErrorBoundary>}
77
+
78
+ <div className="flex flex-row items-center">
79
+ <ErrorBoundary>
80
+ <Typography className="flex-grow pr-2"
81
+ variant={includeName ? "body2" : "subtitle1"}
82
+ component="span"
83
+ color="secondary">
84
+ {fieldConfig?.name}
85
+ </Typography>
86
+ </ErrorBoundary>
87
+ <ErrorBoundary>
88
+ <Typography variant="body2"
89
+ component="span"
90
+ color="disabled">
91
+ {property.dataType}
92
+ </Typography>
93
+ </ErrorBoundary>
94
+
95
+ {disabled && <div
96
+ className="text-xs h-3 ml-0.5">
97
+ <RemoveCircleIcon color={"disabled"}/>
98
+ </div>}
99
+ </div>
100
+ </div>
101
+
102
+ {includeEditButton && <Typography variant={"button"}>
103
+ EDIT
104
+ </Typography>}
105
+
106
+ </Paper>
107
+ </div>
108
+ </ErrorBoundary>
109
+ }
110
+
111
+ export function NonEditablePropertyPreview({
112
+ name,
113
+ selected,
114
+ onClick,
115
+ property
116
+ }: {
117
+ name: string,
118
+ selected: boolean,
119
+ onClick?: () => void,
120
+ property?: PropertyOrBuilder
121
+ }) {
122
+
123
+ const { fields } = useFireCMSContext();
124
+
125
+ const fieldConfig = !isPropertyBuilder(property) && property ? getFieldConfig(property, fields) : undefined;
126
+
127
+ return (
128
+ <div
129
+ onClick={onClick}
130
+ className="flex flex-row w-full cursor-pointer">
131
+ <div className={"relative m-4"}>
132
+ {fieldConfig && <FieldConfigBadge fieldConfig={fieldConfig}/>}
133
+ {!fieldConfig && <div
134
+ className={"h-8 w-8 p-1 rounded-full shadow text-white bg-gray-500"}>
135
+ <FunctionsIcon color={"inherit"} size={"medium"}/>
136
+ </div>}
137
+ <RemoveCircleIcon color={"disabled"} size={"small"} className={"absolute -right-2 -top-2"}/>
138
+ </div>
139
+ <Paper
140
+ className={cn(
141
+ "pl-2 w-full flex flex-row gap-4 items-center",
142
+ cardMixin,
143
+ onClick ? cardClickableMixin : "",
144
+ selected ? cardSelectedMixin : "",
145
+ "flex-grow p-4 border transition-colors duration-200",
146
+ selected ? "border-blue-500" : "border-transparent")}
147
+ >
148
+
149
+ <div className="w-full flex flex-col">
150
+ <Typography variant="body1"
151
+ component="span"
152
+ className="flex-grow pr-2">
153
+ {property?.name
154
+ ? property.name
155
+ : name
156
+ }
157
+ </Typography>
158
+
159
+ <div className="flex flex-row items-center">
160
+ {fieldConfig && <Typography className="flex-grow pr-2"
161
+ variant={"body2"}
162
+ component="span"
163
+ color="secondary">
164
+ {fieldConfig?.name}
165
+ </Typography>}
166
+
167
+ {property && !isPropertyBuilder(property) && <ErrorBoundary>
168
+ <Typography variant="body2"
169
+ component="span"
170
+ color="disabled">
171
+ {property.dataType}
172
+ </Typography>
173
+ </ErrorBoundary>}
174
+
175
+ {property && isPropertyBuilder(property) && <ErrorBoundary>
176
+ <Typography variant="body2"
177
+ component="span"
178
+ color="disabled">
179
+ This property is defined as a property builder in code
180
+ </Typography>
181
+ </ErrorBoundary>}
182
+
183
+ {!property && <ErrorBoundary>
184
+ <Typography variant="body2"
185
+ component="span"
186
+ color="disabled">
187
+ This field is defined as an additional field in code
188
+ </Typography>
189
+ </ErrorBoundary>}
190
+
191
+ </div>
192
+
193
+ {/*<div className="flex flex-row text-xs">*/}
194
+ {/* <Typography className="flex-grow pr-2"*/}
195
+ {/* variant="body2"*/}
196
+ {/* component="span"*/}
197
+ {/* color="secondary">*/}
198
+ {/* This field can only be edited in code*/}
199
+ {/* </Typography>*/}
200
+ {/*</div>*/}
201
+ </div>
202
+
203
+ </Paper>
204
+ </div>)
205
+ }
@@ -0,0 +1,31 @@
1
+ import { cn, FieldConfig, FieldConfigBadge, SelectItem, Typography } from "@firecms/core";
2
+
3
+ export interface PropertySelectItemProps {
4
+ value: string;
5
+ optionDisabled: boolean;
6
+ fieldConfig: FieldConfig;
7
+ existing: boolean;
8
+ }
9
+
10
+ export function PropertySelectItem({ value, optionDisabled, fieldConfig, existing }: PropertySelectItemProps) {
11
+ return <SelectItem value={value}
12
+ disabled={optionDisabled}
13
+ className={"flex flex-row items-center"}>
14
+ <div
15
+ className={cn(
16
+ "flex flex-row items-center text-base min-h-[52px]",
17
+ optionDisabled ? "w-full" : "")}>
18
+ <div className={"mr-8"}>
19
+ <FieldConfigBadge fieldConfig={fieldConfig}/>
20
+ </div>
21
+ <div>
22
+ <div>{fieldConfig.name}</div>
23
+ <Typography variant={"caption"}
24
+ color={"disabled"}
25
+ className={"max-w-sm"}>
26
+ {existing && optionDisabled ? "You can only switch to widgets that use the same data type" : fieldConfig.description}
27
+ </Typography>
28
+ </div>
29
+ </div>
30
+ </SelectItem>
31
+ }
@@ -0,0 +1,228 @@
1
+ import {
2
+ AdditionalFieldDelegate,
3
+ AutoAwesomeIcon,
4
+ CMSType,
5
+ defaultBorderMixin,
6
+ DragHandleIcon,
7
+ ErrorBoundary,
8
+ IconButton, isPropertyBuilder,
9
+ PropertiesOrBuilders, PropertyOrBuilder,
10
+ RemoveIcon,
11
+ Tooltip
12
+ } from "@firecms/core";
13
+ import { NonEditablePropertyPreview, PropertyFieldPreview } from "./PropertyFieldPreview";
14
+ import { DragDropContext, Draggable, DraggableProvided, Droppable } from "@hello-pangea/dnd";
15
+ import { getFullId, idToPropertiesPath } from "./util";
16
+ import { getIn } from "formik";
17
+ import { editableProperty } from "../../utils/entities";
18
+ import { useCallback } from "react";
19
+
20
+ export function PropertyTree<M extends {
21
+ [Key: string]: CMSType
22
+ }>({
23
+ namespace,
24
+ selectedPropertyKey,
25
+ onPropertyClick,
26
+ properties,
27
+ propertiesOrder: propertiesOrderProp,
28
+ additionalFields,
29
+ errors,
30
+ onPropertyMove,
31
+ onPropertyRemove,
32
+ className,
33
+ inferredPropertyKeys
34
+ }: {
35
+ namespace?: string;
36
+ selectedPropertyKey?: string;
37
+ onPropertyClick?: (propertyKey: string, namespace?: string) => void;
38
+ properties: PropertiesOrBuilders<M>;
39
+ propertiesOrder?: string[];
40
+ additionalFields?: AdditionalFieldDelegate<M>[];
41
+ errors: Record<string, any>;
42
+ onPropertyMove?: (propertiesOrder: string[], namespace?: string) => void;
43
+ onPropertyRemove?: (propertyKey: string, namespace?: string) => void;
44
+ className?: string;
45
+ inferredPropertyKeys?: string[];
46
+ }) {
47
+
48
+ const propertiesOrder = propertiesOrderProp ?? Object.keys(properties);
49
+
50
+ const onDragEnd = useCallback((result: any) => {
51
+ // dropped outside the list
52
+ if (!result.destination) {
53
+ return;
54
+ }
55
+ const startIndex = result.source.index;
56
+ const endIndex = result.destination.index;
57
+
58
+ const newPropertiesOrder = Array.from(propertiesOrder);
59
+ const [removed] = newPropertiesOrder.splice(startIndex, 1);
60
+ newPropertiesOrder.splice(endIndex, 0, removed);
61
+ if (onPropertyMove)
62
+ onPropertyMove(newPropertiesOrder, namespace);
63
+ }, [namespace, onPropertyMove, propertiesOrder])
64
+
65
+ return (
66
+ <>
67
+
68
+ <DragDropContext onDragEnd={onDragEnd}>
69
+ <Droppable droppableId={`droppable_${namespace}`}>
70
+ {(droppableProvided, droppableSnapshot) => (
71
+ <div
72
+ {...droppableProvided.droppableProps}
73
+ ref={droppableProvided.innerRef}
74
+ className={className}>
75
+ {propertiesOrder && propertiesOrder
76
+ // .filter((propertyKey) => Boolean(properties[propertyKey]))
77
+ .map((propertyKey: string, index: number) => {
78
+ const property = properties[propertyKey] as PropertyOrBuilder;
79
+ const additionalField = additionalFields?.find(field => field.id === 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
+ />
106
+ </ErrorBoundary>
107
+ );
108
+ }}
109
+ </Draggable>);
110
+ }).filter(Boolean)}
111
+
112
+ {droppableProvided.placeholder}
113
+
114
+ </div>
115
+ )}
116
+ </Droppable>
117
+ </DragDropContext>
118
+
119
+ </>
120
+ );
121
+ }
122
+
123
+ export function PropertyTreeEntry({
124
+ propertyKey,
125
+ namespace,
126
+ propertyOrBuilder,
127
+ additionalField,
128
+ provided,
129
+ selectedPropertyKey,
130
+ errors,
131
+ onPropertyClick,
132
+ onPropertyMove,
133
+ onPropertyRemove,
134
+ inferredPropertyKeys
135
+ }: {
136
+ propertyKey: string;
137
+ namespace?: string;
138
+ propertyOrBuilder: PropertyOrBuilder;
139
+ additionalField?: AdditionalFieldDelegate<any>;
140
+ selectedPropertyKey?: string;
141
+ provided: DraggableProvided;
142
+ errors: Record<string, any>;
143
+ onPropertyClick?: (propertyKey: string, namespace?: string) => void;
144
+ onPropertyMove?: (propertiesOrder: string[], namespace?: string) => void;
145
+ onPropertyRemove?: (propertyKey: string, namespace?: string) => void;
146
+ inferredPropertyKeys?: string[];
147
+ }) {
148
+
149
+ const isPropertyInferred = inferredPropertyKeys?.includes(namespace ? `${namespace}.${propertyKey}` : propertyKey);
150
+
151
+ const fullId = getFullId(propertyKey, namespace);
152
+ let subtree;
153
+ if (typeof propertyOrBuilder === "object") {
154
+ const property = propertyOrBuilder;
155
+ if (property.dataType === "map" && property.properties) {
156
+ subtree = <PropertyTree
157
+ selectedPropertyKey={selectedPropertyKey}
158
+ namespace={fullId}
159
+ properties={property.properties}
160
+ propertiesOrder={property.propertiesOrder}
161
+ errors={errors}
162
+ onPropertyClick={onPropertyClick}
163
+ onPropertyMove={onPropertyMove}
164
+ onPropertyRemove={onPropertyRemove}/>
165
+ }
166
+ }
167
+
168
+ const hasError = fullId ? getIn(errors, idToPropertiesPath(fullId)) : false;
169
+ const selected = selectedPropertyKey === fullId;
170
+ const editable = propertyOrBuilder && editableProperty(propertyOrBuilder);
171
+
172
+ return (
173
+ <div
174
+ ref={provided.innerRef}
175
+ {...provided.draggableProps}
176
+ {...provided.dragHandleProps}
177
+ className="relative -ml-8"
178
+ >
179
+ {subtree && <div
180
+ className={"absolute border-l " + defaultBorderMixin}
181
+ style={{
182
+ left: "32px",
183
+ top: "64px",
184
+ bottom: "16px"
185
+ }}/>}
186
+
187
+ {!isPropertyBuilder(propertyOrBuilder) && !additionalField && editable
188
+ ? <PropertyFieldPreview
189
+ property={propertyOrBuilder}
190
+ onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
191
+ includeName={true}
192
+ selected={selected}
193
+ hasError={hasError}/>
194
+ : <NonEditablePropertyPreview name={propertyKey}
195
+ property={propertyOrBuilder}
196
+ onClick={onPropertyClick ? () => onPropertyClick(propertyKey, namespace) : undefined}
197
+ selected={selected}/>}
198
+
199
+ <div className="absolute top-2 right-2 flex flex-row ">
200
+
201
+ {isPropertyInferred && <Tooltip title={"Inferred property"}>
202
+ <AutoAwesomeIcon size="small" className={"p-2"}/>
203
+ </Tooltip>}
204
+
205
+ {onPropertyRemove && <Tooltip title={"Remove"}>
206
+ <IconButton size="small"
207
+ color="inherit"
208
+ onClick={() => onPropertyRemove(propertyKey, namespace)}>
209
+ <RemoveIcon size={"small"}/>
210
+ </IconButton>
211
+ </Tooltip>}
212
+
213
+ {onPropertyMove && <Tooltip title={"Move"}>
214
+ <IconButton
215
+ component={"span"}
216
+ size="small"
217
+ >
218
+ <DragHandleIcon size={"small"}/>
219
+ </IconButton>
220
+ </Tooltip>}
221
+ </div>
222
+
223
+
224
+ {subtree && <div className={"ml-16"}>{subtree}</div>}
225
+ </div>
226
+ );
227
+
228
+ }
@@ -0,0 +1,72 @@
1
+ import * as React from "react";
2
+ import synonyms from "../../utils/synonyms";
3
+ import { iconsSearch } from "../../utils/icons";
4
+ import { coolIconKeys, debounce, Icon, IconButton, iconKeys, SearchBar, Tooltip } from "@firecms/core";
5
+
6
+ const UPDATE_SEARCH_INDEX_WAIT_MS = 220;
7
+
8
+ if (process.env.NODE_ENV !== "production") {
9
+ Object.keys(synonyms).forEach((icon: string) => {
10
+ if (!iconKeys.includes(icon)) {
11
+ console.warn(`The icon ${icon} no longer exists. Remove it from \`synonyms\``);
12
+ }
13
+ });
14
+ }
15
+
16
+ interface SearchIconsProps {
17
+ selectedIcon?: string;
18
+ onIconSelected: (icon: string) => void;
19
+ }
20
+
21
+ export function SearchIcons({ selectedIcon = "", onIconSelected }: SearchIconsProps) {
22
+ const [keys, setKeys] = React.useState<string[] | null>(null);
23
+ const [query, setQuery] = React.useState<string>("");
24
+
25
+ const updateSearchResults = React.useMemo(() =>
26
+ debounce((value: string) => {
27
+ if (!value || value === "") {
28
+ setKeys(null);
29
+ } else {
30
+ const searchResult = iconsSearch.search(value);
31
+ setKeys(searchResult.map((e:any) => e.key));
32
+ }
33
+ }, UPDATE_SEARCH_INDEX_WAIT_MS), []
34
+ );
35
+
36
+ React.useEffect(() => {
37
+ updateSearchResults(query);
38
+ return () => {
39
+ updateSearchResults.clear();
40
+ };
41
+ }, [query, updateSearchResults]);
42
+
43
+ const icons = keys === null ? coolIconKeys : keys;
44
+
45
+ return (
46
+ <>
47
+ <SearchBar
48
+ autoFocus
49
+ className={"w-full sticky top-0 z-10"}
50
+ onTextSearch={(value?: string) => setQuery(value ?? "")}
51
+ placeholder="Search for more icons…"
52
+ />
53
+
54
+ <div className={"flex max-w-full flex-wrap mt-4"}>
55
+ {icons.map((icon: string) => {
56
+ return (
57
+ <Tooltip title={icon} key={icon}>
58
+ <IconButton
59
+ shape={"square"}
60
+ toggled={selectedIcon === icon}
61
+ onClick={() => onIconSelected(icon)}
62
+ className="box-content m-1"
63
+ >
64
+ <Icon iconKey={icon} size={24} />
65
+ </IconButton>
66
+ </Tooltip>
67
+ );
68
+ })}
69
+ </div>
70
+ </>
71
+ );
72
+ }