Skip to content

feat: Move create, edit, clone and view overrides into a new page#974

Open
Datron wants to merge 1 commit intomainfrom
superposition-separate-forms
Open

feat: Move create, edit, clone and view overrides into a new page#974
Datron wants to merge 1 commit intomainfrom
superposition-separate-forms

Conversation

@Datron
Copy link
Copy Markdown
Collaborator

@Datron Datron commented Apr 20, 2026

Problem

The drawer for overrides form made it inconvenient for users to edit multiple overrides and a flow was not established. For a particular override, it could not be shared by URL so that someone else could view it.

Solution

Move create, edit, clone forms and view overrides to a new pages. The form is now multi-step, allowing more space for editing context and overrides

Screen.Recording.2026-04-20.at.5.39.12.PM.mov

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced dedicated pages for creating, editing, and viewing configuration overrides
    • Added multi-step form workflow with visual step indicator guiding users through context setup and override configuration
    • Enhanced button component with disabled state support
    • Added navigation links to context cards for direct access to detail pages

@Datron Datron requested a review from a team as a code owner April 20, 2026 12:14
Copilot AI review requested due to automatic review settings April 20, 2026 12:14
@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented Apr 20, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7519db9d-8541-4828-959b-6c63b376a8ba

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The pull request introduces a comprehensive override management feature with route-based navigation and multi-step UI flows. It adds new routed pages for creating, editing, and viewing context overrides, implements a reusable multi-step form component with step indicator UI, and refactors the existing context override page to use route navigation instead of drawer-based forms.

Changes

Cohort / File(s) Summary
Routing & Module Wiring
crates/frontend/src/app.rs, crates/frontend/src/components.rs, crates/frontend/src/pages.rs
Added three new override routes (action/create, /:context_id/edit, /:context_id) under admin workspace layout; exported new modules context_override_form and step_indicator in components, and override_page in pages.
Component Property Extensions
crates/frontend/src/components/button.rs, crates/frontend/src/components/context_card.rs
Extended Button with disabled prop and unified disabled state logic; added href prop to ContextCard with "See More" link, removed datetime/metadata UI elements, and updated layout classes.
New Multi-Step Form System
crates/frontend/src/components/step_indicator.rs
Introduced StepIndicator, StepNavigation, and MultiStepForm components with StepType and StepStatus enums for managing multi-step UI workflows with navigation controls and progress tracking.
Override Management Form Component
crates/frontend/src/components/context_override_form.rs
Implemented ContextOverrideForm component with dual-step UI (context conditions + overrides), branching submit logic for create vs. edit with update precheck flows, plus ChangeLogSummary modal and OverrideDetailView read-only display.
Override Page Implementations
crates/frontend/src/pages/override_page.rs
Added three routed page components: OverridePage (detail view with delete action), EditOverride (edit form with preloaded data), and CreateOverride (create form with optional clone support).
Page Refactoring
crates/frontend/src/pages/context_override.rs
Replaced drawer-driven create/edit/clone flows with route-based navigation via ButtonAnchor; removed FormMode::Create and FormMode::Edit variants; exposed use_context_data, ChangeType, and ChangeLogSummary as public; simplified drawer rendering logic.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Page as Override Page
    participant Form as ContextOverrideForm
    participant Modal as ChangeLogSummary
    participant API as Backend API
    
    User->>Page: Navigate to create/edit override
    Page->>Form: Render with initial data
    Form->>Form: Step 1: Enter context conditions
    User->>Form: Fill context & metadata
    Form->>Form: Step 2: Enter override values
    User->>Form: Fill overrides
    User->>Form: Submit
    
    alt Editing with Precheck
        Form->>API: try_update_context_payload()
        API-->>Form: Precheck response
        Form->>Modal: Show ChangeLogSummary
        User->>Modal: Confirm changes
        Modal->>API: update_context(payload)
        API-->>Modal: Success
        Modal->>Page: Navigate to detail/redirect
    else Creating
        Form->>API: create_context(overrides, context, ...)
        API-->>Form: Success with new ID
        Form->>Page: Navigate to override detail
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #835: Modifies the same context override page logic with overlapping changes to use_context_data, ChangeLogSummary, and Conditions serialization handling.
  • #815: Modifies frontend datetime-related UI in components.rs and context_card.rs; this PR removes datetime imports and alters ContextCard structure while that PR updates timestamp formatting.
  • #853: Adds similar action-based create/edit routes in app.rs using ButtonAnchor navigation patterns for resource management flows.

Suggested labels

enhancement, UI, P1

Suggested reviewers

  • knutties
  • mahatoankitkumar
  • ayushjain17

Poem

🐰 A rabbit hops through routes so new,
Multi-step forms in morning dew,
From create to edit, step by step,
The UI takes a graceful leap! 🌿✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: moving override creation, editing, cloning, and viewing functionality from a drawer-based UI to dedicated pages with a new page component.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch superposition-separate-forms

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR moves override creation/editing/cloning and detail viewing out of the drawer flow into dedicated pages, enabling multi-step editing and shareable URLs for specific overrides.

