Skip to content

Fix angular shop issues with multisite usage#584

Merged
uldisrudzitis merged 1 commit intomasterfrom
fix-shop-editor-issues
Oct 11, 2025
Merged

Fix angular shop issues with multisite usage#584
uldisrudzitis merged 1 commit intomasterfrom
fix-shop-editor-issues

Conversation

@uldisrudzitis
Copy link
Copy Markdown
Collaborator

@uldisrudzitis uldisrudzitis commented Oct 11, 2025

Angular shop refactoring:

  • fixes switching between multisites
  • shop guard fix

Summary by CodeRabbit

  • New Features

    • Enhanced shop access control with a new route guard that restricts entry to eligible users.
    • Orders view expanded with detailed customer, shipping, VAT, promo code, totals, and an items table.
    • Clickable headers in shop sections to quickly expand/collapse and navigate between sections.
  • Refactor

    • Switched to site-aware initialization for shop data (products, orders, regional costs, settings) to ensure the correct site’s information loads dynamically.
    • Streamlined routing logic for the shop module to improve reliability and consistency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 11, 2025

Walkthrough

Introduces a shop-specific route guard and switches multiple shop-related NGXS states to site-aware initialization using AppState.getSite. Removes several “Add...SiteAction” actions and refactors Init handlers to use patchState. Updates Shop UI structure and Orders template. Simplifies ShopStateService gating to rely on non-null site.

Changes

Cohort / File(s) Summary
Routing & Guard
editor/src/app/app-routing.module.ts, editor/src/app/auth-guard-shop.service.ts
Route for shop now uses canMatch with new AuthGuardShopService; guard checks token and shop feature via NGXS Store. Removed old canActivate usage.
Shop Core Init & Service & UI
editor/src/app/shop/shop.state.ts, editor/src/app/shop/shop-state.service.ts, editor/src/app/shop/shop.component.ts
Initialization now driven by AppState.getSite and ShopStateService.getInitialState(site); removes login-driven concat and AddSite branches; keeps logout reset. Service gates on non-null site. UI header/section toggle updated; typed parameter for toggleSection.
Orders Module
editor/src/app/shop/orders/shop-orders.actions.ts, editor/src/app/shop/orders/shop-orders.state.ts, editor/src/app/shop/orders/shop-orders.component.ts
Removed AddShopOrdersSiteAction. Orders state initializes per-site from AppState.getSite; dispatches InitShopOrdersAction with { [site]: orders }; handler renamed to initShopOrders using patchState. Component template expanded to detailed order view and items table; adds currency$ and stringToCurrency().
Products Module
editor/src/app/shop/products/shop-products.actions.ts, editor/src/app/shop/products/shop-products.state.ts
Removed AddShopProductSiteAction. Products state now site-scoped via AppState.getSite; dispatches per-site InitShopProductsAction; handler renamed to initShopProducts using patchState.
Regional Costs
editor/src/app/shop/regional-costs/shop-regional-costs.state.ts
Initialization refactored to per-site from AppState.getSite; dispatches InitShopRegionalCostsAction with site-scoped payload; handler renamed to initShopRegionalCosts using patchState; removed AddShopRegionSiteAction handler.
Settings & Settings Config
editor/src/app/shop/settings/shop-settings.actions.ts, editor/src/app/shop/settings/shop-settings.state.ts, editor/src/app/shop/settings/shop-settings-config.state.ts
Removed AddShopSettingsSiteAction. Settings and settings-config states now initialize per-site via AppState.getSite; handlers renamed to initShopSettings/initShopSettingsConfig, switching from setState to patchState; removed login/pairwise flow.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Router
  participant AuthGuardShop as AuthGuardShopService
  participant Store
  participant ShopState as ShopState (NGXS)
  participant AppState as AppState.getSite
  participant Service as ShopStateService
  participant API

  rect rgba(230,240,255,0.5)
  note over Router,AuthGuardShop: Route entry
  Router->>AuthGuardShop: canMatch('shop')
  AuthGuardShop->>Store: selectSnapshot(UserState) / features
  AuthGuardShop-->>Router: allow if token && 'shop' feature
  end

  par Site-aware initialization
    ShopState->>AppState: select(getSite)
    AppState-->>ShopState: site (non-null, distinct)
    ShopState->>Service: getInitialState(site)
    Service->>API: GET /shop/init?site=site
    API-->>Service: initial data
    Service-->>ShopState: initial data
    ShopState->>ShopState: dispatch InitShopAction (sections, urls)
  and Per-feature states
    participant Orders as ShopOrdersState
    participant Products as ShopProductsState
    participant Regions as ShopRegionalCostsState
    participant Settings as ShopSettingsState
    AppState-->>Orders: site
    AppState-->>Products: site
    AppState-->>Regions: site
    AppState-->>Settings: site
    Orders->>API: fetch orders for site
    Products->>API: fetch products for site
    Regions->>API: fetch regional costs for site
    Settings->>API: fetch settings for site
    API-->>Orders: {orders}
    API-->>Products: {products}
    API-->>Regions: {regionalCosts}
    API-->>Settings: {settings}
    Orders->>Orders: dispatch InitShopOrdersAction { [site]: orders } (patchState)
    Products->>Products: dispatch InitShopProductsAction { [site]: products } (patchState)
    Regions->>Regions: dispatch InitShopRegionalCostsAction { [site]: regionalCosts } (patchState)
    Settings->>Settings: dispatch InitShopSettingsAction { [site]: settings } (patchState)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Angular upgrade to v20 #576 — Touches shop orders component and shop guarding/template logic, overlapping with this PR’s shop UI and guard changes.

