@stoker-platform/web-app 0.5.141 → 0.5.143

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 (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/src/Files.tsx +253 -60
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.143
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: fix renaming files created by a different user
8
+
9
+ ## 0.5.142
10
+
11
+ ### Patch Changes
12
+
13
+ - feat: add bulk file rename feature
14
+
3
15
  ## 0.5.141
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoker-platform/web-app",
3
- "version": "0.5.141",
3
+ "version": "0.5.143",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "scripts": {
package/src/Files.tsx CHANGED
@@ -178,10 +178,15 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
178
178
  const [loading, setLoading] = useState(false)
179
179
  const [editingFile, setEditingFile] = useState<string | null>(null)
180
180
  const [newFileName, setNewFileName] = useState("")
181
+ const [bulkRenameMode, setBulkRenameMode] = useState(false)
182
+ const [bulkRenameNames, setBulkRenameNames] = useState<Record<string, string>>({})
183
+ const [bulkRenameInProgress, setBulkRenameInProgress] = useState(false)
181
184
  const [creatingFolder, setCreatingFolder] = useState(false)
182
185
  const [newFolderName, setNewFolderName] = useState("")
183
186
  const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set())
184
187
  const [renamingFiles, setRenamingFiles] = useState<Set<string>>(new Set())
188
+ const [updatingPermissions, setUpdatingPermissions] = useState<Set<string>>(new Set())
189
+ const pendingFileOpsRef = useRef(0)
185
190
  const { setIsRouteLoading } = useRouteLoading()
186
191
 
187
192
  const [showFilenameDialog, setShowFilenameDialog] = useState(false)
@@ -213,6 +218,56 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
213
218
  }, 150)
214
219
  }, [])
215
220
 
221
+ const beginFileOperation = useCallback(() => {
222
+ if (pendingFileOpsRef.current === 0) {
223
+ setIsRouteLoading("+", location.pathname, true)
224
+ }
225
+ pendingFileOpsRef.current++
226
+ }, [location.pathname, setIsRouteLoading])
227
+
228
+ const endFileOperation = useCallback(() => {
229
+ pendingFileOpsRef.current = Math.max(0, pendingFileOpsRef.current - 1)
230
+ if (pendingFileOpsRef.current === 0) {
231
+ setIsRouteLoading("-", location.pathname)
232
+ }
233
+ }, [location.pathname, setIsRouteLoading])
234
+
235
+ const finishDeleteOperation = useCallback(
236
+ (fileName: string) => {
237
+ setDeletingFiles((prev) => {
238
+ const next = new Set(prev)
239
+ next.delete(fileName)
240
+ return next
241
+ })
242
+ endFileOperation()
243
+ },
244
+ [endFileOperation],
245
+ )
246
+
247
+ const finishRenameOperation = useCallback(
248
+ (fileName: string) => {
249
+ setRenamingFiles((prev) => {
250
+ const next = new Set(prev)
251
+ next.delete(fileName)
252
+ return next
253
+ })
254
+ endFileOperation()
255
+ },
256
+ [endFileOperation],
257
+ )
258
+
259
+ const finishPermissionsOperation = useCallback(
260
+ (fileName: string) => {
261
+ setUpdatingPermissions((prev) => {
262
+ const next = new Set(prev)
263
+ next.delete(fileName)
264
+ return next
265
+ })
266
+ endFileOperation()
267
+ },
268
+ [endFileOperation],
269
+ )
270
+
216
271
  const basePath = record ? `${tenantId}/${record.Collection_Path.join("/")}/${record.id}` : ""
217
272
 
218
273
  const totalPages = Math.ceil(items.length / itemsPerPage)
@@ -224,6 +279,12 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
224
279
  setCurrentPage(1)
225
280
  }, [items.length])
226
281
 
