Skip to content

Styling requests#390

Open
brucetony wants to merge 9 commits into
developfrom
388-styling-requests
Open

Styling requests#390
brucetony wants to merge 9 commits into
developfrom
388-styling-requests

Conversation

@brucetony

@brucetony brucetony commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Several stylistic changes to the tables

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to preselect projects when creating data stores via URL parameter
    • Added Clean Up Resources option to user menu
    • Added Update button for managing analyses
  • UI/UX Improvements

    • Redesigned dark mode toggle with icon button interface
    • Consolidated hub status information in analyses table display
    • Enhanced approval/rejection workflow with confirmation dialogs
    • Moved Preferences access to header menu
    • Improved footer positioning in page layout
    • Updated table styling for better readability
  • Tests

    • Expanded test coverage for updated components and features

@brucetony brucetony linked an issue Jun 22, 2026 that may be closed by this pull request
5 tasks
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR consolidates three separate hub status columns (approval/build/distribution) in AnalysesTable into a single grouped column backed by a custom FilterService match mode. It replaces ApproveRejectButtons with a new ApproveRejectToggle using PrimeVue confirmation dialogs. AnalysisControlButtons gains Update button visibility/disabled logic. The Preferences entry moves from AvatarButton to MenuHeader. app.vue gets a flex-column sticky-footer layout. DataStoreProjectInitializer preselects a project from the URL query param.

Changes

Hub Statuses Column and Analysis Controls

Layer / File(s) Summary
HubStatuses interface and ApprovalStatus enum values
app/services/modifiedApiInterfaces.ts, app/types/node.ts
Exports HubStatuses interface deriving approval/build/distribution status types; extends ModifiedAnalysisNode with hub_statuses; gives ApprovalStatus members explicit string literal values.
Hub status filtering logic and parseAnalysis population
app/components/analysis/AnalysesTable.vue
Defines HUB_STATUS_FILTER_MATCH_MODE, HubStatusGroup, grouped filter options, and a FilterService callback matching rows by group::status keys; updates parseAnalysis to populate a hub_statuses object; replaces default filter fields.
Hub Statuses column UI and DataStore navigation
app/components/analysis/AnalysesTable.vue
Replaces three separate hub status columns with a single Column backed by hub_statuses using a grouped MultiSelect filter; makes the missing DataStore icon clickable when datastoreRequired is true, navigating to /data-stores/create?projectId=…; adds progress bar and hub-status CSS.
AnalysisUpdateButton disabled prop and AnalysisControlButtons Update visibility
app/components/analysis/AnalysisUpdateButton.vue, app/components/analysis/AnalysisControlButtons.vue
Adds disabled Boolean prop to AnalysisUpdateButton bound to PrimeVue Button; adds updateButtonHidden/updateButtonDisabled computed values in AnalysisControlButtons; adds watcher to clear localStorage loading flag on status transition; simplifies v-show/:disabled bindings for all action buttons.
DataStoreProjectInitializer URL projectId preselection
app/components/data-stores/create/DataStoreProjectInitializer.vue
Reads route.query.projectId on load and preselects selectedProject from availableProjects by matching id.
Analysis component tests and test infrastructure
test/components/analysis/..., test/components/data-stores/create/DataStoreProjectInitializer.spec.ts, test/mockapi/setup.ts, test/components/events/EventViewer.spec.ts
Updates column index constants and header assertions for the new Hub Statuses layout; adds Update button disabled/visible tests; adds DataStoreProjectInitializer query param tests; registers ConfirmDialog/ToggleSwitch globally and adds localStorage polyfill; fixes locale-dependent date assertion.

ApproveRejectToggle Component and ProjectProposalTable