Poem

A hop to the shop, with a guarded door,
I sniff for the site, then fetch a bit more.
Patch little pockets, per-burrow I keep,
Orders and products, all sorted neat.
With ears to the state and a whisker’s delight—
I ship this carrot-coded flight. 🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly summarizes the main purpose of the changes, which focus on fixing issues in the Angular shop related to multisite support, and it is clear, specific, and free of unnecessary detail.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-shop-editor-issues

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
editor/src/app/shop/products/shop-products.state.ts (1)

88-110: Add error handling for product updates.

This subscription lacks error handling. If getInitialState fails during product updates triggered by entry sync, errors will propagate unhandled and could leave the state inconsistent.

Apply this diff:

     this.actions$
       .pipe(
         ofActionSuccessful(UpdateSectionEntryFromSyncAction),
         filter((action) => {
           const actions = ['cartTitle', 'cartPrice', 'cartAttributes'];
           const prop = action.path.split('/').pop();
           return actions.indexOf(prop) > -1;
         }),
         switchMap(() =>
           this.store$.select(AppState.getSite).pipe(
             filter((site) => site !== null),
             switchMap((site) =>
               this.stateService.getInitialState(site, 'products', true).pipe(
                 take(1),
-                map((products) => ({ site, products }))
+                map((products) => ({ site, products })),
+                catchError((error) => {
+                  console.error(`Failed to reload products for site ${site}:`, error);
+                  return [];
+                })
               )
             )
           )
         )
       )
       .subscribe(({ site, products }) => {
         dispatch(new InitShopProductsAction({ [site]: products[site] }));
       });
🧹 Nitpick comments (4)
editor/src/app/shop/shop-state.service.ts (2)

46-47: Gate on user token too to avoid an initial 401

Since the request uses user.token, also require it before firing. Prevents a futile first call if invoked before login.

Apply this diff:

-        filter(([appState]) => appState.site !== null),
+        filter(([appState, user]) => appState.site !== null && !!user.token),
         take(1),
         tap(() => this.store.dispatch(new AppShowLoading())),
         // `exhaustMap` waits for the first request to complete instead of canceling and starting new ones.
         exhaustMap(([, user]) => {
           return this.http.get('/_api/v1/plugin/shop', {
             headers: { 'X-Authorization': 'Bearer ' + user.token },
             params: site ? { site: site } : {},
           });
         }),

Also applies to: 50-55


