Skip to content

refactor: collections handling for cleaner and more extendable resources system#448

Open
harbassan wants to merge 5 commits into
vps-137-standardize-file-storage-and-metadata-managementfrom
vps-146-refactor-collections-handling-for-cleaner-and-more
Open

refactor: collections handling for cleaner and more extendable resources system#448
harbassan wants to merge 5 commits into
vps-137-standardize-file-storage-and-metadata-managementfrom
vps-146-refactor-collections-handling-for-cleaner-and-more

Conversation

@harbassan

@harbassan harbassan commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Issue

A clear and concise description of what the issue is

Solution

A clear and concise description of what the solution is

Risk

Potential risks that this PR might bring

Checklist

  • Acceptance criteria met
  • Wiki documentation is written and up to date
  • Unit tests written and passing
  • Integration tests written and passing
  • Continuous integration build passing

Summary by CodeRabbit

  • New Features
    • Added unified resource collections with nested files and support for creating, browsing, and deleting collections.
    • Added previews for images, PDFs, Markdown, and text files, with download fallback for unsupported formats.
    • Added loading placeholders and improved resource search, filtering, and expansion controls.
    • Added collection deletion that also removes contained files safely.
  • Improvements
    • Streamlined visibility-conditional creation, editing, and deletion with clearer feedback.
    • Updated resource management and scenario playback to use the new collection structure.

@linear

linear Bot commented Jul 21, 2026

Copy link
Copy Markdown

VPS-146

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Collection groups are removed in favor of collection-type Resource documents linked through parentId. Backend APIs, tests, and frontend resource management, previews, trees, and state conditional workflows are updated to use the unified resource model.

Changes

Resource hierarchy backend

Layer / File(s) Summary
Resource model and API flow
backend/src/db/models/resource.js, backend/src/routes/api/resources.js, backend/src/routes/api/index.js, backend/src/routes/api/collections.js, backend/src/db/models/CollectionGroup.js
Resources now support file and collection types with optional parentId; collection creation and cascading deletion are handled through the resources API, while collection-group models and routes are removed.
Resource API validation coverage
backend/src/routes/api/__tests__/resourcesApi.test.js, backend/src/routes/api/__tests__/collectionsApi.test.js
Tests now seed Resource collections, validate parent-based file creation, cover collection creation, and verify cascading deletion with file reference updates; the collections API suite is removed.

Frontend resource workflows