Layer / File(s) Summary
ApproveRejectToggle component
app/components/table/ApproveRejectToggle.vue
New component with objectId/objectClass/currentStatus props; shows PrimeVue confirmation dialogs before posting approval_status to project-nodes or analysis-nodes; emits updatedRow on success; renders tag-dimming UI with a ToggleSwitch.
ProjectProposalTable integration
app/components/projects/ProjectProposalTable.vue
Replaces ApproveRejectButtons with ApproveRejectToggle; adds sortable "Number of Analyses" and "Number of Nodes" columns; renders ApproveRejectToggle in the approval column body; adds ConfirmDialog to template.
ApproveRejectToggle and ProjectProposalTable tests
test/components/table/ApproveRejectToggle.spec.ts, test/components/projects/ProjectProposalTable.spec.ts
Full coverage of tag dimming, approval/rejection confirmation, cancelled-dialog, and invalid-API paths; updated proposal table header/cell assertions; ApproveRejectButtons spec removed.

Header Refactoring, DarkModeToggle, and App Layout

Layer / File(s) Summary
App.vue flex-column layout and structured-table CSS
app/app.vue, app/assets/css/table.css, app/components/data-stores/DetailedDataStoreTable.vue
Wraps template in .app-container with flex-column/min-height:100vh; adds flex:1 0 auto to page-content; introduces .structured-table CSS for centered non-first-column headers and cells; applies structured-table class to DetailedDataStoreTable.
Preferences moved to MenuHeader; AvatarButton cleanup
app/components/header/MenuHeader.vue, app/components/header/AvatarButton.vue
MenuHeader adds isAuthenticated, links computed filtering to Home-only when unauthenticated, a conditional Preferences Button, and PreferencesDialog binding; removes disabled-link styling. AvatarButton removes the Preferences menu entry and PreferencesDialog instance; adds "Clean Up Resources" entry.
DarkModeToggle refactored to Button
app/components/header/DarkModeToggle.vue
Replaces ToggleSwitch with a PrimeVue Button rendering a themeIcon computed value (moon/sun) that calls toggleDark on click; removes checked ref and onMounted initialization.
Header tests
test/components/header/MenuHeader.spec.ts, test/components/header/DarkModeToggle.spec.ts
MenuHeader spec stubs PreferencesDialog and asserts non-Home items do not exist when unauthenticated; DarkModeToggle spec updates selector to .theme-toggle-btn button.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant AnalysesTable
  participant FilterService
  participant AnalysisControlButtons
  participant HubAPI

  User->>AnalysesTable: select hub status filter option (group::status)
  AnalysesTable->>FilterService: evaluate HUB_STATUS_FILTER_MATCH_MODE(row.hub_statuses, selection)
  FilterService-->>AnalysesTable: matched rows

  User->>AnalysesTable: click missing DataStore icon (datastoreRequired=true)
  AnalysesTable->>HubAPI: navigate to /data-stores/create?projectId=…

  User->>AnalysisControlButtons: click Update button (Executing status)
  AnalysisControlButtons->>HubAPI: POST /analysis/update
  HubAPI-->>AnalysisControlButtons: response
  AnalysisControlButtons-->>AnalysesTable: emit updateAnalysisRow
Loading
sequenceDiagram
  actor User
  participant ApproveRejectToggle
  participant ConfirmService
  participant HubAPI
  participant ProjectProposalTable

  User->>ApproveRejectToggle: click ToggleSwitch
  ApproveRejectToggle->>ConfirmService: require(Approval/Rejection Confirmation)
  User->>ConfirmService: accept
  ConfirmService->>ApproveRejectToggle: onAccept callback
  ApproveRejectToggle->>HubAPI: POST /project-nodes/{objectId} or /analysis-nodes/{objectId}
  HubAPI-->>ApproveRejectToggle: response
  ApproveRejectToggle-->>ProjectProposalTable: emit updatedRow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PrivateAIM/node-ui#309: Both PRs modify MenuHeader.vue's menu model and visible link items — this PR adds auth-based link filtering and the Preferences button, while that PR adds an "Events" route entry to the same menu construction logic.
  • PrivateAIM/node-ui#330: Both PRs modify AnalysesTable.vue and AnalysisControlButtons.vue together — that PR introduced the execution_status/analysisExecutionStatus model that the Update button visibility logic here builds on.
  • PrivateAIM/node-ui#352: Both PRs modify DataStoreProjectInitializer.vue — that PR handles the API-driven project list, and this PR adds URL query param–based project preselection in the same component.