53-54: Minor: pass undefined instead of empty params object

Avoid constructing an empty params object when no site is provided.

-            params: site ? { site: site } : {},
+            params: site ? { site } : undefined,
editor/src/app/shop/settings/shop-settings-config.state.ts (2)

1-1: Remove unused imports and add required ones

pairwise, tap, concat, and UserState are unused; map and distinct are needed after the change above.

-import { take, pairwise, filter, switchMap, tap } from 'rxjs/operators';
+import { take, filter, switchMap, map, distinct } from 'rxjs/operators';
@@
-import { concat } from 'rxjs';
-import { UserState } from '../../user/user.state';
 import { Injectable } from '@angular/core';
 import { AppState } from 'src/app/app-state/app.state';

Also applies to: 9-13


62-65: Nit: method name

resetProducts is misleading in a settings config state. Consider renaming to resetShopSettingsConfig for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c578184 and 3d6e3f9.

📒 Files selected for processing (14)
  • editor/src/app/app-routing.module.ts (2 hunks)
  • editor/src/app/auth-guard-shop.service.ts (1 hunks)
  • editor/src/app/shop/orders/shop-orders.actions.ts (0 hunks)
  • editor/src/app/shop/orders/shop-orders.component.ts (1 hunks)
  • editor/src/app/shop/orders/shop-orders.state.ts (2 hunks)
  • editor/src/app/shop/products/shop-products.actions.ts (0 hunks)
  • editor/src/app/shop/products/shop-products.state.ts (3 hunks)
  • editor/src/app/shop/regional-costs/shop-regional-costs.state.ts (2 hunks)
  • editor/src/app/shop/settings/shop-settings-config.state.ts (3 hunks)
  • editor/src/app/shop/settings/shop-settings.actions.ts (0 hunks)
  • editor/src/app/shop/settings/shop-settings.state.ts (2 hunks)
  • editor/src/app/shop/shop-state.service.ts (1 hunks)
  • editor/src/app/shop/shop.component.ts (2 hunks)
  • editor/src/app/shop/shop.state.ts (4 hunks)
💤 Files with no reviewable changes (3)
  • editor/src/app/shop/products/shop-products.actions.ts
  • editor/src/app/shop/orders/shop-orders.actions.ts
  • editor/src/app/shop/settings/shop-settings.actions.ts
🧰 Additional context used
🧬 Code graph analysis (9)
editor/src/app/shop/shop.component.ts (1)
editor/src/app/shared/animations.ts (1)
  • Animations (9-29)
editor/src/app/shop/shop.state.ts (2)
editor/src/app/shop/shop.interface.ts (1)
  • ShopModel (1-4)
editor/src/app/shop/shop.actions.ts (1)
  • InitShopAction (7-10)
editor/src/app/shop/settings/shop-settings-config.state.ts (2)
editor/src/app/shared/helpers.ts (1)
  • initSettingConfigGroup (144-198)
editor/src/app/shop/settings/shop-settings.actions.ts (1)
  • InitShopSettingsConfigAction (29-32)
editor/src/app/auth-guard-shop.service.ts (2)
editor/src/app/shop/shop-state.service.ts (1)
  • Injectable (32-140)
editor/src/app/user/user.state.model.ts (1)
  • UserStateModel (1-19)
editor/src/app/shop/regional-costs/shop-regional-costs.state.ts (1)
editor/src/app/shop/regional-costs/shop-regional-costs.actions.ts (1)
  • InitShopRegionalCostsAction (82-85)
editor/src/app/shop/shop-state.service.ts (1)
editor/src/app/app-state/app.actions.ts (1)
  • AppShowLoading (16-18)
editor/src/app/shop/orders/shop-orders.state.ts (1)
editor/src/app/shop/orders/shop-orders.actions.ts (1)
  • InitShopOrdersAction (12-15)
editor/src/app/shop/settings/shop-settings.state.ts (1)
editor/src/app/shop/settings/shop-settings.actions.ts (1)
  • InitShopSettingsAction (25-28)