282
+ useEffect(() => {
283
+ setBulkRenameMode(false)
284
+ setBulkRenameNames({})
285
+ setBulkRenameInProgress(false)
286
+ }, [currentPath])
287
+
227
288
  useEffect(() => {
228
289
  const interval = setInterval(() => {
229
290
  setUploadProgress((prev) =>
@@ -572,8 +633,11 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
572
633
  async (permissions: FilePermissions) => {
573
634
  if (!selectedFileForPermissions || !record || !currentUser) return
574
635
 
636
+ const fileName = selectedFileForPermissions.name
637
+
575
638
  try {
576
- setIsRouteLoading("+", location.pathname, true)
639
+ beginFileOperation()
640
+ setUpdatingPermissions((prev) => new Set(prev).add(fileName))
577
641
 
578
642
  const targetRef = selectedFileForPermissions.isFolder
579
643
  ? ref(storage, `${selectedFileForPermissions.fullPath}/.placeholder`)
@@ -651,12 +715,12 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
651
715
  variant: "destructive",
652
716
  })
653
717
  } finally {
654
- setIsRouteLoading("-", location.pathname)
718
+ finishPermissionsOperation(fileName)
655
719
  setShowUpdatePermissionsDialog(false)
656
720
  setSelectedFileForPermissions(null)
657
721
  }
658
722
  },
659
- [selectedFileForPermissions, record, location.pathname, currentPath, currentUser],
723
+ [selectedFileForPermissions, record, currentPath, currentUser, beginFileOperation, finishPermissionsOperation],
660
724
  )
661
725
 
662
726
  const handleUpdatePermissionsCancel = useCallback(() => {
@@ -730,7 +794,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
730
794
  if (!itemToDelete || !record) return
731
795
 
732
796
  try {
733
- setIsRouteLoading("+", location.pathname, true)
797
+ beginFileOperation()
734
798
  setDeletingFiles((prev) => new Set(prev).add(itemToDelete.name))
735
799
 
736
800
  if (itemToDelete.isFolder) {
@@ -757,16 +821,11 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
757
821
  variant: "destructive",
758
822
  })
759
823
  } finally {
760
- setDeletingFiles((prev) => {
761
- const newSet = new Set(prev)
762
- newSet.delete(itemToDelete.name)
763
- return newSet
764
- })
765
- setIsRouteLoading("-", location.pathname)
824
+ finishDeleteOperation(itemToDelete.name)
766
825
  setShowDeleteDialog(false)
767
826
  setItemToDelete(null)
768
827
  }
769
- }, [itemToDelete, currentPath, location.pathname, record])
828
+ }, [itemToDelete, currentPath, record, beginFileOperation, finishDeleteOperation])
770
829
 
771
830
  const handleDeleteCancel = useCallback(() => {
772
831
  setShowDeleteDialog(false)
@@ -774,7 +833,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
774
833
  }, [])
775
834
 
776
835
  const handleEditName = useCallback(
777
- async (item: StorageItem, newName: string) => {
836
+ async (item: StorageItem, newName: string, options?: { skipReload?: boolean }) => {
778
837
  if (!record) return
779
838
  if (newName === item.name) {
780
839
  setEditingFile(null)
@@ -789,7 +848,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
789
848
  }
790
849
 
791
850
  try {
792
- setIsRouteLoading("+", location.pathname, true)
851
+ beginFileOperation()
793
852
  setRenamingFiles((prev) => new Set(prev).add(item.name))
794
853
 
795
854
  const originalRef = ref(storage, item.fullPath)
@@ -833,8 +892,15 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
833
892
  return
834
893
  }
835
894
 
895
+ if (!currentUser?.uid) return
836
896
  const newRef = ref(storage, newPath)
837
- await uploadBytesResumable(newRef, uploadContent, { customMetadata: metadata.customMetadata })
897
+ await uploadBytesResumable(newRef, uploadContent, {
898
+ contentType: metadata.contentType,
899
+ customMetadata: {
900
+ ...metadata.customMetadata,
901
+ createdBy: currentUser?.uid,
902
+ },
903
+ })
838
904
 
839
905
  await deleteObject(originalRef)
840
906
 
@@ -854,7 +920,9 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
854
920
 
855
921
  setEditingFile(null)
856
922
 
857
- loadDirectory(currentPath)
923
+ if (!options?.skipReload) {
924
+ loadDirectory(currentPath)
925
+ }
858
926
  } catch (error) {
859
927
  console.error((error as FirebaseError).message)
860
928
  toast({
@@ -863,33 +931,29 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
863
931
  variant: "destructive",
864
932
  })
865
933
  } finally {
866
- setRenamingFiles((prev) => {
867
- const newSet = new Set(prev)
868
- newSet.delete(item.name)
869
- return newSet
870
- })
871
- setIsRouteLoading("-", location.pathname)
934
+ finishRenameOperation(item.name)
872
935
  }
873
936
  },
874
- [currentPath, fileOptions],
937
+ [currentPath, fileOptions, beginFileOperation, finishRenameOperation, loadDirectory],
875
938
  )
