@stoker-platform/web-app 0.5.109 → 0.5.111

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.111
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: hide convert button when target collection create access not granted
8
+
9
+ ## 0.5.110
10
+
11
+ ### Patch Changes
12
+
13
+ - feat: add image thumbnails option
14
+ - @stoker-platform/node-client@0.5.62
15
+ - @stoker-platform/utils@0.5.53
16
+ - @stoker-platform/web-client@0.5.63
17
+
3
18
  ## 0.5.109
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.109",
3
+ "version": "0.5.111",
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.50.0",
54
- "@stoker-platform/node-client": "0.5.61",
55
- "@stoker-platform/utils": "0.5.52",
56
- "@stoker-platform/web-client": "0.5.62",
54
+ "@stoker-platform/node-client": "0.5.62",
55
+ "@stoker-platform/utils": "0.5.53",
56
+ "@stoker-platform/web-client": "0.5.63",
57
57
  "@tanstack/react-table": "^8.21.3",
58
58
  "@types/react": "18.3.13",
59
59
  "@types/react-dom": "18.3.1",
@@ -59,7 +59,7 @@ export const Breadcrumbs = ({
59
59
  const titleField = field.titleField
60
60
  if (titleField && field.includeFields?.includes(titleField)) {
61
61
  const relationCollection = schema.collections[field.collection]
62
- if (!record[breadcrumb]) continue
62
+ if (!(record[breadcrumb] && Object.values(record[breadcrumb])[0])) continue
63
63
  recordMap[breadcrumb] = {
64
64
  title: (Object.values(record[breadcrumb])[0] as StokerRelation)[titleField],
65
65
  collection: relationCollection,
package/src/Files.tsx CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  getMetadata,
28
28
  updateMetadata,
29
29
  } from "firebase/storage"
30
+ import type { FirebaseStorage } from "firebase/storage"
30
31
  import { getAuth } from "firebase/auth"
31
32
  import { Progress } from "./components/ui/progress"
32
33
  import { Button } from "./components/ui/button"
@@ -71,6 +72,83 @@ import {
71
72
  import { FilePermissionsDialog, FilePermissions } from "./FilePermissions"
72
73
  import { prepareFile } from "./utils/prepareFile"
73
74
 
75
+ const IMAGE_FILE_EXTENSIONS = new Set([
76
+ "avif",
77
+ "bmp",
78
+ "gif",
79
+ "heic",
80
+ "heif",
81
+ "ico",
82
+ "jpeg",
83
+ "jpg",
84
+ "png",
85
+ "svg",
86
+ "webp",
87
+ ])
88
+
89
+ const isImageFile = (name: string): boolean => {
90
+ const dot = name.lastIndexOf(".")
91
+ if (dot <= 0) return false
92
+ return IMAGE_FILE_EXTENSIONS.has(name.slice(dot + 1).toLowerCase())
93
+ }
94
+
95
+ interface FileImageThumbnailProps {
96
+ storage: FirebaseStorage
97
+ fullPath: string
98
+ fileName: string
99
+ pathPrefix: string
100
+ }
101
+
102
+ const FileImageThumbnail = ({ storage, fullPath, fileName, pathPrefix }: FileImageThumbnailProps) => {
103
+ const [phase, setPhase] = useState<"loading" | "display" | "fallback">("loading")
104
+ const [url, setUrl] = useState<string | null>(null)
105
+
106
+ useEffect(() => {
107
+ if (!pathPrefix || !fullPath.startsWith(pathPrefix)) {
108
+ setPhase("fallback")
109
+ return
110
+ }
111
+ let cancelled = false
112
+ setPhase("loading")
113
+ setUrl(null)
114
+ const load = async () => {
115
+ try {
116
+ const fileRef = ref(storage, fullPath)
117
+ const downloadUrl = await getDownloadURL(fileRef)
118
+ if (!cancelled) {
119
+ setUrl(downloadUrl)
120
+ setPhase("display")
121
+ }
122
+ } catch {
123
+ if (!cancelled) {
124
+ setPhase("fallback")
125
+ }
126
+ }
127
+ }
128
+ void load()
129
+ return () => {
130
+ cancelled = true
131
+ }
132
+ }, [storage, fullPath, pathPrefix])
133
+
134
+ if (phase === "loading") {
135
+ return <div className="h-12 w-12 shrink-0 rounded-md border bg-muted animate-pulse" aria-hidden />
136
+ }
137
+ if (phase === "fallback" || !url) {
138
+ return <File className="h-5 w-5 shrink-0 text-gray-500" aria-hidden />
139
+ }
140
+ return (
141
+ <img
142
+ src={url}
143
+ alt={`Thumbnail for ${fileName}`}
144
+ className="max-h-12 w-12 shrink-0 rounded-md object-contain"
145
+ loading="lazy"
146
+ decoding="async"
147
+ onError={() => setPhase("fallback")}
148
+ />
149
+ )
150
+ }
151
+
74
152
  interface FilesProps {
75
153
  collection: CollectionSchema
76
154
  record: StokerRecord | undefined
@@ -1038,9 +1116,20 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1038
1116
  >
1039
1117
  <div className="flex items-center space-x-3 flex-1 min-w-0">
1040
1118
  {item.isFolder ? (
1041
- <Folder className="h-5 w-5 text-blue-500" />
1119
+ <Folder className="h-5 w-5 shrink-0 text-blue-500" />
1120
+ ) : fileOptions?.thumbnails === true && isImageFile(item.name) ? (
1121
+ <FileImageThumbnail
1122
+ storage={storage}
1123
+ fullPath={item.fullPath}
1124
+ fileName={item.name}
1125
+ pathPrefix={
1126
+ record
1127
+ ? `${tenantId}/${labels.collection}/${record.id}`
1128
+ : ""
1129
+ }
1130
+ />
1042
1131
  ) : (
1043
- <File className="h-5 w-5 text-gray-500" />
1132
+ <File className="h-5 w-5 shrink-0 text-gray-500" />
1044
1133
  )}
1045
1134
 
1046
1135
  {editingFile === item.name && !isDisabled ? (
package/src/Form.tsx CHANGED
@@ -2477,7 +2477,6 @@ function RecordForm({
2477
2477
  const [showDuplicateModal, setShowDuplicateModal] = useState(false)
2478
2478
  const [duplicateRecordData, setDuplicateRecordData] = useState<Partial<StokerRecord> | undefined>(undefined)
2479
2479
  const [convert, setConvert] = useState<Convert[] | undefined>(undefined)
2480
- const [convertAllowed, setConvertAllowed] = useState<Record<string, boolean>>({})
2481
2480
  const [showConvertModal, setShowConvertModal] = useState(false)
2482
2481
  const [convertRecordData, setConvertRecordData] = useState<Partial<StokerRecord> | undefined>(undefined)
2483
2482
  const [convertTargetCollection, setConvertTargetCollection] = useState<CollectionSchema | undefined>(undefined)
@@ -2504,6 +2503,21 @@ function RecordForm({
2504
2503
  return collectionAccess("Delete", collectionPermissions)
2505
2504
  }, [collection, permissions])
2506
2505
 
2506
+ const convertMenuItems = useMemo(() => {
2507
+ if (!convert?.length || !permissions?.Role) return []
2508
+ const role = permissions.Role
2509
+ return convert.filter((convertConfig) => {
2510
+ const targetPermissions = permissions.collections?.[convertConfig.collection] as
2511
+ | CollectionPermissions
2512
+ | undefined
2513
+ if (!collectionAccess("Create", targetPermissions as CollectionPermissions)) return false
2514
+ if (convertConfig.roles && convertConfig.roles.length > 0) {
2515
+ return convertConfig.roles.includes(role)
2516
+ }
2517
+ return true
2518
+ })
2519
+ }, [convert, permissions])
2520
+
2507
2521
  const isPending = !!(id && isGlobalLoading.has(id))
2508
2522
  const isPendingServer = !!(id && isGlobalLoading.get(id)?.server)
2509
2523
  const [isAddingServer, setIsAddingServer] = useState(false)
@@ -3624,12 +3638,6 @@ function RecordForm({
3624
3638
  ...prev,
3625
3639
  [collection.labels.collection]: collectionTitles?.record || collection.labels.record,
3626
3640
  }))
3627
- if (convert && convert.length > 0) {
3628
- setConvertAllowed((prev) => ({
3629
- ...prev,
3630
- [collection.labels.collection]: !disableCreate,
3631
- }))
3632
- }
3633
3641
  })
3634
3642
  permissionTitleResults.forEach(({ collection, titles: collectionTitles }) => {
3635
3643
  setPermissionsTitles((prev) => ({
@@ -4502,10 +4510,17 @@ function RecordForm({
4502
4510
  const convertRecord = useCallback(
4503
4511
  async (targetCollection: CollectionSchema) => {
4504
4512
  if (!formValues || !originalRecord) return
4513
+ const targetPermissions = permissions?.collections?.[targetCollection.labels.collection] as
4514
+ | CollectionPermissions
4515
+ | undefined
4516
+ if (!collectionAccess("Create", targetPermissions as CollectionPermissions)) return
4505
4517
  const record = cloneDeep(originalRecord) as Partial<StokerRecord>
4506
4518
 
4507
4519
  const convertConfig = convert?.find((convert) => convert.collection === targetCollection.labels.collection)
4508
4520
  if (!convertConfig) return
4521
+ if (convertConfig.roles && convertConfig.roles.length > 0 && permissions?.Role) {
4522
+ if (!convertConfig.roles.includes(permissions.Role)) return
4523
+ }
4509
4524
 
4510
4525
  const convertedRecord = await convertConfig.convert(record as StokerRecord)
4511
4526
 
@@ -4513,7 +4528,7 @@ function RecordForm({
4513
4528
  setConvertTargetCollection(targetCollection)
4514
4529
  setShowConvertModal(true)
4515
4530
  },
4516
- [formValues, originalRecord, permissions],
4531
+ [formValues, originalRecord, permissions, convert],
4517
4532
  )
4518
4533
 
4519
4534
  const hasBreadcrumbs = useMemo(() => {
@@ -5651,7 +5666,7 @@ function RecordForm({
5651
5666
  Duplicate
5652
5667
  </Button>
5653
5668
  )}
5654
- {convert && convert.length > 0 && (
5669
+ {convertMenuItems.length > 0 && (
5655
5670
  <DropdownMenu>
5656
5671
  <DropdownMenuTrigger asChild>
5657
5672
  <Button variant="outline" disabled={isCreateDisabled}>
@@ -5660,36 +5675,19 @@ function RecordForm({
5660
5675
  </Button>
5661
5676
  </DropdownMenuTrigger>
5662
5677
  <DropdownMenuContent align="end">
5663
- {convert
5664
- .filter((convertConfig) => {
5665
- if (!permissions?.Role) return false
5666
- if (!convertAllowed[convertConfig.collection]) return false
5667
- if (
5668
- convertConfig.roles &&
5669
- convertConfig.roles.length > 0 &&
5670
- permissions.collections?.[convertConfig.collection] &&
5671
- collectionAccess(
5672
- "Create",
5673
- permissions.collections[convertConfig.collection],
5674
- )
5675
- ) {
5676
- return convertConfig.roles.includes(permissions?.Role)
5677
- }
5678
- return true
5679
- })
5680
- .map((convertConfig) => {
5681
- const targetCollection =
5682
- schema.collections[convertConfig.collection]
5683
- if (!targetCollection) return null
5684
- return (
5685
- <DropdownMenuItem
5686
- key={convertConfig.collection}
5687
- onClick={() => convertRecord(targetCollection)}
5688
- >
5689
- {allRecordTitles[convertConfig.collection]}
5690
- </DropdownMenuItem>
5691
- )
5692
- })}
5678
+ {convertMenuItems.map((convertConfig) => {
5679
+ const targetCollection =
5680
+ schema.collections[convertConfig.collection]
5681
+ if (!targetCollection) return null
5682
+ return (
5683
+ <DropdownMenuItem
5684
+ key={convertConfig.collection}
5685
+ onClick={() => convertRecord(targetCollection)}
5686
+ >
5687
+ {allRecordTitles[convertConfig.collection]}
5688
+ </DropdownMenuItem>
5689
+ )
5690
+ })}
5693
5691
  </DropdownMenuContent>
5694
5692
  </DropdownMenu>
5695
5693
  )}