editor/src/app/shop/products/shop-products.state.ts (1)
editor/src/app/shop/products/shop-products.actions.ts (1)
  • InitShopProductsAction (23-26)
🔇 Additional comments (3)
editor/src/app/app-routing.module.ts (1)

95-98: Good switch to canMatch for lazy shop route

Using canMatch on the lazy-loaded shop module is the right choice on Angular 20; it prevents preload and activation when blocked. Based on learnings

editor/src/app/shop/products/shop-products.state.ts (2)

18-19: LGTM: Import additions are appropriate.

The distinct and map operators are correctly imported for the refactored reactive pipeline below.


114-119: LGTM: Correct refactor to patchState and proper naming.

The method is correctly renamed from initializeShopOrders to initShopProducts, and the switch from setState to patchState is the right choice for efficiently applying partial state updates without replacing the entire state.

Comment on lines +12 to +17
canMatch() {
const user: UserStateModel = this.store.selectSnapshot(
(state) => state.user
);
return !!user.token && user.features.includes('shop');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redirect to /login when blocked (return UrlTree, not false)

Returning false may fall through to NotFound. Return a UrlTree to login for a better UX.

Apply this diff:

-import { CanMatch } from '@angular/router';
+import { CanMatch, Router, UrlTree } from '@angular/router';
@@
-export class AuthGuardShopService implements CanMatch {
-  constructor(private store: Store) {}
+export class AuthGuardShopService implements CanMatch {
+  constructor(private store: Store, private router: Router) {}
@@
-  canMatch() {
+  canMatch(): boolean | UrlTree {
     const user: UserStateModel = this.store.selectSnapshot(
       (state) => state.user
     );
-    return !!user.token && user.features.includes('shop');
+    const allowed = !!user.token && user.features.includes('shop');
+    return allowed ? true : this.router.createUrlTree(['/login']);
   }
📝 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
canMatch() {
const user: UserStateModel = this.store.selectSnapshot(
(state) => state.user
);
return !!user.token && user.features.includes('shop');
}
import { CanMatch, Router, UrlTree } from '@angular/router';
export class AuthGuardShopService implements CanMatch {
constructor(private store: Store, private router: Router) {}
canMatch(): boolean | UrlTree {
const user: UserStateModel = this.store.selectSnapshot(
(state) => state.user
);
const allowed = !!user.token && user.features.includes('shop');
return allowed ? true : this.router.createUrlTree(['/login']);
}
}
🤖 Prompt for AI Agents
In editor/src/app/auth-guard-shop.service.ts around lines 12 to 17, the canMatch
currently returns false when access is blocked which can fall through to
NotFound; change it to return a UrlTree to /login instead. Inject Router into
the service (add it to the constructor), update the canMatch signature/return
type to boolean | UrlTree, and replace the failing return value with
this.router.createUrlTree(['/login']) (or this.router.parseUrl('/login')) so the
guard redirects to /login when user.token is missing or user.features does not
include 'shop'.

Comment on lines +52 to +54
.subscribe(({ site, orders }) => {
dispatch(new InitShopOrdersAction({ [site]: orders[site] }));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix payload: orders is already the section value

You’re selecting the 'orders' section when calling getInitialState(site, 'orders'), so orders[site] is undefined. Dispatch the section value directly.

-      .subscribe(({ site, orders }) => {
-        dispatch(new InitShopOrdersAction({ [site]: orders[site] }));
-      });
+      .subscribe(({ site, orders }) => {
+        dispatch(new InitShopOrdersAction({ [site]: orders }));
+      });
📝 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
.subscribe(({ site, orders }) => {
dispatch(new InitShopOrdersAction({ [site]: orders[site] }));
});
.subscribe(({ site, orders }) => {
dispatch(new InitShopOrdersAction({ [site]: orders }));
});
🤖 Prompt for AI Agents
In editor/src/app/shop/orders/shop-orders.state.ts around lines 52 to 54, the
dispatched payload wraps orders with [site] but getInitialState(site, 'orders')
already returns the section value, so orders[site] is undefined; change the
dispatch to pass the orders section directly (dispatch(new
InitShopOrdersAction({ [site]: orders }) should become dispatch(new
InitShopOrdersAction(orders) or dispatch(new InitShopOrdersAction({ site: orders
}) depending on expected shape) — ensure the action receives the section value
rather than indexing into it.

Comment on lines +71 to +85
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService.getInitialState(site, 'products').pipe(
take(1),
map((products) => ({ site, products }))
)
)
)
).subscribe((products) => {
dispatch(new InitShopProductsAction(products));
});
.subscribe(({ site, products }) => {
dispatch(new InitShopProductsAction({ [site]: products[site] }));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace distinct with distinctUntilChanged and add error handling.

The distinct operator keeps all previously seen values in memory indefinitely. In a multisite application with frequent site switching, this accumulates unbounded memory over the application lifetime. Use distinctUntilChanged() instead, which only compares consecutive emissions.

Additionally, the subscription lacks error handling. If getInitialState fails, the error will propagate unhandled.

Apply this diff:

 this.store$
   .select(AppState.getSite)
   .pipe(
     filter((site) => site !== null),
-    distinct((site) => site),
+    distinctUntilChanged(),
     switchMap((site) =>
       this.stateService.getInitialState(site, 'products').pipe(
         take(1),
-        map((products) => ({ site, products }))
+        map((products) => ({ site, products })),
+        catchError((error) => {
+          console.error(`Failed to load products for site ${site}:`, error);
+          return [];
+        })
       )
     )
   )
   .subscribe(({ site, products }) => {
     dispatch(new InitShopProductsAction({ [site]: products[site] }));
   });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In editor/src/app/shop/products/shop-products.state.ts around lines 71 to 85,
replace the RxJS operator distinct(...) with distinctUntilChanged() to avoid
unbounded memory growth, and add error handling to the observable
chain/subscription so failures from getInitialState are caught and handled;
specifically, swap to distinctUntilChanged((a,b)=>a===b) (or default
comparator), and add either a catchError to the pipe that logs/returns EMPTY or
provide an error callback in subscribe that dispatches/logs the error so the app
doesn’t crash on getInitialState failure.

Comment on lines +85 to 101
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService.getInitialState(site, 'regionalCosts').pipe(
take(1),
map((regionalCosts) => ({ site, regionalCosts }))
)
)
)
).subscribe((regionalCosts) => {
dispatch(new InitShopRegionalCostsAction(regionalCosts));
});
.subscribe(({ site, regionalCosts }) => {
dispatch(
new InitShopRegionalCostsAction({ [site]: regionalCosts[site] })
);
});
}

@Action(InitShopRegionalCostsAction)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace distinct with distinctUntilChanged so sites re-initialize after logout

distinct keeps every emitted site name in its internal Set until the stream completes. Because this subscription lives for the whole app lifetime, switching back to a previously visited site—or logging out (which dispatches ResetShopRegionalCostsAction) and then logging in again with the same site—will never emit that site again. The state stays empty and the regional costs UI never reloads, which regresses multi-site usage. Swap to distinctUntilChanged() so we only skip consecutive duplicates but still re-run initialization when the same site appears later.

-import {
-  take,
-  tap,
-  catchError,
-  pairwise,
-  filter,
-  switchMap,
-  distinct,
-  map,
-} from 'rxjs/operators';
+import {
+  take,
+  tap,
+  catchError,
+  pairwise,
+  filter,
+  switchMap,
+  distinctUntilChanged,
+  map,
+} from 'rxjs/operators';
...
-        filter((site) => site !== null),
-        distinct((site) => site),
+        filter((site) => site !== null),
+        distinctUntilChanged(),
📝 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
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService.getInitialState(site, 'regionalCosts').pipe(
take(1),
map((regionalCosts) => ({ site, regionalCosts }))
)
)
)
).subscribe((regionalCosts) => {
dispatch(new InitShopRegionalCostsAction(regionalCosts));
});
.subscribe(({ site, regionalCosts }) => {
dispatch(
new InitShopRegionalCostsAction({ [site]: regionalCosts[site] })
);
});
}
@Action(InitShopRegionalCostsAction)
// editor/src/app/shop/regional-costs/shop-regional-costs.state.ts
// --- Update the import block ---
import {
take,
tap,
catchError,
pairwise,
filter,
switchMap,
distinctUntilChanged,
map,
} from 'rxjs/operators';
// --- In your stream pipeline ---
someSite$
.pipe(
filter((site) => site !== null),
distinctUntilChanged(),
switchMap((site) =>
this.stateService.getInitialState(site, 'regionalCosts').pipe(
take(1),
map((regionalCosts) => ({ site, regionalCosts }))
)
)
)
.subscribe(({ site, regionalCosts }) => {
dispatch(
new InitShopRegionalCostsAction({ [site]: regionalCosts[site] })
);
});
🤖 Prompt for AI Agents
In editor/src/app/shop/regional-costs/shop-regional-costs.state.ts around lines
85-101, replace the use of the RxJS operator distinct(...) with
distinctUntilChanged() so the stream only skips consecutive duplicate site
values but will re-emit the same site later (e.g., after logout/login), and
update imports to include distinctUntilChanged from 'rxjs/operators' (remove or
replace the distinct import) so site initialization runs again when a previously
visited site reappears.

