@firecms/core 3.0.0-rc.1 → 3.0.0-rc.3
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/components/PropertyCollectionView.d.ts +23 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/core/FireCMS.d.ts +0 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/index.es.js +1983 -650
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1981 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/types/collections.d.ts +13 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/firecms.d.ts +15 -0
- package/dist/types/firecms_context.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +41 -6
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/user.d.ts +1 -1
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -1
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +21 -34
- package/src/components/HomePage/HomePageDnD.tsx +143 -83
- package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/PropertyConfigBadge.tsx +2 -2
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/common/useColumnsIds.tsx +1 -8
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/core/FireCMS.tsx +22 -13
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/index.tsx +3 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +6 -2
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/preview/PropertyPreview.tsx +8 -0
- package/src/preview/components/ReferencePreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +14 -0
- package/src/types/entities.ts +7 -1
- package/src/types/firecms.tsx +16 -0
- package/src/types/firecms_context.tsx +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/plugins.tsx +3 -0
- package/src/types/properties.ts +45 -6
- package/src/types/property_config.tsx +1 -0
- package/src/types/user.ts +1 -1
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entities.ts +1 -1
- package/src/util/entity_cache.ts +72 -53
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/objects.ts +40 -2
- package/src/util/useStorageUploadController.tsx +71 -34
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { defaultBorderMixin, Typography } from "@firecms/ui";
|
|
3
|
+
import { PreviewSize, PropertyPreview } from "../preview";
|
|
4
|
+
import { ResolvedProperties, ResolvedProperty } from "../types";
|
|
5
|
+
import { getValueInPath } from "../util";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a readable label for a path and resolve the property
|
|
9
|
+
* Supports map and array (including arrays of maps)
|
|
10
|
+
*/
|
|
11
|
+
export function buildPropertyLabelAndGetProperty(
|
|
12
|
+
properties: ResolvedProperties,
|
|
13
|
+
key: string
|
|
14
|
+
): { label: string; property: ResolvedProperty | undefined } {
|
|
15
|
+
if (!key) return {
|
|
16
|
+
label: "",
|
|
17
|
+
property: undefined
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Parse "a[0].b.c[2]" -> ["a", 0, "b", "c", 2]
|
|
21
|
+
const segments: (string | number)[] = [];
|
|
22
|
+
const re = /([^[.\]]+)|\[(\d+)\]/g;
|
|
23
|
+
let m: RegExpExecArray | null;
|
|
24
|
+
while ((m = re.exec(key)) !== null) {
|
|
25
|
+
if (m[1] !== undefined) segments.push(m[1]);
|
|
26
|
+
else if (m[2] !== undefined) segments.push(Number(m[2]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let currentProps: ResolvedProperties | undefined = properties;
|
|
30
|
+
let currentProp: ResolvedProperty | undefined;
|
|
31
|
+
let lastLabel = "";
|
|
32
|
+
|
|
33
|
+
const getArrayOfProp = (p?: ResolvedProperty): ResolvedProperty | undefined => {
|
|
34
|
+
if (!p || p.dataType !== "array") return undefined;
|
|
35
|
+
return Array.isArray(p.of) ? (p.of[0] as ResolvedProperty) : (p.of as ResolvedProperty | undefined);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const seg of segments) {
|
|
39
|
+
if (typeof seg === "number") {
|
|
40
|
+
// Last segment label should be the index itself
|
|
41
|
+
lastLabel = `[${seg}]`;
|
|
42
|
+
|
|
43
|
+
// Move schema context into the array element
|
|
44
|
+
if (currentProp?.dataType === "array") {
|
|
45
|
+
currentProp = getArrayOfProp(currentProp);
|
|
46
|
+
if (currentProp?.dataType === "map" && currentProp.properties) {
|
|
47
|
+
currentProps = currentProp.properties as ResolvedProperties;
|
|
48
|
+
} else {
|
|
49
|
+
currentProps = undefined;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Index without array schema context
|
|
53
|
+
currentProp = undefined;
|
|
54
|
+
currentProps = undefined;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// seg is a string key
|
|
60
|
+
if (currentProps && (currentProps as any)[seg]) {
|
|
61
|
+
const nextProp = (currentProps as any)[seg] as ResolvedProperty;
|
|
62
|
+
currentProp = nextProp;
|
|
63
|
+
// Last segment label should be the property name (or the raw key)
|
|
64
|
+
lastLabel = nextProp.name || String(seg);
|
|
65
|
+
|
|
66
|
+
if (nextProp.dataType === "map" && nextProp.properties) {
|
|
67
|
+
currentProps = nextProp.properties as ResolvedProperties;
|
|
68
|
+
} else if (nextProp.dataType === "array") {
|
|
69
|
+
// Keep array prop; the next segment (index) will step into its element schema
|
|
70
|
+
currentProps = undefined;
|
|
71
|
+
} else {
|
|
72
|
+
currentProps = undefined;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Unknown key or no schema context
|
|
76
|
+
currentProp = undefined;
|
|
77
|
+
currentProps = undefined;
|
|
78
|
+
lastLabel = String(seg);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
label: lastLabel,
|
|
84
|
+
property: currentProp
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pathEndsWithIndex = (p: string) => /\[\d+\]$/.test(p);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Improved simple layout for nested changes:
|
|
92
|
+
* - Map or Array-of-Map -> section header + indented rows
|
|
93
|
+
* - Leaf or Array-of-Primitives -> single row with label and value
|
|
94
|
+
*/
|
|
95
|
+
export const PropertyCollectionView = ({
|
|
96
|
+
data,
|
|
97
|
+
properties,
|
|
98
|
+
baseKey = "",
|
|
99
|
+
suppressHeader = false,
|
|
100
|
+
size = "small"
|
|
101
|
+
}: {
|
|
102
|
+
data: any;
|
|
103
|
+
properties: ResolvedProperties;
|
|
104
|
+
baseKey?: string;
|
|
105
|
+
suppressHeader?: boolean;
|
|
106
|
+
size?: PreviewSize;
|
|
107
|
+
}) => {
|
|
108
|
+
|
|
109
|
+
const isTopLevel = !!baseKey && !baseKey.includes(".") && !baseKey.includes("[");
|
|
110
|
+
|
|
111
|
+
// Arrays
|
|
112
|
+
if (Array.isArray(data)) {
|
|
113
|
+
const {
|
|
114
|
+
label: arrayLabel,
|
|
115
|
+
property
|
|
116
|
+
} = baseKey
|
|
117
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
118
|
+
: {
|
|
119
|
+
label: "",
|
|
120
|
+
property: undefined as ResolvedProperty | undefined
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ofProp = property?.dataType === "array"
|
|
124
|
+
? (Array.isArray(property.of) ? property.of[0] : property.of) as ResolvedProperty | undefined
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
const isArrayOfMaps = ofProp?.dataType === "map";
|
|
128
|
+
const isArrayOfPrimitives = property?.dataType === "array" && ofProp && ofProp.dataType !== "map";
|
|
129
|
+
|
|
130
|
+
// Array of primitives -> single row
|
|
131
|
+
if (baseKey && property && isArrayOfPrimitives) {
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
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
|
+
<div className="col-span-4 pr-2">
|
|
136
|
+
<Typography variant="caption"
|
|
137
|
+
color={"secondary"}
|
|
138
|
+
component={"span"}
|
|
139
|
+
className="break-words">
|
|
140
|
+
{arrayLabel}
|
|
141
|
+
</Typography>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="col-span-8">
|
|
144
|
+
<PropertyPreview propertyKey={baseKey}
|
|
145
|
+
value={data}
|
|
146
|
+
property={property}
|
|
147
|
+
size={size}/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Array of maps or unknown -> array header + combined item header (MapName [index]) then content
|
|
154
|
+
return (
|
|
155
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
156
|
+
{baseKey && arrayLabel && !suppressHeader && (
|
|
157
|
+
<Typography variant="caption"
|
|
158
|
+
color={"secondary"}
|
|
159
|
+
component={"span"}>
|
|
160
|
+
{arrayLabel}
|
|
161
|
+
</Typography>
|
|
162
|
+
)}
|
|
163
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
164
|
+
{data.map((item, index) => {
|
|
165
|
+
if (item === null || item === undefined) return null;
|
|
166
|
+
const currentKey = baseKey ? `${baseKey}[${index}]` : `[${index}]`;
|
|
167
|
+
|
|
168
|
+
// Combined header text
|
|
169
|
+
const itemHeader = isArrayOfMaps && ofProp?.name
|
|
170
|
+
? `${ofProp.name} [${index}]`
|
|
171
|
+
: `[${index}]`;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div key={currentKey} className="py-1">
|
|
175
|
+
<Typography variant="caption"
|
|
176
|
+
color={"secondary"}
|
|
177
|
+
component={"span"}>
|
|
178
|
+
{itemHeader}
|
|
179
|
+
</Typography>
|
|
180
|
+
<div className={`pl-4 mt-1 border-l ${defaultBorderMixin}`}>
|
|
181
|
+
<PropertyCollectionView
|
|
182
|
+
data={item}
|
|
183
|
+
properties={properties}
|
|
184
|
+
baseKey={currentKey}
|
|
185
|
+
suppressHeader={true} // don’t repeat the inner map header
|
|
186
|
+
size={size}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Objects (maps or plain objects)
|
|
198
|
+
if (typeof data === "object" && data !== null) {
|
|
199
|
+
const {
|
|
200
|
+
label,
|
|
201
|
+
property
|
|
202
|
+
} = baseKey
|
|
203
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
204
|
+
: {
|
|
205
|
+
label: "",
|
|
206
|
+
property: undefined as ResolvedProperty | undefined
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Non-map leaf-like object -> single row
|
|
210
|
+
if (baseKey && (!property || property.dataType !== "map" || !property.properties)) {
|
|
211
|
+
if (!property) return null;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
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
|
+
<div className="col-span-4 pr-2">
|
|
216
|
+
<Typography variant="caption"
|
|
217
|
+
color={"secondary"}
|
|
218
|
+
component={"span"}
|
|
219
|
+
className="break-words">
|
|
220
|
+
{label}
|
|
221
|
+
</Typography>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="col-span-8">
|
|
224
|
+
<PropertyPreview propertyKey={baseKey}
|
|
225
|
+
value={data}
|
|
226
|
+
property={property}
|
|
227
|
+
size={size}/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Map with defined properties -> show map header only if not suppressed
|
|
234
|
+
const showMapHeader =
|
|
235
|
+
baseKey &&
|
|
236
|
+
!suppressHeader &&
|
|
237
|
+
property?.dataType === "map" &&
|
|
238
|
+
(property.name || !pathEndsWithIndex(baseKey));
|
|
239
|
+
|
|
240
|
+
const headerText = property?.name || label;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
244
|
+
{showMapHeader && (
|
|
245
|
+
<Typography variant="caption"
|
|
246
|
+
color={"secondary"}
|
|
247
|
+
component={"span"}
|
|
248
|
+
>
|
|
249
|
+
{headerText}
|
|
250
|
+
</Typography>
|
|
251
|
+
)}
|
|
252
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
253
|
+
{Object.entries(data).map(([key, value]) => {
|
|
254
|
+
if (value === null || value === undefined) return null;
|
|
255
|
+
const currentKey = baseKey ? `${baseKey}.${key}` : key;
|
|
256
|
+
return (
|
|
257
|
+
<PropertyCollectionView
|
|
258
|
+
key={currentKey}
|
|
259
|
+
data={value}
|
|
260
|
+
properties={properties}
|
|
261
|
+
baseKey={currentKey}
|
|
262
|
+
size={size}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Primitives
|
|
272
|
+
if (baseKey) {
|
|
273
|
+
const {
|
|
274
|
+
label,
|
|
275
|
+
property
|
|
276
|
+
} = buildPropertyLabelAndGetProperty(properties, baseKey);
|
|
277
|
+
if (!property) return null;
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
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
|
+
<div className="col-span-4 pr-2">
|
|
282
|
+
<Typography variant="caption"
|
|
283
|
+
color={"secondary"}
|
|
284
|
+
component={"span"}
|
|
285
|
+
className="break-words">
|
|
286
|
+
{label}
|
|
287
|
+
</Typography>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="col-span-8">
|
|
290
|
+
<PropertyPreview propertyKey={baseKey}
|
|
291
|
+
value={data}
|
|
292
|
+
property={property}
|
|
293
|
+
size={size}/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export function buildDataFromPaths(values: object, paths: string[]): object {
|
|
303
|
+
const result = {};
|
|
304
|
+
paths.forEach(path => {
|
|
305
|
+
const value = getValueInPath(values, path);
|
|
306
|
+
if (value === undefined) return;
|
|
307
|
+
|
|
308
|
+
// lodash.set would be perfect here
|
|
309
|
+
const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
310
|
+
let current: any = result;
|
|
311
|
+
segments.forEach((segment, index) => {
|
|
312
|
+
if (index === segments.length - 1) {
|
|
313
|
+
current[segment] = value;
|
|
314
|
+
} else {
|
|
315
|
+
const nextSegment = segments[index + 1];
|
|
316
|
+
const isNextAnIndex = /^\d+$/.test(nextSegment);
|
|
317
|
+
if (!current[segment]) {
|
|
318
|
+
if (isNextAnIndex) {
|
|
319
|
+
current[segment] = [];
|
|
320
|
+
} else {
|
|
321
|
+
current[segment] = {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
current = current[segment];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
@@ -9,7 +9,7 @@ export function PropertyConfigBadge({
|
|
|
9
9
|
propertyConfig: PropertyConfig | undefined,
|
|
10
10
|
disabled?: boolean
|
|
11
11
|
}): React.ReactNode {
|
|
12
|
-
const classes = "h-8 w-8
|
|
12
|
+
const classes = "h-8 w-8 flex items-center justify-center rounded-full shadow text-white " + (disabled ? "bg-surface-400 dark:bg-surface-600" : "");
|
|
13
13
|
|
|
14
14
|
const defaultPropertyConfig = typeof propertyConfig?.property === "object" ? getDefaultFieldConfig(propertyConfig.property) : undefined;
|
|
15
15
|
|
|
@@ -18,6 +18,6 @@ export function PropertyConfigBadge({
|
|
|
18
18
|
style={{
|
|
19
19
|
background: !disabled ? (propertyConfig?.color ?? defaultPropertyConfig?.color ?? "#888") : undefined
|
|
20
20
|
}}>
|
|
21
|
-
{propertyConfig?.Icon ? getIconForWidget(propertyConfig, "
|
|
21
|
+
{propertyConfig?.Icon ? getIconForWidget(propertyConfig, "small") : getIconForWidget(defaultPropertyConfig, "small")}
|
|
22
22
|
</div>
|
|
23
23
|
}
|
|
@@ -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);
|
|
@@ -123,7 +123,7 @@ export function StringNumberFilterField({
|
|
|
123
123
|
: evt.target.value;
|
|
124
124
|
updateFilter(operation, val);
|
|
125
125
|
}}
|
|
126
|
-
endAdornment={internalValue && <IconButton
|
|
126
|
+
endAdornment={internalValue !== undefined && internalValue != null && <IconButton
|
|
127
127
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
128
128
|
<CloseIcon/>
|
|
129
129
|
</IconButton>}
|
|
@@ -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>}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { User } from "../types";
|
|
2
|
+
import { AccountCircleIcon, cls, defaultBorderMixin } from "@firecms/ui";
|
|
3
|
+
import { EmptyValue } from "../preview";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component to render a single user with name and email
|
|
7
|
+
*/
|
|
8
|
+
export function UserDisplay({
|
|
9
|
+
user,
|
|
10
|
+
}: { user: User | null }) {
|
|
11
|
+
if (!user) {
|
|
12
|
+
return <EmptyValue/>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const avatarSizeClass = "w-6 h-6";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={cls(
|
|
19
|
+
"inline-flex items-center gap-4 px-2 py-1 rounded-xl",
|
|
20
|
+
"bg-surface-accent-100 dark:bg-surface-accent-800",
|
|
21
|
+
"border",
|
|
22
|
+
defaultBorderMixin
|
|
23
|
+
)}>
|
|
24
|
+
{user.photoURL ? (
|
|
25
|
+
<img
|
|
26
|
+
src={user.photoURL}
|
|
27
|
+
alt={user.displayName || user.email || "User"}
|
|
28
|
+
className={cls(
|
|
29
|
+
"rounded-full object-cover",
|
|
30
|
+
avatarSizeClass
|
|
31
|
+
)}
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<AccountCircleIcon
|
|
35
|
+
className={cls(
|
|
36
|
+
"text-text-secondary dark:text-text-secondary-dark",
|
|
37
|
+
avatarSizeClass
|
|
38
|
+
)}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
<div className="flex flex-col min-w-0">
|
|
42
|
+
<span className={cls("font-regular truncate", "text-sm")}>
|
|
43
|
+
{user.displayName || user.email || "-"}
|
|
44
|
+
</span>
|
|
45
|
+
{user.displayName && user.email && (
|
|
46
|
+
<span className={cls("text-text-secondary dark:text-text-secondary-dark truncate",
|
|
47
|
+
"text-xs"
|
|
48
|
+
)}>
|
|
49
|
+
{user.email}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from "react";
|
|
2
|
+
import { MultiSelect, MultiSelectItem, Select, SelectItem } from "@firecms/ui";
|
|
3
|
+
import { useInternalUserManagementController } from "../../../hooks";
|
|
4
|
+
import { UserDisplay } from "../../UserDisplay";
|
|
5
|
+
|
|
6
|
+
export function VirtualTableUserSelect(props: {
|
|
7
|
+
name: string;
|
|
8
|
+
error: Error | undefined;
|
|
9
|
+
multiple: boolean;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
small: boolean;
|
|
12
|
+
internalValue: string | string[] | undefined;
|
|
13
|
+
updateValue: (newValue: (string | string[] | null)) => void;
|
|
14
|
+
focused: boolean;
|
|
15
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
|
16
|
+
}) {
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
internalValue,
|
|
20
|
+
disabled,
|
|
21
|
+
small,
|
|
22
|
+
focused,
|
|
23
|
+
updateValue,
|
|
24
|
+
multiple
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
const { users, getUser } = useInternalUserManagementController();
|
|
28
|
+
|
|
29
|
+
const validValue = (Array.isArray(internalValue) && multiple) ||
|
|
30
|
+
(!Array.isArray(internalValue) && !multiple);
|
|
31
|
+
|
|
32
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (ref.current && focused) {
|
|
35
|
+
ref.current?.focus({ preventScroll: true });
|
|
36
|
+
}
|
|
37
|
+
}, [focused, ref]);
|
|
38
|
+
|
|
39
|
+
const onChange = useCallback((updatedValue: string | string[]) => {
|
|
40
|
+
if (!updatedValue) {
|
|
41
|
+
updateValue(null);
|
|
42
|
+
} else {
|
|
43
|
+
updateValue(updatedValue);
|
|
44
|
+
}
|
|
45
|
+
}, [updateValue]);
|
|
46
|
+
|
|
47
|
+
const renderValue = (userId: string) => {
|
|
48
|
+
const user = getUser(userId);
|
|
49
|
+
return <UserDisplay user={user} />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
multiple
|
|
54
|
+
? <MultiSelect
|
|
55
|
+
inputRef={ref}
|
|
56
|
+
className="w-full h-full p-0 bg-transparent"
|
|
57
|
+
position={"item-aligned"}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
includeClear={false}
|
|
60
|
+
useChips={false}
|
|
61
|
+
value={validValue
|
|
62
|
+
? (internalValue as string[])
|
|
63
|
+
: ([])}
|
|
64
|
+
onValueChange={onChange}>
|
|
65
|
+
{users?.map((user) => (
|
|
66
|
+
<MultiSelectItem
|
|
67
|
+
key={user.uid}
|
|
68
|
+
value={user.uid}>
|
|
69
|
+
<UserDisplay
|
|
70
|
+
user={user} />
|
|
71
|
+
</MultiSelectItem>
|
|
72
|
+
))}
|
|
73
|
+
</MultiSelect>
|
|
74
|
+
: <Select
|
|
75
|
+
inputRef={ref}
|
|
76
|
+
size={"large"}
|
|
77
|
+
fullWidth={true}
|
|
78
|
+
className="w-full h-full p-0 bg-transparent"
|
|
79
|
+
position={"item-aligned"}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
padding={false}
|
|
82
|
+
value={validValue
|
|
83
|
+
? internalValue as string
|
|
84
|
+
: ""}
|
|
85
|
+
onValueChange={onChange}
|
|
86
|
+
renderValue={renderValue}>
|
|
87
|
+
{users?.map((user) => (
|
|
88
|
+
<SelectItem
|
|
89
|
+
key={user.uid}
|
|
90
|
+
value={user.uid}>
|
|
91
|
+
<UserDisplay
|
|
92
|
+
user={user}/>
|
|
93
|
+
</SelectItem>
|
|
94
|
+
))}
|
|
95
|
+
</Select>
|
|
96
|
+
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -8,14 +8,7 @@ export const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
|
|
|
8
8
|
export function useColumnIds<M extends Record<string, any>>(collection: ResolvedEntityCollection<M>, includeSubcollections: boolean): PropertyColumnConfig[] {
|
|
9
9
|
return useMemo(() => {
|
|
10
10
|
if (collection.propertiesOrder) {
|
|
11
|
-
|
|
12
|
-
if (collection.collectionGroup) {
|
|
13
|
-
propertyColumnConfigs.push({
|
|
14
|
-
key: COLLECTION_GROUP_PARENT_ID,
|
|
15
|
-
disabled: true
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return propertyColumnConfigs;
|
|
11
|
+
return hideAndExpandKeys(collection, collection.propertiesOrder);
|
|
19
12
|
}
|
|
20
13
|
return getDefaultColumnKeys(collection, includeSubcollections);
|
|
21
14
|
}, [collection, includeSubcollections]);
|