876
939
 
877
940
  const navigateToFolder = useCallback(
878
941
  (folderName: string) => {
942
+ if (bulkRenameInProgress) return
879
943
  const newPath = currentPath ? `${currentPath}/${folderName}` : folderName
880
944
  loadDirectory(newPath)
881
945
  },
882
- [currentPath],
946
+ [currentPath, bulkRenameInProgress],
883
947
  )
884
948
 
885
949
  const navigateUp = useCallback(() => {
886
- if (!currentPath) return
950
+ if (!currentPath || bulkRenameInProgress) return
887
951
 
888
952
  const pathParts = currentPath.split("/")
889
953
  pathParts.pop()
890
954
  const newPath = pathParts.join("/")
891
955
  loadDirectory(newPath)
892
- }, [currentPath])
956
+ }, [currentPath, bulkRenameInProgress])
893
957
 
894
958
  const getPathBreadcrumbs = useCallback(() => {
895
959
  if (!currentPath) return []
@@ -970,6 +1034,59 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
970
1034
  setShowFolderPermissionsDialog(false)
971
1035
  }, [])
972
1036
 
1037
+ const canEditFile = useCallback(
1038
+ (item: StorageItem) => {
1039
+ if (item.isFolder) return false
1040
+ return (
1041
+ (permissions?.Role && item.metadata?.update?.includes(permissions.Role)) ||
1042
+ (currentUser && item.metadata?.createdBy === currentUser.uid)
1043
+ )
1044
+ },
1045
+ [permissions, currentUser],
1046
+ )
1047
+
1048
+ const startBulkRename = useCallback(() => {
1049
+ const editableFiles = items.filter(canEditFile)
1050
+ if (editableFiles.length === 0) return
1051
+ setEditingFile(null)
1052
+ setBulkRenameNames(Object.fromEntries(editableFiles.map((item) => [item.name, item.name])))
1053
+ setBulkRenameMode(true)
1054
+ }, [items, canEditFile])
1055
+
1056
+ const exitBulkRename = useCallback(() => {
1057
+ setBulkRenameMode(false)
1058
+ setBulkRenameNames({})
1059
+ setBulkRenameInProgress(false)
1060
+ }, [])
1061
+
1062
+ const cancelBulkRename = useCallback(() => {
1063
+ if (bulkRenameInProgress) return
1064
+ exitBulkRename()
1065
+ }, [bulkRenameInProgress, exitBulkRename])
1066
+
1067
+ const handleRenameAll = useCallback(async () => {
1068
+ const filesToRename = items
1069
+ .filter((item) => canEditFile(item) && bulkRenameNames[item.name] !== item.name)
1070
+ .map((item) => ({ item, newName: bulkRenameNames[item.name] }))
1071
+
1072
+ if (filesToRename.length === 0) {
1073
+ exitBulkRename()
1074
+ return
1075
+ }
1076
+
1077
+ setBulkRenameInProgress(true)
1078
+ try {
1079
+ await Promise.all(
1080
+ filesToRename.map(({ item, newName }) => handleEditName(item, newName, { skipReload: true })),
1081
+ )
1082
+ await loadDirectory(currentPath)
1083
+ } finally {
1084
+ exitBulkRename()
1085
+ }
1086
+ }, [items, bulkRenameNames, canEditFile, handleEditName, exitBulkRename, loadDirectory, currentPath])
1087
+
1088
+ const editableFileCount = items.filter(canEditFile).length
1089
+
973
1090
  let borderClass = "border-primary/40"
