Skip to content

Resources

Key capabilities:

  • Folder tree with drag‑and‑drop
  • File upload & download
  • Per‑folder/file ACL
  • Trash + restore (soft delete + hard delete)
  • Search across folders/files
  • Usage tracking (file views)

The UX is built around a left folder tree and a main content panel:

  • Tree and global state live in ResourcesRoot.
  • The main file/folder view lives in Resources (module component).
  • Data and mutations are driven by internalResources.ts and React Query hooks in services/reactQuery/internalResourceQuery.ts.

ResourcesRoot
├── ResourceSidebar (search, root folder, trash)
├── TreeListBuilder (folder tree + drag-and-drop)
└── Resources (main panel)
Resources (module.tsx)
├── useInternalResources.* (React Query hooks)
│ │
│ └── internalResources.ts
│ │
│ └── /internal-resources API
└── Child components
├── ResourceTabView
├── ResourceFileView
├── ShareModal
└── ViewStatsModal

  • External resources API (src/services/resources.ts)
    • Provides a path‑based API for /resources, used for legacy/public resources and folder access configs.
  • Internal resources API (src/services/internalResources.ts)
    • Provides ID‑based APIs under /internal-resources/** and /search/resources.
    • Powers the Resources module UI via React Query hooks (useInternalResources).
  • Module shell & tree
    • ResourcesRoot.tsx – root layout, tree sidebar, URL/breadcrumb handling, drag‑and‑drop, and trash integration.
    • module.tsx (exported as Resources) – main content/viewer, uploads, deletions, trash display.
    • Hooks in src/modules/Resources/hooks/* split responsibilities (data, operations, URL, breadcrumbs, tree, state).

Defined in src/services/internalResources.ts:

  • Folder (IRFolder)

    • id: number
    • name: string
    • parentFolderId: number | null
    • hasChildren?: number | boolean – 1/true means folder has (or may have) children.
    • orderIndex?: number / order_index?: number
    • files?: IRFile[]
    • childFolders?: IRFolder[]
    • filesPagination? – pagination metadata for files (page, limit, total, totalPages, hasNextPage, hasPrevPage).
  • File (IRFile)

    • id: number
    • name: string
    • folderId: number | null
  • Folder contents (ContentsResponse)

    • success: boolean
    • message: string
    • data:
      • folders: IRFolder[]
      • files?: IRFile[]
      • filesPagination?: { page, limit, total, totalPages, hasNextPage, hasPrevPage }
  • Search results

    • SearchResponse – top‑level structure from /search/resources.
      • data.total
      • data.sections: SearchSection[]
    • SearchSection:
      • module: "resource_folders" | "resource_files"
      • total: number
      • items: { id: number; title: string; ... }[]
    • SearchContentsResponse (UI‑friendly):
      • data: { folders: IRFolder[]; files: IRFile[] }
      • Built by searchResources (see below).

ACL (Access Control) – Internal Resources

Section titled “ACL (Access Control) – Internal Resources”

At the internal resources layer:

  • PrincipalType = "user" | "group"

  • AccessType = "read"

  • EffectType = "allow" | "deny"

  • ACLRule

    • principalType: PrincipalType
    • principalId: number
    • access: AccessType
    • effect: EffectType
  • ACLConfig

    • inherit: boolean – whether to inherit from parent.
    • rules: ACLRule[]
  • ACLResponse

    • success: boolean
    • data?: ACLConfig
    • error?: string

These are used by the ACL endpoints:

  • getFolderACL(folderId)
  • updateFolderACL(folderId, acl)
  • getFileACL(fileId)
  • updateFileACL(fileId, acl)

Separate from internal ACL, src/types/Resources.ts defines a path‑based access config (used with the /resources/access-config endpoints):

  • AllowedUser

    • userId: number
    • userName: string
  • AllowedGroup

    • groupId: number
    • groupName: string
  • DenyUser

    • userId: number
    • userName: string
  • ResourceAccessConfig

    • allowedUsers?: AllowedUser[]
    • allowedGroups?: AllowedGroup[]
    • inheritFromParent?: boolean
    • denyUsers?: DenyUser[]

Validated via ResourceAccessConfigSchema (Zod).

src/modules/Resources/config.ts:

  • ResourceModuleConfig

    • key: string – route/module key.
    • root: string – root folder path segment used for this sub‑module.
    • title: string – page title.
    • label: string – sidebar label.
  • RESOURCE_MODULES

    • Predefined modules, e.g.:
      • standard-operating-procedures / standard_operating_procedures
      • marketing-material / marketing_material
      • full-team-manual, franchisee-manual, internal-forms, client-forms.

External Resources API (src/services/resources.ts)

Section titled “External Resources API (src/services/resources.ts)”

These are path‑based endpoints (legacy/public resources and folder access config).

  • Get all resources (optionally in a folder path)

    • getAllResources(path?: string)
    • Endpoint: GET /resources?folderPath={path} (query param optional).
    • Returns: Resource[] (from @/modules/Resources/utils/types).
  • Create folder by path

    • createNewFolder(path: string)
    • Endpoint: POST /resources/folders
    • Body: { folderPath: path }
  • Delete folder by path

    • deleteFolder(path: string, force?: boolean)
    • Endpoint: DELETE /resources/folders?folderPath={path}&force=1?
    • force=1 for forced deletion.
  • List folders by parent path

    • getAllFolders(path?: string)
    • Endpoint: GET /resources/folders?parentPath={path}.
    • Returns: Folder[] (from utils/types).
  • Save folder order

    • saveFolderOrder(order: SortOrder)
    • Endpoint: POST /resources/folders/reorder
    • Payload: SortOrder (from utils/types).
  • Delete resource by ID

    • deleteResourceById(id: number)
    • Endpoint: DELETE /resources/{id}
  • Upload resource to a path

    • uploadResource(path: string, file: File)
    • Endpoint: POST /resources
    • Body: FormData with:
      • files (file)
      • folderPath (string)
  • Get folder access config

    • getAccessConfig(folderPath: string): Promise<ResourceAccessConfig | undefined>
    • Endpoint: GET /resources/access-config?folderPath={path} (via ResourceURL).
    • Parses response via ResourceAccessConfigSchema.
  • Save folder access config

    • saveAccessConfig(folderPath: string, config: ResourceAccessConfig)
    • Endpoint: PUT /resources/access-config?folderPath={path}
    • Body: { config }.

Internal Resources API (src/services/internalResources.ts)

Section titled “Internal Resources API (src/services/internalResources.ts)”
  • List folders

    • listFolders(parentId: number | null): Promise<IRFolder[]>
    • Endpoint: GET /internal-resources/folders?parentId={parentId} (or no param for root).
  • List files in a folder

    • listFiles(folderId: number): Promise<IRFile[]>
    • Endpoint: GET /internal-resources/folders/{folderId}/files.
  • Create folder

    • createFolder(name: string, parentFolderId: number | null, isPublic = true)
    • Endpoint: POST /internal-resources/folders.
    • Body: { name, parentFolderId, isPublic }.
    • Returns { success, id?, status?, error? }.
  • Update folder

    • updateFolder(folderId, name?, hasChildren?, parentFolderId?)
    • Endpoint: PATCH /internal-resources/folders/{folderId}
    • Body: combination of:
      • name?: string
      • hasChildren?: 1 | 0 (boolean normalised to number)
      • parentFolderId?: number | null
  • Soft delete folder

    • deleteFolder(folderId)
    • Endpoint: DELETE /internal-resources/folders/{folderId}
  • Hard delete folder

    • permanentlyDeleteFolder(folderId)
    • Endpoint: DELETE /internal-resources/folders/{folderId}/permanent
  • Update folder order

    • updateFolderOrder(parentFolderId: number | null, folderIds: number[])
    • Endpoint:
      • PATCH /internal-resources/folders/{parentIdOr0}/order
      • Root uses 0 when parentFolderId === null.
    • Body: { folders: folderIds }.
  • Upload file

    • uploadFile(folderId: number, file: File)
    • Endpoint: POST /internal-resources/folders/{folderId}/files
    • Body: FormData with files.
    • Returns { success, id?, status?, error? } (first uploaded file).
  • Update file

    • updateFile(fileId: number, name?: string)
    • Endpoint: PATCH /internal-resources/files/{fileId}
    • Body: { name?: string }.
  • Delete file / folder

    • Soft delete
      • Files: DELETE /internal-resources/files/{id}
      • Folders: DELETE /internal-resources/folders/{id}
    • Hard delete
      • Files: DELETE /internal-resources/files/{id}/permanent
      • Folders: DELETE /internal-resources/folders/{id}/permanent
  • Move file

    • moveFile(fileId: number, folderId: number | null)
    • Endpoint: PATCH /internal-resources/files/{fileId}/move
    • Body: { folderId: number | null }.
  • Get folder contents

    • getFolderContents(folderId: number, opts?: { page?: number; limit?: number }): Promise<ContentsResponse | null>
    • Endpoint: GET /internal-resources/folders/{folderId}/contents?page=&limit=.
    • Returns ContentsResponse.
  • Get all root contents

    • getAllContents(): Promise<ContentsResponse | null>
    • Endpoint: GET /internal-resources/contents.
    • Used for root tree & initial load.
  • Search resources
    • searchResources(query: string): Promise<SearchContentsResponse | null>
    • Endpoint: GET /search/resources?q={query}
    • Internally:
      • Groups sections by resource_folders and resource_files.
      • Builds a foldersMap keyed by folder id.
        • For resource_folders items: creates IRFolder with id, name, parentFolderId, hasChildren.
        • For resource_files items:
          • Adds IRFile to files.
          • Ensures folder entries exist for folderId (even if not present in folder section), optionally tracking names to backfill.
      • For folders with missing names, recursively loads from API:
        • Calls listFolders(null) for roots.
        • Uses getFolderContents recursively to search for matching folders.
      • Groups files by folderId and attaches to folders.
    • Returns { success, message, data: { folders, files } }.
  • Folder ACL

    • getFolderACL(folderId: number): Promise<ACLResponse>
    • updateFolderACL(folderId: number, acl: ACLConfig)
  • File ACL

    • getFileACL(fileId: number): Promise<ACLResponse>
    • updateFileACL(fileId: number, acl: ACLConfig)
  • File views – tracking & stats

    • trackFileView(fileId: number)
      • POST /internal-resources/files/{fileId}/views
    • getFileViews(fileId: number)
      • GET /internal-resources/files/{fileId}/views
  • Get trash contents

    • getTrash(opts?: { page?: number; limit?: number })
    • Endpoint: GET /internal-resources/trash?page=&limit=
    • Returns folders and files plus combined pagination/total stats.
  • Restore file/folder from trash

    • restoreFile(fileId: number)POST /internal-resources/files/{fileId}/restore
    • restoreFolder(folderId: number)POST /internal-resources/folders/{folderId}/restore

Use internalResources.ts when:

  • Working inside the Resources module (tree, main panel, trash).
  • Accessing folders/files by ID (folderId, fileId).
  • Managing ACL for internal folders/files.
  • Working with trash, search, or view statistics.

Use resources.ts when:

  • Working with path‑based folders (folderPath, parentPath).
  • Managing public/legacy resources outside of the internal resources tree.
  • Managing folder access config via /resources/access-config using ResourceAccessConfig.

User drops or selects file in Resources UI
Resources.module.ts (handleUploadFile / handleUploadFiles)
useInternalResources.uploadFile(folderId)
POST /internal-resources/folders/{folderId}/files
React Query refetches / invalidates:
["internal-resources", "contents", folderId]
["internal-resources", "files", folderId]
User drags folder in TreeListBuilder
ResourcesRoot.handleTreeMove()
parseFolderId(...) → validates move (no circular refs, not into self)
updateFolderMut({ folderId, parentFolderId: toParentId })
PATCH /internal-resources/folders/{folderId}
Tree and URL/breadcrumbs updated via useResourcesFolderToggle / useResourcesBreadcrumbs
User clicks "Delete" in ResourceFileView
Resources.handleDelete(fileId)
useInternalResources.deleteFile().mutateAsync(fileId)
DELETE /internal-resources/files/{fileId}
React Query invalidates:
["internal-resources", "trash"]
["internal-resources", "contents", folderId]
["internal-resources", "contents", null]
["internal-resources", "files", folderId]
Parent notified via onFolderContentsChanged(folderId) to update tree caret

src/modules/Resources
├── ResourcesRoot.tsx // Shell: layout, tree, URL, breadcrumbs, trash, DnD
├── module.tsx // Main panel: uploads, viewer, trash, list/grid
├── index.ts // Legacy
├── config.ts // Legacy
├── hooks
│ ├── useResourcesState.ts
│ ├── useResourcesUrlState.ts
│ ├── useResourcesData.ts
│ ├── useResourcesOperations.ts
│ ├── useResourcesTree.ts
│ ├── useResourcesBreadcrumbs.ts
│ └── useResourcesFolderToggle.ts
├── components
│ ├── ResourceSidebar.tsx
│ ├── ResourceTabView.tsx
│ ├── ResourceFileView.tsx
│ ├── ResourceFileCreator.tsx
│ ├── ResourceAccessForm.tsx
│ ├── ResourcePropertiesModal.tsx
│ ├── ViewStatsModal.tsx
│ ├── ShareModal.tsx
│ ├── ResourcesPdfRender.tsx
│ ├── ResourceFolderSorter.tsx
│ └── ResourceDragAndDropList.tsx
└── styles
├── ResourcesRoot.module.css
├── resources.module.css
├── resourcesidebar.module.css
├── resourcetabview.module.css
├── resourceFileView.module.css
├── ResourceDragAndDropList.module.css
├── resourcemainview.module.css
├── resourcemobileview.module.css
├── resourcesorter.module.css
├── viewstats.module.css
└── resourcefileselector.module.css

React Query Layer (useInternalResources hooks)

Section titled “React Query Layer (useInternalResources hooks)”

The module primarily uses hooks from src/services/reactQuery/internalResourceQuery.ts such as:

  • Queries:
    • useInternalResources.contents(folderId, page, limit, enabled)
    • useInternalResources.trash(page, limit)
  • Mutations:
    • useInternalResources.uploadFile(folderId)
    • useInternalResources.deleteFile()
    • useInternalResources.permanentlyDeleteFile()
    • useInternalResources.permanentlyDeleteFolder()
    • useInternalResources.restoreFile()
    • useInternalResources.restoreFolder()
    • useInternalResources.createFolder()
    • useInternalResources.updateFolder()
    • useInternalResources.updateFolderOrder()
    • useInternalResources.updateFile()
    • useInternalResources.updateFolderACL()

These wrap the internal resources service functions and standardise query keys such as:

  • ["internal-resources", "contents", folderId]
  • ["internal-resources", "files", folderId]
  • ["internal-resources", "trash"]
  • ["internal-resources", "search", query]

When adding new features, reuse these keys to keep cache invalidation consistent.


File: src/modules/Resources/ResourcesRoot.tsx.

Responsibilities:

  • Integrates module into the global layout:
    • Collapses the main app sidebar (sidebar_collapse localStorage).
    • Responsive left sidebar (mobile overlay vs fixed).
  • Owns global state for:
    • Selected folder/file, breadcrumbs, expanded folder ids, renaming/properties/access modals.
  • Controls URL state and deep‑linking:
    • Uses useResourcesUrlState to read/write urlPath (folder chain) and urlFileId.
    • Expands folders to match URL and sets selection/breadcrumbs on mount.
  • Drives tree data:
    • Uses useResourcesData to:
      • Fetch root contents / search results / expanded folder contents.
      • Maintain contentsByFolderId, hasMoreFilesByFolderId, folderNameById, folderById.
    • Uses useResourcesTree to construct treeData for TreeListBuilder.
    • Tracks expandedFolderIds (internal “known contents”) separate from UI expansion.
  • Handles drag‑and‑drop:
    • handleTreeChange:
      • Detects root‑folder reordering and calls updateFolderOrderMut with parentFolderId: null.
    • handleTreeMove:
      • Distinguishes file vs folder move by ID prefix (file- vs folder-).
      • Validates moves:
        • Files cannot be moved to root.
        • Folders cannot be moved into themselves or descendants.
      • Calls moveFileMut or updateFolderMut({ folderId, parentFolderId }).
  • Implements semi‑lazy loading:
    • Tracks processedFolderIds to avoid re‑processing.
    • When a folder’s contents are loaded, auto‑expands children with hasChildren so their caret/state is ready.
    • When a folder is expanded in the tree, fetches contents and may recursively expand children with hasChildren.
  • Integrates trash view:
    • showTrash toggles between normal view and a “Deleted Items” view (via Resources with showTrash).
    • Stores previous folder & breadcrumbs to restore when exiting trash.

Key components it composes:

  • ResourceSidebar – search, root folder creation, “Trash” entry.
  • TreeListBuilder – interactive tree UI using treeData.
  • Resources – main panel (see below).
  • ResourceFileCreator – modal for creating/renaming folders/files.
  • ResourcePropertiesModal – folder/file properties + ACL launcher.
  • ResourceAccessForm – ACL editing modal for folder/file.

File: src/modules/Resources/module.tsx (also re‑exported from index.ts).

Responsibilities:

  • Fetching & paging:

    • Uses useInternalResources.contents(folderId, filesPage, FILES_PAGE_SIZE, !showTrash) to load:
      • data.folders (IRFolder[])
      • data.files (IRFile[])
      • filesPagination/pagination.
    • Uses useInternalResources.trash(trashPage, FILES_PAGE_SIZE) when showTrash is true.
  • Soft delete vs trash:

    • Normal view:
      • handleDelete calls deleteFileMut(fileId) after confirm.
      • Invalidates:
        • ["internal-resources", "trash"]
        • ["internal-resources", "contents", folderId]
        • ["internal-resources", "contents", null]
        • ["internal-resources", "files", folderId]
      • Notifies parent via onFolderContentsChanged to update carets.
    • Trash view:
      • Presents trash folders/files with pagination.
      • handleRestoreFile, handleRestoreFolder – call restore mutations and refetch trash + main folder contents.
      • handlePermanentlyDeleteFile, handlePermanentlyDeleteFolder – confirm irreversible, then call hard‑delete mutations and refetch trash.
  • Uploads:

    • Single file upload:
      • handleUploadFile(file)
        • Enforces 50MB per file.
        • Calls uploadFileMut(file) with toast messaging.
        • Refetches contents and files queries for the current folder.
    • Multiple files:
      • handleUploadFiles(files[]):
        • Rejects files > 50MB and total batch > 500MB (500MB limit).
        • Shows per‑file error toasts for invalid files.
        • Uploads valid files in parallel.
        • Shows aggregate success/warning/error messages.
        • Refetches folder queries when at least one upload succeeds.
  • Rendering:

    • Wraps content in ResourceTabView, which:
      • Provides title, trash button, breadcrumb bar, and search/sort UI.
      • Calls back with (query, _, __, sortOrder) to render file/folder list or viewer.
    • Normal view:
      • Filters and sorts irFiles/irFolders by search and sort order.
      • Ensures selected fileId is included even if filtered out or not yet loaded.
      • Builds fileItems with download URLs (/internal-resources/files/{id}/download).
      • Shows:
        • ResourceFileView as a file viewer when a file is selected.
        • ResourceFileView as a file/folder grid with optional dropzone otherwise.
        • FileDropzone empty state when no folders or files.
    • Trash view:
      • Renders ResourceFileView with:
        • showTrash={true}
        • onRestoreFile, onRestoreFolder
        • onPermanentlyDeleteFile, onPermanentlyDeleteFolder
        • onExitTrash
  • Sharing:

    • Manages a ShareModal:
      • handleShare(fileId) opens the modal for that file.
      • ShareModal uses fileId and file name to build sharing links/metadata.

File: src/modules/Resources/hooks/useResourcesData.ts.

Responsibilities:

  • Root content and search:

    • rootContentsQuerygetAllContents() when no search query.
    • searchResultsQuerysearchResources(searchQuery) when searching.
    • rootFolders:
      • When searching: uses searchResultsQuery.data.data.folders.
      • Otherwise: rootContentsQuery.data.data.folders.
  • Expanded folder contents (for tree):

    • Tracks treeFilePagesByFolderId to support multiple pages of files in tree display.
    • Uses useQueries over expandedFolderIds:
      • For each folder id, calls getFolderContents(folderId, { page }) up to current page.
      • Merges folders/files from pages into a single folders/files array.
      • Computes treeHasMoreFiles based on hasNextPage or page size.
    • Produces:
      • contentsByFolderId[folderId] = { folders, files }
      • hasMoreFilesByFolderId[folderId] = boolean
  • Properties modal support:

    • propertiesFolderContentsQuery:
      • getFolderContents(propertiesFolder.id) for the folder being inspected, for counts.
  • Folder lookup helpers:

    • folderNameById:
      • Recursively walks rootFolders and contentsByFolderId to build name map.
    • folderById:
      • Same, but storing IRFolder objects.

Exports:

  • queryClient, rootContentsQuery, searchResultsQuery
  • rootFolders
  • contentsQueries, contentsByFolderId
  • hasMoreFilesByFolderId, loadMoreFolderFiles
  • propertiesFolderContentsQuery
  • folderNameById, folderById
  • expandedIdsArray

File: src/modules/Resources/hooks/useResourcesOperations.ts.

Responsibilities:

  • File upload from tree context:

    • handleAddFile(folder):
      • Opens a hidden <input type="file" multiple>.
      • Validates per‑file and batch size limits.
      • Calls uploadFile(folder.id, file) (service, not React Query).
      • Sets hasChildrenByFolderId[folder.id] = true to show caret.
      • Refetches ["internal-resources", "contents", folder.id].
      • If folder is root, invalidates ["internal-resources", "contents", null].
  • Folder creation:

    • handleCreateFolder(folderName):
      • Admin‑only; rejects non‑admins.
      • Uses creatingRootFolder + selectedFolder to infer root vs subfolder.
      • Calls createFolderMut({ name, parentFolderId, isPublic: true }).
      • If admin and userId is known, sets default ACL:
        • inherit: true, with a read allow rule for the creator.
      • For subfolders:
        • Marks parent in hasChildrenByFolderId.
        • Adds parent to expandedFolderIds.
        • Fetches parent contents via getFolderContents.
        • Invalidates parent container’s contents so that hasChildren is recalculated.
      • For root folders:
        • Invalidates the root contents query.
  • Rename folder

    • handleRenameFolder(newName):
      • Admin‑only.
      • Calls updateFolderMut({ folderId, name }).
      • Invalidates:
        • ["internal-resources", "contents"] (all folder contents queries).
        • ["internal-resources", "search"] (all search results).
      • If renamed folder is currently selected, updates selectedFolder.
  • Rename file

    • handleRenameFile(newName):
      • Admin‑only.
      • Calls updateFileMut({ fileId, name }).
      • Invalidates:
        • ["internal-resources", "contents"]
        • ["internal-resources", "search"]
  • Soft delete folder

    • handleDeleteFolder(folder):
      • Confirms, then calls deleteFolderMut(folder.id).
      • Invalidates ["internal-resources", "trash"].
      • Clears contents cache for deleted folder.
      • Invalidates parent’s contents.
      • Removes folder from expandedFolderIds.
      • Clears hasChildrenByFolderId[folder.id].

Exports:

  • handleAddFile
  • handleCreateFolder
  • handleRenameFolder
  • handleRenameFile
  • handleDeleteFolder

While not exhaustive, key components around the core flows:

  • Access control

    • ResourceAccessForm:
      • Opens for either type="folder" or type="file" with id.
      • Uses ACL APIs (getFolderACL, updateFolderACL, getFileACL, updateFileACL).
    • Triggered from ResourcePropertiesModal via “Access” actions.
  • Search

    • Sidebar search in ResourceSidebar updates searchQuery in ResourcesRoot.
    • useResourcesData automatically switches from root contents to searchResources while searching.
    • Tree & main panel both reflect search results (folders/files).
  • Trash

    • “Trash” action in ResourceSidebar toggles showTrash in ResourcesRoot.
    • Resources in showTrash mode:
      • Uses trash query instead of folder contents.
      • Provides restore + hard delete in the UI.

  • Two resource APIs:

    • The module UI is almost entirely driven by internalResources.ts + useInternalResources.
    • resources.ts + ResourceAccessConfig are a separate, path‑based API mostly for legacy/public resources and high‑level access config.
    • Be explicit about which API you’re extending to avoid mixing path‑based and ID‑based logic.
  • Caching & invalidation:

    • Always invalidate or refetch:
      • ["internal-resources", "contents", folderId] when files/folders change in that folder.
      • ["internal-resources", "contents", null] when root folders change (create/delete root, move folder to/from root).
      • ["internal-resources", "trash"] when soft‑deleting/restoring items.
      • ["internal-resources", "search"] when something that affects search results changes (rename, create, delete).
  • expandedFolderIds vs UI expansion:

    • expandedFolderIds is used for data loading (tree contents), not just visual caret state.
    • When you want children to exist in contentsByFolderId and show carets, make sure to add folder ids here.
    • Do not aggressively remove items from expandedFolderIds on UI collapse; otherwise, carets and children can disappear unexpectedly.
  • File size limits:

    • Both Resources and useResourcesOperations enforce:
      • 50MB per file.
      • 500MB per batch.
    • Reuse these constants for any new upload flows to keep behaviour consistent.
  • URL deep‑links:

    • ResourcesRoot expects urlPath to be a chain of folder ids and urlFileId as a file id.
    • When adding new entry points (e.g. directly opening a resource from another module), set these values via useResourcesUrlState so that the tree, breadcrumbs, and main view stay in sync.