Comment on lines +29 to 38
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
take(1),
switchMap((site) =>
this.stateService
.getInitialState(site, 'settingsConfig')
.pipe(take(1))
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Initialize per-site, not just once

Using take(1) on the site stream initializes only the first site. Mirror the orders state: dedupe by site and fetch once per site.

Apply this diff:

-      .pipe(
-        filter((site) => site !== null),
-        take(1),
-        switchMap((site) =>
-          this.stateService
-            .getInitialState(site, 'settingsConfig')
-            .pipe(take(1))
-        )
-      )
+      .pipe(
+        filter((site) => site !== null),
+        distinct((site) => site),
+        switchMap((site) =>
+          this.stateService
+            .getInitialState(site, 'settingsConfig')
+            .pipe(take(1), map((settingsConfig) => ({ site, settingsConfig })))
+        )
+      )
📝 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
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
take(1),
switchMap((site) =>
this.stateService
.getInitialState(site, 'settingsConfig')
.pipe(take(1))
)
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService
.getInitialState(site, 'settingsConfig')
.pipe(
take(1),
map((settingsConfig) => ({ site, settingsConfig }))
)
)
)
🤖 Prompt for AI Agents
In editor/src/app/shop/settings/shop-settings-config.state.ts around lines
29-38, the code uses take(1) on the site stream which only initializes the first
site; instead dedupe by site and initialize once per site like the orders state.
Remove the take(1) that closes the site stream, add a distinctUntilChanged (or
distinctUntilKeyChanged) comparator on the site (e.g. compare site.id or map to
site.id and use distinctUntilChanged()) before switchMap, and keep the inner
getInitialState(...) call wrapped with take(1) so you fetch initial state once
per distinct site.