Poem

🐇 Hop hop, the columns unite,
Three statuses merged into one bright light.
A toggle now flips with a dialog's grace,
The footer stays sticky—it knows its place.
Preferences moved, the header grows wise,
This rabbit approves with wide happy eyes! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
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.
Title check ❓ Inconclusive The PR title 'Styling requests' is too vague and generic. While some styling changes exist (table CSS, dark mode toggle), the changeset encompasses significant functional changes across multiple components including navigation restructuring, approval workflow enhancements, component removal/replacement, and test updates—far beyond what 'Styling requests' conveys. Use a more descriptive title that captures the primary changes, such as 'Refactor UI components and table styling' or 'Update approval workflow and navigation behavior' to better reflect the comprehensive scope of changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 388-styling-requests

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 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 `@app/components/analysis/AnalysesTable.vue`:
- Around line 747-760: The icon element with class 'pi pi-times' and the
onCreateDataStore click handler is not keyboard-accessible for users who cannot
use a mouse. Replace the bare <i> tag with a semantic button element or wrap it
in a button component, ensuring it has proper role semantics and can receive
keyboard focus. Apply the icon classes and click handler (with the
datastoreRequired condition) to the button element instead, and use appropriate
ARIA attributes or button styling to indicate it's clickable when the
datastore-create-link condition is true.
- Around line 372-384: The catch handler in the $hubApi call returns undefined
when the request fails, but the code immediately attempts to access
nextSetResults.length without null/undefined safety, causing a runtime
exception. Add a guard clause before checking nextSetResults.length to verify
that nextSetResults is defined and not undefined, using either an explicit null
check (if (nextSetResults && nextSetResults.length > 0)) or optional chaining
(if (nextSetResults?.length > 0)) to safely handle the case when the API call
fails and undefined is returned.

