feat: Catalog & pricing - product categories, products & filters - #3809
feat: Catalog & pricing - product categories, products & filters#3809ansmonjol wants to merge 4 commits into
Conversation
f0fdc87 to
93780d4
Compare
Fixes BIL-256 Third step of the Products & rate cards initiative, stacked on #3809 (`feat/catalog-pricing-nav`): the product create/edit drawer with its real form, a reusable "Create more" drawer system, and the Product details page. Also carries the catalog Create menu, Products list and product item/filter drawer scaffolds from the previous step (they were only in the working tree). ## What ### Product drawer (create + edit) - Real TanStack form in `useProductDrawer`: name/code (required, zod), description reveal pattern, invoice display name section. `NameAndCodeGroup` auto-generates the code from the name in create mode only (`disableAutoGenerateCode` in edit). - Create: on success the drawer closes, navigates to the new product's details and toasts. - **Create more** toggle (footer left slot, create mode only): the drawer stays open, the form resets with a fade-in + scroll-to-top + Name refocus, and the success toast embeds a router link to the created product (slug-prefixed via `prependOrgSlug`, quotes escaped). - Edit (`openDrawer(product)`): prefilled values, "Save edits", code locked when `attachedToPlanOrSubscription`, cleared optional fields sent as `null` so they actually clear. - Duplicate code (`ValueAlreadyExist`): inline error under the Code input, drawer stays open, typing clears it. ### Product details page - New route `/product-catalog/products/:productId/:tab` (Overview + Product items / Plans / Activity logs placeholder tabs; activity logs gated premium + `auditLogsView`). - Breadcrumb with a link back to the catalog and a static grey "Product" crumb (new pathless `BreadcrumbItem` support). - Actions dropdown: Edit (`productsUpdate`) reopens the drawer, Delete (`productsDelete`) opens a danger dialog that destroys, evicts from the Apollo cache (no post-delete 404 flash) and navigates back to the list. - Overview tab: product information grid with copyable code and an inline Edit entry point. - Products list rows now link to the details overview, and each row exposes Edit (opens the drawer prefilled) and Delete (confirm dialog) actions, permission-gated with a composed tooltip. - Products list layout is inset to the page gutter (padded wrapper + `containerSize` 4 + non-sticky pager, same as the customer subscriptions tab) so row dividers stop at the gutter per the design. ### Shared groundwork - `src/components/drawers/createMore/`: reusable create-more system for the upcoming product item / item filter drawers. - Duplicate-code helpers hoisted to `~/core/form/existingCodeError.ts` (charge drawers keep their historical import via re-export). - `ToastContainer` moved inside `BrowserRouter`: toast messages can now embed router links. Toasts fired from the devtools `MemoryRouter` still render (global `toastsVar`) and navigate the main app router, which is the intent. - New `fade-in-right` tailwind animation; `BASE_DRAWER_CONTENT_TEST_ID` moved to `drawers/const.ts` so drawer bodies can target the scroll container without pulling BaseDrawer's `import.meta` chain into Jest. ## Notes - `MainHeader.Configure` gets a `snapshotKey` covering `description`/`invoiceDisplayName`/`attachedToPlanOrSubscription`: the config snapshot strips the action closures, so without it a description-only edit left the header Edit action holding the stale product and re-saving reverted the edit (same pitfall documented in `SubscriptionDetails`). - Known edge case left open on purpose (product decision): clicking the create-more toast link while the next product's form is dirty navigates away and discards the typed data without the unsaved-changes prompt. - 9 test suites (66 tests) cover the drawer flows, create-more system, details page, delete dialog, breadcrumb and toast container. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Pull request overview
This PR introduces the first slice of the “Products & rate cards” initiative by adding a new Catalog & pricing section in the left sidebar, wiring new catalog-domain routes/pages, and extending permissions, filters, and activity-log handling to support new catalog resources.
Changes:
- Add catalog routing + sidebar navigation section (feature-flagged) with placeholder pages/tabs and details shells.
- Add product/product-item/product-item-filter list UI scaffolding (tables, filters, actions) plus delete dialogs and supporting form helpers.
- Update GraphQL-generated surface area consumers (permissions + activity logs) and Apollo cache policies for new paginated root fields.
Reviewed changes
Copilot reviewed 104 out of 105 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/settings/teamAndSecurity/roles/common/tests/snapshots/permissionsConst.test.ts.snap | Snapshot updated for new permissions |
| src/pages/catalog/useProductItemTableColumns.tsx | Product-item table column definitions |
| src/pages/catalog/useProductItemTableActions.tsx | Product-item row actions + links |
| src/pages/catalog/useProductItemFilterTableColumns.tsx | Product-item-filter table column definitions |
| src/pages/catalog/useProductItemFilterTableActions.tsx | Product-item-filter row actions + links |
| src/pages/catalog/RateCardsList.tsx | Rate cards placeholder panel |
| src/pages/catalog/ProductItemsList.tsx | Product items list query/table |
| src/pages/catalog/ProductItemFiltersList.tsx | Product item filters list query/table |
| src/pages/catalog/ProductCatalog.tsx | Catalog tabs + create dropdown |
| src/pages/catalog/Plans.tsx | Plans placeholder page |
| src/pages/catalog/drawers/productItemFilter/ProductItemFilterValuesEditor.tsx | Multi-value editor for item-filter values |
| src/pages/catalog/drawers/productItemFilter/constants.ts | Item-filter drawer schema/defaults |
| src/pages/catalog/drawers/productItemFilter/tests/ProductItemFilterValuesEditor.test.tsx | Tests for values editor behavior |
| src/pages/catalog/drawers/productItem/constants.ts | Product-item drawer defaults/types |
| src/pages/catalog/drawers/product/ProductDrawerContent.tsx | Product drawer body + create-more reset behavior |
| src/pages/catalog/drawers/product/constants.ts | Product drawer defaults/types |
| src/pages/catalog/dialogs/useDeleteProductItemFilterDialog.tsx | Delete dialog + cache eviction for item-filters |
| src/pages/catalog/dialogs/useDeleteProductItemDialog.tsx | Delete dialog + cache eviction for product-items |
| src/pages/catalog/dialogs/useDeleteProductDialog.tsx | Delete dialog + cache eviction for products |
| src/pages/catalog/dialogs/tests/useDeleteProductItemFilterDialog.test.tsx | Integration-ish test of centralized dialog |
| src/pages/catalog/dialogs/tests/useDeleteProductItemDialog.test.tsx | Hook test for delete product-item dialog |
| src/pages/catalog/dialogs/tests/useDeleteProductDialog.test.tsx | Hook test for delete product dialog |
| src/pages/catalog/details/ProductItemFilterDetailsOverview.tsx | Item-filter details overview section |
| src/pages/catalog/details/ProductItemFilterActivityLogs.tsx | Item-filter activity logs table |
| src/pages/catalog/details/ProductItemDetailsOverview.tsx | Product-item details overview section |
| src/pages/catalog/details/ProductItemDetails.tsx | Product-item details page + tabs |
| src/pages/catalog/details/ProductDetailsProductItems.tsx | Product details “product items” preview list |
| src/pages/catalog/details/ProductDetailsOverview.tsx | Product details overview section |
| src/pages/catalog/details/ProductDetails.tsx | Product details page + tabs |
| src/pages/catalog/details/tests/ProductItemDetailsOverview.test.tsx | Tests for item overview rendering/actions |
| src/pages/catalog/details/tests/ProductItemDetails.test.tsx | Tests for item details header/tabs/actions |
| src/pages/catalog/details/tests/ProductDetailsOverview.test.tsx | Tests for product overview rendering/actions |
| src/pages/catalog/details/tests/ProductDetails.test.tsx | Tests for product details header/tabs/actions |
| src/pages/catalog/tests/useProductItemTableColumns.test.tsx | Tests for item columns hook |
| src/pages/catalog/tests/useProductItemFilterTableColumns.test.tsx | Tests for item-filter columns hook |
| src/pages/catalog/tests/useProductItemFilterTableActions.test.tsx | Tests for item-filter actions hook |
| src/pages/catalog/tests/RateCardsList.test.tsx | Rate cards placeholder test |
| src/pages/catalog/tests/ProductCatalog.test.tsx | Catalog tabs + create dropdown tests |
| src/pages/catalog/tests/Plans.test.tsx | Plans placeholder test |
| src/layouts/MainNavLayout/MainNavMenuSections.tsx | Add “Catalog & pricing” nav section |
| src/layouts/MainNavLayout/tests/MainNavMenuSections.test.tsx | Tests for new nav section ordering/visibility |
| src/hooks/usePermissions.ts | Add catalog permissions to permissions query |
| src/hooks/activityLogs/useActivityLogsInformation.ts | Map new catalog activity/resource types |
| src/hooks/activityLogs/tests/useActivityLogsInformation.test.ts | Tests for new activity/resource mappings |
| src/core/router/index.tsx | Register and re-export catalog routes |
| src/core/router/CatalogRoutes.tsx | New catalog domain routes/constants |
| src/core/form/existingCodeError.ts | Shared “duplicate code” form helper |
| src/core/form/tests/existingCodeError.test.tsx | Tests for duplicate-code helper |
| src/core/constants/tabsOptions.ts | Add catalog/detail tab enums |
| src/core/constants/filters.ts | Add catalog list filter prefixes |
| src/core/apolloClient/cache.ts | Add single-page policies for new lists |
| src/components/plans/drawers/common/chargeCode.ts | Re-export duplicate-code helpers from core |
| src/components/MainHeader/types.ts | BreadcrumbItem.path made optional |
| src/components/MainHeader/Breadcrumb.tsx | Support static (pathless) breadcrumb items |
| src/components/MainHeader/tests/Breadcrumb.test.tsx | Tests for pathless breadcrumb behavior |
| src/components/form/NameAndCodeGroup/NameAndCodeGroup.tsx | Auto-clear duplicate-code error on edit |
| src/components/form/NameAndCodeGroup/tests/NameAndCodeGroup.test.tsx | Test for clearing duplicate-code error |
| src/components/drawers/createMore/useCreateMore.tsx | New reusable “create more” toggle/reset signal |
| src/components/drawers/createMore/CreateMoreControl.tsx | UI control for “create more” toggle |
| src/components/drawers/createMore/tests/useCreateMore.test.tsx | Tests for hook + reset iteration |
| src/components/drawers/createMore/tests/CreateMoreControl.test.tsx | Tests for toggle control |
| src/components/drawers/const.ts | Export drawer content selectors/test ids |
| src/components/drawers/BaseDrawer.tsx | Apply content attr + re-export test id |
| src/components/developers/activityLogs/ActivityLogDetails.tsx | Add id selection for new union members |
| src/components/designSystem/Toasts/tests/ToastContainer.test.tsx | ToastContainer behavior tests |
| src/components/designSystem/Filters/utils.ts | Add catalog filter formatting + display |
| src/components/designSystem/Filters/types.ts | Add catalog filter enums + sentinels |
| src/components/designSystem/Filters/FiltersPanelItemTypeSwitch.tsx | Wire new filter UI components |
| src/components/designSystem/Filters/filtersElements/utils.ts | Add labeled multi-filter parsing helper |
| src/components/designSystem/Filters/filtersElements/FiltersItemProductItemType.tsx | Filter UI: product-item type |
| src/components/designSystem/Filters/filtersElements/FiltersItemProductItemProduct.tsx | Filter UI: product for product-items |
| src/components/designSystem/Filters/filtersElements/FiltersItemProductItemFilterProductItem.tsx | Filter UI: product item for item-filters |
| src/components/designSystem/Filters/filtersElements/FiltersItemProductItemFilterProduct.tsx | UI-only filter: product for item-filters |
| src/components/designSystem/Filters/filtersElements/tests/utils.test.ts | Tests for new labeled parser |
| src/components/designSystem/Filters/filtersElements/tests/FiltersItemProductItemType.test.tsx | Tests for item-type filter UI |
| src/components/designSystem/Filters/filtersElements/tests/FiltersItemProductItemProduct.test.tsx | Tests for product filter UI |
| src/components/designSystem/Filters/filtersElements/tests/FiltersItemProductItemFilterProductItem.test.tsx | Tests for product-item filter UI |
| src/components/designSystem/Filters/filtersElements/tests/FiltersItemProductItemFilterProduct.test.tsx | Tests for UI-only product filter |
| src/App.tsx | Move ToastContainer inside BrowserRouter |
| packages/configs/tailwind.config.ts | Add fade-in-right animation |
| .agents/skills/migrate-formik-to-tanstack/SKILL.md | Document duplicate-code error handling |
| .agents/docs/typescript-conventions.md | Guidance on extracting render logic |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 104 out of 105 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (5)
src/core/router/CatalogRoutes.tsx:18
- PR description says the Plans entry should route to
/plan-catalog, but this route constant is set to/plan-pricing, so the sidebar link and route won't match the intended URL.
src/core/router/CatalogRoutes.tsx:31 - This route should be gated by the ProductCatalog feature flag (same as the nav), otherwise users can access it directly even when the flag is disabled.
src/core/router/CatalogRoutes.tsx:37 - This route should be gated by the ProductCatalog feature flag (same as the nav), otherwise users can access it directly even when the flag is disabled.
src/core/router/CatalogRoutes.tsx:43 - This route should be gated by the ProductCatalog feature flag (same as the nav), otherwise users can access it directly even when the flag is disabled.
src/core/router/CatalogRoutes.tsx:49 - This route should be gated by the ProductCatalog feature flag (same as the nav), otherwise users can access it directly even when the flag is disabled.
c86b86d to
62afd1f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 104 out of 105 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/pages/catalog/tests/ProductCatalog.test.tsx:1
- The Products drawer hook mock returns
mockOpenCreateProductCategoryDrawerinstead ofmockOpenCreateProductDrawer, which will wire the create-product-item action to the wrong mock and can make the test (and intent) incorrect. Update the mock to returnopenDrawer: mockOpenCreateProductDrawer.
src/pages/catalog/drawers/productFilter/ProductFilterValuesEditor.tsx:1 - The component accepts a
disabledprop, but chip deletion remains enabled even whendisabledis true. This allows users to modifyvalueswhile the editor is supposed to be read-only/disabled. Gate deletion by omittingonDelete(and/or delete affordance) whendisabledis true, or pass a disabled prop to the Chip if supported.
src/pages/catalog/drawers/productCategory/constants.ts:1 PRODUCT_FORM_ID/PRODUCT_FORM_DEFAULTSare defined underproductCategory/but named generically as 'PRODUCT', which is easy to confuse with the product-item drawer constants (PRODUCT_ITEM_FORM_ID, etc.). Renaming these toPRODUCT_CATEGORY_FORM_ID/PRODUCT_CATEGORY_FORM_DEFAULTSwould make call sites self-explanatory and reduce accidental cross-usage.
src/components/MainHeader/Breadcrumb.tsx:61- Using
item.path ?? item.labelas a React key can collide when multiple breadcrumb items share the same label and have no path (e.g., repeated static segments), causing incorrect reconciliation. Prefer a guaranteed-unique key such as incorporating the index (e.g.${index}-${item.path ?? item.label}) or a dedicated id.
{items.map((item, index) => {
return (
<Fragment key={item.path ?? item.label}>
{index > 0 && (
<Typography variant="captionHl" color="primary600" className="shrink-0">
/
</Typography>
)}
<BreadcrumbItemContent item={item} />
</Fragment>
)
})}
src/components/Filters/graphql/filtersElements/FiltersItemProductType.tsx:22
- The default branch returns an empty translation key. If
ProductTypeEnumgains new values, the dropdown will render blank/invalid options. Consider switching to an explicit mapping object (Record) and filtering out unmapped values (or providing a safe fallback label) so unknown enum values don't silently degrade the UI.
const productTypeMapping = (productType: ProductTypeEnum): string => {
switch (productType) {
case ProductTypeEnum.Fixed:
return 'text_1783980718113ritmy7z94je'
case ProductTypeEnum.Usage:
return 'text_17839807181133l3z83156s6'
default:
return ''
}
}
3-level rename to match backend: ProductItem->Product, Product->ProductCategory, ProductItemFilter->ProductFilter (+ camelCase, plurals, permissions, gql, files/dirs). Routes/URLs migrated too: path segments, route consts, tab slugs. ProductCatalog feature name preserved; filter-query prefixes (pit/pif) kept as opaque codes. graphql.tsx regenerated via codegen.
Tabs/menu/drawers/toasts to product category > product > product filter; fix interpolation placeholders; add missing product-filter drawer title+description (2 new keys); clean mangled comments.
62afd1f to
0713b1f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 104 out of 105 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/core/router/CatalogRoutes.tsx:50
MainNavMenuSectionshides the Catalog section whenFeatureFlagEnum.ProductCatalogis off, but these routes are not feature-flagged. BecauseuseLocationHistoryenforcesrouteConfig.featureFlag, direct navigation to/product-catalog/*or/plan-pricingwould still be allowed when the feature flag is disabled. AddfeatureFlag: FeatureFlagEnum.ProductCatalogto the relevantcatalogRoutesentries (including details routes and plan-pricing) to make the gating consistent.
src/pages/catalog/ProductCatalog.tsx:32useLocation()returns a slug-aware location wherepathnamestill includes/:organizationSlug, while the tab route constants are slug-unaware. Thispathname === PRODUCT_CATALOG_ROUTEcheck will not match on real org-scoped URLs (e.g./acme/product-catalog), so the base-route redirect may never run. UsestrippedPathname(or alias it) for the comparison.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 104 out of 105 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/core/router/CatalogRoutes.tsx:2
- Catalog pages are hidden behind the ProductCatalog feature flag in the main nav, but the routes themselves aren’t feature-flagged. That allows deep-link access when the flag is off. Consider importing
FeatureFlagEnumhere so each catalog route can addfeatureFlag: FeatureFlagEnum.ProductCatalog(same pattern as QuotesRoutes).
src/core/router/CatalogRoutes.tsx:26 - This route should be gated by the ProductCatalog feature flag (to match the main nav gating) so users can’t deep-link into catalog details when the flag is off.
src/core/router/CatalogRoutes.tsx:32 - This route should be gated by the ProductCatalog feature flag (to match the main nav gating) so users can’t deep-link into catalog details when the flag is off.
src/core/router/CatalogRoutes.tsx:38 - This route should be gated by the ProductCatalog feature flag (to match the main nav gating) so users can’t deep-link into catalog details when the flag is off.
src/core/router/CatalogRoutes.tsx:44 - This route should be gated by the ProductCatalog feature flag (to match the main nav gating) so users can’t deep-link into the catalog when the flag is off.
src/core/router/CatalogRoutes.tsx:50 - If “Plan pricing” is part of the Catalog & pricing feature-flagged area (it’s currently hidden in the nav when ProductCatalog is off), this route should also be feature-flagged to prevent deep-link access while the flag is off.
|


Adds the Catalog & pricing area: product categories, products, and product filters (list + drawer + details each), plus the nav section. The rate cards tab is a placeholder here (the rate cards feature lands in #4005).
Aligned to the new backend model naming (ProductCategory > Product > ProductFilter) and rebased onto main.