Comment on lines +40 to +51
.subscribe((settingsConfig) => {
const settingGroups = {};

for (const groupSlug in settingsConfig) {
settingGroups[groupSlug] = initSettingConfigGroup(
settingsConfig[groupSlug]
);
delete settingGroups[groupSlug][groupSlug];
}
for (const groupSlug in settingsConfig) {
settingGroups[groupSlug] = initSettingConfigGroup(
settingsConfig[groupSlug]
);
delete settingGroups[groupSlug][groupSlug];
}

dispatch(new InitShopSettingsConfigAction(settingGroups));
});
dispatch(new InitShopSettingsConfigAction(settingGroups));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: payload must be keyed by site and reducer should patch, not replace

Currently you dispatch raw settingGroups without the site key and overwrite the whole state with setState. This breaks multi‑site. Key by site and use patchState.

Apply this diff:

-      .subscribe((settingsConfig) => {
+      .subscribe(({ site, settingsConfig }) => {
         const settingGroups = {};
 
         for (const groupSlug in settingsConfig) {
           settingGroups[groupSlug] = initSettingConfigGroup(
             settingsConfig[groupSlug]
           );
           delete settingGroups[groupSlug][groupSlug];
         }
 
-        dispatch(new InitShopSettingsConfigAction(settingGroups));
+        dispatch(new InitShopSettingsConfigAction({ [site]: settingGroups }));
       });

And update the action handler to patch:

-  initShopSettingsConfig(
-    { setState }: StateContext<ShopSettingsConfigModel>,
-    action: InitShopSettingsConfigAction
-  ) {
-    setState(action.payload);
-  }
+  initShopSettingsConfig(
+    { patchState }: StateContext<ShopSettingsConfigModel>,
+    action: InitShopSettingsConfigAction
+  ) {
+    patchState(action.payload);
+  }

Also applies to: 55-60

🤖 Prompt for AI Agents
In editor/src/app/shop/settings/shop-settings-config.state.ts around lines 40 to
51 (and similarly for lines 55 to 60), the dispatched payload is an unkeyed
settingGroups object and the reducer replaces the entire state; change the
payload to be keyed by the current site (e.g., { [siteId]: settingGroups }) and
update the action handler to use patchState to merge the new site entry instead
of setState so existing sites are preserved; ensure the action creator payload
structure matches the keyed shape and update any consumers accordingly.

Comment on lines +86 to +101
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService.getInitialState(site, 'settings').pipe(
take(1),
map((settings) => ({ site, settings }))
)
)
)
).subscribe((settings) => {
const newState: { [k: string]: any } = {};

for (const siteSlug in settings) {
newState[siteSlug] = this.initializeShopSettingsForSite(
settings[siteSlug]
);
}

dispatch(new InitShopSettingsAction(newState));
});
.subscribe(({ site, settings }) => {
const newState: { [k: string]: any } = {};
newState[site] = this.initializeShopSettingsForSite(settings[site]);
dispatch(new InitShopSettingsAction(newState));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use distinctUntilChanged instead of distinct to allow re-initialization

Here too, distinct permanently memorizes every site value. After ResetShopSettingsAction on logout, logging back in with the same site never emits, leaving shop settings empty. Switching back to a previously visited site in the same session also fails to re-trigger initialization if the state was cleared. Swap to distinctUntilChanged() so we only suppress consecutive duplicates:

-import {
-  take,
-  tap,
-  catchError,
-  filter,
-  switchMap,
-  map,
-  distinct,
-} from 'rxjs/operators';
+import {
+  take,
+  tap,
+  catchError,
+  filter,
+  switchMap,
+  map,
+  distinctUntilChanged,
+} from 'rxjs/operators';
...
-        filter((site) => site !== null),
-        distinct((site) => site),
+        filter((site) => site !== null),
+        distinctUntilChanged(),

This keeps the initialization firing anytime the same site appears again after a reset.

📝 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
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinct((site) => site),
switchMap((site) =>
this.stateService.getInitialState(site, 'settings').pipe(
take(1),
map((settings) => ({ site, settings }))
)
)
)
).subscribe((settings) => {
const newState: { [k: string]: any } = {};
for (const siteSlug in settings) {
newState[siteSlug] = this.initializeShopSettingsForSite(
settings[siteSlug]
);
}
dispatch(new InitShopSettingsAction(newState));
});
.subscribe(({ site, settings }) => {
const newState: { [k: string]: any } = {};
newState[site] = this.initializeShopSettingsForSite(settings[site]);
dispatch(new InitShopSettingsAction(newState));
});
// --- at the top of shop-settings.state.ts ---
-import {
- take,
- tap,
- catchError,
- filter,
- switchMap,
- map,
- distinct,
import {
take,
tap,
catchError,
filter,
switchMap,
map,
distinctUntilChanged,
} from 'rxjs/operators';
// --- around lines 86–101: replace the distinct call in your pipe ---
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinctUntilChanged(),
switchMap((site) =>
this.stateService.getInitialState(site, 'settings').pipe(
take(1),
map((settings) => ({ site, settings }))
)
)
)
.subscribe(({ site, settings }) => {
const newState: { [k: string]: any } = {};
newState[site] = this.initializeShopSettingsForSite(settings[site]);
dispatch(new InitShopSettingsAction(newState));
});
🤖 Prompt for AI Agents
In editor/src/app/shop/settings/shop-settings.state.ts around lines 86 to 101,
the pipeline uses distinct(...) which permanently memorizes values; replace it
with distinctUntilChanged() from rxjs/operators so only consecutive duplicate
site values are suppressed and re-initialization can occur after resets; update
the imports to import distinctUntilChanged and remove or replace the existing
distinct import; keep the rest of the pipe logic unchanged.

