@vtex/faststore-plugin-buyer-portal 1.3.86 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,11 +41,12 @@ jobs:
41
41
  - name: Update npm to latest
42
42
  run: npm install -g npm@latest
43
43
 
44
- - name: Generate CHANGELOG and update Version Const
45
- run: yarn release
44
+ # Temporarily disabled for v2.0.0 major release — restore after publish with [skip ci]
45
+ # - name: Generate CHANGELOG and update Version Const
46
+ # run: yarn release
46
47
 
47
- - name: Update version to patch
48
- run: yarn version --patch
48
+ # - name: Update version to patch
49
+ # run: yarn version --patch
49
50
 
50
51
  - name: Publish
51
52
  run: npm publish
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0] - Unreleased
11
+
12
+ ### Breaking Changes
13
+
14
+ - Requires **FastStore v4** (`@faststore/core` and `@faststore/ui` >= 4.x) — stores on v3 must pin a `1.x` version
15
+
16
+ ## [1.3.87] - 2026-05-21
17
+
18
+ ### Fixed
19
+
20
+ - Assortments page: use consistent "Assortments" title during loading and after page load by updating `routeLayoutMapping` and replacing the custom header with `HeaderInside`, aligning title position with other Contract settings pages
21
+
10
22
  ## [1.3.86] - 2026-05-19
11
23
 
12
24
  ### Added
@@ -640,7 +652,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
640
652
  - Add CHANGELOG file
641
653
  - Add README file
642
654
 