In `@app/components/analysis/AnalysisControlButtons.vue`:
- Around line 65-71: The onMounted hook is restoring the loading state from
localStorage even when the analysisExecutionStatus is null or empty string,
which causes the controls to remain locked in loading state. Modify the
condition in the onMounted block to only restore the loading flag when
localStorage contains "true" AND the analysisExecutionStatus is in
playButtonActiveStates (not when it's null or empty). Additionally, ensure the
watcher that monitors analysisExecutionStatus properly clears the
loadingStorageKey from localStorage whenever the status transitions out of the
play-active states, preventing the stale flag from persisting after a hard
refresh.

In `@app/components/data-stores/DetailedDataStoreTable.vue`:
- Around line 283-287: The checkingConnection and deleteLoading boolean states
are global, causing all rows to show loading indicators when any single row's
action is triggered. Convert these global booleans into objects or maps that
track loading state per row using the row name or key as the identifier. Update
the :loading bindings on both the test-connection button (line 283) and the
delete button (line 314) to reference the specific row's loading state instead
of the global state. Modify the onCheckConnection and delete handler methods to
set and clear loading state for only the specific row being acted upon, using
slotProps.data.name as the row identifier.

In `@app/components/projects/ProjectProposalTable.vue`:
- Around line 91-95: The Button component in ProjectProposalTable.vue has an
aria-label attribute set to "Filter" which is incorrect and misleading for
screen readers, since the button is actually a refresh action (as shown by the
icon "pi pi-refresh" and the v-tooltip text "Refresh table"). Change the
aria-label attribute value from "Filter" to "Refresh table" to accurately
describe the button's purpose and functionality to assistive technologies.

In `@app/components/table/ApproveRejectToggle.vue`:
- Around line 22-28: The toggleApproval function does not prevent concurrent
submissions while a request is in flight, allowing rapid toggles to create race
conditions. Add a guard clause at the beginning of the toggleApproval function
to check if the loading state is true, and return early if it is, preventing the
function from calling confirmRejection or confirmApproval until the current
request completes.
- Around line 85-97: The code currently logs an error and displays a toast
message when props.objectClass is neither "project" nor "analysis", but
execution continues and the undefined endpoint variable is used in a subsequent
API call. Add a return statement at the end of the else block (after the
toast.add call that handles "Invalid approval prop") to exit the function early
and prevent further execution with invalid props.
- Around line 99-105: The TypeScript assertion in the useNuxtApp() call is using
a variable name instead of a type. Replace the type assertion `as approvalResp`
with the correct type `AnalysisNode | ProjectNode | undefined` (the actual type
declared earlier). Additionally, the catch block should explicitly return a
value (such as undefined) to ensure type consistency with the union type, rather
than relying on implicit returns from console.error().
🪄 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

Run ID: 3a2c9a80-bf8e-4425-9822-492cb31b854f

📥 Commits

Reviewing files that changed from the base of the PR and between f21205b and 343f664.

📒 Files selected for processing (26)
  • app/app.vue
  • app/assets/css/table.css
  • app/components/analysis/AnalysesTable.vue
  • app/components/analysis/AnalysisControlButtons.vue
  • app/components/analysis/AnalysisUpdateButton.vue
  • app/components/data-stores/DetailedDataStoreTable.vue
  • app/components/data-stores/create/DataStoreProjectInitializer.vue
  • app/components/header/AvatarButton.vue
  • app/components/header/DarkModeToggle.vue
  • app/components/header/MenuHeader.vue
  • app/components/projects/ProjectProposalTable.vue
  • app/components/table/ApproveRejectButtons.vue
  • app/components/table/ApproveRejectToggle.vue
  • app/services/modifiedApiInterfaces.ts
  • app/types/node.ts
  • test/components/analysis/AnalysisControlButtons.spec.ts
  • test/components/analysis/AnalysisTable.spec.ts
  • test/components/analysis/AnalysisUpdateButton.spec.ts
  • test/components/data-stores/create/DataStoreProjectInitializer.spec.ts
  • test/components/events/EventViewer.spec.ts
  • test/components/header/DarkModeToggle.spec.ts
  • test/components/header/MenuHeader.spec.ts
  • test/components/projects/ApproveRejectButtons.spec.ts
  • test/components/projects/ProjectProposalTable.spec.ts
  • test/components/table/ApproveRejectToggle.spec.ts
  • test/mockapi/setup.ts
💤 Files with no reviewable changes (3)
  • test/components/projects/ApproveRejectButtons.spec.ts
  • app/components/table/ApproveRejectButtons.vue
  • app/components/header/AvatarButton.vue

Comment on lines +372 to 384
.$hubApi("/analysis-nodes", {
method: "GET",
query: {
page: {
offset: currentOffset,
limit: queryLimit,
},
include: "analysis,node",
sort: "-updated_at",
},
include: "analysis,node",
sort: "-updated_at",
},
})
.catch(() => undefined)) as AnalysisNode[];
})
.catch(() => undefined)) as AnalysisNode[];
if (nextSetResults.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard failed page fetches before reading .length.

On Line 384, nextSetResults can be undefined when $hubApi fails, causing a runtime exception on nextSetResults.length and breaking table pagination refresh paths.

Suggested fix
-  const nextSetResults = (await useNuxtApp()
+  const nextSetResults = (await useNuxtApp()
       .$hubApi("/analysis-nodes", {
         method: "GET",
         query: {
           page: {
             offset: currentOffset,
             limit: queryLimit,
           },
           include: "analysis,node",
           sort: "-updated_at",
         },
       })
-      .catch(() => undefined)) as AnalysisNode[];
-  if (nextSetResults.length > 0) {
+      .catch(() => undefined)) as AnalysisNode[] | undefined;
+  if (nextSetResults && nextSetResults.length > 0) {
     if (nextSetResults.length < queryLimit) {
       // Fewer than limit means we are at the end
       allResultsRetrieved = true;
     }
     currentOffset += queryLimit; // Increment offset value
     await compileAnalysisTable("success", nextSetResults, true, true);
   } else {
     // No results returned means all were retrieved
     allResultsRetrieved = true;
   }
📝 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
.$hubApi("/analysis-nodes", {
method: "GET",
query: {
page: {
offset: currentOffset,
limit: queryLimit,
},
include: "analysis,node",
sort: "-updated_at",
},
include: "analysis,node",
sort: "-updated_at",
},
})
.catch(() => undefined)) as AnalysisNode[];
})
.catch(() => undefined)) as AnalysisNode[];
if (nextSetResults.length > 0) {
.$hubApi("/analysis-nodes", {
method: "GET",
query: {
page: {
offset: currentOffset,
limit: queryLimit,
},
include: "analysis,node",
sort: "-updated_at",
},
})
.catch(() => undefined)) as AnalysisNode[] | undefined;
if (nextSetResults && nextSetResults.length > 0) {
if (nextSetResults.length < queryLimit) {
// Fewer than limit means we are at the end
allResultsRetrieved = true;
}
currentOffset += queryLimit; // Increment offset value
await compileAnalysisTable("success", nextSetResults, true, true);
} else {
// No results returned means all were retrieved
allResultsRetrieved = true;
}
🤖 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 `@app/components/analysis/AnalysesTable.vue` around lines 372 - 384, The catch
handler in the $hubApi call returns undefined when the request fails, but the
code immediately attempts to access nextSetResults.length without null/undefined
safety, causing a runtime exception. Add a guard clause before checking
nextSetResults.length to verify that nextSetResults is defined and not
undefined, using either an explicit null check (if (nextSetResults &&
nextSetResults.length > 0)) or optional chaining (if (nextSetResults?.length >
0)) to safely handle the case when the API call fails and undefined is returned.

