@stoker-platform/web-app 0.5.141 → 0.5.142

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,11 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.142
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: add bulk file rename feature
8
+
3
9
  ## 0.5.141
4
10
 
5
11
  ### 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.142",
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)
@@ -854,7 +913,9 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
854
913
 
855
914
  setEditingFile(null)
856
915
 
857
- loadDirectory(currentPath)
916
+ if (!options?.skipReload) {
917
+ loadDirectory(currentPath)
918
+ }
858
919
  } catch (error) {
859
920
  console.error((error as FirebaseError).message)
860
921
  toast({
@@ -863,33 +924,29 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
863
924
  variant: "destructive",
864
925
  })
865
926
  } finally {
866
- setRenamingFiles((prev) => {
867
- const newSet = new Set(prev)
868
- newSet.delete(item.name)
869
- return newSet
870
- })
871
- setIsRouteLoading("-", location.pathname)
927
+ finishRenameOperation(item.name)
872
928
  }
873
929
  },
874
- [currentPath, fileOptions],
930
+ [currentPath, fileOptions, beginFileOperation, finishRenameOperation, loadDirectory],
875
931
  )
876
932
 
877
933
  const navigateToFolder = useCallback(
878
934
  (folderName: string) => {
935
+ if (bulkRenameInProgress) return
879
936
  const newPath = currentPath ? `${currentPath}/${folderName}` : folderName
880
937
  loadDirectory(newPath)
881
938
  },
882
- [currentPath],
939
+ [currentPath, bulkRenameInProgress],
883
940
  )
884
941
 
885
942
  const navigateUp = useCallback(() => {
886
- if (!currentPath) return
943
+ if (!currentPath || bulkRenameInProgress) return
887
944
 
888
945
  const pathParts = currentPath.split("/")
889
946
  pathParts.pop()
890
947
  const newPath = pathParts.join("/")
891
948
  loadDirectory(newPath)
892
- }, [currentPath])
949
+ }, [currentPath, bulkRenameInProgress])
893
950
 
894
951
  const getPathBreadcrumbs = useCallback(() => {
895
952
  if (!currentPath) return []
@@ -970,6 +1027,59 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
970
1027
  setShowFolderPermissionsDialog(false)
971
1028
  }, [])
972
1029
 
1030
+ const canEditFile = useCallback(
1031
+ (item: StorageItem) => {
1032
+ if (item.isFolder) return false
1033
+ return (
1034
+ (permissions?.Role && item.metadata?.update?.includes(permissions.Role)) ||
1035
+ (currentUser && item.metadata?.createdBy === currentUser.uid)
1036
+ )
1037
+ },
1038
+ [permissions, currentUser],
1039
+ )
1040
+
1041
+ const startBulkRename = useCallback(() => {
1042
+ const editableFiles = items.filter(canEditFile)
1043
+ if (editableFiles.length === 0) return
1044
+ setEditingFile(null)
1045
+ setBulkRenameNames(Object.fromEntries(editableFiles.map((item) => [item.name, item.name])))
1046
+ setBulkRenameMode(true)
1047
+ }, [items, canEditFile])
1048
+
1049
+ const exitBulkRename = useCallback(() => {
1050
+ setBulkRenameMode(false)
1051
+ setBulkRenameNames({})
1052
+ setBulkRenameInProgress(false)
1053
+ }, [])
1054
+
1055
+ const cancelBulkRename = useCallback(() => {
1056
+ if (bulkRenameInProgress) return
1057
+ exitBulkRename()
1058
+ }, [bulkRenameInProgress, exitBulkRename])
1059
+
1060
+ const handleRenameAll = useCallback(async () => {
1061
+ const filesToRename = items
1062
+ .filter((item) => canEditFile(item) && bulkRenameNames[item.name] !== item.name)
1063
+ .map((item) => ({ item, newName: bulkRenameNames[item.name] }))
1064
+
1065
+ if (filesToRename.length === 0) {
1066
+ exitBulkRename()
1067
+ return
1068
+ }
1069
+
1070
+ setBulkRenameInProgress(true)
1071
+ try {
1072
+ await Promise.all(
1073
+ filesToRename.map(({ item, newName }) => handleEditName(item, newName, { skipReload: true })),
1074
+ )
1075
+ await loadDirectory(currentPath)
1076
+ } finally {
1077
+ exitBulkRename()
1078
+ }
1079
+ }, [items, bulkRenameNames, canEditFile, handleEditName, exitBulkRename, loadDirectory, currentPath])
1080
+
1081
+ const editableFileCount = items.filter(canEditFile).length
1082
+
973
1083
  let borderClass = "border-primary/40"