Layer / File(s) Summary
Resource management and preview
frontend/src/features/resources/ManageResourcesPage.jsx, frontend/src/features/resources/ResourcePreview.jsx, frontend/src/features/resources/ResourcesSkeleton.jsx, frontend/src/features/resources/util.js
Resource management uses React Query, optimistic mutations, collection/file trees, shared previews, skeleton loading, and the new _id/parentId data shape.
Scenario resource tree
frontend/src/features/playScenario/components/ResourcesPanel.jsx, frontend/src/features/playScenario/components/ResourceTree.jsx
Scenario resource loading, filtering, expansion, selection, and rendering now use collection _id values and children.
Resource state conditional mutations
frontend/src/components/StateVariables/*.jsx
State conditional creation, editing, deletion, and menu wiring now use selected resources and React Query mutations with cache updates and invalidation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • UoaWDCC/VPS#393: Adds the earlier collection-level state conditional CRUD that this PR replaces with Resource-based conditionals.
  • UoaWDCC/VPS#429: Adds the collection and resource API tests affected by this migration.
  • UoaWDCC/VPS#446: Introduces the file reference-counting helpers used by the updated resource deletion flow.

Suggested labels: backend

Suggested reviewers: rlee64

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is only the template skeleton and lacks any filled-in issue, solution, risk, or checklist information. Replace the placeholders with a concrete issue summary, solution description, risk assessment, and completed checklist items for tests, docs, and CI.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: refactoring collection handling into a more extensible resources system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vps-146-refactor-collections-handling-for-cleaner-and-more

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@harbassan harbassan changed the title refactor: collections handling for cleaner and more refactor: collections handling for cleaner and more extendable resources system Jul 21, 2026
@harbassan

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
backend/src/db/models/resource.js (1)

23-27: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enforce fileId requirement conditionally.

Since fileId is now optional to accommodate collections, consider adding a custom validator to ensure it remains strictly required for "file" type resources.

♻️ Proposed refactor
     fileId: {
       type: Schema.Types.ObjectId,
       ref: "UploadedFile",
-      required: false,
+      required: function () {
+        return this.type === "file";
+      },
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/models/resource.js` around lines 23 - 27, Update the resource
schema’s fileId validation to require a value when the resource type is "file"
while allowing it to remain absent for collection resources. Add the conditional
validator alongside fileId and reference the existing resource type field rather
than changing the general optional schema setting.
frontend/src/features/playScenario/components/ResourcesPanel.jsx (1)

62-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

stateVariables in the query key causes redundant refetches.

queryFn uses only user/scenarioId, yet stateVariables is part of the key, so every state-variable change refetches /api/resources/:scenarioId even though the response doesn't depend on it. Since filtering is client-side, drop it from the key.

♻️ Proposed change
   const { data, isLoading, isError, error } = useQuery({
-    queryKey: ["resources", scenarioId, stateVariables],
+    queryKey: ["resources", scenarioId],
     queryFn: () => getResources(user, scenarioId),
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/playScenario/components/ResourcesPanel.jsx` around
lines 62 - 65, Update the useQuery configuration in ResourcesPanel by removing
stateVariables from the queryKey, leaving only the resource-fetch inputs used by
getResources: user and scenarioId. Preserve the existing client-side filtering
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/routes/api/resources.js`:
- Around line 58-62: Update the parent validation in the resource creation flow
around Resource.exists to require the parent resource’s type to be a collection,
preventing files from being used as parents. Preserve the existing not-found
response while changing its message from “group not found” to
collection-appropriate resource terminology.
- Around line 129-138: Update the resource deletion logic in the collection/else
branches to skip children without a fileId before converting the identifier,
preventing null access. Normalize all fileRefDeltas keys to strings, including
the non-collection resource.fileId path, so the Map uses a consistent key type.

In `@frontend/src/components/StateVariables/CreateStateConditional.jsx`:
- Around line 56-71: Move the useMutation call identified as
createConditionalMutation in
frontend/src/components/StateVariables/CreateStateConditional.jsx#L37-L71 above
all stateVariables guards, while preserving its callbacks and behavior. Apply
the same hoisting to the mutation in
frontend/src/components/StateVariables/EditStateConditional.jsx#L48-L100,
ensuring both components invoke their mutations unconditionally before any early
return based on asynchronously loaded stateVariables.

In `@frontend/src/components/StateVariables/EditStateConditional.jsx`:
- Around line 96-99: Update the onError handler for updateConditionalMutation in
EditStateConditional.jsx so its toast message refers to updating or editing the
state conditional, not deleting it. Leave the delete mutation’s error message
unchanged.

In `@frontend/src/features/playScenario/components/ResourcesPanel.jsx`:
- Around line 70-87: Update the filteredTree computation in ResourcesPanel so
filterTreeByConditions is always applied before the search/no-search branch,
including when search is empty. Pass each group’s files property expected by
filterTreeByConditions rather than children, and preserve the tree’s established
shape when applying search filtering so both default and searched views retain
conditionally visible groups and files.

In `@frontend/src/features/resources/ManageResourcesPage.jsx`:
- Around line 109-132: Update the temporary resource object created in
addResourceCollectionMutation.onMutate to include type: "collection", matching
the criterion used by buildResourceTree and the existing file-upload mutation
pattern. Preserve the current optimistic insertion and rollback behavior.

In `@frontend/src/features/resources/ResourcePreview.jsx`:
- Around line 12-16: Update the text loading condition in ResourcePreview to use
text.isInitialLoading, or gate text.isLoading with the same enabled predicate
used by the file-text query, so disabled queries do not render the text
skeleton.

In `@frontend/src/features/resources/util.js`:
- Line 9: Update the resource object field used by ResourceTree’s file badge
from type to contentType, so shortType renders the actual file format instead of
the generic resource kind. Preserve the rest of normaliseFile’s mapping
unchanged.

---

Nitpick comments:
In `@backend/src/db/models/resource.js`:
- Around line 23-27: Update the resource schema’s fileId validation to require a
value when the resource type is "file" while allowing it to remain absent for
collection resources. Add the conditional validator alongside fileId and
reference the existing resource type field rather than changing the general
optional schema setting.

In `@frontend/src/features/playScenario/components/ResourcesPanel.jsx`:
- Around line 62-65: Update the useQuery configuration in ResourcesPanel by
removing stateVariables from the queryKey, leaving only the resource-fetch
inputs used by getResources: user and scenarioId. Preserve the existing
client-side filtering behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a6e7734-ff7b-4690-94d6-a8e58aefe1b4

📥 Commits

Reviewing files that changed from the base of the PR and between 6f00e5f and 9576066.

📒 Files selected for processing (17)
  • backend/src/db/models/CollectionGroup.js
  • backend/src/db/models/resource.js
  • backend/src/routes/api/__tests__/collectionsApi.test.js
  • backend/src/routes/api/__tests__/resourcesApi.test.js
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/index.js
  • backend/src/routes/api/resources.js
  • frontend/src/components/StateVariables/CreateStateConditional.jsx
  • frontend/src/components/StateVariables/EditStateConditional.jsx
  • frontend/src/components/StateVariables/StateConditionalMenu.jsx
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • frontend/src/features/playScenario/components/ResourceTree.jsx
  • frontend/src/features/playScenario/components/ResourcesPanel.jsx
  • frontend/src/features/resources/ManageResourcesPage.jsx
  • frontend/src/features/resources/ResourcePreview.jsx
  • frontend/src/features/resources/ResourcesSkeleton.jsx
  • frontend/src/features/resources/util.js
💤 Files with no reviewable changes (5)
  • backend/src/db/models/CollectionGroup.js
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/tests/collectionsApi.test.js
  • backend/src/routes/api/index.js

Comment on lines +58 to +62
if (parentId) {
const parent = await Resource.exists({ _id: parentId, scenarioId });
if (!parent)
throw new HttpError("group not found", HttpStatusCode.NotFound);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Verify that the parent resource is a collection.

The current lookup checks if the parent exists but does not verify its type. This allows clients to pass a file's ID as the parentId, creating a nested file structure that breaks deletion cascades and tree rendering logic. Additionally, the error message should be updated to reflect the new resource model instead of the old "group" terminology.

🐛 Proposed fix
     if (parentId) {
-      const parent = await Resource.exists({ _id: parentId, scenarioId });
+      const parent = await Resource.exists({ _id: parentId, scenarioId, type: "collection" });
       if (!parent)
-        throw new HttpError("group not found", HttpStatusCode.NotFound);
+        throw new HttpError("parent collection not found", HttpStatusCode.NotFound);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (parentId) {
const parent = await Resource.exists({ _id: parentId, scenarioId });
if (!parent)
throw new HttpError("group not found", HttpStatusCode.NotFound);
}
if (parentId) {
const parent = await Resource.exists({ _id: parentId, scenarioId, type: "collection" });
if (!parent)
throw new HttpError("parent collection not found", HttpStatusCode.NotFound);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/api/resources.js` around lines 58 - 62, Update the parent
validation in the resource creation flow around Resource.exists to require the
parent resource’s type to be a collection, preventing files from being used as
parents. Preserve the existing not-found response while changing its message
from “group not found” to collection-appropriate resource terminology.

Comment on lines +129 to +138
if (resource.type === "collection") {
const children = await Resource.find({ parentId: resource._id });
for (const child of children) {
const fileId = child.fileId.toString();
fileRefDeltas.set(fileId, (fileRefDeltas.get(fileId) ?? 0) - 1);
}
await Resource.deleteMany({ parentId: resource._id });
} else {
fileRefDeltas.set(resource.fileId, -1);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent NPE on missing fileId and normalize map keys.

If a child resource does not have a fileId (which is now explicitly allowed by the schema), calling child.fileId.toString() will throw a TypeError and cause the deletion to fail with a 500 error.

Additionally, in the else branch, resource.fileId is passed as an ObjectId, mixing object references and strings as keys in the fileRefDeltas Map.

🐛 Proposed fix
     if (resource.type === "collection") {
       const children = await Resource.find({ parentId: resource._id });
       for (const child of children) {
-        const fileId = child.fileId.toString();
-        fileRefDeltas.set(fileId, (fileRefDeltas.get(fileId) ?? 0) - 1);
+        if (child.fileId) {
+          const fileId = child.fileId.toString();
+          fileRefDeltas.set(fileId, (fileRefDeltas.get(fileId) ?? 0) - 1);
+        }
       }
       await Resource.deleteMany({ parentId: resource._id });
     } else {
-      fileRefDeltas.set(resource.fileId, -1);
+      if (resource.fileId) {
+        fileRefDeltas.set(resource.fileId.toString(), -1);
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (resource.type === "collection") {
const children = await Resource.find({ parentId: resource._id });
for (const child of children) {
const fileId = child.fileId.toString();
fileRefDeltas.set(fileId, (fileRefDeltas.get(fileId) ?? 0) - 1);
}
await Resource.deleteMany({ parentId: resource._id });
} else {
fileRefDeltas.set(resource.fileId, -1);
}
if (resource.type === "collection") {
const children = await Resource.find({ parentId: resource._id });
for (const child of children) {
if (child.fileId) {
const fileId = child.fileId.toString();
fileRefDeltas.set(fileId, (fileRefDeltas.get(fileId) ?? 0) - 1);
}
}
await Resource.deleteMany({ parentId: resource._id });
} else {
if (resource.fileId) {
fileRefDeltas.set(resource.fileId.toString(), -1);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/api/resources.js` around lines 129 - 138, Update the
resource deletion logic in the collection/else branches to skip children without
a fileId before converting the identifier, preventing null access. Normalize all
fileRefDeltas keys to strings, including the non-collection resource.fileId
path, so the Map uses a consistent key type.

Comment on lines +56 to +71
const createConditionalMutation = useMutation({
mutationFn: (conditional) =>
createStateConditional(user, scenarioId, resource._id, conditional),
onSettled: () => queryClient.invalidateQueries(["resources", scenarioId]),
onError: (e) => {
console.error(e);
toast.error("Error creating state conditional");
},
onSuccess: () => {
setSelectedState(null);
setComparator(null);
setValue(null);
setOpen(false);
toast.success("State conditional created!");
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '== CreateStateConditional.jsx outline ==\n'
ast-grep outline frontend/src/components/StateVariables/CreateStateConditional.jsx --view expanded || true
printf '\n== EditStateConditional.jsx outline ==\n'
ast-grep outline frontend/src/components/StateVariables/EditStateConditional.jsx --view expanded || true
printf '\n== CreateStateConditional.jsx relevant lines ==\n'
nl -ba frontend/src/components/StateVariables/CreateStateConditional.jsx | sed -n '1,140p'
printf '\n== EditStateConditional.jsx relevant lines ==\n'
nl -ba frontend/src/components/StateVariables/EditStateConditional.jsx | sed -n '1,180p'

Repository: UoaWDCC/VPS

Length of output: 895


🏁 Script executed:

set -e
printf '== Find ScenarioContext ==\n'
rg -n "ScenarioContext|stateVariables" frontend/src -g '!**/node_modules/**' | sed -n '1,220p'
printf '\n== Context/provider files ==\n'
fd -a "ScenarioContext" frontend/src || true
fd -a "Scenario.*Context" frontend/src || true

Repository: UoaWDCC/VPS

Length of output: 13929


🏁 Script executed:

set -e
for f in frontend/src/components/StateVariables/CreateStateConditional.jsx frontend/src/components/StateVariables/EditStateConditional.jsx; do
  echo "== $f =="
  nl -ba "$f" | sed -n '1,140p'
  echo
done

Repository: UoaWDCC/VPS

Length of output: 365


🏁 Script executed:

set -e
for f in frontend/src/components/StateVariables/CreateStateConditional.jsx frontend/src/components/StateVariables/EditStateConditional.jsx frontend/src/context/ScenarioContextProvider.jsx; do
  echo "== $f =="
  awk '{printf "%4d %s\n", NR, $0}' "$f" | sed -n '1,180p'
  echo
done

Repository: UoaWDCC/VPS

Length of output: 17390


Hoist the mutations above the stateVariables guards. ScenarioContextProvider fills stateVariables asynchronously, so these components can skip useMutation on one render and call it on the next. That changes hook order and can crash the subtree.

  • frontend/src/components/StateVariables/CreateStateConditional.jsx#L37-L71
  • frontend/src/components/StateVariables/EditStateConditional.jsx#L48-L100
📍 Affects 2 files
  • frontend/src/components/StateVariables/CreateStateConditional.jsx#L56-L71 (this comment)
  • frontend/src/components/StateVariables/EditStateConditional.jsx#L52-L100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StateVariables/CreateStateConditional.jsx` around
lines 56 - 71, Move the useMutation call identified as createConditionalMutation
in frontend/src/components/StateVariables/CreateStateConditional.jsx#L37-L71
above all stateVariables guards, while preserving its callbacks and behavior.
Apply the same hoisting to the mutation in
frontend/src/components/StateVariables/EditStateConditional.jsx#L48-L100,
ensuring both components invoke their mutations unconditionally before any early
return based on asynchronously loaded stateVariables.

Comment on lines +96 to +99
onError: (e) => {
console.error(e);
toast.error("Error deleting state conditional");
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrong toast message on the update mutation.

updateConditionalMutation's onError reports "Error deleting state conditional" — copy-paste from the delete mutation. It should reference updating/editing.

✏️ Fix
     onError: (e) => {
       console.error(e);
-      toast.error("Error deleting state conditional");
+      toast.error("Error updating state conditional");
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onError: (e) => {
console.error(e);
toast.error("Error deleting state conditional");
},
onError: (e) => {
console.error(e);
toast.error("Error updating state conditional");
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StateVariables/EditStateConditional.jsx` around lines
96 - 99, Update the onError handler for updateConditionalMutation in
EditStateConditional.jsx so its toast message refers to updating or editing the
state conditional, not deleting it. Leave the delete mutation’s error message
unchanged.

Comment on lines +70 to +87
const filteredTree = (() => {
if (!search.trim()) return resourceTree;
const q = search.trim().toLowerCase();
return tree

// NOTE: the filtering by state variables should ideally be done on the server to prevent cheating, but here we filter before rendering

return filterTreeByConditions(resourceTree, stateVariables)
.map((g) => {
const matchingFiles = (g.files || []).filter((f) => {
const matchingFiles = (g.children || []).filter((f) => {
const inName = f.name.toLowerCase().includes(q);
const inPath = g.name.toLowerCase().includes(q);
return inName || inPath;
});
if (matchingFiles.length === 0) return null;
return { ...g, files: matchingFiles };
return { ...g, children: matchingFiles };
})
.filter(Boolean);
}, [tree, search]);
})();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\b(files|children)\b' frontend/src/utils/stateConditionalEvaluator.js

Repository: UoaWDCC/VPS

Length of output: 766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant files with line numbers.
for f in frontend/src/features/playScenario/components/ResourcesPanel.jsx frontend/src/utils/stateConditionalEvaluator.js; do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,220p'
  echo
done

# Find where the resource tree is built to see whether it uses `children` or `files`.
rg -n "buildResourceTree|resourceTree|children:|files:" frontend/src -g '!**/node_modules/**'

Repository: UoaWDCC/VPS

Length of output: 14654


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrowly inspect any builder/helper that produces the tree shape.
rg -n -C3 "buildResourceTree|resourceTree" frontend/src -g '!**/node_modules/**'

Repository: UoaWDCC/VPS

Length of output: 6862


Apply conditionals before the search split. The empty-search branch returns resourceTree directly, so state conditionals are skipped in the default view. The search branch also passes children into filterTreeByConditions, which expects files, so it drops every group and makes search return nothing. Keep the tree shape consistent and filter conditionals unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/playScenario/components/ResourcesPanel.jsx` around
lines 70 - 87, Update the filteredTree computation in ResourcesPanel so
filterTreeByConditions is always applied before the search/no-search branch,
including when search is empty. Pass each group’s files property expected by
filterTreeByConditions rather than children, and preserve the tree’s established
shape when applying search filtering so both default and searched views retain
conditionally visible groups and files.

Comment on lines +109 to +132
const addResourceCollectionMutation = useMutation({
mutationFn: (name) => createResourceCollection(user, scenarioId, name),
onMutate: async (name) => {
await queryClient.cancelQueries(["resources", scenarioId]);
const tempId = `temp.${uuid()}`;
const temp = { name, _id: tempId };
queryClient.setQueryData(["resources", scenarioId], (prev) => [
...prev,
temp,
]);
return { tempId };
},
onSuccess: () => queryClient.invalidateQueries(["resources", scenarioId]),
onError: (e, _, context) => {
const tempId = context?.tempId;
if (tempId) {
queryClient.setQueryData(["resources", scenarioId], (prev) =>
prev.filter((r) => r._id !== tempId)
);
}
const idToken = await user.getIdToken();
await axios.delete(`/api/collections/groups/${groupId}`, {
headers: { Authorization: `Bearer ${idToken}` },
});

setGroups((prev) => prev.filter((g) => g.id !== groupId));

if (selectedFile && selectedFile.groupId === groupId)
setSelectedFile(null);
if (selectedGroup?.id === groupId) setSelectedGroup(null);

toast.success("Group deleted");
} catch (err) {
console.error(err);
toast.error(err?.response?.data?.error || "Failed to delete group");
}
}
console.error(e);
toast.error("Something went wrong creating the collection");
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Optimistic collection is dropped by buildResourceTree (missing type).

The temp entry is { name, _id: tempId }, but buildResourceTree (Line 46) keeps only r.type === "collection". The optimistic collection therefore never renders — the new group only appears after onSuccess invalidation refetches. The file-upload mutation correctly sets type: "file"; mirror that here.

🐛 Proposed fix
       const tempId = `temp.${uuid()}`;
-      const temp = { name, _id: tempId };
+      const temp = { name, _id: tempId, type: "collection", children: [] };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addResourceCollectionMutation = useMutation({
mutationFn: (name) => createResourceCollection(user, scenarioId, name),
onMutate: async (name) => {
await queryClient.cancelQueries(["resources", scenarioId]);
const tempId = `temp.${uuid()}`;
const temp = { name, _id: tempId };
queryClient.setQueryData(["resources", scenarioId], (prev) => [
...prev,
temp,
]);
return { tempId };
},
onSuccess: () => queryClient.invalidateQueries(["resources", scenarioId]),
onError: (e, _, context) => {
const tempId = context?.tempId;
if (tempId) {
queryClient.setQueryData(["resources", scenarioId], (prev) =>
prev.filter((r) => r._id !== tempId)
);
}
const idToken = await user.getIdToken();
await axios.delete(`/api/collections/groups/${groupId}`, {
headers: { Authorization: `Bearer ${idToken}` },
});
setGroups((prev) => prev.filter((g) => g.id !== groupId));
if (selectedFile && selectedFile.groupId === groupId)
setSelectedFile(null);
if (selectedGroup?.id === groupId) setSelectedGroup(null);
toast.success("Group deleted");
} catch (err) {
console.error(err);
toast.error(err?.response?.data?.error || "Failed to delete group");
}
}
console.error(e);
toast.error("Something went wrong creating the collection");
},
});
const addResourceCollectionMutation = useMutation({
mutationFn: (name) => createResourceCollection(user, scenarioId, name),
onMutate: async (name) => {
await queryClient.cancelQueries(["resources", scenarioId]);
const tempId = `temp.${uuid()}`;
const temp = { name, _id: tempId, type: "collection", children: [] };
queryClient.setQueryData(["resources", scenarioId], (prev) => [
...prev,
temp,
]);
return { tempId };
},
onSuccess: () => queryClient.invalidateQueries(["resources", scenarioId]),
onError: (e, _, context) => {
const tempId = context?.tempId;
if (tempId) {
queryClient.setQueryData(["resources", scenarioId], (prev) =>
prev.filter((r) => r._id !== tempId)
);
}
console.error(e);
toast.error("Something went wrong creating the collection");
},
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/resources/ManageResourcesPage.jsx` around lines 109 -
132, Update the temporary resource object created in
addResourceCollectionMutation.onMutate to include type: "collection", matching
the criterion used by buildResourceTree and the existing file-upload mutation
pattern. Preserve the current optimistic insertion and rollback behavior.

Comment on lines +12 to +16
const text = useQuery({
queryKey: ["file-text", file?.url],
queryFn: () => loadText(file.url),
enabled: !!(file?.contentType?.startsWith("text") && file?.url),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@tanstack/react-query v4 isLoading true for disabled query isInitialLoading

💡 Result:

In TanStack Query v4, the isLoading state is true for disabled queries that have no cached data [1][2][3]. This is an intentional change resulting from the removal of the idle state in v4 [2][4]. In previous versions, a disabled query without data would be in an idle status [2][3]. In v4, status now represents whether data exists ('loading', 'success', or 'error') [5][4], while fetchStatus represents whether the function is running ('fetching', 'paused', or 'idle') [5]. Because a disabled query has no data, it defaults to the loading status, even though it is not actively fetching [1][2][4]. To correctly identify when to show a loading spinner—specifically when the query is both in a loading state and actually fetching data—you should use the isInitialLoading flag [1][2][6]. The isInitialLoading flag is a derived property computed as: isInitialLoading = isLoading && isFetching [1][6] This ensures that the flag is only true when the query is currently in its initial fetch [1][6]. During migration from v3 to v4, replacing isLoading with isInitialLoading is the recommended way to maintain previous loading spinner behavior for disabled or lazy queries [2][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files frontend/src/features/resources/ResourcePreview.jsx
wc -l frontend/src/features/resources/ResourcePreview.jsx
nl -ba frontend/src/features/resources/ResourcePreview.jsx | sed -n '1,220p'

Repository: UoaWDCC/VPS

Length of output: 296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n frontend/src/features/resources/ResourcePreview.jsx | sed -n '1,220p'

Repository: UoaWDCC/VPS

Length of output: 2972


Use isInitialLoading for the text skeleton
isText can be true while this query is disabled (for example, application/json), and text.isLoading stays true for a disabled query in React Query v4. Use text.isInitialLoading here, or gate the branch on the same enabled condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/resources/ResourcePreview.jsx` around lines 12 - 16,
Update the text loading condition in ResourcePreview to use
text.isInitialLoading, or gate text.isLoading with the same enabled predicate
used by the file-text query, so disabled queries do not render the text
skeleton.

size: resource.fileId.size,
fileId: resource.fileId?._id,
url: resource.fileId?.url,
type: resource.type,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C2 'shortType\(' frontend/src/features/playScenario/components/ResourceTree.jsx

Repository: UoaWDCC/VPS

Length of output: 547


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== util.js ==\n'
sed -n '1,120p' frontend/src/features/resources/util.js

printf '\n== ResourceTree.jsx ==\n'
sed -n '1,140p' frontend/src/features/playScenario/components/ResourceTree.jsx

printf '\n== search for resource.type/contentType/fileType assignments ==\n'
rg -n --hidden -S 'contentType|fileType|resource\.type|type:\s*resource\.type|shortType\(' frontend/src/features -g '!**/node_modules/**'

Repository: UoaWDCC/VPS

Length of output: 4769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/features/resources/util.js')
print(p.exists())
PY

Repository: UoaWDCC/VPS

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== ResourceTree usages ==\n'
rg -n --hidden -S 'ResourceTree' frontend/src -g '!**/node_modules/**'

printf '\n== nearby tree shaping code in playScenario/resources ==\n'
rg -n --hidden -S 'normaliseFile\(|children\s*:|tree\s*=' frontend/src/features -g '!**/node_modules/**'

printf '\n== ManageResourcesPage relevant section ==\n'
sed -n '160,240p' frontend/src/features/resources/ManageResourcesPage.jsx

Repository: UoaWDCC/VPS

Length of output: 5715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [
    Path('frontend/src/features/resources/ManageResourcesPage.jsx'),
    Path('frontend/src/features/playScenario/components/ResourceTree.jsx'),
]:
    print(p, p.exists())
PY

Repository: UoaWDCC/VPS

Length of output: 278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' frontend/src/features/playScenario/components/ResourcesPanel.jsx

Repository: UoaWDCC/VPS

Length of output: 4404


Use contentType for the file badge
normaliseFile() sets type to the resource kind, so ResourceTree’s {shortType(f.type)} will render file for every file row. Switch this to contentType.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/resources/util.js` at line 9, Update the resource
object field used by ResourceTree’s file badge from type to contentType, so
shortType renders the actual file format instead of the generic resource kind.
Preserve the rest of normaliseFile’s mapping unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant