Skip to content

Commit ed7f58d

Browse files
authored
Merge branch 'main' into docs/flyway-test-fixture-strategy
2 parents 6de2694 + c7159e6 commit ed7f58d

14 files changed

Lines changed: 271 additions & 201 deletions

File tree

frontend/src/components/core/ScheduleActions/index.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ const ScheduleActions: FC<ScheduleActionsProps> = ({
3636
<Button kind="primary" size="md" disabled={!editable || saving} onClick={onSave}>
3737
Save
3838
</Button>
39-
<Button
40-
kind="tertiary"
41-
size="md"
42-
disabled={!editable || saving}
43-
onClick={onCheckStatus}
44-
>
39+
<Button kind="tertiary" size="md" disabled={!editable || saving} onClick={onCheckStatus}>
4540
Check Status
4641
</Button>
4742
{showDelete && (

frontend/src/components/schedule11/index.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import apiService from '@/service/api-service'
3030
import useMillYear from '@/context/millYear/useMillYear'
3131
import { useScheduleDocument } from '@/hooks/useScheduleDocument'
3232
import { extractDetail } from '@/utils/error'
33-
import { numStr } from '@/utils/number'
33+
import { numStr, numStrFixed } from '@/utils/number'
3434
import LoadingScreen from '@/components/core/LoadingScreen'
3535
import PageState from '@/components/core/PageState'
3636
import ScheduleTombstone from '@/components/core/ScheduleTombstone'
@@ -53,17 +53,12 @@ const BEC_CATALOGUE_PATH = '/v1/schedule11/biogeoclimatic-catalogue'
5353
const BEC_DEBOUNCE_MS = 250
5454

5555
// Legacy display masks (AD-5 no recompute — the values are server-computed, this only formats them).
56-
// Null renders BLANK, never "0": a null total means "no contributors", which is meaningful.
57-
const mask = (value: number | null | undefined, minFrac: number, maxFrac: number): string =>
58-
value === null || value === undefined
59-
? ''
60-
: value.toLocaleString('en-US', {
61-
minimumFractionDigits: minFrac,
62-
maximumFractionDigits: maxFrac,
63-
})
64-
const money = (value: number | null | undefined): string => mask(value, 0, 0) // #,###,##0
65-
const area = (value: number | null | undefined): string => mask(value, 1, 1) // #,###,##0.0
66-
const ratio = (value: number | null | undefined): string => mask(value, 2, 2) // #,###,##0.00
56+
// Delegate to the shared en-CA numStrFixed (Story 29.8): one locale app-wide, no local toLocaleString
57+
// copy. numStrFixed keeps the same contract — fixed decimals, and BLANK (never "0") on null, since a
58+
// null total means "no contributors", which is meaningful.
59+
const money = (value: number | null | undefined): string => numStrFixed(value, 0) // #,###,##0
60+
const area = (value: number | null | undefined): string => numStrFixed(value, 1) // #,###,##0.0
61+
const ratio = (value: number | null | undefined): string => numStrFixed(value, 2) // #,###,##0.00
6762

6863
// Whole-dollar costs: legacy accepted fractional input (ILCRCostConverter BigDecimal parse) and
6964
// Oracle COST NUMBER(15) ROUNDED it on insert, while the modern Integer wire would silently

frontend/src/components/schedule11/validation.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// the authoritative response.
66

77
import type { BiogeoclimaticOption } from '@/interfaces/Schedule11Response'
8+
import { parseDecimalInput } from '@/utils/number'
89

910
const LOCATION_MAX = 30
1011
const NET_AREA = { min: 0, max: 999_999.9 }
@@ -48,22 +49,10 @@ export interface LocationFormValues {
4849
comments: string
4950
}
5051

51-
// Legacy JSF numeric fields (NAR, costs) bind through a US-locale DecimalFormat converter, so the
52-
// accepted syntax is: an optional leading '-', digits with optional comma grouping, and an optional
53-
// '.' fractional part — the same format the page DISPLAYS (the money/area masks in index.tsx). Native
54-
// Number() diverges both ways and must NOT be used here: it rejects grouped input the legacy app
55-
// accepts (`Number('1,000')` -> NaN) and silently accepts JS-only forms the legacy app never allowed
56-
// (`1e2` -> 100, `0x10` -> 16, `Infinity`). Parsing with an explicit format keeps both this advisory
57-
// gate AND the submitted body (index.tsx buildBody) faithful to legacy. Returns the numeric value, or
58-
// null when the string is blank or not a valid decimal in that format.
59-
const DECIMAL_INPUT = /^-?(\d{1,3}(,\d{3})+|\d+)(\.\d+)?$/
60-
export const parseDecimalInput = (raw: string): number | null => {
61-
const trimmed = raw.trim()
62-
if (trimmed === '' || !DECIMAL_INPUT.test(trimmed)) {
63-
return null
64-
}
65-
return Number(trimmed.replace(/,/g, ''))
66-
}
52+
// The strict legacy-format decimal parser lives in @/utils/number (Story 29.8 consolidation — this
53+
// file's identical local copy was removed). Re-exported so callers importing it from this module keep
54+
// working; used below for the advisory NAR/cost gate.
55+
export { parseDecimalInput }
6756

6857
const validateCost = (raw: string): string | undefined => {
6958
const trimmed = raw.trim()

frontend/src/components/schedule2/__tests__/Schedule2.test.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -350,16 +350,11 @@ describe('Schedule2 page', () => {
350350
})
351351

352352
test('stale PUT is ignored when context changes before it settles (Story 29.6)', async () => {
353-
let putGate: (v: unknown) => void = () => {}
354-
const putPromise = new Promise((resolve) => {
355-
putGate = resolve
356-
})
357353
let releasePut = () => {}
358354
const releasePromise = new Promise<void>((resolve) => {
359355
releasePut = resolve
360356
})
361357

362-
let putCalled = false
363358
server.use(
364359
http.get(URL, ({ request }) =>
365360
new window.URL(request.url).searchParams.get('millId') === '999'
@@ -373,8 +368,6 @@ describe('Schedule2 page', () => {
373368
: HttpResponse.json(schedule2Doc),
374369
),
375370
http.put(URL, async () => {
376-
putCalled = true
377-
putGate(null)
378371
await releasePromise
379372
return HttpResponse.json({
380373
...schedule2Doc,
@@ -385,6 +378,7 @@ describe('Schedule2 page', () => {
385378

386379
render(
387380
<MillYearProvider initial={{ millId: 514, year: 2021 }}>
381+
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
388382
<StaleRaceHarness />
389383
</MillYearProvider>,
390384
)

frontend/src/components/schedule3/__tests__/Schedule3.test.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -434,16 +434,11 @@ describe('Schedule3 sub-page navigation (AC6)', () => {
434434
})
435435

436436
test('stale PUT is ignored when context changes before it settles (Story 29.6)', async () => {
437-
let putGate: (v: unknown) => void = () => {}
438-
const putPromise = new Promise((resolve) => {
439-
putGate = resolve
440-
})
441437
let releasePut = () => {}
442438
const releasePromise = new Promise<void>((resolve) => {
443439
releasePut = resolve
444440
})
445441

446-
let putCalled = false
447442
server.use(
448443
http.get(URL, ({ request }) =>
449444
new window.URL(request.url).searchParams.get('millId') === '999'
@@ -457,8 +452,6 @@ describe('Schedule3 sub-page navigation (AC6)', () => {
457452
: HttpResponse.json(schedule3Doc),
458453
),
459454
http.put(URL, async () => {
460-
putCalled = true
461-
putGate(null)
462455
await releasePromise
463456
return HttpResponse.json({
464457
...schedule3Doc,
@@ -469,6 +462,7 @@ describe('Schedule3 sub-page navigation (AC6)', () => {
469462

470463
render(
471464
<MillYearProvider initial={{ millId: 514, year: 2021 }}>
465+
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
472466
<StaleRaceHarness />
473467
</MillYearProvider>,
474468
)

frontend/src/components/schedule4/__tests__/Schedule4.test.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -786,16 +786,11 @@ describe('Schedule4 context, load + write error, edit, delete and status paths',
786786
})
787787

788788
test('stale PUT is ignored when context changes before it settles (Story 29.6)', async () => {
789-
let putGate: (v: unknown) => void = () => {}
790-
const putPromise = new Promise((resolve) => {
791-
putGate = resolve
792-
})
793789
let releasePut = () => {}
794790
const releasePromise = new Promise<void>((resolve) => {
795791
releasePut = resolve
796792
})
797793

798-
let putCalled = false
799794
server.use(
800795
http.get(URL, ({ request }) =>
801796
new window.URL(request.url).searchParams.get('millId') === '999'
@@ -804,14 +799,21 @@ describe('Schedule4 context, load + write error, edit, delete and status paths',
804799
millId: 999,
805800
year: 2020,
806801
editable: false,
807-
locations: [{ id: 999, revisionCount: 1, name: 'Context 999/2020 loaded', comments: null, categories: [], subPageRows: [] }],
802+
locations: [
803+
{
804+
id: 999,
805+
revisionCount: 1,
806+
name: 'Context 999/2020 loaded',
807+
comments: null,
808+
categories: [],
809+
subPageRows: [],
810+
},
811+
],
808812
}),
809813
)
810814
: HttpResponse.json(doc()),
811815
),
812816
http.put(LOCATIONS_URL, async () => {
813-
putCalled = true
814-
putGate(null)
815817
await releasePromise
816818
return HttpResponse.json({
817819
...doc(),
@@ -827,6 +829,7 @@ describe('Schedule4 context, load + write error, edit, delete and status paths',
827829
validateSearch: realScheduleRoute.options.validateSearch,
828830
component: () => (
829831
<MillYearProvider initial={{ millId: 514, year: 2021 }}>
832+
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
830833
<StaleRaceHarness />
831834
</MillYearProvider>
832835
),

0 commit comments

Comments
 (0)