From 0d8afcd6173030c5708ebaf0f675af417c4694ac Mon Sep 17 00:00:00 2001 From: Daniel Rodowicz Date: Mon, 31 Aug 2026 14:46:15 -0400 Subject: [PATCH 1/2] AAP-90564 - fix(frontend): sync default manual time reactively across UI Row manual-time input used defaultValue (uncontrolled), so it never picked up a refreshed avgRunTime after "Apply default to all templates" without a page reload; keying the input on avgRunTime forces a remount when it changes. The default-manual-time field also saved and refetched on every keystroke, moving Total savings live while Current page savings stayed stuck, since current-page templates keep their own persisted effort. Stage the default locally instead, like a per-template manual time edit, and persist it only when Apply is confirmed so both savings figures move together. --- cypress/e2e/AutomationCalculator.spec.js | 41 ++++++++++++++++++- .../AutomationCalculator.tsx | 21 +++++++--- .../TemplatesTable/Row.tsx | 1 + 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/AutomationCalculator.spec.js b/cypress/e2e/AutomationCalculator.spec.js index 1c8e41995..a92361bf3 100644 --- a/cypress/e2e/AutomationCalculator.spec.js +++ b/cypress/e2e/AutomationCalculator.spec.js @@ -223,12 +223,13 @@ describe('Automation Calculator page', () => { return; } + // The default is staged locally and only persisted when "Apply default + // to all templates" is confirmed, so typing here must not hit the + // network — no waitToLoad(). cy.get('#default-manual-effort').clear(); - waitToLoad(); cy.get('#default-manual-effort').should('have.value', '0'); cy.get('#default-manual-effort').type('60'); - waitToLoad(); cy.get('#default-manual-effort') .invoke('val') .then((val) => { @@ -445,6 +446,42 @@ describe('Automation Calculator page', () => { }); }); + it('does not update savings when editing the default before applying', () => { + // The default manual time is staged locally, like a per-template manual + // time edit — neither savings figure should move until "Apply default to + // all templates" is confirmed. + visitWithStubbedTemplates(); + + cy.intercept('POST', '/api/tower-analytics/v1/roi_cost_effort_data/').as( + 'roiCostEffortSave', + ); + cy.intercept('POST', '**/roi_templates_apply_default/').as('applyDefault'); + + cy.getByCy('total_savings') + .find('h3') + .invoke('text') + .then((originalTotalSavings) => { + cy.getByCy('current_page_savings') + .find('h3') + .invoke('text') + .then((originalCurrentPageSavings) => { + cy.get('#default-manual-effort').clear(); + cy.get('#default-manual-effort').type('45'); + + cy.getByCy('total_savings') + .find('h3') + .invoke('text') + .should('eq', originalTotalSavings); + cy.getByCy('current_page_savings') + .find('h3') + .invoke('text') + .should('eq', originalCurrentPageSavings); + cy.get('@roiCostEffortSave.all').should('have.length', 0); + cy.get('@applyDefault.all').should('have.length', 0); + }); + }); + }); + it('cancels apply default without calling the endpoint', () => { cy.intercept('POST', '**/roi_templates_apply_default/', { updated_count: 3, diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx index 68ef38ecc..f303fd596 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx @@ -204,6 +204,7 @@ const AutomationCalculator: FC = ({ res.successful_hosts_saved_hours_current_page; api.result.successful_hosts_saved_hours_other_pages = res.successful_hosts_saved_hours_other_pages; + api.result.cost = res.cost; setValue( mapApi( res.meta as any, @@ -215,16 +216,21 @@ const AutomationCalculator: FC = ({ const updateCalculationValues = async (varName: string, value: number) => { if (isNaN(value)) return; + if (varName === 'default_manual_effort_minutes') { + // Staged only: the default is committed via the explicit "Apply + // default to all templates" action (applyDefaultToAll), not on every + // keystroke. Mirrors per-template manual time, which also only + // commits on its own action (blur). + setDefaultManualEffort(value); + return; + } const hourly_automation_cost = varName === 'automation_cost' ? value : costAutomation; const hourly_manual_labor_cost = varName === 'manual_cost' ? value : costManual; - const manual_effort = - varName === 'default_manual_effort_minutes' ? value : defaultManualEffort; const humanVarNames: Record = { automation_cost: 'Automation cost', manual_cost: 'Manual cost', - default_manual_effort_minutes: 'Default manual time', }; const humanVarName = humanVarNames[varName] ?? varName; try { @@ -233,7 +239,7 @@ const AutomationCalculator: FC = ({ api.result.items, hourly_manual_labor_cost, hourly_automation_cost, - manual_effort, + defaultManualEffort, ) as any, ); } catch { @@ -250,8 +256,6 @@ const AutomationCalculator: FC = ({ setCostManual(value); } else if (varName === 'automation_cost') { setCostAutomation(value); - } else if (varName === 'default_manual_effort_minutes') { - setDefaultManualEffort(value); } }; @@ -314,6 +318,11 @@ const AutomationCalculator: FC = ({ const applyDefaultToAll = async () => { let count = 0; try { + // The default is staged locally (see updateCalculationValues) and only + // committed here, so persist it before the apply endpoint reads it — + // that endpoint POSTs no body and relies on the server-persisted + // default_manual_effort_minutes. + await saveROI(getROISaveData(api.result.items) as any); // backend applies to all templates tenant-wide; no params needed const res = (await applyDefaultROITemplates()) as { updated_count?: number; diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx index e9e764359..1f59853e4 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx @@ -117,6 +117,7 @@ const Row: FunctionComponent = ({ Date: Wed, 2 Sep 2026 08:20:49 -0400 Subject: [PATCH 2/2] AAP-90564 - fix(frontend): address review feedback on default manual time UX Add a divider separating the auto-save cost fields from the batch-action default manual time field, clarifying why only two of the three fields update the savings numbers immediately. Drop the apply-default notification's template count: the backend's updated_count only reflects templates with a prior manual-effort row, not the true total the default is applied to, so the message is misleading. --- cypress/e2e/AutomationCalculator.spec.js | 8 +++++--- .../AutomationCalculator/AutomationCalculator.tsx | 13 +++++-------- .../AutomationCalculator/CalculationCost.tsx | 7 +++++++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/AutomationCalculator.spec.js b/cypress/e2e/AutomationCalculator.spec.js index a92361bf3..9d1d8748b 100644 --- a/cypress/e2e/AutomationCalculator.spec.js +++ b/cypress/e2e/AutomationCalculator.spec.js @@ -432,7 +432,7 @@ describe('Automation Calculator page', () => { waitForStubbedLoad(); cy.getByCy('apply_default_modal').should('not.exist'); - cy.contains('Default manual time applied to 3 templates.').should( + cy.contains('Default manual time applied to all templates.').should( 'exist', ); @@ -551,7 +551,7 @@ describe('Automation Calculator page', () => { cy.getByCy('apply_default_button').should('not.be.disabled'); }); - it('falls back to 0 when the apply response has no updated_count', () => { + it('shows success message when the apply response has no body', () => { cy.intercept('POST', '**/roi_templates_apply_default/', {}).as( 'applyDefault', ); @@ -563,7 +563,9 @@ describe('Automation Calculator page', () => { cy.wait('@applyDefault'); waitForStubbedLoad(); - cy.contains('Default manual time applied to 0 templates.').should('exist'); + cy.contains('Default manual time applied to all templates.').should( + 'exist', + ); }); it('disables cancel/close while a request is in flight', () => { diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx index f303fd596..d53a33c57 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx @@ -316,7 +316,6 @@ const AutomationCalculator: FC = ({ }; const applyDefaultToAll = async () => { - let count = 0; try { // The default is staged locally (see updateCalculationValues) and only // committed here, so persist it before the apply endpoint reads it — @@ -324,10 +323,7 @@ const AutomationCalculator: FC = ({ // default_manual_effort_minutes. await saveROI(getROISaveData(api.result.items) as any); // backend applies to all templates tenant-wide; no params needed - const res = (await applyDefaultROITemplates()) as { - updated_count?: number; - }; - count = typeof res.updated_count === 'number' ? res.updated_count : 0; + await applyDefaultROITemplates(); } catch { addNotification({ title: 'Unable to apply default manual time', @@ -340,9 +336,10 @@ const AutomationCalculator: FC = ({ } addNotification({ - title: `Default manual time applied to ${count} template${ - count === 1 ? '' : 's' - }.`, + // updated_count from the backend only reflects templates with a prior + // manual-effort row, not the true total the default was applied to — + // so the message stays count-free. + title: 'Default manual time applied to all templates.', variant: 'success', dismissable: true, }); diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx index 8a943ded6..ab31b1082 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx @@ -14,6 +14,12 @@ const WInputGroup = styled(InputGroup)` width: 170px; `; +const Divider = styled.hr` + margin: 16px 0 12px 0; + border: none; + border-top: 1px solid var(--pf-t--global--border--color--200); +`; + const validFloat = (value: number): number => +value && +value < 0 ? NaN : value; @@ -97,6 +103,7 @@ const CalculationCost: FunctionComponent = ({ /> /hr +

Default manual time per template (minutes)