Comment on lines 747 to +760
<Badge
class="w-8 h-8 rounded-full"
:severity="datastoreBadgeSeverity"
><i
:severity="datastoreBadgeSeverity"
class="w-8 h-8 rounded-full"
><i
v-tooltip.top="datastoreBadgeTooltip"
class="pi pi-times"
></i
:class="[
'pi pi-times',
{ 'datastore-create-link': datastoreRequired },
]"
@click="
datastoreRequired &&
onCreateDataStore(data.analysis.project_id)
"
></i

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a keyboard-accessible control for datastore creation.

The click action is attached to an <i> icon (Line 756), which is not keyboard-accessible by default. This makes the “create datastore” action harder/impossible for keyboard-only users in this path.

🤖 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 `@app/components/analysis/AnalysesTable.vue` around lines 747 - 760, The icon
element with class 'pi pi-times' and the onCreateDataStore click handler is not
keyboard-accessible for users who cannot use a mouse. Replace the bare <i> tag
with a semantic button element or wrap it in a button component, ensuring it has
proper role semantics and can receive keyboard focus. Apply the icon classes and
click handler (with the datastoreRequired condition) to the button element
instead, and use appropriate ARIA attributes or button styling to indicate it's
clickable when the datastore-create-link condition is true.

Comment on lines +65 to +71
() => `analysis-start-loading-${props.analysisId}`,
);