Comment on lines +105 to +126
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
take(1),
switchMap((site) =>
this.stateService.getInitialState(site).pipe(take(1))
)
)
).subscribe((state) => {
dispatch(
new InitShopAction({
sections: Object.keys(state).filter((key) => {
return (
Object.keys(defaultState).indexOf(key) === -1 &&
!/config$/i.test(key)
);
}),
urls: state.urls,
})
);
});
.subscribe((state) => {
dispatch(
new InitShopAction({
sections: Object.keys(state).filter((key) => {
return (
Object.keys(defaultState).indexOf(key) === -1 &&
!/config$/i.test(key)
);
}),
urls: state.urls,
})
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove the outer take(1) so new logins/sites reinitialize

By calling take(1) before switchMap, this subscription completes after the first non-null site. Any later site change—or a logout followed by a login (after ResetShopAction clears the state)—never triggers InitShopAction, so ShopState.getURLs keeps pointing at the previous site and multi-site flows break. Keep the stream alive and just ignore consecutive duplicates, e.g.:

-      .pipe(
-        filter((site) => site !== null),
-        take(1),
-        switchMap((site) =>
-          this.stateService.getInitialState(site).pipe(take(1))
-        )
-      )
+      .pipe(
+        filter((site) => site !== null),
+        distinctUntilChanged(),
+        switchMap((site) =>
+          this.stateService.getInitialState(site).pipe(take(1))
+        )
+      )

(Remember to import distinctUntilChanged.) This keeps initialization working for every site visit.

📝 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
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
take(1),
switchMap((site) =>
this.stateService.getInitialState(site).pipe(take(1))
)
)
).subscribe((state) => {
dispatch(
new InitShopAction({
sections: Object.keys(state).filter((key) => {
return (
Object.keys(defaultState).indexOf(key) === -1 &&
!/config$/i.test(key)
);
}),
urls: state.urls,
})
);
});
.subscribe((state) => {
dispatch(
new InitShopAction({
sections: Object.keys(state).filter((key) => {
return (
Object.keys(defaultState).indexOf(key) === -1 &&
!/config$/i.test(key)
);
}),
urls: state.urls,
})
);
});
this.store$
.select(AppState.getSite)
.pipe(
filter((site) => site !== null),
distinctUntilChanged(),
switchMap((site) =>
this.stateService.getInitialState(site).pipe(take(1))
)
)
.subscribe((state) => {
dispatch(
new InitShopAction({
sections: Object.keys(state).filter((key) => {
return (
Object.keys(defaultState).indexOf(key) === -1 &&
!/config$/i.test(key)
);
}),
urls: state.urls,
})
);
});
🤖 Prompt for AI Agents
In editor/src/app/shop/shop.state.ts around lines 105-126, remove the outer
take(1) so the subscription does not complete after the first non-null site;
instead keep filter((site) => site !== null), add distinctUntilChanged() (import
it) after the filter to ignore consecutive duplicate sites, and keep the inner
getInitialState(...).pipe(take(1)) inside switchMap so each new (distinct) site
triggers a fresh InitShopAction without completing the outer stream.

@uldisrudzitis uldisrudzitis merged commit 09ee1cf into master Oct 11, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant