-
Notifications
You must be signed in to change notification settings - Fork 31
Bugfix: Feedback step should be able to navigate to a data task #3645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…e() instead. Moving things around to make tests/mocking work.
… same as for every other form page, and can be rendered out more easily in ProcessWrapper.tsx
… check and fix wrong navigation in FixWrongReceiptType.tsx.
… the current one' page shows up correctly
…out to a separate hook for re-use
…e data is not yet fetched again when we arrive at the DataModelsProvider. This causes it to use outdated instance data, marking the data model as read-only when it has been unlocked. This should fix the problem, and hopefully also solve the earlier bug with subforms.
…ithout flickering this message when navigating.
📝 WalkthroughWalkthroughReplace type-driven presentation and receipt routing with receipt-detection hooks; add routing/search param types and navigation helpers; refactor process/feedback polling and datamodel shape; introduce several new routing hooks/components; update many import sites and tests to the new APIs. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/features/propagateTraceWhenPdf/index.ts (2)
6-13
: Fix cookie parsing typing and edge-cases.
Current code creates a{}
and assigns dynamic keys (unsafe) and may create empty keys.-function getCookies(): { [key: string]: string } { - const cookie = {}; - document.cookie.split(';').forEach((el) => { - const split = el.split('='); - cookie[split[0].trim()] = split.slice(1).join('='); - }); - return cookie; -} +function getCookies(): Record<string, string> { + const cookies: Record<string, string> = {}; + if (!document.cookie) return cookies; + document.cookie.split(';').forEach((raw) => { + const el = raw.trim(); + if (!el) return; + const idx = el.indexOf('='); + if (idx === -1) return; + const key = el.slice(0, idx).trim(); + const value = el.slice(idx + 1); + if (!key) return; + cookies[key] = decodeURIComponent(value); + }); + return cookies; +}
21-46
: Avoid multiple interceptor registrations.
IfpropagateTraceWhenPdf()
runs more than once, interceptors stack up.- if (isPdf) { - const cookies = getCookies(); - axios.interceptors.request.use((config) => { + if (isPdf) { + const cookies = getCookies(); + if (pdfInterceptorId != null) return; + pdfInterceptorId = axios.interceptors.request.use((config) => { try { if (config.url?.startsWith(appPath) !== true) { return config; } // This header is caught in app-lib/backend and used to allow injection of traceparent/context - config.headers['X-Altinn-IsPdf'] = 'true'; + (config.headers ??= {})['X-Altinn-IsPdf'] = 'true'; const traceparent = cookies['altinn-telemetry-traceparent']; const tracestate = cookies['altinn-telemetry-tracestate']; if (traceparent) { - config.headers['traceparent'] = traceparent; + (config.headers ??= {})['traceparent'] = traceparent; } if (tracestate) { - config.headers['tracestate'] = tracestate; + (config.headers ??= {})['tracestate'] = tracestate; } return config; } catch (err) { console.error('Error configuring propagation of W3C trace for request', err); return config; } }); }Additional top-level addition (outside the selected range):
// At module scope let pdfInterceptorId: number | null = null;src/features/instance/useProcessQuery.ts (1)
66-71
: Bug: wrong task lookup uses currentTask regardless of match.
You ignore the found task inprocessTasks
and always returncurrentTask
, which misclassifies other tasks.- return (taskId: string | undefined) => { - const task = - (processData?.processTasks?.find((t) => t.elementId === taskId) ?? processData?.currentTask?.elementId === taskId) - ? processData?.currentTask - : undefined; + return (taskId: string | undefined) => { + const task = + processData?.processTasks?.find((t) => t.elementId === taskId) ?? + (processData?.currentTask?.elementId === taskId ? processData?.currentTask : undefined);src/components/form/Form.tsx (1)
53-56
: Bug: search param not removed due to returning stale objectYou mutate params but return the outer searchParams, so ?validate persists and can cause repeated validation/replace loops. Return a new instance (and prefer replace).
Apply:
- setSearchParams((params) => { - params.delete(SearchParams.Validate); - return searchParams; - }); + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + next.delete(SearchParams.Validate); + return next; + }, { replace: true });src/hooks/useNavigatePage.ts (1)
248-252
: Bug: URLSearchParams.has does not accept a value argument
has(name, 'true')
is invalid and always checks only presence. You likely want to check for the value being 'true'.Apply this diff:
- const shouldExitSubform = options?.searchParams?.has(SearchParams.ExitSubform, 'true') ?? false; + const shouldExitSubform = options?.searchParams?.get(SearchParams.ExitSubform) === 'true';src/features/processEnd/feedback/Feedback.tsx (1)
60-71
: Prevent overlapping refetches and handle errors in backoff loopCurrently, the next tick is scheduled immediately after triggering
callback()
, which can cause overlapping requests if a fetch takes longer than the backoff delay. Also, unhandled promise rejections can leak to the console. Await the callback (with catch) before scheduling the next attempt.- setTimeout(() => { - if (!shouldContinue) { - return; - } - callback().then(); - attempts.current++; - shouldContinue && continueCalling(); - }, backoff); + setTimeout(() => { + if (!shouldContinue) { + return; + } + Promise.resolve() + .then(() => callback()) + .catch((err) => { + // Log once per failure; consider using a dedicated logger if available + console.error('Feedback polling failed', err); + }) + .finally(() => { + attempts.current++; + if (shouldContinue) { + continueCalling(); + } + }); + }, backoff);
🧹 Nitpick comments (18)
src/core/routing/types.ts (1)
1-7
: Optional: use a const object + union type instead of a TS enumThis avoids enum runtime artifacts, plays nicer with ESM tree-shaking, and retains the same call-site API.
-export enum SearchParams { - FocusComponentId = 'focusComponentId', - FocusErrorBinding = 'focusErrorBinding', - ExitSubform = 'exitSubform', - Validate = 'validate', - Pdf = 'pdf', -} +export const SearchParams = { + FocusComponentId: 'focusComponentId', + FocusErrorBinding: 'focusErrorBinding', + ExitSubform: 'exitSubform', + Validate: 'validate', + Pdf: 'pdf', +} as const; +export type SearchParamKey = keyof typeof SearchParams;src/components/form/LinkToPotentialNode.tsx (1)
21-23
: Tighten search extraction for robustness.
Safer handling when no query string and clearer intent.- const searchParams = typeof to === 'string' ? to.split('?').at(1) : to.search; + const searchParams = + typeof to === 'string' + ? (to.includes('?') ? to.slice(to.indexOf('?') + 1) : '') + : (to.search ?? '');test/e2e/integration/stateless-app/receipt.ts (2)
19-23
: Make the manual back-navigation URL replace resilient.
Preserve query/hash and avoid brittle end-of-string assumptions.- cy.url().then((url) => cy.visit(url.replace(/\/Task_2\/1$/, '/Task_1'))); + cy.location('href').then((href) => + cy.visit(href.replace(/\/Task_2\/1(?=(?:\?|#|$))/, '/Task_1')), + );
55-63
: Ditto for navigation away from receipt.
Use the same resilient pattern.- cy.url().then((url) => cy.visit(url.replace(/\/ProcessEnd$/, '/Task_4'))); + cy.location('href').then((href) => + cy.visit(href.replace(/\/ProcessEnd(?=(?:\?|#|$))/, '/Task_4')), + );src/features/instance/useProcessNext.tsx (1)
44-47
: Remove duplicate hook call; reuse one reference.
useOnFormSubmitValidation()
is called twice under two names.- const onFormSubmitValidation = useOnFormSubmitValidation(); + const runFormSubmitValidation = useOnFormSubmitValidation(); @@ - const onSubmitFormValidation = useOnFormSubmitValidation(); @@ - const hasErrors = await onFormSubmitValidation(); + const hasErrors = await runFormSubmitValidation(); @@ - const hasValidationErrors = await onSubmitFormValidation(true); + const hasValidationErrors = await runFormSubmitValidation(true);src/features/datamodel/utils.ts (2)
104-108
: Strengthen typing: explicit return type and tuple alias for readabilityGive the function an explicit boolean return type and use a named, labeled tuple type instead of raw indices to avoid “magic numbers”.
-export function isDataTypeWritable( - dataType: string | undefined, - isStateless: boolean, - dataElements: (readonly [string, boolean])[], -) { +export function isDataTypeWritable( + dataType: string | undefined, + isStateless: boolean, + dataElements: readonly DataElementTuple[], +): boolean {Add this type once in the module (outside the selected range):
export type DataElementTuple = readonly [dataType: string, locked: boolean];
115-116
: Minor clarity: handle undefined result explicitlyAvoid relying on coercion; return false when the element isn’t found, and keep the boolean semantics obvious.
- return !!dataElement && !dataElement[1]; + return dataElement ? !dataElement[1] : false;src/features/datamodel/DataModelsProvider.tsx (3)
159-163
: Prefer isSuccess over isFetching; tighten select typing to dropas const
Using isSuccess better models “data is ready” than isFetching (which also toggles during background refetches). Also, provide a concrete return type for select to avoid the
as const
assertion.- const { data: dataElements, isFetching } = useInstanceDataQuery({ + const { data: dataElements, isSuccess } = useInstanceDataQuery<readonly DataElementTuple[]>({ enabled: !isStateless, - select: (data) => data.data.map((element) => [element.dataType, element.locked] as const), + select: (data) => data.data.map(({ dataType, locked }) => [dataType, locked]) as const, });(Assumes
DataElementTuple
from utils is exported; otherwise define a local alias.)
195-205
: Avoid repeated linear scans by precomputing a SetMicro-optimization and clearer intent when checking for presence across many data types.
- if (!isStateless && !dataElements?.find(([dt]) => dt === dataType)) { + const types = new Set((dataElements ?? []).map(([dt]) => dt)); + if (!isStateless && !types.has(dataType)) { const error = new MissingDataElementException(dataType); window.logErrorOnce(error.message); continue; } @@ - if (isDataTypeWritable(dataType, isStateless, dataElements ?? [])) { + if (isDataTypeWritable(dataType, isStateless, dataElements ?? [])) { writableDataTypes.push(dataType); }Note: declare
types
once before the loop if you keep using it multiple times.
209-209
: Keep dependencies in sync with control flowIf you adopt
isSuccess
, removeisFetching
from the deps and addisSuccess
. Also,dataElements
can be omitted if you derive the Set inside the effect whenisSuccess
is true.- }, [applicationMetadata, defaultDataType, isStateless, layouts, setDataTypes, dataElements, layoutSetId, isFetching]); + }, [applicationMetadata, defaultDataType, isStateless, layouts, setDataTypes, layoutSetId, isSuccess]);src/features/navigation/utils.ts (1)
22-27
: Ensure boolean return value
pageGroups || taskGroups.length
can yield a non-boolean (object or number). Cast to boolean to keep types predictable.- return !isReceiptPage && (pageGroups || taskGroups.length); + return !isReceiptPage && !!(pageGroups || taskGroups.length > 0);src/components/form/Form.tsx (1)
239-258
: Remove unused dependency and variableonFormSubmitValidation is declared but not used; also listed in deps causing needless effect runs.
- const onFormSubmitValidation = useOnFormSubmitValidation(); const exitSubform = useQueryKey(SearchParams.ExitSubform)?.toLocaleLowerCase() === 'true'; const validate = useQueryKey(SearchParams.Validate)?.toLocaleLowerCase() === 'true'; const navigate = useNavigate(); const searchStringRef = useAsRef(useLocation().search); @@ - }, [navigate, searchStringRef, exitSubform, validate, onFormSubmitValidation]); + }, [navigate, searchStringRef, exitSubform, validate]);src/core/routing/useIsReceiptPage.ts (1)
4-7
: Receipt detection hook looks good; minor readability tweak optionalIf the list grows, a Set reads cleaner.
- return taskId === TaskKeys.CustomReceipt || taskId === TaskKeys.ProcessEnd; + return new Set([TaskKeys.CustomReceipt, TaskKeys.ProcessEnd]).has(taskId);src/hooks/useNavigatePage.ts (1)
235-240
: Avoid emitting trailing “?” when no search paramsWhen stateless, the URL can end with a bare “?” if there are no params.
Apply this diff:
- if (isStatelessApp && orderRef.current[0] !== undefined && (!currentPageId || !isValidPageId(currentPageId))) { - navigate(`/${orderRef.current[0]}?${searchParamsRef.current}`, { replace: true }); - } + if (isStatelessApp && orderRef.current[0] !== undefined && (!currentPageId || !isValidPageId(currentPageId))) { + const q = searchParamsRef.current.toString(); + const qs = q ? `?${q}` : ''; + navigate(`/${orderRef.current[0]}${qs}`, { replace: true }); + }src/features/receipt/FixWrongReceiptType.tsx (1)
21-26
: Prefer const expression for redirect targetMinor readability: compute
redirectTo
as a single const expression.Apply this diff:
- let redirectTo: undefined | TaskKeys = undefined; - if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt) { - redirectTo = TaskKeys.CustomReceipt; - } else if (taskId === TaskKeys.CustomReceipt && !hasCustomReceipt) { - redirectTo = TaskKeys.ProcessEnd; - } + const redirectTo: TaskKeys | undefined = + taskId === TaskKeys.ProcessEnd && hasCustomReceipt + ? TaskKeys.CustomReceipt + : taskId === TaskKeys.CustomReceipt && !hasCustomReceipt + ? TaskKeys.ProcessEnd + : undefined;src/layout/GenericComponent.tsx (1)
230-233
: Non-standard scroll behavior value
scrollIntoView({ behavior: 'instant' })
is non-standard. Use'auto'
(or'smooth'
) to avoid cross-browser issues.- !abortController.current.signal.aborted && div.scrollIntoView({ behavior: 'instant' }); + !abortController.current.signal.aborted && div.scrollIntoView({ behavior: 'auto' });src/components/presentation/Presentation.test.tsx (1)
91-97
: Rename tests to match new hook-driven behavior (cosmetic)Titles still reference ProcessTaskType; assertions are now driven by
useIsReceiptPage
. Rename for clarity.Example:
- “background is greyLight when not receipt page”
- “background is greenLight when receipt page”
Also applies to: 99-107
src/features/processEnd/feedback/Feedback.tsx (1)
55-58
: Comment/backoff mismatchThe current formula reaches the 30s max delay around attempt ≈ 39 (starting from 0), not 20. Either adjust the formula or update the comment to reflect reality.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (33)
src/App.tsx
(3 hunks)src/components/form/Form.tsx
(2 hunks)src/components/form/LinkToPotentialNode.tsx
(1 hunks)src/components/presentation/Presentation.test.tsx
(6 hunks)src/components/presentation/Presentation.tsx
(5 hunks)src/components/wrappers/ProcessWrapper.tsx
(3 hunks)src/core/loading/Loader.tsx
(0 hunks)src/core/routing/types.ts
(1 hunks)src/core/routing/useIsNavigating.ts
(1 hunks)src/core/routing/useIsReceiptPage.ts
(1 hunks)src/features/datamodel/DataModelsProvider.tsx
(4 hunks)src/features/datamodel/utils.ts
(1 hunks)src/features/expressions/expression-functions.ts
(1 hunks)src/features/instance/useProcessNext.tsx
(1 hunks)src/features/instance/useProcessQuery.ts
(2 hunks)src/features/instantiate/selection/InstanceSelection.tsx
(1 hunks)src/features/navigation/AppNavigation.tsx
(2 hunks)src/features/navigation/utils.ts
(1 hunks)src/features/pdf/PDFWrapper.test.tsx
(1 hunks)src/features/processEnd/feedback/Feedback.tsx
(3 hunks)src/features/propagateTraceWhenPdf/index.ts
(1 hunks)src/features/receipt/FixWrongReceiptType.tsx
(1 hunks)src/features/receipt/ReceiptContainer.tsx
(3 hunks)src/hooks/navigation.ts
(1 hunks)src/hooks/useIsPdf.ts
(1 hunks)src/hooks/useNavigatePage.ts
(2 hunks)src/layout/GenericComponent.tsx
(2 hunks)src/layout/RepeatingGroup/EditContainer/RepeatingGroupEditContext.tsx
(1 hunks)src/layout/RepeatingGroup/Providers/RepeatingGroupFocusContext.tsx
(1 hunks)src/layout/Subform/SubformWrapper.tsx
(1 hunks)src/layout/Tabs/Tabs.tsx
(1 hunks)test/e2e/integration/stateless-app/feedback.ts
(1 hunks)test/e2e/integration/stateless-app/receipt.ts
(2 hunks)
💤 Files with no reviewable changes (1)
- src/core/loading/Loader.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/features/propagateTraceWhenPdf/index.ts
src/features/pdf/PDFWrapper.test.tsx
src/features/instance/useProcessNext.tsx
src/layout/Subform/SubformWrapper.tsx
src/features/instantiate/selection/InstanceSelection.tsx
src/features/receipt/FixWrongReceiptType.tsx
src/core/routing/useIsReceiptPage.ts
src/hooks/useIsPdf.ts
src/layout/Tabs/Tabs.tsx
src/layout/RepeatingGroup/EditContainer/RepeatingGroupEditContext.tsx
src/features/navigation/utils.ts
test/e2e/integration/stateless-app/receipt.ts
src/features/instance/useProcessQuery.ts
src/components/form/LinkToPotentialNode.tsx
src/features/processEnd/feedback/Feedback.tsx
src/core/routing/types.ts
src/core/routing/useIsNavigating.ts
test/e2e/integration/stateless-app/feedback.ts
src/components/presentation/Presentation.test.tsx
src/components/form/Form.tsx
src/features/expressions/expression-functions.ts
src/features/receipt/ReceiptContainer.tsx
src/layout/RepeatingGroup/Providers/RepeatingGroupFocusContext.tsx
src/components/presentation/Presentation.tsx
src/features/datamodel/DataModelsProvider.tsx
src/layout/GenericComponent.tsx
src/features/datamodel/utils.ts
src/hooks/navigation.ts
src/components/wrappers/ProcessWrapper.tsx
src/App.tsx
src/features/navigation/AppNavigation.tsx
src/hooks/useNavigatePage.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/features/pdf/PDFWrapper.test.tsx
src/components/presentation/Presentation.test.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to **/*.{ts,tsx} : For TanStack Query, use objects to manage query keys and functions, and centralize shared options via `queryOptions`
Applied to files:
src/features/instance/useProcessQuery.ts
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to **/*.test.{ts,tsx} : In tests, use `renderWithProviders` from `src/test/renderWithProviders.tsx` to supply required form layout context
Applied to files:
src/components/presentation/Presentation.test.tsx
src/features/datamodel/DataModelsProvider.tsx
🧬 Code graph analysis (12)
src/features/pdf/PDFWrapper.test.tsx (1)
src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)
src/layout/Subform/SubformWrapper.tsx (1)
src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)
src/features/instantiate/selection/InstanceSelection.tsx (1)
src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)
src/features/receipt/FixWrongReceiptType.tsx (5)
src/hooks/navigation.ts (1)
useNavigationParam
(51-55)src/features/form/layoutSets/LayoutSetsProvider.tsx (1)
useLayoutSets
(52-52)src/utils/formLayout.ts (1)
behavesLikeDataTask
(8-14)src/hooks/useNavigatePage.ts (1)
useNavigateToTask
(152-182)src/core/loading/Loader.tsx (1)
Loader
(14-36)
src/core/routing/useIsReceiptPage.ts (1)
src/hooks/navigation.ts (1)
useNavigationParam
(51-55)
src/features/processEnd/feedback/Feedback.tsx (3)
src/features/instance/useProcessQuery.ts (2)
useProcessQuery
(23-26)useOptimisticallyUpdateProcess
(99-106)src/hooks/useNavigatePage.ts (1)
useNavigateToTask
(152-182)src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)
test/e2e/integration/stateless-app/feedback.ts (1)
test/e2e/pageobjects/app-frontend.ts (1)
AppFrontend
(3-371)
src/features/receipt/ReceiptContainer.tsx (3)
src/features/receipt/FixWrongReceiptType.tsx (1)
FixWrongReceiptType
(15-39)src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)src/features/form/FormContext.tsx (1)
FormProvider
(43-86)
src/components/presentation/Presentation.tsx (7)
src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)src/features/form/layout/UiConfigContext.tsx (1)
useUiConfigContext
(44-44)src/features/navigation/utils.ts (1)
useHasGroupedNavigation
(22-27)src/core/routing/useIsReceiptPage.ts (1)
useIsReceiptPage
(4-7)src/features/language/Lang.tsx (1)
Lang
(15-23)src/theme/altinnAppTheme.tsx (1)
AltinnPalette
(2-25)src/features/form/layoutSettings/LayoutSettingsContext.tsx (1)
usePageSettings
(131-140)
src/features/datamodel/DataModelsProvider.tsx (2)
src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)src/features/datamodel/utils.ts (2)
MissingDataElementException
(27-36)isDataTypeWritable
(104-117)
src/layout/GenericComponent.tsx (1)
src/core/routing/useIsNavigating.ts (1)
useIsNavigating
(3-7)
src/components/wrappers/ProcessWrapper.tsx (6)
src/hooks/useNavigatePage.ts (1)
useIsValidTaskId
(184-202)src/features/instance/useProcessQuery.ts (2)
useGetTaskTypeById
(61-89)useProcessQuery
(23-26)src/features/form/FormContext.tsx (1)
FormProvider
(43-86)src/features/receipt/ReceiptContainer.tsx (1)
CustomReceipt
(92-112)src/features/instance/useProcessNext.tsx (1)
getProcessNextMutationKey
(30-35)src/core/routing/useIsNavigating.ts (1)
useIsNavigating
(3-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
🔇 Additional comments (34)
src/core/routing/types.ts (2)
1-7
: Centralized SearchParams: LGTMGood move consolidating routing search keys into a single exported source.
1-7
: All SearchParams imports have been migrated
No occurrences ofimport { SearchParams } from 'src/hooks/navigation'
remain.src/layout/RepeatingGroup/Providers/RepeatingGroupFocusContext.tsx (1)
6-6
: Import path switched to core/routing: LGTMConsistent with the centralization of SearchParams.
src/features/expressions/expression-functions.ts (1)
4-4
: Import path switched to core/routing: LGTMNo behavior change; linkToComponent still builds URLs with FocusComponentId as before.
src/layout/RepeatingGroup/EditContainer/RepeatingGroupEditContext.tsx (1)
6-6
: Import path switched to core/routing: LGTMMatches the new single source of truth for search params.
src/layout/Tabs/Tabs.tsx (1)
7-7
: Import path switched to core/routing: LGTMKeeps tab auto-activation behavior intact when focusing a component via URL.
src/components/form/LinkToPotentialNode.tsx (1)
5-5
: Import path migration looks correct.
Keeps routing params centralized in core/routing.src/features/propagateTraceWhenPdf/index.ts (1)
3-3
: Import path migration looks correct.
Aligns with new routing/types location.src/features/instance/useProcessQuery.ts (1)
25-26
: Good: queryOptions + useQuery pattern.
Matches our TanStack Query guideline for centralized keys/options.src/features/instance/useProcessNext.tsx (1)
92-92
: Nice: parallel refetch cuts latency.
Refetching process and instance in parallel is a solid improvement.src/features/datamodel/DataModelsProvider.tsx (1)
293-295
: Hook return shapes unchanged
useInstanceDataElements
still returnsIData[]
(objects with.id
and.dataType
), andgetFirstDataElementId
continues to accept that array—no downstream updates required.src/features/pdf/PDFWrapper.test.tsx (1)
56-59
: LGTM: aligns test with PresentationComponent APIRemoving the obsolete
type
prop matches the new receipt-driven Presentation logic.src/features/navigation/utils.ts (1)
13-13
: LGTM: updated import pathImporting
useIsReceiptPage
from the new routing location is consistent with the refactor.src/features/instantiate/selection/InstanceSelection.tsx (2)
48-51
: LGTM: remove deprecatedtype
propPresentation is now receipt-aware; dropping the
type
prop here is correct.
1-340
: Audit ProcessTaskType usage; PresentationComponent check passed
- No lingering
type=
props found onPresentationComponent
.ProcessTaskType
is still referenced in multiple files (e.g.,src/types/index.ts
,src/hooks/useNavigatePage.ts
,src/layout/SigneeList/index.tsx
, etc.)—verify and remove or refactor any obsolete occurrences.src/components/form/Form.tsx (2)
10-10
: Centralize SearchParams import — OKImport moved to core routing. Consistent with the refactor.
47-48
: Clarify allowed values for validate paramHere any truthy value triggers validation, while elsewhere you strictly compare to 'true'. Align semantics or document accepted values ('true' vs '1'/presence).
src/hooks/useIsPdf.ts (1)
1-2
: Import path update — OKSearchParams now comes from core routing; hook behavior unchanged.
src/layout/Subform/SubformWrapper.tsx (1)
26-29
: Updated PresentationComponent usage — OKRemoval of the type prop matches the new Presentation API.
src/features/navigation/AppNavigation.tsx (2)
7-7
: useIsReceiptPage import path update — OKAligned with new core routing hook location; no logic change.
16-16
: Retained useIsSubformPage from hooks — OKSplit sources are consistent.
src/hooks/useNavigatePage.ts (2)
5-5
: Import move looks goodImporting SearchParams from core routing types is consistent with the refactor.
165-171
: Receipt remapping in task navigation is solidGood normalization of receipt destinations to the actual configured receipt type.
src/features/receipt/ReceiptContainer.tsx (2)
84-88
: Good: Centralized receipt-type correction wrapperWrapping DefaultReceipt with FixWrongReceiptType ensures correct redirection without duplicating route logic.
106-111
: Good: Custom receipt guarded and wrappedGuarding missing data model and using the wrapper keeps navigation consistent while avoiding redirect loops.
src/App.tsx (5)
13-13
: Route import simplification is correctUsing only DefaultReceipt aligns with the wrapper-based redirection.
36-36
: Removal of type-prop from PresentationComponent is consistentMatches the move to receipt-page detection.
61-63
: Receipt route wiring looks correctProcessEnd now renders DefaultReceipt (with fix wrapper), simplifying routing.
84-86
: Presentation without type-prop in PDF flow is consistentGood alignment with Presentation changes.
60-74
: Verify deep links to CustomReceipt still work as intendedSince CustomReceipt has no dedicated route, confirm that deep links to
/instance/:partyId/:guid/CustomReceipt
still render correctly (custom layout) or are redirected appropriately via FixWrongReceiptType or process logic.Steps:
- In an app with a custom receipt, open an instance at
/ProcessEnd
and ensure redirect to custom receipt occurs.- In an app without a custom receipt, navigate to
/CustomReceipt
and verify redirect/fallback to/ProcessEnd
.- Confirm browser history contains a single entry after redirects (replace = true).
src/hooks/navigation.ts (1)
4-5
: Good: centralizing SearchParams typing improves safetySwitching
useQueryKey
to accept the sharedSearchParams
type removes stringly-typed keys and prevents typos. LGTM.Also applies to: 60-61
src/components/presentation/Presentation.tsx (3)
43-47
: Receipt-aware presentation is correctUsing
useIsReceiptPage()
to select header, background, and hide navigation on receipts is consistent with the new routing model.
73-78
: Substatus only on receiptsConditionally rendering
AltinnSubstatus
only for receipts (and when a substatus exists) avoids confusing intermediate states.
84-85
: Progress bar placement and simplification look goodMoving the
ProgressBar
intoHeader
and rendering only whenshowProgress
is true reads well and avoids special-casing in receipt views.Also applies to: 95-99
…t looking all that hard.
…/>, so it depended on having its own routing. Baking what's left into FixWrongReceiptType.tsx and removing everything that makes it look like CustomReceipt isn't just any other taskId
… it for refetching
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/presentation/Presentation.test.tsx (1)
111-116
: Remove legacytype
prop and import; they’re no longer part of the APITests still pass a
type
prop and importProcessTaskType
, which conflicts with the stated refactor.Apply this diff:
-import { ProcessTaskType } from 'src/types'; @@ const allProps = { header: 'Header text', - type: ProcessTaskType.Unknown, ...props, };
♻️ Duplicate comments (5)
src/components/presentation/Presentation.test.tsx (1)
32-35
: Restore property spies to avoid cross-test bleed — addressedAdding
jest.restoreAllMocks()
fixes the teardown gap flagged earlier. Nice.src/features/processEnd/feedback/Feedback.tsx (2)
24-31
: LGTM: ended-first check and data-task navigationPrioritizing
ended
before task change prevents transient misnavigation and enables routing to non-EndEvent tasks (e.g., data tasks). This addresses the core bug.
13-14
: Fix stale previousData capture and avoid unnecessary backoff resetsThe callback closes over
previousData
, and including it in the dependency array restarts polling frequently. Track the last task id in a ref and compare against that inside a stable callback.- const { refetch: reFetchProcessData, data: previousData } = useProcessQuery(); + const { refetch: reFetchProcessData, data: previousData } = useProcessQuery(); + const previousTaskRef = useRef<string | undefined>(previousData?.currentTask?.elementId); + useEffect(() => { + previousTaskRef.current = previousData?.currentTask?.elementId; + }, [previousData?.currentTask?.elementId]); @@ - const callback = useCallback(async () => { + const callback = useCallback(async () => { const result = await reFetchProcessData(); let navigateTo: undefined | string; if (result.data?.ended) { navigateTo = TaskKeys.ProcessEnd; } else if ( result.data?.currentTask?.elementId && - result.data.currentTask.elementId !== previousData?.currentTask?.elementId + result.data.currentTask.elementId !== previousTaskRef.current ) { navigateTo = result.data.currentTask.elementId; } if (navigateTo && result.data) { optimisticallyUpdateProcess(result.data); await reFetchInstanceData(); navigateToTask(navigateTo); } - }, [ - navigateToTask, - optimisticallyUpdateProcess, - previousData?.currentTask?.elementId, - reFetchInstanceData, - reFetchProcessData, - ]); + }, [navigateToTask, optimisticallyUpdateProcess, reFetchInstanceData, reFetchProcessData]); @@ - useBackoff(callback); + useBackoff(callback);Also applies to: 21-44, 47-47
src/components/wrappers/ProcessWrapper.tsx (2)
71-94
: Make process/next gating reactive with useIsMutatingUsing
queryClient.isMutating
snapshots once and may miss updates; preferuseIsMutating
.Apply this diff:
@@ - const processNextKey = getProcessNextMutationKey(); - const queryClient = useQueryClient(); - const isRunningProcessNext = queryClient.isMutating({ mutationKey: processNextKey }); + const processNextKey = getProcessNextMutationKey(); + const isRunningProcessNext = useIsMutating({ mutationKey: processNextKey }) > 0;And below:
@@ -import { useQueryClient } from '@tanstack/react-query'; -import type { QueryClient } from '@tanstack/react-query'; +import { useIsMutating, useQueryClient } from '@tanstack/react-query'; +import type { QueryClient } from '@tanstack/react-query';
181-193
: Simplify and modernize useIsRunningProcessNextReplace stateful snapshot with
useIsMutating
to avoid stale state.Apply this diff:
-function useIsRunningProcessNext() { - const queryClient = useQueryClient(); - const [isMutating, setIsMutating] = useState<boolean | null>(null); - - // Intentionally wrapped in a useEffect() and saved as a state. If this happens, we'll seemingly be locked out - // with a <Loader /> forever, but when this happens, we also know we'll be re-rendered soon. This is only meant to - // block rendering when we're calling process/next. - useEffect(() => { - setIsMutating(isRunningProcessNext(queryClient)); - }, [queryClient]); - - return isMutating; -} +function useIsRunningProcessNext() { + const count = useIsMutating({ mutationKey: getProcessNextMutationKey() }); + return count > 0; +}
🧹 Nitpick comments (9)
src/components/presentation/Presentation.test.tsx (3)
37-50
: Assert the query function is actually usedOptional: wrap
fetchReturnUrl
in a mock and assert it was called; improves test intent and guards refactors.Apply this diff within this test:
it('should link to query parameter returnUrl if valid URL', async () => { const returnUrl = 'foo'; const mockedLocation = { ...realLocation, search: `?returnUrl=${returnUrl}` }; jest.spyOn(window, 'location', 'get').mockReturnValue(mockedLocation); - await render({}, { fetchReturnUrl: async () => returnUrl }); + const fetchReturnUrlMock = jest.fn().mockResolvedValue(returnUrl); + await render({}, { fetchReturnUrl: fetchReturnUrlMock }); const closeButton = screen.getByRole('link', { name: 'Tilbake', }); expect(closeButton).toHaveAttribute('href', returnUrl); + expect(fetchReturnUrlMock).toHaveBeenCalled(); });
92-98
: Rename test to reflect hook semantics, notProcessTaskType.Data
The component no longer uses a
type
prop; the name should expressisReceiptPage=false
.Apply this diff:
- it('the background color should be greyLight if type is "ProcessTaskType.Data"', async () => { + it('the background color should be greyLight when isReceiptPage=false', async () => {
100-108
: Tighten semantics and scope the mock to this test
- Rename to reflect
isReceiptPage=true
.- Prefer
mockReturnValueOnce(true)
for tighter scope.Apply this diff:
- it('the background color should be lightGreen if type is "ProcessTaskType.Archived"', async () => { - mockUseIsReceiptPage.mockReturnValue(true); + it('the background color should be lightGreen when isReceiptPage=true', async () => { + mockUseIsReceiptPage.mockReturnValueOnce(true);src/features/instance/useProcessNext.tsx (1)
92-93
: Parallel refetch is right; consider allSettled to avoid aborting on a single failureIf either refetch rejects,
Promise.all
will reject and skip invalidation/navigation work. UsingallSettled
is a low-risk hardening.- await Promise.all([refetchProcessData(), reFetchInstanceData()]); + await Promise.allSettled([refetchProcessData(), reFetchInstanceData()]);src/features/processEnd/feedback/Feedback.tsx (1)
59-85
: Backoff comments don’t match the math; also clear timers on unmount
- The code reaches 30s delay near attempt ~40, not 20. Fix the comment for accuracy.
- Clear the timeout on cleanup to avoid stray queued callbacks; you already guard with
shouldContinue
, but clearing is cleaner.function useBackoff(callback: () => Promise<void>) { // The backoff algorithm is used to check the process data, and slow down the requests after a while. // At first, it starts off once a second (every 1000ms) for 10 seconds. - // After that, it slows down by one more second for every request. - // Once it reaches 20 attempts, it will reach the max delay of 30 seconds between each request. + // After that, it slows down by one more second for every request. + // It reaches the max delay of 30 seconds between each request after ~40 attempts with the current formula. const attempts = useRef(0); + const timeoutRef = useRef<number | undefined>(undefined); useEffect(() => { let shouldContinue = true; function continueCalling() { const backoff = attempts.current < 10 ? 1000 : Math.min(30000, 1000 + (attempts.current - 10) * 1000); - setTimeout(() => { + timeoutRef.current = window.setTimeout(() => { if (!shouldContinue) { return; } callback().then(); attempts.current++; shouldContinue && continueCalling(); }, backoff); } continueCalling(); return () => { shouldContinue = false; + if (timeoutRef.current !== undefined) { + clearTimeout(timeoutRef.current); + } }; }, [callback]); }src/features/receipt/FixWrongReceiptType.tsx (1)
29-33
: Harden logging
window.logWarnOnce
may be undefined in some environments; guard it and fall back toconsole.warn
.See the logging changes included in the diff above.
src/features/receipt/ReceiptContainer.tsx (1)
101-110
: Avoid duplicate useLanguage calls and use the existing langToolsMinor cleanup: you call
useLanguage()
twice; reuselangTools
forlangAsString
.Apply this diff:
@@ - const langTools = useLanguage(); + const langTools = useLanguage(); @@ - const { langAsString } = useLanguage(); @@ - <title>{`${getPageTitle(appName, langAsString('receipt.title'), appOwner)}`}</title> + <title>{`${getPageTitle(appName, langTools.langAsString('receipt.title'), appOwner)}`}</title>Also applies to: 176-179
src/components/wrappers/ProcessWrapper.tsx (2)
195-228
: Return a strict boolean from useIsWrongTaskEnsure the hook returns
true|false
, notnull
or a truthy value; avoids ambiguous checks.Apply this diff:
- return isWrongTask && !isCurrentTask && !isNavigating; + return isWrongTask === true && !isCurrentTask && !isNavigating;
120-126
: Optional: Prefer auto-navigation to current task over showing an error on Feedback/ConfirmTo better satisfy “Feedback step should navigate to a data task,” render Feedback/Confirm even if
isWrongTask
just flipped and let them drive navigation; move theisWrongTask
guard below these branches or auto-navigate when wrong.Proposed reordering:
- if (isWrongTask) { - return ( - <PresentationComponent showNavigation={false}> - <NavigationError label='general.part_of_form_completed' /> - </PresentationComponent> - ); - } @@ if (taskType === ProcessTaskType.Feedback) { return ( <PresentationComponent> <Feedback /> </PresentationComponent> ); } + + if (isWrongTask) { + return ( + <PresentationComponent showNavigation={false}> + <NavigationError label='general.part_of_form_completed' /> + </PresentationComponent> + ); + }Also applies to: 136-142
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
src/App.tsx
(4 hunks)src/components/presentation/Presentation.test.tsx
(6 hunks)src/components/wrappers/ProcessWrapper.tsx
(3 hunks)src/features/instance/useProcessNext.tsx
(2 hunks)src/features/processEnd/feedback/Feedback.tsx
(3 hunks)src/features/receipt/FixWrongReceiptType.tsx
(1 hunks)src/features/receipt/ReceiptContainer.tsx
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/features/instance/useProcessNext.tsx
src/App.tsx
src/features/receipt/FixWrongReceiptType.tsx
src/components/wrappers/ProcessWrapper.tsx
src/features/receipt/ReceiptContainer.tsx
src/components/presentation/Presentation.test.tsx
src/features/processEnd/feedback/Feedback.tsx
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/components/presentation/Presentation.test.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-28T12:00:31.684Z
Learnt from: olemartinorg
PR: Altinn/app-frontend-react#3645
File: src/features/datamodel/DataModelsProvider.tsx:172-175
Timestamp: 2025-08-28T12:00:31.684Z
Learning: In DataModelsProvider.tsx, the effect that processes referenced data types must run for both stateless and non-stateless apps. The `isFetching` guard is appropriate because stateless apps don't fetch instance data (useInstanceDataQuery is disabled with `enabled: !isStateless`) but still need the data type processing to occur.
Applied to files:
src/features/instance/useProcessNext.tsx
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to **/*.test.{ts,tsx} : In tests, use `renderWithProviders` from `src/test/renderWithProviders.tsx` to supply required form layout context
Applied to files:
src/components/presentation/Presentation.test.tsx
🧬 Code graph analysis (4)
src/App.tsx (3)
src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)src/features/receipt/FixWrongReceiptType.tsx (1)
FixWrongReceiptType
(16-47)src/features/form/FormContext.tsx (1)
FormProvider
(43-86)
src/features/receipt/FixWrongReceiptType.tsx (6)
src/hooks/navigation.ts (1)
useNavigationParam
(51-55)src/features/form/layoutSets/LayoutSetsProvider.tsx (1)
useLayoutSets
(52-52)src/utils/formLayout.ts (1)
behavesLikeDataTask
(8-14)src/hooks/useNavigatePage.ts (1)
useNavigateToTask
(152-182)src/features/datamodel/useBindingSchema.tsx (1)
useCurrentDataModelGuid
(32-50)src/core/loading/Loader.tsx (1)
Loader
(14-36)
src/features/receipt/ReceiptContainer.tsx (2)
src/features/receipt/FixWrongReceiptType.tsx (1)
FixWrongReceiptType
(16-47)src/components/presentation/Presentation.tsx (1)
PresentationComponent
(32-93)
src/features/processEnd/feedback/Feedback.tsx (3)
src/features/instance/useProcessQuery.ts (2)
useProcessQuery
(23-26)useOptimisticallyUpdateProcess
(99-106)src/hooks/useNavigatePage.ts (1)
useNavigateToTask
(152-182)src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Analyze (javascript)
🔇 Additional comments (10)
src/components/presentation/Presentation.test.tsx (5)
8-8
: Switch to receipt-page hook aligns tests with new routing logicImporting and relying on
useIsReceiptPage
is the right pivot away from the legacytype
prop.
18-23
: Mock is set up correctly and typedGood use of a module factory and
jest.MockedFunction
foruseIsReceiptPage
.
27-31
: DefaultingisReceiptPage
to false in beforeEach prevents leakageSolid baseline for each test.
59-60
: Hook-driven default rendering path is correctRendering with no explicit
type
prop matches the new behavior.
75-76
: Good: default path whenreturnUrl
is missingKeeps behavior consistent and readable.
src/features/instance/useProcessNext.tsx (1)
38-38
: Good: lazy instance refetch prevents extra observerSwitching to
useInstanceDataQuery({ enabled: false }).refetch
avoids an unnecessary query observer and matches our prior learning for stateless vs. non-stateless flows.src/features/processEnd/feedback/Feedback.tsx (1)
19-20
: Good: disable instance query and use only refetch
useInstanceDataQuery({ enabled: false }).refetch
avoids an extra live observer on the feedback page while still letting you pull when needed.src/features/receipt/ReceiptContainer.tsx (1)
76-83
: Good use of wrapper to normalize receipt routingWrapping DefaultReceipt with FixWrongReceiptType ensures correct target receipt without duplicating routing concerns elsewhere.
src/App.tsx (2)
61-65
: Explicit ProcessEnd route rendering via DefaultReceipt looks correctDirectly routing ProcessEnd to DefaultReceipt aligns with the new receipt normalization.
66-76
: Receipt guard applied to entire task flow is appropriateWrapping the task route with FixWrongReceiptType centralizes receipt redirection.
export function FixWrongReceiptType({ children }: PropsWithChildren) { | ||
const taskId = useNavigationParam('taskId'); | ||
const layoutSets = useLayoutSets(); | ||
const hasCustomReceipt = behavesLikeDataTask(TaskKeys.CustomReceipt, layoutSets); | ||
const navigateToTask = useNavigateToTask(); | ||
const dataModelGuid = useCurrentDataModelGuid(); | ||
const customReceiptDataModelNotFound = hasCustomReceipt && !dataModelGuid; | ||
|
||
let redirectTo: undefined | TaskKeys = undefined; | ||
if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt) { | ||
redirectTo = TaskKeys.CustomReceipt; | ||
} else if (taskId === TaskKeys.CustomReceipt && (!hasCustomReceipt || customReceiptDataModelNotFound)) { | ||
if (customReceiptDataModelNotFound) { | ||
window.logWarnOnce( | ||
'You specified a custom receipt, but the data model is missing. Falling back to default receipt.', | ||
); | ||
} | ||
redirectTo = TaskKeys.ProcessEnd; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent receipt ping-pong: gate redirects on data readiness and require data model when leaving ProcessEnd
Redirects can oscillate between ProcessEnd and CustomReceipt when the data model GUID is loading or missing. Only redirect to CustomReceipt once the instance data is fetched and a data model GUID exists, and only redirect away from CustomReceipt once we've confirmed data is fetched and missing.
Apply this diff:
@@
-import { Loader } from 'src/core/loading/Loader';
+import { Loader } from 'src/core/loading/Loader';
+import { useInstanceDataQuery } from 'src/features/instance/InstanceContext';
@@
export function FixWrongReceiptType({ children }: PropsWithChildren) {
const taskId = useNavigationParam('taskId');
const layoutSets = useLayoutSets();
const hasCustomReceipt = behavesLikeDataTask(TaskKeys.CustomReceipt, layoutSets);
const navigateToTask = useNavigateToTask();
const dataModelGuid = useCurrentDataModelGuid();
- const customReceiptDataModelNotFound = hasCustomReceipt && !dataModelGuid;
+ const { isFetched: instanceDataFetched } = useInstanceDataQuery();
+ const customReceiptDataModelNotFound = hasCustomReceipt && instanceDataFetched && !dataModelGuid;
let redirectTo: undefined | TaskKeys = undefined;
- if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt) {
+ // Only go to CustomReceipt once the data model is known and present
+ if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt && !!dataModelGuid) {
redirectTo = TaskKeys.CustomReceipt;
- } else if (taskId === TaskKeys.CustomReceipt && (!hasCustomReceipt || customReceiptDataModelNotFound)) {
+ } else if (
+ taskId === TaskKeys.CustomReceipt &&
+ (!hasCustomReceipt || customReceiptDataModelNotFound)
+ ) {
if (customReceiptDataModelNotFound) {
- window.logWarnOnce(
- 'You specified a custom receipt, but the data model is missing. Falling back to default receipt.',
- );
+ window.logWarnOnce?.(
+ 'You specified a custom receipt, but the data model is missing. Falling back to default receipt.',
+ ) ?? console.warn('You specified a custom receipt, but the data model is missing. Falling back to default receipt.');
}
redirectTo = TaskKeys.ProcessEnd;
}
📝 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.
export function FixWrongReceiptType({ children }: PropsWithChildren) { | |
const taskId = useNavigationParam('taskId'); | |
const layoutSets = useLayoutSets(); | |
const hasCustomReceipt = behavesLikeDataTask(TaskKeys.CustomReceipt, layoutSets); | |
const navigateToTask = useNavigateToTask(); | |
const dataModelGuid = useCurrentDataModelGuid(); | |
const customReceiptDataModelNotFound = hasCustomReceipt && !dataModelGuid; | |
let redirectTo: undefined | TaskKeys = undefined; | |
if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt) { | |
redirectTo = TaskKeys.CustomReceipt; | |
} else if (taskId === TaskKeys.CustomReceipt && (!hasCustomReceipt || customReceiptDataModelNotFound)) { | |
if (customReceiptDataModelNotFound) { | |
window.logWarnOnce( | |
'You specified a custom receipt, but the data model is missing. Falling back to default receipt.', | |
); | |
} | |
redirectTo = TaskKeys.ProcessEnd; | |
} | |
// At the top of src/features/receipt/FixWrongReceiptType.tsx | |
import { Loader } from 'src/core/loading/Loader'; | |
import { useInstanceDataQuery } from 'src/features/instance/InstanceContext'; | |
export function FixWrongReceiptType({ children }: PropsWithChildren) { | |
const taskId = useNavigationParam('taskId'); | |
const layoutSets = useLayoutSets(); | |
const hasCustomReceipt = behavesLikeDataTask(TaskKeys.CustomReceipt, layoutSets); | |
const navigateToTask = useNavigateToTask(); | |
const dataModelGuid = useCurrentDataModelGuid(); | |
// New: wait until instance data is fetched before deciding if data model is truly missing | |
const { isFetched: instanceDataFetched } = useInstanceDataQuery(); | |
const customReceiptDataModelNotFound = | |
hasCustomReceipt && instanceDataFetched && !dataModelGuid; | |
let redirectTo: undefined | TaskKeys = undefined; | |
// Only go to CustomReceipt once we actually have a dataModelGuid | |
if (taskId === TaskKeys.ProcessEnd && hasCustomReceipt && !!dataModelGuid) { | |
redirectTo = TaskKeys.CustomReceipt; | |
} | |
// Only leave CustomReceipt after data is fetched and found to be missing (or if it’s not enabled) | |
else if ( | |
taskId === TaskKeys.CustomReceipt && | |
(!hasCustomReceipt || customReceiptDataModelNotFound) | |
) { | |
if (customReceiptDataModelNotFound) { | |
window.logWarnOnce?.( | |
'You specified a custom receipt, but the data model is missing. Falling back to default receipt.', | |
) ?? console.warn( | |
'You specified a custom receipt, but the data model is missing. Falling back to default receipt.', | |
); | |
} | |
redirectTo = TaskKeys.ProcessEnd; | |
} | |
// …rest of component (e.g. rendering Loader or children, invoking navigateToTask if redirectTo is set) | |
} |
…avigating you to the first page. Updating the hook to make it work without a current location as well.
…cause network takes time the navigation hadn't happened quite yet. Waiting for these to finish before showing this message to the user fixes things.
# Conflicts: # src/features/datamodel/DataModelsProvider.tsx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/features/processEnd/feedback/Feedback.tsx (1)
63-90
: Prevent overlapping polls and handle errors; align backoff math with comments
- The current loop fires the next timeout without awaiting
callback()
, so polls can overlap and spike load; re-schedule only after the promise settles.- Swallow rejections to keep polling alive.
- Fix the off-by-one in backoff (attempt 10 should move to 2s) and update comments to reflect actual behavior.
function useBackoff(callback: () => Promise<void>) { - // The backoff algorithm is used to check the process data, and slow down the requests after a while. - // At first, it starts off once a second (every 1000ms) for 10 seconds. - // After that, it slows down by one more second for every request. - // Once it reaches 20 attempts, it will reach the max delay of 30 seconds between each request. + // Poll with increasing delay to reduce load over time. + // - 1s delay for the first 10 attempts + // - then increase by 1s per attempt, up to a max of 30s const attempts = useRef(0); useEffect(() => { - let shouldContinue = true; - function continueCalling() { - const backoff = attempts.current < 10 ? 1000 : Math.min(30000, 1000 + (attempts.current - 10) * 1000); - setTimeout(() => { - if (!shouldContinue) { - return; - } - callback().then(); - attempts.current++; - shouldContinue && continueCalling(); - }, backoff); - } + let shouldContinue = true; + let timer: ReturnType<typeof setTimeout> | undefined; + const computeBackoff = () => + attempts.current < 10 ? 1000 : Math.min(30000, 1000 + (attempts.current - 9) * 1000); // 10th -> 2s + const scheduleNext = (delay: number) => { + timer = setTimeout(async () => { + if (!shouldContinue) return; + try { + await callback(); + } catch { + // keep polling even if one iteration fails + } finally { + attempts.current++; + if (shouldContinue) { + scheduleNext(computeBackoff()); + } + } + }, delay); + }; - continueCalling(); + scheduleNext(1000); return () => { shouldContinue = false; + if (timer) { + clearTimeout(timer); + } }; }, [callback]); }
♻️ Duplicate comments (3)
src/features/processEnd/feedback/Feedback.tsx (1)
13-20
: Avoid stale previousData by comparing via a ref; drop deep dep to reduce effect churnCurrent code closes over
previousData
and re-memos on a deep property, which can still race under async polling. Track the latest value in a ref and read from it inside the callback. This also simplifies dependencies.@@ - const { refetch: reFetchProcessData, data: previousData } = useProcessQuery(); + const { refetch: reFetchProcessData, data: previousData } = useProcessQuery(); + const previousDataRef = useRef(previousData); + useEffect(() => { + previousDataRef.current = previousData; + }, [previousData]); @@ - result.data.currentTask.elementId !== previousData?.currentTask?.elementId + result.data.currentTask.elementId !== previousDataRef.current?.currentTask?.elementId @@ - optimisticallyUpdateProcess, - previousData?.currentTask?.elementId, + optimisticallyUpdateProcess, + // previousData is read from previousDataRef to avoid stale capturesAlso applies to: 31-35, 42-48
src/core/routing/useIsNavigating.ts (1)
1-10
: Return actual router navigation state; drop brittle hash checkCurrent logic inverts idle and couples to window.location.hash. Use react-router’s state directly.
Apply this diff:
-import { useLocation, useNavigation } from 'react-router-dom'; +import { useNavigation } from 'react-router-dom'; export function useIsNavigating() { - const isIdle = useNavigation().state === 'idle'; - const location = useLocation(); - const expectedLocation = `${location.pathname}${location.search}`.replace(/^\//, ''); - const locationIsUpToDate = window.location.hash.endsWith(expectedLocation); - - return !locationIsUpToDate || !isIdle; + const { state } = useNavigation(); + return state !== 'idle'; // true while submitting/loading }src/components/wrappers/ProcessWrapper.tsx (1)
109-112
: Archived→receipt loader is OK (navigation handled elsewhere)Acknowledging the intentional design per project guidance; no change needed.
🧹 Nitpick comments (2)
src/features/datamodel/DataModelsProvider.tsx (2)
173-175
: Keep the isFetching guard; add an explanatory comment to prevent regressionsGiven instanceless apps disable the query, the guard must remain on isFetching (not success). Add a clarifying comment.
// Find all data types referenced in dataModelBindings in the layout useEffect(() => { - if (isFetching) { + // Important: For instanceless apps, useInstanceDataQuery is disabled. + // We still need to parse referenced data types in that case. + // Only bail while actively fetching for instance-based apps. + if (isFetching) { return; }
152-160
: Avoid double useApplicationMetadata() call; derive isStateless from the existing variableUse the already-read applicationMetadata to prevent redundant context reads and ensure consistency if metadata updates.
const applicationMetadata = useApplicationMetadata(); @@ - const isStateless = useApplicationMetadata().isStatelessApp; + const isStateless = applicationMetadata.isStatelessApp;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/components/wrappers/ProcessWrapper.tsx
(5 hunks)src/core/routing/useIsNavigating.ts
(1 hunks)src/features/datamodel/DataModelsProvider.tsx
(5 hunks)src/features/processEnd/feedback/Feedback.tsx
(3 hunks)src/hooks/useWaitForQueries.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/hooks/useWaitForQueries.ts
src/features/processEnd/feedback/Feedback.tsx
src/components/wrappers/ProcessWrapper.tsx
src/core/routing/useIsNavigating.ts
src/features/datamodel/DataModelsProvider.tsx
🧠 Learnings (5)
📚 Learning: 2025-08-28T12:25:27.289Z
Learnt from: olemartinorg
PR: Altinn/app-frontend-react#3645
File: src/components/wrappers/ProcessWrapper.tsx:107-110
Timestamp: 2025-08-28T12:25:27.289Z
Learning: In ProcessWrapper.tsx, when taskType === ProcessTaskType.Archived and taskId !== TaskKeys.CustomReceipt, the component shows a loader with reason 'redirect-to-receipt' but does not perform explicit navigation. This is intentional - other parts of the system handle the actual navigation closer to where user interactions occur.
Applied to files:
src/features/processEnd/feedback/Feedback.tsx
src/components/wrappers/ProcessWrapper.tsx
📚 Learning: 2025-08-28T12:25:27.289Z
Learnt from: olemartinorg
PR: Altinn/app-frontend-react#3645
File: src/components/wrappers/ProcessWrapper.tsx:107-110
Timestamp: 2025-08-28T12:25:27.289Z
Learning: In the app-frontend-react codebase, the architecture favors navigation happening closer to user interactions or specific process state changes, rather than using universal effects that automatically redirect users. This is part of a design philosophy to avoid "free-standing effects that throw you around" and make navigation more predictable and contextual.
Applied to files:
src/components/wrappers/ProcessWrapper.tsx
📚 Learning: 2025-08-28T12:00:32.967Z
Learnt from: olemartinorg
PR: Altinn/app-frontend-react#3645
File: src/features/datamodel/DataModelsProvider.tsx:172-175
Timestamp: 2025-08-28T12:00:32.967Z
Learning: In DataModelsProvider.tsx, the effect that processes referenced data types must run for both stateless and non-stateless apps. The `isFetching` guard is appropriate because stateless apps don't fetch instance data (useInstanceDataQuery is disabled with `enabled: !isStateless`) but still need the data type processing to occur.
Applied to files:
src/features/datamodel/DataModelsProvider.tsx
📚 Learning: 2025-08-28T12:00:32.967Z
Learnt from: olemartinorg
PR: Altinn/app-frontend-react#3645
File: src/features/datamodel/DataModelsProvider.tsx:172-175
Timestamp: 2025-08-28T12:00:32.967Z
Learning: In the Altinn app-frontend-react codebase, "stateless" refers to "instanceless" applications. While stateless/instanceless apps don't have server-side instance data, they still require data models to be loaded and processed for local state management.
Applied to files:
src/features/datamodel/DataModelsProvider.tsx
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to **/*.test.{ts,tsx} : In tests, use `renderWithProviders` from `src/test/renderWithProviders.tsx` to supply required form layout context
Applied to files:
src/features/datamodel/DataModelsProvider.tsx
🧬 Code graph analysis (3)
src/features/processEnd/feedback/Feedback.tsx (3)
src/features/instance/useProcessQuery.ts (2)
useProcessQuery
(23-26)useOptimisticallyUpdateProcess
(99-106)src/hooks/useNavigatePage.ts (1)
useNavigateToTask
(152-182)src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)
src/components/wrappers/ProcessWrapper.tsx (4)
src/core/routing/useIsNavigating.ts (1)
useIsNavigating
(3-10)src/hooks/useNavigatePage.ts (1)
useIsValidTaskId
(184-202)src/features/instance/useProcessQuery.ts (2)
useGetTaskTypeById
(61-89)useProcessQuery
(23-26)src/hooks/useWaitForQueries.ts (1)
useWaitForQueries
(11-31)
src/features/datamodel/DataModelsProvider.tsx (2)
src/features/instance/InstanceContext.tsx (1)
useInstanceDataQuery
(123-132)src/features/datamodel/utils.ts (2)
MissingDataElementException
(27-36)isDataTypeWritable
(104-117)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Analyze (javascript)
🔇 Additional comments (6)
src/features/processEnd/feedback/Feedback.tsx (1)
21-36
: Good: ended-first check prevents transient misnavigationPrioritizing
result.data.ended
before task-change comparison is correct and avoids routing to a transient task after process end.src/features/datamodel/DataModelsProvider.tsx (5)
4-4
: Good: Blocking UI on in-flight saves with useMutationStateImport looks right and is used to prevent racey initial writes in BlockUntilLoaded.
30-30
: Good: Consolidated instance data access via InstanceContext hooksSwitching to useInstanceDataQuery/useInstanceDataElements simplifies data sourcing and typing across the file.
160-163
: Tuple-select for dataElements is precise and lightweightThe enabled: !isStateless gate and as const tuple shape work well with downstream utils like isDataTypeWritable.
196-201
: Correct: Only require instance data elements for stateful appsThe non-stateless check avoids false positives for instanceless flows; logErrorOnce keeps noise low.
204-206
: Writable detection aligned with new dataElements shapeUsing isDataTypeWritable(dataType, isStateless, dataElements ?? []) matches the tuple refactor and preserves stateless semantics.
const processNextKey = getProcessNextMutationKey(); | ||
const queryClient = useQueryClient(); | ||
const isRunningProcessNext = queryClient.isMutating({ mutationKey: processNextKey }); | ||
const isNavigating = useIsNavigating(); | ||
|
||
const currentLocation = location.pathname + location.search; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Make process/next mutation state reactive with useIsMutating; remove snapshot helper
Snapshotting with queryClient.isMutating and local state can miss updates. Use TanStack’s useIsMutating for live gating and drop the helper.
Apply this diff:
import { useQueryClient } from '@tanstack/react-query';
-import type { QueryClient } from '@tanstack/react-query';
+import { useIsMutating } from '@tanstack/react-query';
@@
const processNextKey = getProcessNextMutationKey();
- const queryClient = useQueryClient();
- const isRunningProcessNext = queryClient.isMutating({ mutationKey: processNextKey });
+ const isRunningProcessNext = useIsMutating({ mutationKey: processNextKey }) > 0;
@@
- const isRunningProcessNext = useIsRunningProcessNext();
+ const isRunningProcessNext = useIsMutating({ mutationKey: getProcessNextMutationKey() }) > 0;
@@
- if (isRunningProcessNext === null || isRunningProcessNext || isWrongTask === null) {
+ if (isRunningProcessNext || isWrongTask === null) {
return <Loader reason='process-wrapper' />;
}
@@
-function isRunningProcessNext(queryClient: QueryClient) {
- return queryClient.isMutating({ mutationKey: getProcessNextMutationKey() }) > 0;
-}
-
-function useIsRunningProcessNext() {
- const queryClient = useQueryClient();
- const [isMutating, setIsMutating] = useState<boolean | null>(null);
-
- // Intentionally wrapped in a useEffect() and saved as a state. If this happens, we'll seemingly be locked out
- // with a <Loader /> forever, but when this happens, we also know we'll be re-rendered soon. This is only meant to
- // block rendering when we're calling process/next.
- useEffect(() => {
- setIsMutating(isRunningProcessNext(queryClient));
- }, [queryClient]);
-
- return isMutating;
-}
+// helper removed; use useIsMutating inline
Also applies to: 103-106, 179-195
🤖 Prompt for AI Agents
In src/components/wrappers/ProcessWrapper.tsx around lines 78-83 (and also
update the similar code at 103-106 and 179-195), the current code snapshots
mutation state using queryClient.isMutating and local state; replace that
pattern with TanStack’s useIsMutating hook to make the process/next mutation
state reactive. Remove the snapshot helper and any local state that stored the
isMutating result, import and call useIsMutating with the same mutation key
returned by getProcessNextMutationKey(), and use its boolean result directly
where gating logic previously used the snapshot; ensure imports are updated and
cleanup any now-unused variables or helper functions.
// We intentionally delay this state from being set until after queries/mutations finish, so the navigation error | ||
// does not show up while we're navigating. Without this, the message will flash over the screen shortly | ||
// in-between all the <Loader /> components. | ||
useEffect(() => { | ||
if (isCurrentTask) { | ||
setIsWrongTask(false); | ||
} else { | ||
let cancelled = false; | ||
const delayedCheck = async () => { | ||
await waitForQueries(); | ||
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait a bit longer, for navigation to maybe occur | ||
if (!cancelled) { | ||
setIsWrongTask(true); | ||
} | ||
}; | ||
delayedCheck().then(); | ||
|
||
return () => { | ||
cancelled = true; | ||
}; | ||
} | ||
}, [isCurrentTask, waitForQueries]); | ||
|
||
return isWrongTask && !isCurrentTask && !isNavigating; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Prevent timer leaks in useIsWrongTask; cancel waitForQueries; return strict boolean
Clear timeouts on unmount/re-run and pass an AbortSignal to cancel waitForQueries. Ensure strict boolean return.
Apply this diff:
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
@@
useEffect(() => {
if (isCurrentTask) {
setIsWrongTask(false);
} else {
- let cancelled = false;
+ let cancelled = false;
+ const abort = new AbortController();
+ const timeoutRef = { id: 0 as number | undefined };
const delayedCheck = async () => {
- await waitForQueries();
- await new Promise((resolve) => setTimeout(resolve, 100)); // Wait a bit longer, for navigation to maybe occur
+ await waitForQueries({ signal: abort.signal });
+ await new Promise((resolve) => {
+ timeoutRef.id = window.setTimeout(resolve, 100); // small grace period for navigation
+ });
if (!cancelled) {
setIsWrongTask(true);
}
};
delayedCheck().then();
return () => {
cancelled = true;
+ abort.abort();
+ if (timeoutRef.id) {
+ clearTimeout(timeoutRef.id);
+ }
};
}
}, [isCurrentTask, waitForQueries]);
- return isWrongTask && !isCurrentTask && !isNavigating;
+ return isWrongTask === true && !isCurrentTask && !isNavigating;
Also applies to: 1-1
🤖 Prompt for AI Agents
In src/components/wrappers/ProcessWrapper.tsx around lines 207 to 231, the
delayedCheck uses a plain timeout and an un-cancelled waitForQueries which can
leak timers/promises and the hook returns a non-strict boolean; update the
effect to create and clear a real timer ID on cleanup, create an AbortController
and pass its signal into waitForQueries so the pending promise can be aborted on
unmount/re-run, check the abort flag before setting state, and finally ensure
the hook returns a strict boolean (e.g., return !!isWrongTask && !isCurrentTask
&& !isNavigating).
export function useWaitForQueries(options?: WaitForQueriesOptions) { | ||
const queryClient = useQueryClient(); | ||
return useCallback( | ||
async (): Promise<void> => | ||
new Promise((resolve) => { | ||
const checkQueries = () => { | ||
const isFetching = queryClient.isFetching(options?.queryFilters) > 0; | ||
const isMutating = queryClient.isMutating(options?.mutationFilters) > 0; | ||
|
||
if (!isFetching && !isMutating) { | ||
resolve(); | ||
} else { | ||
setTimeout(checkQueries, 10); | ||
} | ||
}; | ||
|
||
checkQueries(); | ||
}), | ||
[queryClient, options?.queryFilters, options?.mutationFilters], | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid 10ms busy-poll; add cancellation and cache subscriptions
Polling every 10ms is wasteful and can leak timers. Resolve by subscribing to query/mutation caches and supporting AbortSignal for cancellation.
Apply this diff:
-import { useQueryClient } from '@tanstack/react-query';
-import type { MutationFilters, QueryFilters } from '@tanstack/react-query';
+import { useQueryClient } from '@tanstack/react-query';
+import type { MutationFilters, QueryFilters } from '@tanstack/react-query';
interface WaitForQueriesOptions {
queryFilters?: QueryFilters;
mutationFilters?: MutationFilters;
+ signal?: AbortSignal;
}
-export function useWaitForQueries(options?: WaitForQueriesOptions) {
+export function useWaitForQueries(defaultOptions?: WaitForQueriesOptions) {
const queryClient = useQueryClient();
return useCallback(
- async (): Promise<void> =>
- new Promise((resolve) => {
- const checkQueries = () => {
- const isFetching = queryClient.isFetching(options?.queryFilters) > 0;
- const isMutating = queryClient.isMutating(options?.mutationFilters) > 0;
-
- if (!isFetching && !isMutating) {
- resolve();
- } else {
- setTimeout(checkQueries, 10);
- }
- };
-
- checkQueries();
- }),
- [queryClient, options?.queryFilters, options?.mutationFilters],
+ (override?: WaitForQueriesOptions): Promise<void> => {
+ const { queryFilters, mutationFilters, signal } = { ...defaultOptions, ...override };
+ return new Promise((resolve) => {
+ const check = () => {
+ const fetching = queryClient.isFetching(queryFilters) > 0;
+ const mutating = queryClient.isMutating(mutationFilters) > 0;
+ if (!fetching && !mutating) {
+ unsub();
+ resolve();
+ }
+ };
+ const unsubs: Array<() => void> = [];
+ const unsub = () => unsubs.splice(0).forEach((u) => u());
+ // Initial check (fast-path)
+ check();
+ // Subscribe to changes to re-check reactively
+ unsubs.push(queryClient.getQueryCache().subscribe(check));
+ unsubs.push(queryClient.getMutationCache().subscribe(check));
+ if (signal) {
+ signal.addEventListener(
+ 'abort',
+ () => {
+ unsub();
+ resolve();
+ },
+ { once: true },
+ );
+ }
+ });
+ },
+ [queryClient, defaultOptions?.queryFilters, defaultOptions?.mutationFilters, defaultOptions?.signal],
);
}
📝 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.
export function useWaitForQueries(options?: WaitForQueriesOptions) { | |
const queryClient = useQueryClient(); | |
return useCallback( | |
async (): Promise<void> => | |
new Promise((resolve) => { | |
const checkQueries = () => { | |
const isFetching = queryClient.isFetching(options?.queryFilters) > 0; | |
const isMutating = queryClient.isMutating(options?.mutationFilters) > 0; | |
if (!isFetching && !isMutating) { | |
resolve(); | |
} else { | |
setTimeout(checkQueries, 10); | |
} | |
}; | |
checkQueries(); | |
}), | |
[queryClient, options?.queryFilters, options?.mutationFilters], | |
); | |
} | |
import { useQueryClient } from '@tanstack/react-query'; | |
import type { MutationFilters, QueryFilters } from '@tanstack/react-query'; | |
interface WaitForQueriesOptions { | |
queryFilters?: QueryFilters; | |
mutationFilters?: MutationFilters; | |
signal?: AbortSignal; | |
} | |
export function useWaitForQueries(defaultOptions?: WaitForQueriesOptions) { | |
const queryClient = useQueryClient(); | |
return useCallback( | |
(override?: WaitForQueriesOptions): Promise<void> => { | |
const { queryFilters, mutationFilters, signal } = { ...defaultOptions, ...override }; | |
return new Promise((resolve) => { | |
const check = () => { | |
const fetching = queryClient.isFetching(queryFilters) > 0; | |
const mutating = queryClient.isMutating(mutationFilters) > 0; | |
if (!fetching && !mutating) { | |
unsub(); | |
resolve(); | |
} | |
}; | |
const unsubs: Array<() => void> = []; | |
const unsub = () => unsubs.splice(0).forEach((u) => u()); | |
// Initial check (fast-path) | |
check(); | |
// Subscribe to changes to re-check reactively | |
unsubs.push(queryClient.getQueryCache().subscribe(check)); | |
unsubs.push(queryClient.getMutationCache().subscribe(check)); | |
if (signal) { | |
signal.addEventListener( | |
'abort', | |
() => { | |
unsub(); | |
resolve(); | |
}, | |
{ once: true }, | |
); | |
} | |
}); | |
}, | |
[queryClient, defaultOptions?.queryFilters, defaultOptions?.mutationFilters, defaultOptions?.signal], | |
); | |
} |
|
Description
Related Issue(s)
Verification/QA
kind/*
andbackport*
label to this PR for proper release notes groupingSummary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests