libraryFolders.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import type { LibraryFolderTree } from '../api/client';
  2. /**
  3. * Drag payload type for a File Manager folder being moved (#3305). Distinct
  4. * from the browser's `Files` type so a folder drag never trips the page-wide
  5. * upload overlay, and from any future file-row drag type.
  6. */
  7. export const FOLDER_DRAG_TYPE = 'application/x-bambuddy-folder';
  8. function collectSubtreeIds(folder: LibraryFolderTree, into: Set<number>): void {
  9. into.add(folder.id);
  10. for (const child of folder.children) collectSubtreeIds(child, into);
  11. }
  12. function findFolder(folders: LibraryFolderTree[], id: number): LibraryFolderTree | null {
  13. for (const folder of folders) {
  14. if (folder.id === id) return folder;
  15. const hit = findFolder(folder.children, id);
  16. if (hit) return hit;
  17. }
  18. return null;
  19. }
  20. /**
  21. * Whether the current user may put things into `folder`. A library:*_own user
  22. * sees shared folders they cannot write to (#3201); those stay listed in the
  23. * pickers but are never a destination. The picker's disabled state and the
  24. * folder drop targets both use this, so the two cannot drift apart.
  25. */
  26. export function isWritableFolder(folder: Pick<LibraryFolderTree, 'can_write'>): boolean {
  27. return folder.can_write !== false;
  28. }
  29. /**
  30. * Folder ids that may NOT become the new parent of `folderId` (#3305): the
  31. * folder itself, everything below it (the server rejects those with 400 too,
  32. * this just keeps them out of the picker), and every external folder with its
  33. * subtree — external folders mirror a path on disk, so nothing can be moved
  34. * into one.
  35. */
  36. export function moveDisallowedFolderIds(folders: LibraryFolderTree[], folderId: number): Set<number> {
  37. const disallowed = new Set<number>();
  38. const self = findFolder(folders, folderId);
  39. if (self) collectSubtreeIds(self, disallowed);
  40. const walk = (items: LibraryFolderTree[]) => {
  41. for (const item of items) {
  42. if (item.is_external) collectSubtreeIds(item, disallowed);
  43. else walk(item.children);
  44. }
  45. };
  46. walk(folders);
  47. return disallowed;
  48. }
  49. /**
  50. * Whether dropping / moving `folder` onto `destinationId` (null = root) would
  51. * actually change anything the server accepts. False for the folder itself,
  52. * its descendants, external destinations, destinations the user may not write
  53. * to, and the parent it already has.
  54. */
  55. export function isValidFolderMove(
  56. folders: LibraryFolderTree[],
  57. folder: Pick<LibraryFolderTree, 'id' | 'parent_id' | 'is_external'>,
  58. destinationId: number | null,
  59. ): boolean {
  60. if (folder.is_external) return false;
  61. if ((folder.parent_id ?? null) === destinationId) return false;
  62. if (destinationId === null) return true;
  63. const destination = findFolder(folders, destinationId);
  64. if (!destination || !isWritableFolder(destination)) return false;
  65. return !moveDisallowedFolderIds(folders, folder.id).has(destinationId);
  66. }