974
1084
  let textClass = "text-primary/50"
975
1085
  if (isDragOver) {
@@ -988,7 +1098,13 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
988
1098
 
989
1099
  <div className="flex items-center space-x-2 mb-4">
990
1100
  {currentPath && (
991
- <Button variant="outline" size="sm" onClick={navigateUp} className="flex items-center space-x-1">
1101
+ <Button
1102
+ variant="outline"
1103
+ size="sm"
1104
+ onClick={navigateUp}
1105
+ disabled={bulkRenameInProgress}
1106
+ className="flex items-center space-x-1"
1107
+ >
992
1108
  <ArrowLeft className="h-4 w-4" />
993
1109
  <span>Back</span>
994
1110
  </Button>
@@ -1019,7 +1135,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1019
1135
  >
1020
1136
  <div className={cn("text-center font-bold", textClass)}>Drop files here</div>
1021
1137
  </div>
1022
- <div className="flex items-center space-x-2 mt-4">
1138
+ <div className="flex flex-col gap-2 mt-4 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2">
1023
1139
  <input
1024
1140
  type="file"
1025
1141
  multiple
@@ -1033,15 +1149,52 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1033
1149
  cursor-pointer"
1034
1150
  onChange={handleFileUpload}
1035
1151
  />
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>
1152
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2 shrink-0">
1153
+ <Button
1154
+ variant="outline"
1155
+ size="sm"
1156
+ onClick={() => setCreatingFolder(true)}
1157
+ disabled={bulkRenameMode}
1158
+ className="flex items-center space-x-1 whitespace-nowrap"
1159
+ >
1160
+ <Plus className="h-4 w-4" />
1161
+ <span>New Folder</span>
1162
+ </Button>
1163
+ {bulkRenameMode ? (
1164
+ <>
1165
+ <Button
1166
+ size="sm"
1167
+ onClick={handleRenameAll}
1168
+ disabled={bulkRenameInProgress}
1169
+ className="flex items-center space-x-1 whitespace-nowrap"
1170
+ >
1171
+ <Edit className="h-4 w-4" />
1172
+ <span>Rename All</span>
1173
+ </Button>
1174
+ <Button
1175
+ variant="outline"
1176
+ size="sm"
1177
+ onClick={cancelBulkRename}
1178
+ disabled={bulkRenameInProgress}
1179
+ className="whitespace-nowrap"
1180
+ >
1181
+ Cancel
1182
+ </Button>
1183
+ </>
1184
+ ) : (
1185
+ editableFileCount > 0 && (
1186
+ <Button
1187
+ variant="outline"
1188
+ size="sm"
1189
+ onClick={startBulkRename}
1190
+ className="flex items-center space-x-2 whitespace-nowrap"
1191
+ >
1192
+ <Edit className="h-4 w-4" />
1193
+ <span>Rename Files</span>
1194
+ </Button>
1195
+ )
1196
+ )}
1197
+ </div>
1045
1198
  </div>
1046
1199
 
1047
1200
  {uploadProgress.length > 0 && (
@@ -1130,7 +1283,8 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1130
1283
  {currentItems.map((item, index) => {
1131
1284
  const isDeleting = deletingFiles.has(item.name)
1132
1285
  const isRenaming = renamingFiles.has(item.name)
1133
- const isDisabled = isDeleting || isRenaming
1286
+ const isUpdatingPermissions = updatingPermissions.has(item.name)
1287
+ const isDisabled = isDeleting || isRenaming || isUpdatingPermissions
1134
1288
 
1135
1289
  return (
1136
1290
  <div
@@ -1158,40 +1312,68 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1158
1312
  <FileIcon className="h-5 w-5 shrink-0 text-gray-500" />
1159
1313
  )}
1160
1314
 
1161
- {editingFile === item.name && !isDisabled ? (
1315
+ {(bulkRenameMode && canEditFile(item)) ||
1316
+ (editingFile === item.name && !isDisabled) ? (
1162
1317
  <div className="flex items-center space-x-2 flex-1">
1163
1318
  <Input
1164
- value={newFileName}
1165
- onChange={(e) => setNewFileName(e.target.value)}
1319
+ value={
1320
+ bulkRenameMode
1321
+ ? bulkRenameNames[item.name]
1322
+ : newFileName
1323
+ }
1324
+ disabled={bulkRenameMode && bulkRenameInProgress}
1325
+ onChange={(event) => {
1326
+ if (bulkRenameMode) {
1327
+ setBulkRenameNames((prev) => ({
1328
+ ...prev,
1329
+ [item.name]: event.target.value,
1330
+ }))
1331
+ } else {
1332
+ setNewFileName(event.target.value)
1333
+ }
1334
+ }}
1166
1335
  className="flex-1"
1167
1336
  onKeyDown={(e) => {
1168
- if (e.key === "Enter") {
1169
- handleEditName(item, newFileName)
1170
- } else if (e.key === "Escape") {
1171
- setEditingFile(null)
1337
+ if (!bulkRenameMode) {
1338
+ if (e.key === "Enter") {
1339
+ handleEditName(item, newFileName)
1340
+ } else if (e.key === "Escape") {
1341
+ setEditingFile(null)
1342
+ }
1172
1343
  }
1173
1344
  }}
1174
1345
  />
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>
1346
+ {!bulkRenameMode && (
1347
+ <>
1348
+ <Button
1349
+ size="sm"
1350
+ onClick={() => handleEditName(item, newFileName)}
1351
+ >
1352
+ Save
1353
+ </Button>
1354
+ <Button
1355
+ size="sm"
1356
+ variant="outline"
1357
+ onClick={() => setEditingFile(null)}
1358
+ >
1359
+ Cancel
1360
+ </Button>
1361
+ </>
1362
+ )}
1188
1363
  </div>
1189
1364
  ) : (
1190
1365
  <div className="flex-1 min-w-0">
1191
1366
  {item.isFolder ? (
1192
1367
  <button
1368
+ type="button"
1193
1369
  onClick={() => navigateToFolder(item.name)}
1194
- className="text-left hover:underline block w-full"
1370
+ disabled={bulkRenameInProgress}
1371
+ className={cn(
1372
+ "text-left block w-full",
1373
+ bulkRenameInProgress
1374
+ ? "cursor-not-allowed opacity-50"
1375
+ : "hover:underline",
1376
+ )}
1195
1377
  >
1196
1378
  {item.name}
1197
1379
  </button>
@@ -1202,7 +1384,10 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1202
1384
  )}
1203
1385
  </div>
1204
1386
 
1205
- {!(editingFile === item.name && !isDisabled) && (
1387
+ {!(
1388
+ (bulkRenameMode && canEditFile(item)) ||
1389
+ (editingFile === item.name && !isDisabled)
1390
+ ) && (
1206
1391
  <div className="flex items-center space-x-2">
1207
1392
  {!item.isFolder && (
1208
1393
  <>
@@ -1222,10 +1407,11 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
1222
1407
  size="sm"
1223
1408
  variant="outline"
1224
1409
  onClick={() => {
1410
+ cancelBulkRename()
1225
1411
  setEditingFile(item.name)
1226
1412
  setNewFileName(item.name)
1227
1413
  }}
1228
- disabled={isDisabled}
1414
+ disabled={isDisabled || bulkRenameMode}
1229
1415
  >
1230
1416
  <Edit className="h-4 w-4" />
1231
1417
  </Button>