Skip to content

Conversation

olemartinorg
Copy link
Contributor

@olemartinorg olemartinorg commented Aug 28, 2025

Description

Related Issue(s)

Verification/QA

  • Manual functionality testing
    • I have tested these changes manually
    • Creator of the original issue (or service owner) has been contacted for manual testing (or will be contacted when released in alpha)
    • No testing done/necessary
  • Automated tests
    • Unit test(s) have been added/updated
    • Cypress E2E test(s) have been added/updated
    • No automatic tests are needed here (no functional changes/additions)
    • I want someone to help me make some tests
  • UU/WCAG (follow these guidelines until we have our own)
    • I have tested with a screen reader/keyboard navigation/automated wcag validator
    • No testing done/necessary (no DOM/visual changes)
    • I want someone to help me perform accessibility testing
  • User documentation @ altinn-studio-docs
    • Has been added/updated
    • No functionality has been changed/added, so no documentation is needed
    • I will do that later/have created an issue
  • Support in Altinn Studio
    • Issue(s) created for support in Studio
    • This change/feature does not require any changes to Altinn Studio
  • Sprint board
    • The original issue (or this PR itself) has been added to the Team Apps project and to the current sprint board
    • I don't have permissions to do that, please help me out
  • Labels
    • I have added a kind/* and backport* label to this PR for proper release notes grouping
    • I don't have permissions to add labels, please help me out

Summary by CodeRabbit

  • New Features

    • Receipt pages now auto-detected: green styling, hidden navigation and no progress bar.
    • Automatic redirection to the correct receipt when arriving at an incorrect receipt route.
    • Improved feedback flow with optimistic polling that advances the process more smoothly.
  • Bug Fixes

    • Reduced transient “not available” warnings in feedback/receipt flows.
    • Improved focus and loading reliability during navigation.
  • Refactor

    • Centralized routing/search-parameter handling and simplified presentation logic.
  • Tests

    • Added e2e tests covering stateless feedback and receipt flows.

Ole Martin Handeland added 9 commits August 27, 2025 12:37
…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.
…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.
@olemartinorg olemartinorg added kind/bug Something isn't working backport-ignore This PR is a new feature and should not be cherry-picked onto release branches labels Aug 28, 2025
Copy link

coderabbitai bot commented Aug 28, 2025

📝 Walkthrough

Walkthrough

Replace 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

Cohort / File(s) Summary
App & Receipt routing
src/App.tsx, src/features/receipt/ReceiptContainer.tsx, src/features/receipt/FixWrongReceiptType.tsx
Removed CustomReceipt route, wrapped receipt routes with FixWrongReceiptType, unified receipt rendering under DefaultReceipt, and removed type prop usage on Presentation.
Presentation core & tests
src/components/presentation/Presentation.tsx, src/components/presentation/Presentation.test.tsx
Removed type prop from Presentation API; replaced type-based branches with useIsReceiptPage; updated header/background/navigation/progress logic; tests now mock receipt detection.
Process wrapper & loaders
src/components/wrappers/ProcessWrapper.tsx, src/core/loading/Loader.tsx
Replaced type-driven checks with new hooks (useIsWrongTask, useIsRunningProcessNext); adjusted archived/redirect and loader gating; removed type prop from Presentation usages.
Feedback & process hooks
src/features/processEnd/feedback/Feedback.tsx, src/features/instance/useProcessNext.tsx, src/features/instance/useProcessQuery.ts
Feedback polling now inspects server result and navigates optimistically; useProcessNext awaits refetches in parallel; useProcessQuery simplified to return the query directly.
Datamodel refactor
src/features/datamodel/DataModelsProvider.tsx, src/features/datamodel/utils.ts
Switched dataElements to tuples [dataType, locked]; replaced TanStack-query manual lookups with useInstanceDataQuery; updated isDataTypeWritable signature and usages.
Routing core additions
src/core/routing/types.ts, src/core/routing/useIsNavigating.ts, src/core/routing/useIsReceiptPage.ts
Added exported SearchParams enum, useIsNavigating hook, and useIsReceiptPage hook.
SearchParams & imports migration
src/hooks/navigation.ts, src/hooks/useIsPdf.ts, src/hooks/useNavigatePage.ts, src/features/expressions/expression-functions.ts, src/features/propagateTraceWhenPdf/index.ts, src/layout/..., src/components/form/..., src/layout/Tabs/Tabs.tsx, src/layout/RepeatingGroup/..., src/features/navigation/...
Moved SearchParams (and useIsReceiptPage) out of src/hooks/navigation into core routing types/hooks and updated many import sites.
Presentation callsite updates
src/features/instantiate/selection/InstanceSelection.tsx, src/layout/Subform/SubformWrapper.tsx, src/features/pdf/PDFWrapper.test.tsx
Removed ProcessTaskType imports and type props passed to Presentation; callers adapted to new API.
Navigation/focus logic
src/layout/GenericComponent.tsx
Replaced prior navigation/location checks with useIsNavigating for focus gating and removed location-search based gating.
New hooks & utilities
src/hooks/useWaitForQueries.ts, src/core/routing/useIsNavigating.ts
Added useWaitForQueries to await React Query activity and useIsNavigating to infer ongoing navigation state.
E2E tests
test/e2e/integration/stateless-app/feedback.ts, test/e2e/integration/stateless-app/receipt.ts
Added/extended stateless E2E tests covering feedback navigation and receipt navigation constraints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Assessment against linked issues

Objective Addressed Explanation
Feedback step should navigate to next task when gateway leads to non-EndEvent (#3614)
Handle error pathway after feedback and navigate accordingly (#3614)
Prevent user from getting stuck on feedback step due to frontend logic (#3614)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Presentation API and theming overhaul (src/components/presentation/Presentation.tsx) Broad removal of type prop and UI/theming shifts are UI contract changes not required by the feedback gateway fix.
SearchParams enum migration and mass import updates (src/core/routing/types.ts, src/hooks/navigation.ts, many files) Organizational migration of routing constants and widespread import updates are not necessary to resolve the feedback navigation bug.
Datamodel provider tuple refactor (src/features/datamodel/DataModelsProvider.tsx, src/features/datamodel/utils.ts) Changing dataElements shape to tuples and provider logic is unrelated to the feedback/receipt navigation objective.
New navigation-state hook and focus logic change (src/core/routing/useIsNavigating.ts, src/layout/GenericComponent.tsx) Introducing useIsNavigating and altering focus gating is orthogonal to the feedback-step navigation bug fix.

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/feedback-step

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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.
If propagateTraceWhenPdf() 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 in processTasks and always return currentTask, 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 object

You 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 loop

Currently, 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 enum

This 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 readability

Give 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 explicitly

Avoid 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 drop as 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 Set

Micro-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 flow

If you adopt isSuccess, remove isFetching from the deps and add isSuccess. Also, dataElements can be omitted if you derive the Set inside the effect when isSuccess 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 variable

onFormSubmitValidation 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 optional

If 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 params

When 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 target

Minor 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 mismatch

The 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a18a687 and d55fe41.

📒 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 using any 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 via queryOptions

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 from src/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: LGTM

Good move consolidating routing search keys into a single exported source.


1-7: All SearchParams imports have been migrated
No occurrences of import { SearchParams } from 'src/hooks/navigation' remain.

src/layout/RepeatingGroup/Providers/RepeatingGroupFocusContext.tsx (1)

6-6: Import path switched to core/routing: LGTM

Consistent with the centralization of SearchParams.

src/features/expressions/expression-functions.ts (1)

4-4: Import path switched to core/routing: LGTM

No 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: LGTM

Matches the new single source of truth for search params.

src/layout/Tabs/Tabs.tsx (1)

7-7: Import path switched to core/routing: LGTM

Keeps 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 returns IData[] (objects with .id and .dataType), and getFirstDataElementId continues to accept that array—no downstream updates required.

src/features/pdf/PDFWrapper.test.tsx (1)

56-59: LGTM: aligns test with PresentationComponent API

Removing the obsolete type prop matches the new receipt-driven Presentation logic.

src/features/navigation/utils.ts (1)

13-13: LGTM: updated import path

Importing useIsReceiptPage from the new routing location is consistent with the refactor.

src/features/instantiate/selection/InstanceSelection.tsx (2)

48-51: LGTM: remove deprecated type prop

Presentation is now receipt-aware; dropping the type prop here is correct.


1-340: Audit ProcessTaskType usage; PresentationComponent check passed

  • No lingering type= props found on PresentationComponent.
  • 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 — OK

Import moved to core routing. Consistent with the refactor.


47-48: Clarify allowed values for validate param

Here 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 — OK

SearchParams now comes from core routing; hook behavior unchanged.

src/layout/Subform/SubformWrapper.tsx (1)

26-29: Updated PresentationComponent usage — OK

Removal of the type prop matches the new Presentation API.

src/features/navigation/AppNavigation.tsx (2)

7-7: useIsReceiptPage import path update — OK

Aligned with new core routing hook location; no logic change.


16-16: Retained useIsSubformPage from hooks — OK

Split sources are consistent.

src/hooks/useNavigatePage.ts (2)

5-5: Import move looks good

Importing SearchParams from core routing types is consistent with the refactor.


165-171: Receipt remapping in task navigation is solid

Good normalization of receipt destinations to the actual configured receipt type.

src/features/receipt/ReceiptContainer.tsx (2)

84-88: Good: Centralized receipt-type correction wrapper

Wrapping DefaultReceipt with FixWrongReceiptType ensures correct redirection without duplicating route logic.


106-111: Good: Custom receipt guarded and wrapped

Guarding missing data model and using the wrapper keeps navigation consistent while avoiding redirect loops.

src/App.tsx (5)

13-13: Route import simplification is correct

Using only DefaultReceipt aligns with the wrapper-based redirection.


36-36: Removal of type-prop from PresentationComponent is consistent

Matches the move to receipt-page detection.


61-63: Receipt route wiring looks correct

ProcessEnd now renders DefaultReceipt (with fix wrapper), simplifying routing.


84-86: Presentation without type-prop in PDF flow is consistent

Good alignment with Presentation changes.


60-74: Verify deep links to CustomReceipt still work as intended

Since 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 safety

Switching useQueryKey to accept the shared SearchParams 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 correct

Using useIsReceiptPage() to select header, background, and hide navigation on receipts is consistent with the new routing model.


73-78: Substatus only on receipts

Conditionally rendering AltinnSubstatus only for receipts (and when a substatus exists) avoids confusing intermediate states.


84-85: Progress bar placement and simplification look good

Moving the ProgressBar into Header and rendering only when showProgress is true reads well and avoids special-casing in receipt views.

Also applies to: 95-99

Ole Martin Handeland added 7 commits August 28, 2025 12:56
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 legacy type prop and import; they’re no longer part of the API

Tests still pass a type prop and import ProcessTaskType, 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 — addressed

Adding 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 navigation

Prioritizing 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 resets

The 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 useIsMutating

Using queryClient.isMutating snapshots once and may miss updates; prefer useIsMutating.

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 useIsRunningProcessNext

Replace 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 used

Optional: 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, not ProcessTaskType.Data

The component no longer uses a type prop; the name should express isReceiptPage=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 failure

If either refetch rejects, Promise.all will reject and skip invalidation/navigation work. Using allSettled 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 to console.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 langTools

Minor cleanup: you call useLanguage() twice; reuse langTools for langAsString.

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 useIsWrongTask

Ensure the hook returns true|false, not null 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/Confirm

To better satisfy “Feedback step should navigate to a data task,” render Feedback/Confirm even if isWrongTask just flipped and let them drive navigation; move the isWrongTask 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.

📥 Commits

Reviewing files that changed from the base of the PR and between c417bae and 9ca4439.

📒 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 using any 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 via queryOptions

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 from src/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 logic

Importing and relying on useIsReceiptPage is the right pivot away from the legacy type prop.


18-23: Mock is set up correctly and typed

Good use of a module factory and jest.MockedFunction for useIsReceiptPage.


27-31: Defaulting isReceiptPage to false in beforeEach prevents leakage

Solid baseline for each test.


59-60: Hook-driven default rendering path is correct

Rendering with no explicit type prop matches the new behavior.


75-76: Good: default path when returnUrl is missing

Keeps behavior consistent and readable.

src/features/instance/useProcessNext.tsx (1)

38-38: Good: lazy instance refetch prevents extra observer

Switching 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 routing

Wrapping 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 correct

Directly routing ProcessEnd to DefaultReceipt aligns with the new receipt normalization.


66-76: Receipt guard applied to entire task flow is appropriate

Wrapping the task route with FixWrongReceiptType centralizes receipt redirection.

Comment on lines +16 to +34
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;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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)
}

Ole Martin Handeland added 4 commits August 28, 2025 15:32
…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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 churn

Current 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 captures

Also applies to: 31-35, 42-48

src/core/routing/useIsNavigating.ts (1)

1-10: Return actual router navigation state; drop brittle hash check

Current 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 regressions

Given 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 variable

Use 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca4439 and b555580.

📒 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 using any 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 via queryOptions

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 misnavigation

Prioritizing 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 useMutationState

Import looks right and is used to prevent racey initial writes in BlockUntilLoaded.


30-30: Good: Consolidated instance data access via InstanceContext hooks

Switching to useInstanceDataQuery/useInstanceDataElements simplifies data sourcing and typing across the file.


160-163: Tuple-select for dataElements is precise and lightweight

The 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 apps

The non-stateless check avoids false positives for instanceless flows; logErrorOnce keeps noise low.


204-206: Writable detection aligned with new dataElements shape

Using isDataTypeWritable(dataType, isStateless, dataElements ?? []) matches the tuple refactor and preserves stateless semantics.

Comment on lines 78 to 83
const processNextKey = getProcessNextMutationKey();
const queryClient = useQueryClient();
const isRunningProcessNext = queryClient.isMutating({ mutationKey: processNextKey });
const isNavigating = useIsNavigating();

const currentLocation = location.pathname + location.search;
Copy link

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.

Comment on lines +207 to +231
// 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;
}
Copy link

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).

Comment on lines +11 to +31
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],
);
}
Copy link

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.

Suggested change
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],
);
}

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
36.7% Coverage on New Code (required ≥ 45%)
23.81% Condition Coverage on New Code (required ≥ 45%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-ignore This PR is a new feature and should not be cherry-picked onto release branches kind/bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feedback step only recognizes EndEvent
1 participant