Changes:

  • Adds new override routes/pages for create, edit, and detail views.
  • Introduces a multi-step override form UI (step indicator + navigation) and a shared form component.
  • Updates the overrides listing page/cards to navigate to the new pages (and adds a “See More” link).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/frontend/src/pages/override_page.rs New pages for override detail, edit, and create (with clone support).
crates/frontend/src/pages/context_override.rs Listing page updated to navigate to new routes; exports change-log confirmation UI for reuse.
crates/frontend/src/pages.rs Exposes the new override_page module.
crates/frontend/src/components/step_indicator.rs New step indicator + step navigation components for multi-step forms.
crates/frontend/src/components/context_override_form.rs New multi-step create/edit form and a read-only override detail view component.
crates/frontend/src/components/context_card.rs Adds per-card navigation (“See More”) and prevents dropdown click propagation.
crates/frontend/src/components/button.rs Adds an explicit disabled prop to Button.
crates/frontend/src/components.rs Exposes new components (context_override_form, step_indicator).
crates/frontend/src/app.rs Registers new routes for override create/edit/detail pages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +82
class:cursor-pointer=move || {
let cs = current_step.get();
on_step_click.is_some() && step_type <= cs
}
class:hover:ring-2=move || {
let cs = current_step.get();
on_step_click.is_some() && step_type <= cs
}
class:hover:ring-purple-300=move || {
let cs = current_step.get();
on_step_click.is_some() && step_type <= cs
}
class:transition-all=true
on:click=move |_| {
let cs = current_step.get_untracked();
if let Some(callback) = on_step_click {
if step_type <= cs {
callback.call(idx);
}
}
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

on_step_click is moved into multiple move closures (class:* and on:click) and is also moved out via if let Some(callback) = on_step_click { ... }. Since Option<Callback<..>> isn’t Copy, this won’t compile and (even if it did) would consume the callback on first click. Consider storing/cloning the callback (e.g., wrap on_step_click in StoredValue/Rc, or clone the Callback into each closure and use as_ref()/cloned() inside the click handler).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nah bro, Callbacks implement Copy trait so they can do this. Don't worry about it, I personally tested it

#[derive(Debug, Clone)]
enum FormMode {
Edit(String),
Clone(String),
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

FormMode::Clone (and the corresponding drawer header/match arm) appears to be dead code now: on_context_clone navigates to the new create page and there’s no remaining set_form_mode.set(Some(FormMode::Clone(...))) call. Consider removing the Clone variant and the associated drawer UI/arms (or reintroduce setting form_mode if the drawer-based clone flow is still intended).

Suggested change
Clone(String),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (10)
crates/frontend/src/components/context_card.rs (1)

89-110: Consider whether href needs URL-escaping, and note the removed per-card metadata now only exists on the detail page.

href is forwarded straight to <A href=...>. Context IDs in this codebase appear to be hash-style strings, so practically safe, but if an ID ever contains ?, #, /, or whitespace, the link will misroute. Consider js_sys::encode_uri_component (or equivalent) at the one call site (crates/frontend/src/pages/context_override.rs:658).

The removed "Created / Last Modified" metadata is only visible now via the new OverridePage. Worth confirming that’s the intended UX tradeoff and that OverrideDetailView covers everything the drawer used to show.

Also applies to: 129-132

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/context_card.rs` around lines 89 - 110, The
href string is passed directly into the link component (see the local href
variable created via store_value and the <A href=...> usage), so URL-escape the
context ID before rendering (e.g., use js_sys::encode_uri_component or
equivalent at the call site where href is produced) to prevent misrouting for
IDs containing ?, #, /, or whitespace; also confirm the removed per-card
"Created / Last Modified" metadata was intentionally migrated to the
OverrideDetailView/OverridePage and that OverrideDetailView renders all
previously visible drawer metadata.
crates/frontend/src/components/button.rs (1)

19-49: LGTM, is_disabled = loading || disabled is applied to both class and the native disabled attribute, and the change is backward-compatible (default false).

Optional nit: the on:click handler is still attached even when disabled; native disabled suppresses clicks on a <button>, but if this component is ever re-used with a non-button tag this would regress silently. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/button.rs` around lines 19 - 49, The on:click
handler is still attached even when is_disabled is true; update the button
rendering logic in the component (where is_disabled, on:click and the <button>
are defined) to only set the on:click attribute when is_disabled is false (i.e.,
conditionally attach the handler instead of always using on:click=move |e|
on_click.call(e)) so clicks are ignored both via the native disabled attribute
and by not registering the event handler when disabled.
crates/frontend/src/pages/override_page.rs (2)

271-298: clone_context returns None for the no-clone case and is then re-read unconditionally — fine, but note double blocking.

Both clone_context and page_resource are create_blocking_resource. SSR will block on both. If cloning is rare, consider making clone_context a non-blocking create_resource so the common (non-clone) Create path isn’t penalized by a wasted blocking roundtrip whenever clone_id is None (the async body already short-circuits on id?, but SSR still waits).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/pages/override_page.rs` around lines 271 - 298, The
clone_context create_blocking_resource call causes an extra SSR blocking
roundtrip even when clone_id is None; change clone_context to use
create_resource (non-blocking) instead of create_blocking_resource, keeping the
same move || clone_id.get() loader and the async body that short-circuits on id?
so no behavior change for the clone path, while avoiding blocking in the common
no-clone case; leave page_resource as create_blocking_resource to continue
blocking for the heavier page loads.

228-239: redirect_url_cancel and redirect_url_success are identical — consider collapsing.

Both build the same /admin/{org}/{workspace}/overrides/{ctx_id} URL. Either this is intentional (and a single variable named e.g. redirect_url would be clearer), or success is meant to land somewhere different (e.g., back to the list). Worth double-checking against the UX intent described in the PR.

-                let redirect_url_cancel = format!(
-                    "/admin/{}/{}/overrides/{}",
-                    org.get().0,
-                    workspace.get().0,
-                    ctx_id,
-                );
-                let redirect_url_success = format!(
-                    "/admin/{}/{}/overrides/{}",
-                    org.get().0,
-                    workspace.get().0,
-                    ctx_id,
-                );
+                let redirect_url = format!(
+                    "/admin/{}/{}/overrides/{}",
+                    org.get().0,
+                    workspace.get().0,
+                    ctx_id,
+                );

and pass redirect_url_cancel=redirect_url.clone() redirect_url_success=redirect_url.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/pages/override_page.rs` around lines 228 - 239, The two
identical variables redirect_url_cancel and redirect_url_success should be
collapsed into a single redirect_url built from org.get().0, workspace.get().0,
and ctx_id (the same format!"/admin/{}/{}/overrides/{}"), and update call sites
to pass redirect_url_cancel=redirect_url.clone() and
redirect_url_success=redirect_url so you keep ownership semantics while removing
duplication; locate the current declarations of
redirect_url_cancel/redirect_url_success and replace them with one redirect_url,
then update where those names are used.
crates/frontend/src/components/step_indicator.rs (3)

152-159: Drop the is_last_step prop until it’s actually used (YAGNI).

is_last_step is accepted, immediately rebound as _is_last_step with a "future extensibility" comment, and never read. The real "last step" decision is inlined elsewhere via current_step.get() == StepType::OverrideStep. This is confusing for callers because the prop looks wired but isn’t. Removing it now is trivial; re-add when the extensibility lands.

-    #[prop(default = false)] is_last_step: bool,
     #[prop(default = false)] next_disabled: bool,
     ...
-    let _is_last_step = is_last_step; // Used for future extensibility
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/step_indicator.rs` around lines 152 - 159,
Remove the unused prop and its binding: delete the is_last_step parameter from
the prop list and remove the line that rebinding it to _is_last_step in the step
indicator component (the unused symbols are is_last_step and _is_last_step);
keep the existing runtime logic that determines last-step via current_step.get()
== StepType::OverrideStep and update any tests or callers that pass is_last_step
to stop providing it.

7-12: StepType ordering relies on variant declaration order — leave a note.

Because PartialOrd is derived, the enum literally depends on ContextStep being declared before OverrideStep for step_type < cs to correctly mean “completed”. A future maintainer adding a step in the middle (or re-ordering alphabetically out of habit) will silently break the indicator. A short doc comment will save someone an hour:

 #[derive(Clone, Copy, PartialEq, PartialOrd, Display)]
 #[strum(serialize_all = "snake_case")]
+/// Variant order is load-bearing: derived `PartialOrd` uses declaration order,
+/// and `StepIndicator` treats `step_type < current_step` as "completed".
+/// Keep variants listed in flow order.
 pub enum StepType {
     ContextStep,
     OverrideStep,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/step_indicator.rs` around lines 7 - 12, The
derived PartialOrd on StepType relies on variant declaration order (ContextStep
before OverrideStep) so comparisons like step_type < cs depend on that ordering;
add a short doc comment above the StepType enum warning future editors not to
reorder or alphabetize variants, and either (recommended) give explicit
discriminant values to each variant or implement Ord/PartialOrd manually for
explicit, maintainable ordering; reference the StepType enum and the
ContextStep/OverrideStep variants in the comment so maintainers know why order
matters.

193-222: Branching submit vs next on StepType::OverrideStep duplicates the "last step" concept.

The component accepts both an is_last_step prop and derives last-step-ness from current_step == OverrideStep. Right now they agree (because OverrideStep happens to be the last step), but the mismatch will bite if/when another step is added. Consider keying this branch off is_last_step (once it is actually threaded) or exposing a last_step_type: StepType prop, so StepNavigation stops hard-coding knowledge of the flow’s terminal step.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/step_indicator.rs` around lines 193 - 222, The
branch that decides whether to render the "submit" vs "next" button relies on
current_step == StepType::OverrideStep, duplicating the "last step" concept;
change it to use the component's canonical last-step indicator (use is_last_step
prop or a provided last_step_type) instead of hard-coding StepType::OverrideStep
so the logic becomes: if is_last_step { render submit button that uses
on_submit, submit_text, submit_loading } else { render next button that uses
on_next, next_text, next_disabled }; update the closure that currently
references current_step and StepType::OverrideStep to reference is_last_step (or
compare against last_step_type) and keep on_submit, on_next, submit_text,
next_text, submit_loading, and next_disabled unchanged.
crates/frontend/src/components/context_override_form.rs (3)

100-116: Collapse the no-op match arms in on_previous/on_next.

Both callbacks only ever transition in one direction; the same-step arms are dead branches. An if let or matches! guard is clearer.

♻️ Proposed simplification
-    let on_previous = Callback::new(move |_| {
-        current_step.update(|step| {
-            *step = match step {
-                StepType::OverrideStep => StepType::ContextStep,
-                StepType::ContextStep => StepType::ContextStep,
-            };
-        });
-    });
-
-    let on_next = Callback::new(move |_| {
-        current_step.update(|step| {
-            *step = match step {
-                StepType::ContextStep => StepType::OverrideStep,
-                StepType::OverrideStep => StepType::OverrideStep,
-            };
-        });
-    });
+    let on_previous = Callback::new(move |_| {
+        current_step.update(|step| {
+            if matches!(step, StepType::OverrideStep) {
+                *step = StepType::ContextStep;
+            }
+        });
+    });
+
+    let on_next = Callback::new(move |_| {
+        current_step.update(|step| {
+            if matches!(step, StepType::ContextStep) {
+                *step = StepType::OverrideStep;
+            }
+        });
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/context_override_form.rs` around lines 100 -
116, The on_previous and on_next callbacks contain no-op match arms; simplify
them by replacing the match in current_step.update closures with a single-branch
conditional that only changes the step when it matches the expected variant. For
example, in on_previous update only when *step == StepType::OverrideStep to set
StepType::ContextStep, and in on_next update only when *step ==
StepType::ContextStep to set StepType::OverrideStep (use if let or matches!
against current_step within the closure), removing the dead same-step branches.

356-420: Move the Effect out of the view body and de‑duplicate error logging.

Two small concerns in ChangeLogSummary:

  1. Effect::new(...) is being created inside the <Suspense> children block (lines 360–367). The returned () is discarded by view!, which works incidentally but is an anti‑pattern — the effect should be registered in the component body so ownership/disposal is obvious and it isn't re-registered if the view re‑renders.
  2. On resource error the same error is logged twice: once in the effect at line 365 and again in the match arm at line 415. Keep a single log site (the match arm shows the user-visible error UI, so log there and drop it from the effect).
♻️ Proposed refactor
-    view! {
+    Effect::new(move |_| {
+        if let Some(Ok(_)) = context_resource.get() {
+            disabled_rws.set(false);
+        }
+    });
+
+    view! {
         <ChangeLogPopup
             title
             description
             confirm_text
             on_confirm
             on_close
             disabled=disabled_rws
             inprogress
         >
             <Suspense fallback=move || {
                 view! { <Skeleton variant=SkeletonVariant::Block style_class="h-10".to_string() /> }
             }>
-                {
-                    Effect::new(move |_| {
-                        let context = context_resource.get();
-                        if let Some(Ok(_)) = context {
-                            disabled_rws.set(false);
-                        } else if let Some(Err(e)) = context {
-                            logging::error!("Error fetching context: {}", e);
-                        }
-                    });
-                }
                 {move || match context_resource.get() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/context_override_form.rs` around lines 356 -
420, Move the Effect::new(...) that reads context_resource and sets disabled_rws
out of the Suspense view body and register it in the component body (e.g. at the
top of the function) so it isn’t recreated on each render; keep its behavior
limited to checking context_resource.get() and calling disabled_rws.set(false)
when Some(Ok(_)) and remove the logging from this effect. Keep the single error
log in the Suspense match arm that handles Some(Err(e)) (the block that
currently does logging::error!("Error fetching context: {}", e) and renders the
error UI) so errors are only logged once. Ensure you reference the existing
symbols Effect::new, context_resource, and disabled_rws when moving the logic.

431-468: Change change_reason parameter from String to ChangeReason type.

The caller in override_page.rs already holds the value as ChangeReason (via context.change_reason), converts it to String unnecessarily, and passes it to OverrideDetailView, which then converts it back using try_from().unwrap_or_default(). This round-trip conversion silently discards validation errors: empty strings and overlong inputs trigger unwrap_or_default() without any indication to the user on a read-only detail view.

Accept the value as-is:

🔧 Proposed change
 pub fn OverrideDetailView(
     context: Conditions,
     overrides: Vec<(String, Value)>,
     description: Description,
     override_id: String,
-    change_reason: String,
+    change_reason: ChangeReason,
     created_by: String,
     ...
 ) -> impl IntoView {
     ...
             <crate::components::description::ContentDescription
                 description=description
-                change_reason=ChangeReason::try_from(change_reason).unwrap_or_default()
+                change_reason=change_reason
                 created_by=created_by

Remove the .deref().to_string() conversion in override_page.rs and pass change_reason directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/context_override_form.rs` around lines 431 -
468, The function currently accepts change_reason: String and then does
ChangeReason::try_from(change_reason).unwrap_or_default(), which causes silent
validation loss; change the parameter type to ChangeReason (e.g., change_reason:
ChangeReason) in the function signature in context_override_form.rs and update
the call site in override_page.rs to pass the ChangeReason directly (remove the
.deref().to_string() conversion). Also update where the value is used (the prop
passed into crate::components::description::ContentDescription) to use the
ChangeReason value directly so you no longer need try_from()/unwrap_or_default()
inside this component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/frontend/src/components/context_override_form.rs`:
- Around line 290-302: The ChangeLogSummary confirm button doesn't reflect the
real loading state because it uses its default inprogress Signal; forward the
existing req_inprogress_rs into the component so the popup uses the actual
in‑flight signal. In the view where update_request_rws is matched (the block
that creates <ChangeLogSummary ...>), add a prop like
inprogress=req_inprogress_rs.clone() (or pass the Signal directly) and ensure
ChangeLogSummary's prop name (inprogress) is used rather than its internal
Signal::derive default; this ensures update_context's in‑flight signal prevents
double submissions and provides loading feedback.

In `@crates/frontend/src/pages/override_page.rs`:
- Around line 38-43: The code currently silently falls back to the string "1"
when extracting the "context_id" route param in the Memo created for context_id
(using leptos_router::use_params_map and Memo::new), which masks routing bugs;
change the extraction to not invent a default—either call expect/unwrap with a
clear message or convert the param to an Option and make the surrounding
component return an early error view when the param is missing. Apply the same
change in the EditOverride component (the code block building its context_id at
lines ~174–176): stop using unwrap_or("1".into()) and instead propagate/handle
the missing param explicitly so a misconfigured route surfaces an error.
- Around line 263-309: The redirect URL values are built using reactive reads
workspace.get() and org.get() at component setup which triggers "called outside
reactive context" warnings and can produce stale values; update CreateOverride
to call get_untracked() on the workspace and org signals (or move the formatting
into the view closure) when constructing redirect_url_cancel and
redirect_url_success so the reads are non-reactive and silence warnings while
keeping intent explicit (apply the same get_untracked() change for any similar
URL construction in EditOverride if present).
- Around line 54-86: The confirm_delete closure currently calls use_navigate()
and reads org.get()/workspace.get() inside the spawn_local async block; capture
the navigate function and the untracked org/workspace values synchronously when
building the closure so the async task uses those captured values: call
use_navigate() outside spawn_local and store its result in a local (e.g.,
navigate), call org.get_untracked() and workspace.get_untracked() before
awaiting delete_context and move those captured values into the async block,
then use them to build redirect_url and navigate after the await; ensure
delete_context still receives workspace.get_untracked() and org.get_untracked()
as before.

---

Nitpick comments:
In `@crates/frontend/src/components/button.rs`:
- Around line 19-49: The on:click handler is still attached even when
is_disabled is true; update the button rendering logic in the component (where
is_disabled, on:click and the <button> are defined) to only set the on:click
attribute when is_disabled is false (i.e., conditionally attach the handler
instead of always using on:click=move |e| on_click.call(e)) so clicks are
ignored both via the native disabled attribute and by not registering the event
handler when disabled.

In `@crates/frontend/src/components/context_card.rs`:
- Around line 89-110: The href string is passed directly into the link component
(see the local href variable created via store_value and the <A href=...>
usage), so URL-escape the context ID before rendering (e.g., use
js_sys::encode_uri_component or equivalent at the call site where href is
produced) to prevent misrouting for IDs containing ?, #, /, or whitespace; also
confirm the removed per-card "Created / Last Modified" metadata was
intentionally migrated to the OverrideDetailView/OverridePage and that
OverrideDetailView renders all previously visible drawer metadata.

In `@crates/frontend/src/components/context_override_form.rs`:
- Around line 100-116: The on_previous and on_next callbacks contain no-op match
arms; simplify them by replacing the match in current_step.update closures with
a single-branch conditional that only changes the step when it matches the
expected variant. For example, in on_previous update only when *step ==
StepType::OverrideStep to set StepType::ContextStep, and in on_next update only
when *step == StepType::ContextStep to set StepType::OverrideStep (use if let or
matches! against current_step within the closure), removing the dead same-step
branches.
- Around line 356-420: Move the Effect::new(...) that reads context_resource and
sets disabled_rws out of the Suspense view body and register it in the component
body (e.g. at the top of the function) so it isn’t recreated on each render;
keep its behavior limited to checking context_resource.get() and calling
disabled_rws.set(false) when Some(Ok(_)) and remove the logging from this
effect. Keep the single error log in the Suspense match arm that handles
Some(Err(e)) (the block that currently does logging::error!("Error fetching
context: {}", e) and renders the error UI) so errors are only logged once.
Ensure you reference the existing symbols Effect::new, context_resource, and
disabled_rws when moving the logic.
- Around line 431-468: The function currently accepts change_reason: String and
then does ChangeReason::try_from(change_reason).unwrap_or_default(), which
causes silent validation loss; change the parameter type to ChangeReason (e.g.,
change_reason: ChangeReason) in the function signature in
context_override_form.rs and update the call site in override_page.rs to pass
the ChangeReason directly (remove the .deref().to_string() conversion). Also
update where the value is used (the prop passed into
crate::components::description::ContentDescription) to use the ChangeReason
value directly so you no longer need try_from()/unwrap_or_default() inside this
component.

In `@crates/frontend/src/components/step_indicator.rs`:
- Around line 152-159: Remove the unused prop and its binding: delete the
is_last_step parameter from the prop list and remove the line that rebinding it
to _is_last_step in the step indicator component (the unused symbols are
is_last_step and _is_last_step); keep the existing runtime logic that determines
last-step via current_step.get() == StepType::OverrideStep and update any tests
or callers that pass is_last_step to stop providing it.
- Around line 7-12: The derived PartialOrd on StepType relies on variant
declaration order (ContextStep before OverrideStep) so comparisons like
step_type < cs depend on that ordering; add a short doc comment above the
StepType enum warning future editors not to reorder or alphabetize variants, and
either (recommended) give explicit discriminant values to each variant or
implement Ord/PartialOrd manually for explicit, maintainable ordering; reference
the StepType enum and the ContextStep/OverrideStep variants in the comment so
maintainers know why order matters.
- Around line 193-222: The branch that decides whether to render the "submit" vs
"next" button relies on current_step == StepType::OverrideStep, duplicating the
"last step" concept; change it to use the component's canonical last-step
indicator (use is_last_step prop or a provided last_step_type) instead of
hard-coding StepType::OverrideStep so the logic becomes: if is_last_step {
render submit button that uses on_submit, submit_text, submit_loading } else {
render next button that uses on_next, next_text, next_disabled }; update the
closure that currently references current_step and StepType::OverrideStep to
reference is_last_step (or compare against last_step_type) and keep on_submit,
on_next, submit_text, next_text, submit_loading, and next_disabled unchanged.

In `@crates/frontend/src/pages/override_page.rs`:
- Around line 271-298: The clone_context create_blocking_resource call causes an
extra SSR blocking roundtrip even when clone_id is None; change clone_context to
use create_resource (non-blocking) instead of create_blocking_resource, keeping
the same move || clone_id.get() loader and the async body that short-circuits on
id? so no behavior change for the clone path, while avoiding blocking in the
common no-clone case; leave page_resource as create_blocking_resource to
continue blocking for the heavier page loads.
- Around line 228-239: The two identical variables redirect_url_cancel and
redirect_url_success should be collapsed into a single redirect_url built from
org.get().0, workspace.get().0, and ctx_id (the same
format!"/admin/{}/{}/overrides/{}"), and update call sites to pass
redirect_url_cancel=redirect_url.clone() and redirect_url_success=redirect_url
so you keep ownership semantics while removing duplication; locate the current
declarations of redirect_url_cancel/redirect_url_success and replace them with
one redirect_url, then update where those names are used.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfe92187-7ac3-410f-ac8f-206794f4f6ca

📥 Commits

Reviewing files that changed from the base of the PR and between 7a21581 and 1375330.

📒 Files selected for processing (9)
  • crates/frontend/src/app.rs
  • crates/frontend/src/components.rs
  • crates/frontend/src/components/button.rs
  • crates/frontend/src/components/context_card.rs
  • crates/frontend/src/components/context_override_form.rs
  • crates/frontend/src/components/step_indicator.rs
  • crates/frontend/src/pages.rs
  • crates/frontend/src/pages/context_override.rs
  • crates/frontend/src/pages/override_page.rs

Comment on lines +290 to +302
{move || match update_request_rws.get() {
None => ().into_view(),
Some((ctx_id, update_request)) => {
view! {
<ChangeLogSummary
context_id=ctx_id
change_type=ChangeType::Update(update_request)
on_confirm=on_submit
on_close=Callback::new(move |_| update_request_rws.set(None))
/>
}
}
}}
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.

⚠️ Potential issue | 🟡 Minor

Forward req_inprogress_rs to ChangeLogSummary so the confirm button reflects loading.

ChangeLogSummary defaults inprogress to Signal::derive(|| false) (line 312), so while the confirm‑triggered update_context future is awaiting, the popup's confirm button remains enabled. There is a small race window between confirm (line 119) and the synchronous update_request_rws.set(None) at line 130 in which the user can double‑click confirm and issue two concurrent update_context calls. Piping the existing in‑flight signal closes that window and gives users feedback.

🔧 Proposed fix
                 <ChangeLogSummary
                     context_id=ctx_id
                     change_type=ChangeType::Update(update_request)
                     on_confirm=on_submit
                     on_close=Callback::new(move |_| update_request_rws.set(None))
+                    inprogress=req_inprogress_rs.into()
                 />
📝 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
{move || match update_request_rws.get() {
None => ().into_view(),
Some((ctx_id, update_request)) => {
view! {
<ChangeLogSummary
context_id=ctx_id
change_type=ChangeType::Update(update_request)
on_confirm=on_submit
on_close=Callback::new(move |_| update_request_rws.set(None))
/>
}
}
}}
{move || match update_request_rws.get() {
None => ().into_view(),
Some((ctx_id, update_request)) => {
view! {
<ChangeLogSummary
context_id=ctx_id
change_type=ChangeType::Update(update_request)
on_confirm=on_submit
on_close=Callback::new(move |_| update_request_rws.set(None))
inprogress=req_inprogress_rs.into()
/>
}
}
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/components/context_override_form.rs` around lines 290 -
302, The ChangeLogSummary confirm button doesn't reflect the real loading state
because it uses its default inprogress Signal; forward the existing
req_inprogress_rs into the component so the popup uses the actual in‑flight
signal. In the view where update_request_rws is matched (the block that creates
<ChangeLogSummary ...>), add a prop like inprogress=req_inprogress_rs.clone()
(or pass the Signal directly) and ensure ChangeLogSummary's prop name
(inprogress) is used rather than its internal Signal::derive default; this
ensures update_context's in‑flight signal prevents double submissions and
provides loading feedback.

Comment on lines +38 to +43
let path_params = leptos_router::use_params_map();
let workspace = use_context::<Signal<Workspace>>().unwrap();
let org = use_context::<Signal<OrganisationId>>().unwrap();
let context_id = Memo::new(move |_| {
path_params.with(|params| params.get("context_id").cloned().unwrap_or("1".into()))
});
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.

⚠️ Potential issue | 🟡 Minor

Silent "1" fallback for a missing context_id hides routing bugs.

params.get("context_id").cloned().unwrap_or("1".into()) means any misconfigured route would silently fetch/edit the context with id "1" instead of surfacing the problem. The route guarantees the param exists, so prefer early-returning an error view (or unwrap_or_default() and letting the fetch fail) over inventing "1".

-    let context_id = Memo::new(move |_| {
-        path_params.with(|params| params.get("context_id").cloned().unwrap_or("1".into()))
-    });
+    let context_id = Memo::new(move |_| {
+        path_params.with(|params| params.get("context_id").cloned().unwrap_or_default())
+    });

Same comment applies to EditOverride at lines 174–176.

Also applies to: 170-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/pages/override_page.rs` around lines 38 - 43, The code
currently silently falls back to the string "1" when extracting the "context_id"
route param in the Memo created for context_id (using
leptos_router::use_params_map and Memo::new), which masks routing bugs; change
the extraction to not invent a default—either call expect/unwrap with a clear
message or convert the param to an Option and make the surrounding component
return an early error view when the param is missing. Apply the same change in
the EditOverride component (the code block building its context_id at lines
~174–176): stop using unwrap_or("1".into()) and instead propagate/handle the
missing param explicitly so a misconfigured route surfaces an error.

Comment on lines +54 to +86
let confirm_delete = move |context_id: String| {
delete_inprogress_rws.set(true);
spawn_local(async move {
let result = delete_context(
context_id.clone(),
&workspace.get_untracked(),
&org.get_untracked(),
)
.await;
delete_inprogress_rws.set(false);
match result {
Ok(_) => {
logging::log!("Context deleted successfully");
let navigate = use_navigate();
let redirect_url = format!(
"/admin/{}/{}/overrides",
org.get().0,
workspace.get().0,
);
navigate(&redirect_url, Default::default());
enqueue_alert(
String::from("Override deleted successfully"),
AlertType::Success,
5000,
);
}
Err(e) => {
logging::error!("Error deleting context: {:?}", e);
enqueue_alert(e, AlertType::Error, 5000);
}
}
});
};
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In leptos 0.6, is it safe to call use_navigate() from inside spawn_local, or must it be obtained in the component/callback synchronously?

💡 Result:

No, it is not safe to call use_navigate directly from inside spawn_local. use_navigate is a hook that must be called synchronously during component setup or in synchronous reactive contexts (like effects or callbacks) within the component's reactive scope. spawn_local spawns an asynchronous task that runs outside the synchronous reactive Owner scope of the component, so calling use_navigate inside it will fail because it cannot access the router context provided by the Router component. To navigate from an async task like spawn_local, obtain the navigate function synchronously in the component first (let navigate = use_navigate;), clone it (as it implements Clone), and move the clone into the spawn_local closure. Then call the cloned navigate function inside the async block. This pattern is demonstrated in Leptos examples and aligns with hook usage rules enforced in later versions (e.g., explicit Router scope checks in 0.7+), and context access patterns where values must be retrieved synchronously before async use.

Citations:


Capture use_navigate outside spawn_local, and use get_untracked() consistently for post-await reads.

Two issues in confirm_delete (lines 54–86):

  1. use_navigate() is invoked inside the async task (line 67). In Leptos, use_navigate is a hook that requires the synchronous reactive scope of the component; calling it inside spawn_local (which runs outside that scope) will fail to access the router context. Instead, capture the navigate function synchronously when building the callback and move it into the async block.
  2. Lines 70–71 read org.get() / workspace.get() reactively inside spawn_local, while lines 59–60 use get_untracked(). Use get_untracked() consistently here as well—the values cannot change meaningfully mid-delete.
🛠️ Proposed fix
     let confirm_delete = move |context_id: String| {
         delete_inprogress_rws.set(true);
+        let navigate = use_navigate();
         spawn_local(async move {
             let result = delete_context(
                 context_id.clone(),
                 &workspace.get_untracked(),
                 &org.get_untracked(),
             )
             .await;
             delete_inprogress_rws.set(false);
             match result {
                 Ok(_) => {
                     logging::log!("Context deleted successfully");
-                    let navigate = use_navigate();
                     let redirect_url = format!(
                         "/admin/{}/{}/overrides",
-                        org.get().0,
-                        workspace.get().0,
+                        org.get_untracked().0,
+                        workspace.get_untracked().0,
                     );
                     navigate(&redirect_url, Default::default());
📝 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
let confirm_delete = move |context_id: String| {
delete_inprogress_rws.set(true);
spawn_local(async move {
let result = delete_context(
context_id.clone(),
&workspace.get_untracked(),
&org.get_untracked(),
)
.await;
delete_inprogress_rws.set(false);
match result {
Ok(_) => {
logging::log!("Context deleted successfully");
let navigate = use_navigate();
let redirect_url = format!(
"/admin/{}/{}/overrides",
org.get().0,
workspace.get().0,
);
navigate(&redirect_url, Default::default());
enqueue_alert(
String::from("Override deleted successfully"),
AlertType::Success,
5000,
);
}
Err(e) => {
logging::error!("Error deleting context: {:?}", e);
enqueue_alert(e, AlertType::Error, 5000);
}
}
});
};
let confirm_delete = move |context_id: String| {
delete_inprogress_rws.set(true);
let navigate = use_navigate();
spawn_local(async move {
let result = delete_context(
context_id.clone(),
&workspace.get_untracked(),
&org.get_untracked(),
)
.await;
delete_inprogress_rws.set(false);
match result {
Ok(_) => {
logging::log!("Context deleted successfully");
let redirect_url = format!(
"/admin/{}/{}/overrides",
org.get_untracked().0,
workspace.get_untracked().0,
);
navigate(&redirect_url, Default::default());
enqueue_alert(
String::from("Override deleted successfully"),
AlertType::Success,
5000,
);
}
Err(e) => {
logging::error!("Error deleting context: {:?}", e);
enqueue_alert(e, AlertType::Error, 5000);
}
}
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/pages/override_page.rs` around lines 54 - 86, The
confirm_delete closure currently calls use_navigate() and reads
org.get()/workspace.get() inside the spawn_local async block; capture the
navigate function and the untracked org/workspace values synchronously when
building the closure so the async task uses those captured values: call
use_navigate() outside spawn_local and store its result in a local (e.g.,
navigate), call org.get_untracked() and workspace.get_untracked() before
awaiting delete_context and move those captured values into the async block,
then use them to build redirect_url and navigate after the await; ensure
delete_context still receives workspace.get_untracked() and org.get_untracked()
as before.

Comment on lines +263 to +309
pub fn CreateOverride() -> impl IntoView {
let workspace = use_context::<Signal<Workspace>>().unwrap();
let org = use_context::<Signal<OrganisationId>>().unwrap();
let query = leptos_router::use_query_map();
let clone_id = Memo::new(move |_| {
query.with(|q| q.get("clone").cloned())
});

let clone_context = create_blocking_resource(
move || clone_id.get(),
move |id| async move {
let id = id?;
get_context(&id, &workspace.get().0, &org.get().0).await.ok()
},
);

let page_resource = create_blocking_resource(
move || (workspace.get().0, org.get().0),
|(workspace, org_id)| async move {
let empty_list_filters = PaginationParams::all_entries();
let default_config_filters = DefaultConfigFilters::default();
let (dimensions_result, default_config_result) = join!(
dimensions::list(&empty_list_filters, &workspace, &org_id),
default_configs::list(
&empty_list_filters,
&default_config_filters,
&workspace,
&org_id
),
);
FormPageResource {
dimensions: dimensions_result.unwrap_or_default().data,
default_config: default_config_result.unwrap_or_default().data,
}
},
);

let redirect_url_cancel = store_value(format!(
"/admin/{}/{}/overrides",
org.get().0,
workspace.get().0
));
let redirect_url_success = store_value(format!(
"/admin/{}/{}/overrides",
org.get().0,
workspace.get().0
));
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.

⚠️ Potential issue | 🟡 Minor

org.get() / workspace.get() at component setup — use get_untracked() (or move into the view closure) to avoid reactive-read-outside-context warnings and stale URLs.

Lines 300–309 call org.get() / workspace.get() at component-body scope (outside any reactive closure) and stash the formatted URLs in store_value. Two minor issues:

  • In leptos 0.6 these reactive reads outside a reactive context typically log at_whole_app: called outside reactive context (or similar) warnings and won’t re-subscribe correctly. Since this is top-level setup, get_untracked() expresses the intent and silences the warning.
  • If the user switches org/workspace while the CreateOverride page is mounted, these URLs won’t update — probably impossible in practice but worth a sanity check.
-    let redirect_url_cancel = store_value(format!(
-        "/admin/{}/{}/overrides",
-        org.get().0,
-        workspace.get().0
-    ));
-    let redirect_url_success = store_value(format!(
-        "/admin/{}/{}/overrides",
-        org.get().0,
-        workspace.get().0
-    ));
+    let redirect_url = store_value(format!(
+        "/admin/{}/{}/overrides",
+        org.get_untracked().0,
+        workspace.get_untracked().0
+    ));

Same pattern could be applied to the EditOverride URLs inside the view closure — those are fine reactively, but also effectively never change, so get_untracked() is more precise there too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/frontend/src/pages/override_page.rs` around lines 263 - 309, The
redirect URL values are built using reactive reads workspace.get() and org.get()
at component setup which triggers "called outside reactive context" warnings and
can produce stale values; update CreateOverride to call get_untracked() on the
workspace and org signals (or move the formatting into the view closure) when
constructing redirect_url_cancel and redirect_url_success so the reads are
non-reactive and silence warnings while keeping intent explicit (apply the same
get_untracked() change for any similar URL construction in EditOverride if
present).

@Datron Datron force-pushed the superposition-separate-forms branch from 1375330 to 05402d8 Compare April 20, 2026 12:31
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.

2 participants