@stoker-platform/web-app 0.5.27 → 0.5.29
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/CHANGELOG.md +15 -0
- package/package.json +4 -4
- package/src/Form.tsx +156 -52
- package/src/utils/getFormattedFieldValue.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.29
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: skip file permissions selection when no optional permissions present
|
|
8
|
+
|
|
9
|
+
## 0.5.28
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- feat: add month picker
|
|
14
|
+
- @stoker-platform/node-client@0.5.19
|
|
15
|
+
- @stoker-platform/utils@0.5.13
|
|
16
|
+
- @stoker-platform/web-client@0.5.14
|
|
17
|
+
|
|
3
18
|
## 0.5.27
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stoker-platform/web-app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"scripts": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
52
|
"@react-google-maps/api": "^2.20.8",
|
|
53
53
|
"@sentry/react": "^10.38.0",
|
|
54
|
-
"@stoker-platform/node-client": "0.5.
|
|
55
|
-
"@stoker-platform/utils": "0.5.
|
|
56
|
-
"@stoker-platform/web-client": "0.5.
|
|
54
|
+
"@stoker-platform/node-client": "0.5.19",
|
|
55
|
+
"@stoker-platform/utils": "0.5.13",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.14",
|
|
57
57
|
"@tanstack/react-table": "^8.21.3",
|
|
58
58
|
"@types/react": "18.3.13",
|
|
59
59
|
"@types/react-dom": "18.3.1",
|
package/src/Form.tsx
CHANGED
|
@@ -158,6 +158,7 @@ import Collection from "./Collection"
|
|
|
158
158
|
import { Separator } from "./components/ui/separator"
|
|
159
159
|
import { SearchResult } from "minisearch"
|
|
160
160
|
import { sortList } from "./utils/sortList"
|
|
161
|
+
import MonthPicker from "./components/ui/month-picker"
|
|
161
162
|
|
|
162
163
|
interface FormLabelWithIconProps {
|
|
163
164
|
collection: CollectionSchema
|
|
@@ -286,6 +287,7 @@ const RecordFormField = (props: FieldProps) => {
|
|
|
286
287
|
const [isTextarea, setIsTextarea] = useState(false)
|
|
287
288
|
const [isSwitch, setIsSwitch] = useState(false)
|
|
288
289
|
const [isTime, setIsTime] = useState(false)
|
|
290
|
+
const [isMonth, setIsMonth] = useState(false)
|
|
289
291
|
const [isSlider, setIsSlider] = useState(false)
|
|
290
292
|
const [isRichText, setIsRichText] = useState(false)
|
|
291
293
|
const [isLocation, setIsLocation] = useState<LocationFieldAdmin | undefined>(undefined)
|
|
@@ -306,6 +308,7 @@ const RecordFormField = (props: FieldProps) => {
|
|
|
306
308
|
textarea,
|
|
307
309
|
isSwitch,
|
|
308
310
|
isTime,
|
|
311
|
+
isMonth,
|
|
309
312
|
isSlider,
|
|
310
313
|
isRichText,
|
|
311
314
|
image,
|
|
@@ -320,6 +323,7 @@ const RecordFormField = (props: FieldProps) => {
|
|
|
320
323
|
tryPromise(admin?.textarea),
|
|
321
324
|
tryPromise(admin?.switch),
|
|
322
325
|
tryPromise(admin?.time),
|
|
326
|
+
tryPromise(admin?.month),
|
|
323
327
|
tryPromise(admin?.slider),
|
|
324
328
|
tryPromise(admin?.richText),
|
|
325
329
|
tryPromise(admin?.image),
|
|
@@ -336,6 +340,7 @@ const RecordFormField = (props: FieldProps) => {
|
|
|
336
340
|
setIsTextarea(!!textarea)
|
|
337
341
|
setIsSwitch(!!isSwitch)
|
|
338
342
|
setIsTime(!!isTime)
|
|
343
|
+
setIsMonth(!!isMonth)
|
|
339
344
|
setIsSlider(!!isSlider)
|
|
340
345
|
setIsRichText(!!isRichText)
|
|
341
346
|
setIsImage(!!image)
|
|
@@ -462,6 +467,7 @@ const RecordFormField = (props: FieldProps) => {
|
|
|
462
467
|
description={description}
|
|
463
468
|
isDisabled={isDisabled}
|
|
464
469
|
isTime={isTime}
|
|
470
|
+
isMonth={isMonth}
|
|
465
471
|
icon={icon}
|
|
466
472
|
/>
|
|
467
473
|
)
|
|
@@ -1139,8 +1145,9 @@ function TimestampField({
|
|
|
1139
1145
|
form,
|
|
1140
1146
|
isDisabled,
|
|
1141
1147
|
isTime,
|
|
1148
|
+
isMonth,
|
|
1142
1149
|
icon,
|
|
1143
|
-
}: FieldProps & { isTime?: boolean }) {
|
|
1150
|
+
}: FieldProps & { isTime?: boolean; isMonth?: boolean }) {
|
|
1144
1151
|
const [open, setOpen] = useState(false)
|
|
1145
1152
|
const globalConfig = getGlobalConfigModule()
|
|
1146
1153
|
const timezone = getTimezone()
|
|
@@ -1163,7 +1170,39 @@ function TimestampField({
|
|
|
1163
1170
|
const currentValue = formField.value
|
|
1164
1171
|
? DateTime.fromJSDate(formField.value.toDate()).setZone(timezone)
|
|
1165
1172
|
: undefined
|
|
1166
|
-
if (
|
|
1173
|
+
if (isMonth) {
|
|
1174
|
+
return (
|
|
1175
|
+
<FormItem>
|
|
1176
|
+
<FormLabelWithIcon
|
|
1177
|
+
collection={collection}
|
|
1178
|
+
label={label}
|
|
1179
|
+
field={field}
|
|
1180
|
+
operation={operation}
|
|
1181
|
+
icon={icon}
|
|
1182
|
+
form={form}
|
|
1183
|
+
/>
|
|
1184
|
+
<FormControl>
|
|
1185
|
+
<div className="flex w-fit flex-col gap-2 rounded-md border border-input">
|
|
1186
|
+
<MonthPicker
|
|
1187
|
+
currentMonth={
|
|
1188
|
+
currentValue
|
|
1189
|
+
? keepTimezone(currentValue.toJSDate(), timezone)
|
|
1190
|
+
: keepTimezone(new Date(), timezone)
|
|
1191
|
+
}
|
|
1192
|
+
onMonthChange={(date: Date | undefined) => {
|
|
1193
|
+
if (!date) return
|
|
1194
|
+
const newDate = DateTime.fromJSDate(date).setZone(timezone)
|
|
1195
|
+
formField.onChange(Timestamp.fromDate(newDate.toJSDate()))
|
|
1196
|
+
}}
|
|
1197
|
+
disabled={isDisabled}
|
|
1198
|
+
/>
|
|
1199
|
+
</div>
|
|
1200
|
+
</FormControl>
|
|
1201
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
1202
|
+
<FormMessage className="bg-destructive p-4 rounded-md text-background dark:text-primary" />
|
|
1203
|
+
</FormItem>
|
|
1204
|
+
)
|
|
1205
|
+
} else if (isTime) {
|
|
1167
1206
|
// Use HH:mm for broad browser compatibility (iOS Safari returns HH:mm)
|
|
1168
1207
|
const timeString = currentValue?.toFormat("HH:mm") || "00:00"
|
|
1169
1208
|
|
|
@@ -2283,6 +2322,36 @@ function RecordForm({
|
|
|
2283
2322
|
[collectionPath, record, path],
|
|
2284
2323
|
)
|
|
2285
2324
|
|
|
2325
|
+
const getUserRoleAssignment = useCallback(() => {
|
|
2326
|
+
const userRole = permissions?.Role
|
|
2327
|
+
if (!userRole) return null
|
|
2328
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
2329
|
+
const assignment = collection.access?.files?.assignment?.[userRole]
|
|
2330
|
+
return assignment || null
|
|
2331
|
+
}, [collection, permissions])
|
|
2332
|
+
|
|
2333
|
+
const shouldSkipPermissionsDialog = useCallback(() => {
|
|
2334
|
+
const assignment = getUserRoleAssignment()
|
|
2335
|
+
if (!assignment) return false
|
|
2336
|
+
const optional = assignment.optional || {}
|
|
2337
|
+
const hasOptional = Boolean(
|
|
2338
|
+
(optional.read && optional.read.length) ||
|
|
2339
|
+
(optional.update && optional.update.length) ||
|
|
2340
|
+
(optional.delete && optional.delete.length),
|
|
2341
|
+
)
|
|
2342
|
+
return !hasOptional
|
|
2343
|
+
}, [getUserRoleAssignment])
|
|
2344
|
+
|
|
2345
|
+
const getDefaultPermissions = useCallback((): FilePermissions => {
|
|
2346
|
+
const assignment = getUserRoleAssignment()
|
|
2347
|
+
const required = assignment?.required || {}
|
|
2348
|
+
return {
|
|
2349
|
+
read: (required.read || []).join(","),
|
|
2350
|
+
update: (required.update || []).join(","),
|
|
2351
|
+
delete: (required.delete || []).join(","),
|
|
2352
|
+
}
|
|
2353
|
+
}, [getUserRoleAssignment])
|
|
2354
|
+
|
|
2286
2355
|
const uploadFilesToRecord = useCallback(
|
|
2287
2356
|
async (targetId: string, files: File[] | FileList, permissions: FilePermissions, customFilename?: string) => {
|
|
2288
2357
|
if (!files || !currentUser) return
|
|
@@ -2368,56 +2437,6 @@ function RecordForm({
|
|
|
2368
2437
|
[computeBasePath, currentUser, path, record],
|
|
2369
2438
|
)
|
|
2370
2439
|
|
|
2371
|
-
const enqueueImageForCreate = useCallback((fieldName: string, file: File) => {
|
|
2372
|
-
setPermissionsContext("image-create")
|
|
2373
|
-
setPendingImageFieldName(fieldName)
|
|
2374
|
-
setPendingUploadFile(file)
|
|
2375
|
-
setPendingUploadField(fieldName)
|
|
2376
|
-
setEditingFilename(file.name)
|
|
2377
|
-
setIsMultipleFileUpload(false)
|
|
2378
|
-
setShowPermissionsDialog(true)
|
|
2379
|
-
}, [])
|
|
2380
|
-
|
|
2381
|
-
const uploadImageForUpdate = useCallback(async (fieldName: string, file: File) => {
|
|
2382
|
-
return await new Promise<void>((resolve) => {
|
|
2383
|
-
setPermissionsContext("image-update")
|
|
2384
|
-
setPendingImageForUpdate({ fieldName, file })
|
|
2385
|
-
setEditingFilename(file.name)
|
|
2386
|
-
setIsMultipleFileUpload(false)
|
|
2387
|
-
setShowPermissionsDialog(true)
|
|
2388
|
-
setImageUpdateResolver(() => resolve)
|
|
2389
|
-
})
|
|
2390
|
-
}, [])
|
|
2391
|
-
|
|
2392
|
-
const handleFormFileUpload = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
2393
|
-
const files = event.target.files
|
|
2394
|
-
if (!files) return
|
|
2395
|
-
if (files.length === 1) {
|
|
2396
|
-
const file = files[0]
|
|
2397
|
-
setPendingUploadFile(file)
|
|
2398
|
-
setPendingUploadField(null)
|
|
2399
|
-
setEditingFilename(file.name)
|
|
2400
|
-
setIsMultipleFileUpload(false)
|
|
2401
|
-
setShowFilenameDialog(true)
|
|
2402
|
-
} else {
|
|
2403
|
-
setPendingUploadFiles(Array.from(files))
|
|
2404
|
-
setIsMultipleFileUpload(true)
|
|
2405
|
-
setShowPermissionsDialog(true)
|
|
2406
|
-
}
|
|
2407
|
-
event.target.value = ""
|
|
2408
|
-
}, [])
|
|
2409
|
-
|
|
2410
|
-
const handleConfirmFilename = useCallback(() => {
|
|
2411
|
-
if (!pendingUploadFile) return
|
|
2412
|
-
const trimmed = editingFilename.trim()
|
|
2413
|
-
const validationError = validateStorageName(trimmed)
|
|
2414
|
-
if (validationError) {
|
|
2415
|
-
toast({ title: "Invalid file name", description: validationError, variant: "destructive" })
|
|
2416
|
-
return
|
|
2417
|
-
}
|
|
2418
|
-
setShowPermissionsDialog(true)
|
|
2419
|
-
}, [pendingUploadFile, editingFilename])
|
|
2420
|
-
|
|
2421
2440
|
const handlePermissionsConfirm = useCallback(
|
|
2422
2441
|
async (selectedPermissions: FilePermissions) => {
|
|
2423
2442
|
if (permissionsContext === "files") {
|
|
@@ -2581,6 +2600,91 @@ function RecordForm({
|
|
|
2581
2600
|
],
|
|
2582
2601
|
)
|
|
2583
2602
|
|
|
2603
|
+
const enqueueImageForCreate = useCallback(
|
|
2604
|
+
(fieldName: string, file: File) => {
|
|
2605
|
+
if (shouldSkipPermissionsDialog()) {
|
|
2606
|
+
setQueuedImageUploads((prev) => ({
|
|
2607
|
+
...prev,
|
|
2608
|
+
[fieldName]: { file, permissions: getDefaultPermissions() },
|
|
2609
|
+
}))
|
|
2610
|
+
} else {
|
|
2611
|
+
setPermissionsContext("image-create")
|
|
2612
|
+
setPendingImageFieldName(fieldName)
|
|
2613
|
+
setPendingUploadFile(file)
|
|
2614
|
+
setEditingFilename(file.name)
|
|
2615
|
+
setIsMultipleFileUpload(false)
|
|
2616
|
+
setShowPermissionsDialog(true)
|
|
2617
|
+
}
|
|
2618
|
+
},
|
|
2619
|
+
[shouldSkipPermissionsDialog, getDefaultPermissions],
|
|
2620
|
+
)
|
|
2621
|
+
|
|
2622
|
+
const uploadImageForUpdate = useCallback(
|
|
2623
|
+
async (fieldName: string, file: File) => {
|
|
2624
|
+
return await new Promise<void>((resolve) => {
|
|
2625
|
+
setPermissionsContext("image-update")
|
|
2626
|
+
setPendingImageForUpdate({ fieldName, file })
|
|
2627
|
+
setEditingFilename(file.name)
|
|
2628
|
+
setIsMultipleFileUpload(false)
|
|
2629
|
+
setImageUpdateResolver(() => resolve)
|
|
2630
|
+
if (shouldSkipPermissionsDialog()) {
|
|
2631
|
+
setTimeout(() => handlePermissionsConfirm(getDefaultPermissions()), 0)
|
|
2632
|
+
} else {
|
|
2633
|
+
setShowPermissionsDialog(true)
|
|
2634
|
+
}
|
|
2635
|
+
})
|
|
2636
|
+
},
|
|
2637
|
+
[shouldSkipPermissionsDialog, getDefaultPermissions, handlePermissionsConfirm],
|
|
2638
|
+
)
|
|
2639
|
+
|
|
2640
|
+
const handleFormFileUpload = useCallback(
|
|
2641
|
+
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
2642
|
+
const files = event.target.files
|
|
2643
|
+
if (!files) return
|
|
2644
|
+
if (files.length === 1) {
|
|
2645
|
+
const file = files[0]
|
|
2646
|
+
setPendingUploadFile(file)
|
|
2647
|
+
setPendingUploadField(null)
|
|
2648
|
+
setEditingFilename(file.name)
|
|
2649
|
+
setIsMultipleFileUpload(false)
|
|
2650
|
+
setShowFilenameDialog(true)
|
|
2651
|
+
} else {
|
|
2652
|
+
const fileList = Array.from(files)
|
|
2653
|
+
if (shouldSkipPermissionsDialog()) {
|
|
2654
|
+
setQueuedUploads((prev) => [...prev, { files: fileList, permissions: getDefaultPermissions() }])
|
|
2655
|
+
} else {
|
|
2656
|
+
setPendingUploadFiles(fileList)
|
|
2657
|
+
setIsMultipleFileUpload(true)
|
|
2658
|
+
setShowPermissionsDialog(true)
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
event.target.value = ""
|
|
2662
|
+
},
|
|
2663
|
+
[shouldSkipPermissionsDialog, getDefaultPermissions],
|
|
2664
|
+
)
|
|
2665
|
+
|
|
2666
|
+
const handleConfirmFilename = useCallback(() => {
|
|
2667
|
+
if (!pendingUploadFile) return
|
|
2668
|
+
const trimmed = editingFilename.trim()
|
|
2669
|
+
const validationError = validateStorageName(trimmed)
|
|
2670
|
+
if (validationError) {
|
|
2671
|
+
toast({ title: "Invalid file name", description: validationError, variant: "destructive" })
|
|
2672
|
+
return
|
|
2673
|
+
}
|
|
2674
|
+
if (shouldSkipPermissionsDialog()) {
|
|
2675
|
+
setQueuedUploads((prev) => [
|
|
2676
|
+
...prev,
|
|
2677
|
+
{ files: [pendingUploadFile], permissions: getDefaultPermissions(), customFilename: trimmed },
|
|
2678
|
+
])
|
|
2679
|
+
setShowFilenameDialog(false)
|
|
2680
|
+
setPendingUploadFile(null)
|
|
2681
|
+
setPendingUploadField(null)
|
|
2682
|
+
setEditingFilename("")
|
|
2683
|
+
} else {
|
|
2684
|
+
setShowPermissionsDialog(true)
|
|
2685
|
+
}
|
|
2686
|
+
}, [pendingUploadFile, editingFilename, shouldSkipPermissionsDialog, getDefaultPermissions])
|
|
2687
|
+
|
|
2584
2688
|
const handlePermissionsCancel = useCallback(() => {
|
|
2585
2689
|
if (permissionsContext === "image-create" && pendingImageFieldName) {
|
|
2586
2690
|
setTimeout(() => {
|