974
1091
  let textClass = "text-primary/50"
975
1092
  if (isDragOver) {
@@ -988,7 +1105,13 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
988
1105
 
989
1106
  <div className="flex items-center space-x-2 mb-4">
990
1107
  {currentPath && (
991
- <Button variant="outline" size="sm" onClick={navigateUp} className="flex items-center space-x-1">
1108
+ <Button
1109
+ variant="outline"
1110
+ size="sm"
1111
+ onClick={navigateUp}
1112
+ disabled={bulkRenameInProgress}
1113
+ className="flex items-center space-x-1"
1114
+ >
992
1115
  <ArrowLeft className="h-4 w-4" />
993
1116
  <span>Back</span>
994
1117
  </Button>
@@ -1019,7 +1142,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1019
1142
  >
1020
1143
  <div className={cn("text-center font-bold", textClass)}>Drop files here</div>
1021
1144
  </div>
1022
- <div className="flex items-center space-x-2 mt-4">
1145
+ <div className="flex flex-col gap-2 mt-4 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2">
1023
1146
  <input
1024
1147
  type="file"
1025
1148
  multiple
@@ -1033,15 +1156,52 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1033
1156
  cursor-pointer"
1034
1157
  onChange={handleFileUpload}
1035
1158
  />
1036
- <Button
1037
- variant="outline"
1038
- size="sm"
1039
- onClick={() => setCreatingFolder(true)}
1040
- className="flex items-center space-x-1 whitespace-nowrap"
1041
- >
1042
- <Plus className="h-4 w-4" />
1043
- <span>New Folder</span>
1044
- </Button>
1159
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2 shrink-0">
1160
+ <Button
1161
+ variant="outline"
1162
+ size="sm"
1163
+ onClick={() => setCreatingFolder(true)}
1164
+ disabled={bulkRenameMode}
1165
+ className="flex items-center space-x-1 whitespace-nowrap"
1166
+ >
1167
+ <Plus className="h-4 w-4" />
1168
+ <span>New Folder</span>
1169
+ </Button>
1170
+ {bulkRenameMode ? (
1171
+ <>
1172
+ <Button
1173
+ size="sm"
1174
+ onClick={handleRenameAll}
1175
+ disabled={bulkRenameInProgress}
1176
+ className="flex items-center space-x-1 whitespace-nowrap"
1177
+ >
1178
+ <Edit className="h-4 w-4" />
1179
+ <span>Rename All</span>
1180
+ </Button>
1181
+ <Button
1182
+ variant="outline"
1183
+ size="sm"
1184
+ onClick={cancelBulkRename}
1185
+ disabled={bulkRenameInProgress}
1186
+ className="whitespace-nowrap"
1187
+ >
1188
+ Cancel
1189
+ </Button>
1190
+ </>
1191
+ ) : (
1192
+ editableFileCount > 0 && (
1193
+ <Button
1194
+ variant="outline"
1195
+ size="sm"
1196
+ onClick={startBulkRename}
1197
+ className="flex items-center space-x-2 whitespace-nowrap"
1198
+ >
1199
+ <Edit className="h-4 w-4" />
1200
+ <span>Rename Files</span>
1201
+ </Button>
1202
+ )
1203
+ )}
1204
+ </div>
1045
1205
  </div>
1046
1206
 
1047
1207
  {uploadProgress.length > 0 && (
@@ -1130,7 +1290,8 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1130
1290
  {currentItems.map((item, index) => {
1131
1291
  const isDeleting = deletingFiles.has(item.name)
1132
1292
  const isRenaming = renamingFiles.has(item.name)
1133
- const isDisabled = isDeleting || isRenaming
1293
+ const isUpdatingPermissions = updatingPermissions.has(item.name)
1294
+ const isDisabled = isDeleting || isRenaming || isUpdatingPermissions
1134
1295
 
1135
1296
  return (
1136
1297
  <div
@@ -1158,40 +1319,68 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1158
1319
  <FileIcon className="h-5 w-5 shrink-0 text-gray-500" />
1159
1320
  )}
1160
1321
 
1161
- {editingFile === item.name && !isDisabled ? (
1322
+ {(bulkRenameMode && canEditFile(item)) ||
1323
+ (editingFile === item.name && !isDisabled) ? (
1162
1324
  <div className="flex items-center space-x-2 flex-1">
1163
1325
  <Input
1164
- value={newFileName}
1165
- onChange={(e) => setNewFileName(e.target.value)}
1326
+ value={
1327
+ bulkRenameMode
1328
+ ? bulkRenameNames[item.name]
1329
+ : newFileName
1330
+ }
1331
+ disabled={bulkRenameMode && bulkRenameInProgress}
1332
+ onChange={(event) => {
1333
+ if (bulkRenameMode) {
1334
+ setBulkRenameNames((prev) => ({
1335
+ ...prev,
1336
+ [item.name]: event.target.value,
1337
+ }))
1338
+ } else {
1339
+ setNewFileName(event.target.value)
1340
+ }
1341
+ }}
1166
1342
  className="flex-1"
1167
1343
  onKeyDown={(e) => {
1168
- if (e.key === "Enter") {
1169
- handleEditName(item, newFileName)
1170
- } else if (e.key === "Escape") {
1171
- setEditingFile(null)
1344
+ if (!bulkRenameMode) {
1345
+ if (e.key === "Enter") {
1346
+ handleEditName(item, newFileName)
1347
+ } else if (e.key === "Escape") {
1348
+ setEditingFile(null)
1349
+ }
1172
1350
  }
1173
1351
  }}
1174
1352
  />
1175
- <Button
1176
- size="sm"
1177
- onClick={() => handleEditName(item, newFileName)}
1178
- >
1179
- Save
1180
- </Button>
1181
- <Button
1182
- size="sm"
1183
- variant="outline"
1184
- onClick={() => setEditingFile(null)}
1185
- >
1186
- Cancel
1187
- </Button>
1353
+ {!bulkRenameMode && (
1354
+ <>
1355
+ <Button
1356
+ size="sm"
1357
+ onClick={() => handleEditName(item, newFileName)}
1358
+ >
1359
+ Save
1360
+ </Button>
1361
+ <Button
1362
+ size="sm"
1363
+ variant="outline"
1364
+ onClick={() => setEditingFile(null)}
1365
+ >
1366
+ Cancel
1367
+ </Button>
1368
+ </>
1369
+ )}
1188
1370
  </div>
1189
1371
  ) : (
1190
1372
  <div className="flex-1 min-w-0">
1191
1373
  {item.isFolder ? (
1192
1374
  <button
1375
+ type="button"
1193
1376
  onClick={() => navigateToFolder(item.name)}
1194
- className="text-left hover:underline block w-full"
1377
+ disabled={bulkRenameInProgress}
1378
+ className={cn(
1379
+ "text-left block w-full",
1380
+ bulkRenameInProgress
1381
+ ? "cursor-not-allowed opacity-50"
1382
+ : "hover:underline",
1383
+ )}
1195
1384
  >
1196
1385
  {item.name}
1197
1386
  </button>
@@ -1202,7 +1391,10 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1202
1391
  )}
1203
1392
  </div>
1204
1393
 
1205
- {!(editingFile === item.name && !isDisabled) && (
1394
+ {!(
1395
+ (bulkRenameMode && canEditFile(item)) ||
1396
+ (editingFile === item.name && !isDisabled)
1397
+ ) && (
1206
1398
  <div className="flex items-center space-x-2">
1207
1399
  {!item.isFolder && (
1208
1400
  <>
@@ -1222,10 +1414,11 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1222
1414
  size="sm"
1223
1415
  variant="outline"
1224
1416
  onClick={() => {
1417
+ cancelBulkRename()
1225
1418
  setEditingFile(item.name)
1226
1419
  setNewFileName(item.name)
1227
1420
  }}
1228
- disabled={isDisabled}
1421
+ disabled={isDisabled || bulkRenameMode}
1229
1422
  >
1230
1423
  <Edit className="h-4 w-4" />
1231
1424
  </Button>