@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 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.27",
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.18",
55
- "@stoker-platform/utils": "0.5.12",
56
- "@stoker-platform/web-client": "0.5.13",
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 (isTime) {
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(() => {
@@ -79,7 +79,7 @@ export const getFormattedFieldValue = (
79
79
  if (badge) {
80
80
  if (badge === true) {
81
81
  return (
82
- <Badge variant="outline" className="text-center">
82
+ <Badge variant="outline" className="text-xs text-center">
83
83
  {value}
84
84
  </Badge>
85
85
  )