643
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/1.3.86...HEAD
655
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.87...HEAD
644
656
  [1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
645
657
  [1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
646
658
  [1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
@@ -725,4 +737,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
725
737
  [1.3.70]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.69...v1.3.70
726
738
  [1.3.85]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.85
727
739
 
740
+ [1.3.87]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.86...v1.3.87
728
741
  [1.3.86]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.86
@@ -6,7 +6,7 @@ function visitProductAssortmentPage() {
6
6
 
7
7
  // Navigate to product assortment page
8
8
  cy.get("[data-fs-vertical-nav-menu-item]")
9
- .contains("Product assortment")
9
+ .contains("Assortments")
10
10
  .click({ timeout: TEST_CONFIG.TIMEOUTS.PAGE_LOAD });
11
11
 
12
12
  cy.wait(1000);
@@ -58,7 +58,7 @@ describe(
58
58
 
59
59
  // Wait for product assortment section to be visible
60
60
  cy.get("[data-fs-bp-header-inside-title]")
61
- .contains("Product assortment")
61
+ .contains("Assortments")
62
62
  .should("be.visible");
63
63
 
64
64
  // Wait for product assortment table to be loaded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.86",
3
+ "version": "2.0.0",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,222 @@
1
+ # Assortments Page Fixes
2
+
3
+ > **Status**: Approved
4
+ > **Created**: 2026-05-19
5
+
6
+ ## 1. Business Context
7
+
8
+ ### Problem Statement
9
+
10
+ The **Assortments** page was recently released in the Buyer Portal (Contract settings), but manual testing revealed two UX inconsistencies compared to sibling pages in the same group (Profile, Addresses, Payment methods, Credit cards):
11
+
12
+ 1. **Unstable title during loading** — when navigating via the sidebar, users temporarily see "Product Assortment", and after the page finishes loading the title changes to "Assortments".
13
+ 2. **Incorrect title vertical position** — the "Assortments" title appears higher on the page than on sibling pages.
14
+
15
+ These issues affect B2B administrators who manage organizational units and need to select the correct product assortment for each unit.
16
+
17
+ ### Goals
18
+
19
+ - Ensure the title "Assortments" is displayed consistently during and after page load.
20
+ - Align the page header positioning and typography with other Contract settings pages.
21
+
22
+ ### User Stories
23
+
24
+ #### US-1: Consistent title during navigation
25
+
26
+ - **Story**: As a contract administrator, I want the page title to remain "Assortments" while the page loads, so that I am not confused by a temporary, incorrect label.
27
+ - **Acceptance Criteria**:
28
+ - **Given** I am on any Contract settings page, **when** I click "Assortments" in the sidebar, **then** the loading state shows the title "Assortments" (not "Product Assortment" or "Product assortment").
29
+ - **Given** the Assortments page finishes loading, **when** the content is rendered, **then** the title remains "Assortments" with no visible title swap.
30
+
31
+ #### US-2: Header aligned with sibling pages
32
+
33
+ - **Story**: As a contract administrator, I want the Assortments page header to match Profile, Addresses, and Payment methods, so that the portal feels cohesive.
34
+ - **Acceptance Criteria**:
35
+ - **Given** I open Assortments and any other Contract settings page (e.g. Addresses), **when** I compare the page title position, **then** both titles share the same vertical offset from the top of the content area.
36
+ - **Given** the Assortments page is loaded, **when** I view the header, **then** it uses the shared `HeaderInside` component and styling used by sibling pages.
37
+ - **Given** the Assortments page is loaded, **when** I read the descriptive text below the title, **then** the subtitle "Select the product assortment that users in this organizational unit should access" is still visible.
38
+
39
+ ### Key Scenarios
40
+
41
+ | Scenario | Pre-conditions | Steps | Expected Result |
42
+ |---|---|---|---|
43
+ | Happy path — list and selection | Contract with assortments; one assortment enabled | Open Assortments → select a different assortment | Title stays "Assortments"; all assortments listed; selection saves with success toast |
44
+ | Error case — failed selection save | User on Assortments page; API PUT fails | Select a different assortment | Radio reverts to previous selection; error toast "Failed to save assortment" |
45
+ | Edge case — loading transition | User on Profile page | Click "Assortments" in sidebar | Loading skeleton shows `HeaderInside` with title "Assortments"; no flicker to a different label after load |
46
+
47
+ ### Functional Requirements
48
+
49
+ - FR-1: The assortments route `pageTitle` in `routeLayoutMapping.ts` must be `"Assortments"`.
50
+ - FR-2: `ProductAssortmentLayout` must use `HeaderInside title="Assortments"` instead of the custom header (`h1` + custom styles).
51
+ - FR-3: The descriptive subtitle must remain below `HeaderInside`, with scoped styles in the page section.
52
+ - FR-4: Radio selection behavior and persistence via `useSetAssortment` remain unchanged.
53
+ - FR-5: The listing must display all assortments returned by the API in a single request (maximum of 20 per contract — pagination is not required).
54
+
55
+ ### Non-Functional Requirements
56
+
57
+ - Maintain plugin boundary compatibility — changes limited to `src/features/product-assortment/` and existing shared utilities.
58
+ - Pass `make lint`, `make typecheck`, and `make test`.
59
+ - Update Cypress tests only where the title or navigation label changed to "Assortments".
60
+
61
+ ### Out of Scope
62
+
63
+ - **Pagination** — a contract supports at most 20 assortments; the API returns all of them in a single response, making pagination controls unnecessary.
64
+ - Add/remove assortment drawer functionality (present in legacy Cypress tests, absent from the current implementation).
65
+ - Name search/filter (the hook already accepts `search`, but it is not part of this fix).
66
+ - Changes to the API logic that places the enabled assortment at the top.
67
+ - Renaming routes or paths (`/product-assortment/...` remains unchanged).
68
+
69
+ ---
70
+
71
+ ## 2. Arch Decisions
72
+
73
+ ### Proposed Solution
74
+
75
+ Fix the two UX bugs by aligning the Assortments page header with other Contract settings pages, reusing `HeaderInside` and unifying the title via `routeLayoutMapping`.
76
+
77
+ ### Architecture Overview
78
+
79
+ ```mermaid
80
+ sequenceDiagram
81
+ participant User
82
+ participant LoadingTabsLayout
83
+ participant ProductAssortmentLayout
84
+ participant useGetProductAssortment
85
+ participant API
86
+
87
+ User->>LoadingTabsLayout: Click "Assortments"
88
+ LoadingTabsLayout->>User: HeaderInside title="Assortments" (from routeLayoutMapping)
89
+ LoadingTabsLayout->>ProductAssortmentLayout: Route loaded
90
+ ProductAssortmentLayout->>useGetProductAssortment: orgUnitId, contractId
91
+ useGetProductAssortment->>API: GET .../assortments/available
92
+ API-->>ProductAssortmentLayout: items (up to 20)
93
+ ProductAssortmentLayout->>User: HeaderInside + table
94
+ ```
95
+
96
+ ### Root Cause Analysis
97
+
98
+ | Bug | Root cause (original code) |
99
+ |---|---|
100
+ | Title flicker | `LoadingTabsLayout` reads `pageTitle: "Product assortment"` from `routeLayoutMapping.ts` and renders it via `HeaderInside` (with `text-transform: capitalize` → "Product Assortment"). After load, `ProductAssortmentLayout` renders a custom `h1` with text "Assortments". |
101
+ | Title position | `ProductAssortmentLayout` does not use `HeaderInside`; it uses `[data-fs-bp-assortments-header]` with `padding: var(--fs-spacing-3) 0` and custom typography, unlike `[data-fs-bp-header-inside]` (`padding: var(--fs-bp-padding-4/8/9) 0`). |
102
+
103
+ ### Alternatives Considered
104
+
105
+ | Alternative | Pros | Cons | Verdict |
106
+ |---|---|---|---|
107
+ | A. Unified `HeaderInside` + `routeLayoutMapping` | Consistent with sibling pages; fixes flicker and position | Requires subtitle adjustment below the header | **Accepted** |
108
+ | B. Keep custom header and only adjust padding | Minimal diff | Still diverges from `HeaderInside` typography/responsiveness; does not fix flicker alone | Rejected |
109
+ | C. Server-side pagination | Supports large lists | Unnecessary — maximum of 20 assortments per contract | Rejected |
110
+ | D. Remove subtitle to match Profile | Less markup | Loses useful context for the user; subtitle is a product requirement | Rejected |
111
+
112
+ ### Risks & Mitigations
113
+
114
+ | Risk | Impact | Likelihood | Mitigation |
115
+ |---|---|---|---|
116
+ | Legacy Cypress tests fail | Med | High | Update only title and navigation label expectations; leave legacy drawer/filter scenarios unchanged |
117
+ | Contract assortment limit increases in the future | Low | Low | Re-evaluate pagination if the business limit changes |
118
+
119
+ ### Key Decisions
120
+
121
+ #### Decision 1: Unify title via `routeLayoutMapping` + `HeaderInside`
122
+
123
+ - **Status**: Accepted
124
+ - **Context**: Loading and loaded states use different title mechanisms.
125
+ - **Decision**: Change `pageTitle` to `"Assortments"` and migrate the loaded layout to `HeaderInside title="Assortments"`.
126
+ - **Consequences**: `LoadingTabsLayout`, `ErrorTabsLayout`, and the loaded page all display the same title. Removes the custom `[data-fs-bp-assortments-title]`.
127
+
128
+ #### Decision 2: No pagination
129
+
130
+ - **Status**: Accepted
131
+ - **Context**: Contracts support at most 20 assortments; the API returns all of them in a single response.
132
+ - **Decision**: Do not implement pagination, `useUrlPaginatedSearch`, or a `page` query param.
133
+ - **Consequences**: Simpler layout; single fetch via `useGetProductAssortment` without lazy/router sync.
134
+
135
+ #### Decision 3: Keep subtitle as a separate element
136
+
137
+ - **Status**: Accepted
138
+ - **Context**: No sibling page has a subtitle, but Assortments needs explanatory text.
139
+ - **Decision**: Render `<p data-fs-bp-assortments-subtitle>` immediately below `HeaderInside`, keeping existing scoped styles.
140
+ - **Consequences**: Title position aligns with sibling pages; subtitle sits below without affecting the `h1` offset.
141
+
142
+ ### Implementation Plan
143
+
144
+ 1. **Consistent title**
145
+ - Edit `src/features/shared/utils/routeLayoutMapping.ts`: `pageTitle: "Assortments"`.
146
+
147
+ 2. **Aligned header**
148
+ - Refactor `ProductAssortmentLayout.tsx`:
149
+ - Import and use `HeaderInside title="Assortments"`.
150
+ - Remove `div[data-fs-bp-assortments-header]` and the custom `h1`.
151
+ - Keep the subtitle below the header.
152
+ - Remove obsolete `[data-fs-bp-assortments-title]` and header styles from `product-assortment-layout.scss`; preserve subtitle styles.
153
+
154
+ 3. **Tests**
155
+ - Update `cypress/integration/product-assortment.test.ts` only where the title or navigation label changed to "Assortments".
156
+
157
+ 4. **Verification**
158
+ - Run `make check`.
159
+ - Manual test against a linked host store: navigate to Assortments, confirm stable title, visual alignment vs Addresses, and full assortment listing.
160
+
161
+ ---
162
+
163
+ ## 3. Technical Contract
164
+
165
+ ### Data Models
166
+
167
+ No changes to existing types in `src/features/product-assortment/types/index.ts`. The API response may include `paging`, but the frontend does not consume pagination in this scope.
168
+
169
+ ### Interfaces
170
+
171
+ #### API — GET assortments (consumed by the plugin)
172
+
173
+ | Field | Type | Notes |
174
+ |---|---|---|
175
+ | Path | `GET /customers/{contractId}/units/{unitId}/assortments/available` | Existing |
176
+ | Query `name` | `string` (optional) | Existing, out of scope |
177
+ | Response `items[]` | `{ id, name, enabled }` | Rendered in full (up to 20) |
178
+
179
+ #### `useGetProductAssortment`
180
+
181
+ ```typescript
182
+ useGetProductAssortment({
183
+ contractId: string;
184
+ orgUnitId: string;
185
+ search?: string;
186
+ options?: QueryOptions<...>;
187
+ })
188
+ // Returns: { data: { items, paging }, isProductAssortmentLoading, ... }
189
+ ```
190
+
191
+ Single fetch on mount — no `page`, no `lazy`, no URL sync.
192
+
193
+ #### Component boundaries
194
+
195
+ | Module | Responsibility |
196
+ |---|---|
197
+ | `routeLayoutMapping.ts` | Loading/error title: `"Assortments"` |
198
+ | `ProductAssortmentLayout` | Header, data fetch, empty state |
199
+ | `ProductAssortmentTable` | Table rendering + radio selection |
200
+ | `useSetAssortment` | PUT selection (unchanged) |
201
+
202
+ ### Integration Points
203
+
204
+ - **LoadingTabsLayout / ErrorTabsLayout** — read `pageTitle` via `getTabsLayoutConfigFromRoute`; fixed indirectly by the `routeLayoutMapping` change.
205
+ - **BFF/API** — `assortments/available` endpoint returns all contract assortments (max. 20).
206
+ - **Host store** — no changes to `plugin.config.js` (route already registered).
207
+
208
+ ### Invariants & Constraints
209
+
210
+ - The sidebar menu continues to display **"Assortments"** (`getContractSettingsLinks.ts` — already correct).
211
+ - The loaded, loading, and error page title must always be **"Assortments"**.
212
+ - The assortment with `enabled: true` remains selected via radio; the API is responsible for placing it at the top when applicable.
213
+ - No new cross-domain dependencies — only `shared` (`HeaderInside`).
214
+
215
+ ### Files to Change
216
+
217
+ | File | Change |
218
+ |---|---|
219
+ | `src/features/shared/utils/routeLayoutMapping.ts` | `pageTitle` → `"Assortments"` |
220
+ | `src/features/product-assortment/layouts/ProductAssortmentLayout/ProductAssortmentLayout.tsx` | `HeaderInside`, simple fetch |
221
+ | `src/features/product-assortment/layouts/ProductAssortmentLayout/product-assortment-layout.scss` | Remove custom title styles; keep subtitle |
222
+ | `cypress/integration/product-assortment.test.ts` | Update title and navigation label to "Assortments" |
@@ -1,4 +1,4 @@
1
- import { EmptyState } from "../../../shared/components/EmptyState/EmptyState";
1
+ import { EmptyState, HeaderInside } from "../../../shared/components";
2
2
  import { useBuyerPortal } from "../../../shared/hooks";
3
3
  import { ContractTabsLayout, GlobalLayout } from "../../../shared/layouts";
4
4
  import { ProductAssortmentTable } from "../../components/ProductAssortmentTable/ProductAssortmentTable";
@@ -19,7 +19,8 @@ export const ProductAssortmentLayout = ({
19
19
  orgUnitId,
20
20
  });
21
21
 
22
- const isEmpty = productAssortmentData.items.length === 0;
22
+ const isEmpty =
23
+ !isProductAssortmentLoading && productAssortmentData.items.length === 0;
23
24
 
24
25
  return (
25
26
  <GlobalLayout>
@@ -31,15 +32,14 @@ export const ProductAssortmentLayout = ({
31
32
  pageName="Contract"
32
33
  >
33
34
  <section data-fs-bp-product-assortment-container>
34
- <div data-fs-bp-assortments-header>
35
- <h1 data-fs-bp-assortments-title>Assortments</h1>
36
- <p data-fs-bp-assortments-subtitle>
37
- Select the product assortment that users in this organizational
38
- unit should access
39
- </p>
40
- </div>
35
+ <HeaderInside title="Assortments" />
41
36
 
42
- {!isProductAssortmentLoading && isEmpty ? (
37
+ <p data-fs-bp-assortments-subtitle>
38
+ Select the product assortment that users in this organizational unit
39
+ should access
40
+ </p>
41
+
42
+ {isEmpty ? (
43
43
  <EmptyState
44
44
  iconName="Shapes"
45
45
  title="No product assortments found."
@@ -5,23 +5,6 @@
5
5
 
6
6
  width: 100%;
7
7
 
8
- [data-fs-bp-assortments-header] {
9
- display: flex;
10
- flex-direction: column;
11
- gap: var(--fs-spacing-1);
12
- padding: var(--fs-spacing-3) 0;
13
- }
14
-
15
- [data-fs-bp-assortments-title] {
16
- font-family: Inter;
17
- font-weight: var(--fs-text-weight-semibold);
18
- font-size: calc(var(--fs-text-size-0) * 2);
19
- line-height: calc(var(--fs-text-size-base) * 2);
20
- letter-spacing: -0.04em;
21
- color: #1f1f1f;
22
- margin: 0;
23
- }
24
-
25
8
  [data-fs-bp-assortments-subtitle] {
26
9
  font-family: Inter;
27
10
  font-weight: var(--fs-text-weight-regular);
@@ -31,35 +14,6 @@
31
14
  margin: 0;
32
15
  }
33
16
 
34
- [data-fs-bp-product-assortment-layout] {
35
- display: flex;
36
- flex-direction: column;
37
- gap: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
38
- }
39
-
40
- [data-fs-bp-product-assortment-paginator] {
41
- display: flex;
42
- align-items: center;
43
- justify-content: flex-start;
44
- margin-top: var(--fs-bp-margin-1);
45
- padding: var(--fs-bp-padding-3) 0;
46
- gap: var(--fs-bp-gap-4);
47
-
48
- [data-fs-bp-paginator-counter] {
49
- margin-left: var(--fs-bp-margin-auto);
50
- }
51
-
52
- [data-fs-paginator-next-page-button]:disabled {
53
- cursor: not-allowed;
54
- color: var(--fs-bp-color-neutral-6);
55
-
56
- &:hover {
57
- color: var(--fs-bp-color-neutral-6);
58
- background-color: transparent;
59
- }
60
- }
61
- }
62
-
63
17
  [data-fs-empty-state-section] {
64
18
  gap: var(--fs-bp-gap-3);
65
19
 
@@ -22,7 +22,7 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "1.3.86";
25
+ export const CURRENT_VERSION = "2.0.0";
26
26
 
27
27
  export const CHANGES_TIMEOUT_MESSAGE =
28
28
  "Changes may take up to 10 minutes to apply.";
@@ -40,7 +40,7 @@ export const ROUTE_TABS_LAYOUT_MAPPING: Record<string, RouteTabsLayoutConfig> =
40
40
  "/pvt/organization-account/product-assortment/[orgUnitId]/[contractId]": {
41
41
  layout: "ContractTabsLayout",
42
42
  pageName: "Contract",
43
- pageTitle: "Product assortment",
43
+ pageTitle: "Assortments",
44
44
  },
45
45
  "/pvt/organization-account/po-numbers/[orgUnitId]/[contractId]": {
46
46
  layout: "ContractTabsLayout",