onMounted(() => {
if (
localStorage.getItem(loadingStorageKey.value) === "true" &&
playButtonActiveStates.includes(props.analysisExecutionStatus)
localStorage.getItem(loadingStorageKey.value) === "true" &&
playButtonActiveStates.includes(props.analysisExecutionStatus)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound restored loading to prevent stale localStorage lock-in.

A stale analysis-start-loading-* flag can keep controls in loading forever after a hard refresh: mount restores loading when status is null/"", and the watcher never clears unless status leaves play-active states.

💡 Suggested patch
 const loadingStorageKey = computed(
     () => `analysis-start-loading-${props.analysisId}`,
 );
+const LOADING_RESTORE_TTL_MS = 2 * 60_000;

 onMounted(() => {
+  const startedAt = Number(localStorage.getItem(loadingStorageKey.value));
   if (
-      localStorage.getItem(loadingStorageKey.value) === "true" &&
+      Number.isFinite(startedAt) &&
+      Date.now() - startedAt < LOADING_RESTORE_TTL_MS &&
       playButtonActiveStates.includes(props.analysisExecutionStatus)
   ) {
@@
 async function onStartAnalysis() {
@@
-  localStorage.setItem(loadingStorageKey.value, "true");
+  localStorage.setItem(loadingStorageKey.value, String(Date.now()));

Also applies to: 81-90

🤖 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 `@app/components/analysis/AnalysisControlButtons.vue` around lines 65 - 71, The
onMounted hook is restoring the loading state from localStorage even when the
analysisExecutionStatus is null or empty string, which causes the controls to
remain locked in loading state. Modify the condition in the onMounted block to
only restore the loading flag when localStorage contains "true" AND the
analysisExecutionStatus is in playButtonActiveStates (not when it's null or
empty). Additionally, ensure the watcher that monitors analysisExecutionStatus
properly clears the loadingStorageKey from localStorage whenever the status
transitions out of the play-active states, preventing the stale flag from
persisting after a hard refresh.

Comment on lines +283 to +287
:loading="checkingConnection"
aria-label="test-connection"
icon="pi pi-wifi"
severity="contrast"
@click="onCheckConnection(slotProps.data.name)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use row-scoped loading state for row actions.

checkingConnection and deleteLoading are global booleans, so clicking one row makes every row’s corresponding button show loading (Line 283, Line 314). Scope these to the row key/name.

💡 Suggested patch
-const deleteLoading = ref(false);
-const checkingConnection = ref(false);
+const deleteLoadingFor = ref<string | null>(null);
+const checkingConnectionFor = ref<string | null>(null);

 async function onConfirmDeleteDataStore(dsName: string) {
-  deleteLoading.value = true;
+  deleteLoadingFor.value = dsName;
   const {status} = await deleteDataStore(dsName);
   if (status.value === "success") {
@@
   } else {
@@
   }
-  deleteLoading.value = false;
+  deleteLoadingFor.value = null;
 }

 async function onCheckConnection(dsName: string) {
-  checkingConnection.value = true;
+  checkingConnectionFor.value = dsName;
   const [dsType, projectId] = extractUuid(dsName);
@@
-  checkingConnection.value = false;
+  checkingConnectionFor.value = null;
 }
-:loading="checkingConnection"
+:loading="checkingConnectionFor === slotProps.data.name"
-:loading="deleteLoading"
+:loading="deleteLoadingFor === slotProps.data.name"

Also applies to: 314-318

🤖 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 `@app/components/data-stores/DetailedDataStoreTable.vue` around lines 283 -
287, The checkingConnection and deleteLoading boolean states are global, causing
all rows to show loading indicators when any single row's action is triggered.
Convert these global booleans into objects or maps that track loading state per
row using the row name or key as the identifier. Update the :loading bindings on
both the test-connection button (line 283) and the delete button (line 314) to
reference the specific row's loading state instead of the global state. Modify
the onCheckConnection and delete handler methods to set and clear loading state
for only the specific row being acted upon, using slotProps.data.name as the row
identifier.

Comment on lines 91 to +95
<Button
v-tooltip.top="'Refresh table'"
:loading="status === 'pending'"
aria-label="Filter"
icon="pi pi-refresh"
severity="contrast"
@click="onTableRefresh"
v-tooltip.top="'Refresh table'"
:loading="status === 'pending'"
aria-label="Filter"
icon="pi pi-refresh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the refresh button’s accessible label.

Line 94 sets aria-label="Filter" on a refresh action button, which is misleading for screen readers.

Suggested fix
-                aria-label="Filter"
+                aria-label="Refresh table"
📝 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
<Button
v-tooltip.top="'Refresh table'"
:loading="status === 'pending'"
aria-label="Filter"
icon="pi pi-refresh"
severity="contrast"
@click="onTableRefresh"
v-tooltip.top="'Refresh table'"
:loading="status === 'pending'"
aria-label="Filter"
icon="pi pi-refresh"
<Button
v-tooltip.top="'Refresh table'"
:loading="status === 'pending'"
aria-label="Refresh table"
icon="pi pi-refresh"
🤖 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 `@app/components/projects/ProjectProposalTable.vue` around lines 91 - 95, The
Button component in ProjectProposalTable.vue has an aria-label attribute set to
"Filter" which is incorrect and misleading for screen readers, since the button
is actually a refresh action (as shown by the icon "pi pi-refresh" and the
v-tooltip text "Refresh table"). Change the aria-label attribute value from
"Filter" to "Refresh table" to accurately describe the button's purpose and
functionality to assistive technologies.

Comment on lines +22 to +28
const toggleApproval = () => {
if (checked.value) {
confirmRejection();
} else {
confirmApproval();
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent overlapping submissions while a request is in flight.

loading is set but not used to gate interactions; rapid toggles can dispatch competing POSTs and race the final approval state.

Suggested fix
 const toggleApproval = () => {
+  if (loading.value) {
+    checked.value = !checked.value;
+    return;
+  }
   if (checked.value) {
     confirmRejection();
   } else {
     confirmApproval();
   }
 };
 
 async function onSubmitApproval(isApproved: boolean) {
+  if (loading.value) return;
   loading.value = true;
-  const formData = new FormData();
-  formData.append("approval_status", isApproved ? "approved" : "rejected");
-  let approvalResp: AnalysisNode | ProjectNode | undefined;
-  let endpoint: string;
+  try {
+    const formData = new FormData();
+    formData.append("approval_status", isApproved ? "approved" : "rejected");
+    let approvalResp: AnalysisNode | ProjectNode | undefined;
+    let endpoint: string;
 
-  if (props.objectClass == "project") {
-    endpoint = `/project-nodes/${props.objectId}`;
-  } else if (props.objectClass == "analysis") {
-    endpoint = `/analysis-nodes/${props.objectId}`;
-  } else {
-    console.error("Unknown approval props: ", props);
-    toast.add({
-      severity: "error",
-      summary: "Invalid approval prop",
-      detail: `Unable to submit request due to invalid props: ${props}`,
-      life: 6000,
-    });
-  }
+    // existing submit logic...
 
-  if (approvalResp && "approval_status" in approvalResp) {
-    showSuccessfulSubmission(isApproved);
-    // Send data to parent component
-    emit("updatedRow", approvalResp);
-  } else {
-    // Reset toggle and show failed toast
-    checked.value = !checked.value
-    showFailedSubmission();
+    if (approvalResp && "approval_status" in approvalResp) {
+      showSuccessfulSubmission(isApproved);
+      emit("updatedRow", approvalResp);
+    } else {
+      checked.value = !checked.value;
+      showFailedSubmission();
+    }
+  } finally {
+    loading.value = false;
   }
-  loading.value = false;
 }

Also applies to: 78-80, 115-115, 146-151

🤖 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 `@app/components/table/ApproveRejectToggle.vue` around lines 22 - 28, The
toggleApproval function does not prevent concurrent submissions while a request
is in flight, allowing rapid toggles to create race conditions. Add a guard
clause at the beginning of the toggleApproval function to check if the loading
state is true, and return early if it is, preventing the function from calling
confirmRejection or confirmApproval until the current request completes.

Comment on lines +85 to +97
if (props.objectClass == "project") {
endpoint = `/project-nodes/${props.objectId}`;
} else if (props.objectClass == "analysis") {
endpoint = `/analysis-nodes/${props.objectId}`;
} else {
console.error("Unknown approval props: ", props);
toast.add({
severity: "error",
summary: "Invalid approval prop",
detail: `Unable to submit request due to invalid props: ${props}`,
life: 6000,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return early on invalid objectClass before calling the API.

The invalid-props branch logs/toasts but execution continues, so endpoint can remain unset and still be used.

Suggested fix
   } else {
     console.error("Unknown approval props: ", props);
     toast.add({
       severity: "error",
       summary: "Invalid approval prop",
       detail: `Unable to submit request due to invalid props: ${props}`,
       life: 6000,
     });
+    checked.value = !checked.value;
+    loading.value = false;
+    return;
   }
🤖 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 `@app/components/table/ApproveRejectToggle.vue` around lines 85 - 97, The code
currently logs an error and displays a toast message when props.objectClass is
neither "project" nor "analysis", but execution continues and the undefined
endpoint variable is used in a subsequent API call. Add a return statement at
the end of the else block (after the toast.add call that handles "Invalid
approval prop") to exit the function early and prevent further execution with
invalid props.

Comment on lines +99 to +105
approvalResp = (await useNuxtApp()
.$hubApi(endpoint, {
method: "POST",
body: formData,
})
.catch((e) => console.error(e))) as approvalResp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=vue -C2 'as\s+approvalResp\b' app/components/table/ApproveRejectToggle.vue

Repository: PrivateAIM/node-ui

Length of output: 90


🏁 Script executed:

cat -n app/components/table/ApproveRejectToggle.vue | sed -n '95,110p'

Repository: PrivateAIM/node-ui

Length of output: 570


🏁 Script executed:

grep -n "approvalResp" app/components/table/ApproveRejectToggle.vue | head -20

Repository: PrivateAIM/node-ui

Length of output: 330


🏁 Script executed:

grep -n "type\|interface" app/components/table/ApproveRejectToggle.vue | head -30

Repository: PrivateAIM/node-ui

Length of output: 174


Invalid TypeScript assertion uses a variable name instead of a type.

Line 104 uses as approvalResp, but approvalResp is a variable, not a type. The assertion should use the actual type declaration: AnalysisNode | ProjectNode | undefined (declared at line 82). Additionally, the catch block should return a value to maintain type consistency.

Suggested fix
-  approvalResp = (await useNuxtApp()
+  approvalResp = (await useNuxtApp()
       .$hubApi(endpoint, {
         method: "POST",
         body: formData,
       })
-      .catch((e) => console.error(e))) as approvalResp;
+      .catch((e) => {
+        console.error(e);
+        return undefined;
+      })) as AnalysisNode | ProjectNode | undefined;
📝 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
approvalResp = (await useNuxtApp()
.$hubApi(endpoint, {
method: "POST",
body: formData,
})
.catch((e) => console.error(e))) as approvalResp;
approvalResp = (await useNuxtApp()
.$hubApi(endpoint, {
method: "POST",
body: formData,
})
.catch((e) => {
console.error(e);
return undefined;
})) as AnalysisNode | ProjectNode | undefined;
🤖 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 `@app/components/table/ApproveRejectToggle.vue` around lines 99 - 105, The
TypeScript assertion in the useNuxtApp() call is using a variable name instead
of a type. Replace the type assertion `as approvalResp` with the correct type
`AnalysisNode | ProjectNode | undefined` (the actual type declared earlier).
Additionally, the catch block should explicitly return a value (such as
undefined) to ensure type consistency with the union type, rather than relying
on implicit returns from console.error().

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.

Styling requests

1 participant