@propeller-commerce/propeller-v2-react-ui 0.20.1 → 0.21.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,66 @@ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
8
  between minor versions; breaking changes are called out below and in
9
9
  [MIGRATION.md](./MIGRATION.md).
10
10
 
11
+ ## [0.21.0] - 2026-09-23
12
+
13
+ `MachineGrid` stops losing rows, and becomes usable in a translated or
14
+ customised storefront. One project had given up on it and rebuilt the surface
15
+ by hand (~450 lines), losing the breadcrumb, module grid and filter wiring.
16
+
17
+ ### Fixed
18
+
19
+ - **A machine with no slug in the tree language is no longer dropped from the
20
+ list.** `childSlugHref` returned `null` and the card was filtered out, so a
21
+ customer saw three of four machines with nothing saying so — and the same
22
+ helper guards the child-module grid, so a module could vanish from a machine
23
+ the same way. Two changes: `ROOT_MACHINE_FIELDS` no longer passes
24
+ `(language: $language)` to `name` / `description` / `slug`, so the
25
+ cross-language fallback in `getLocalizedValue` has entries to fall back to;
26
+ and a machine with no slug in *any* language now renders as an unlinked card
27
+ rather than vanishing. Needs SDK ≥ 0.17.0 for the child-machine half, which
28
+ comes through `getMachine`. (PWP-993)
29
+
30
+ The asymmetry that made this hard to spot: the **name** falls back across
31
+ languages, the **slug** did not — so a half-translated tree rendered names in
32
+ the wrong language rather than failing, but lost whole rows when the slug was
33
+ the missing half.
34
+
35
+ ### Added
36
+
37
+ - **`MachineGrid` forwards label overrides to the parts list.** It rendered its
38
+ parts through `ProductGrid` but forwarded none of `productCardLabels`,
39
+ `addToCartLabels`, `stockLabels`, `priceLabels` or `labels`, and there was no
40
+ way to reach them from outside — so "In stock", "Add" and "Search parts…"
41
+ appeared in English in the middle of translated copy. A monolingual shop
42
+ never noticed. (PWP-995a)
43
+ - **`MachineGrid.productCardComponent`** — the `ProductGrid` prop of the same
44
+ name, forwarded. Without it a quote-only product rendered with the host's
45
+ custom card on a normal listing and with the stock card inside a machine, so
46
+ the same product looked different depending on the page it was reached from.
47
+ (PWP-995c)
48
+ - **`useSpareParts({ page })`** — a controlled page, mirroring the existing
49
+ controlled `parts`. Every other listing input (`term`, `textFilters`,
50
+ `sortField`, `pageSize`) was already an option, so the absence of `page` read
51
+ as "paging is internal" — and it was not: driving the hook from URL state and
52
+ rendering `<GridPagination>` changed the URL and re-rendered the same first
53
+ twelve parts. The only way in was a `useEffect(() => goToPage(page))` that
54
+ `MachineGrid` did internally and nothing documented. `goToPage` still works
55
+ for uncontrolled hosts; omit `page` for the previous behaviour. (PWP-995b)
56
+
57
+ ### Changed
58
+
59
+ - `MachineCard.href` is now optional — a machine with no URL renders as a
60
+ `<div>` instead of an `<a>`.
61
+ - **`MachineGrid` drives `useSpareParts` with `page` directly** instead of the
62
+ internal `goToPage` effect.
63
+
64
+ ### Documentation
65
+
66
+ - **`MachineCardProps.labels` documented one key while `MachineGrid` read
67
+ three.** The doc named only `viewMachine`; `MachineGrid` also reads `loading`
68
+ and `noMachines` off the same object, so anyone following the doc left those
69
+ two in English. Both sides now say so. (PWP-995d)
70
+
11
71
  ## [0.20.1] - 2026-09-23
12
72
 
13
73
  ### Fixed
package/dist/index.cjs CHANGED
@@ -2776,7 +2776,8 @@ function useSpareParts(options) {
2776
2776
  const [childMachines, setChildMachines] = React6.useState([]);
2777
2777
  const [itemsFound, setItemsFound] = React6.useState(0);
2778
2778
  const [internalLoading, setInternalLoading] = React6.useState(false);
2779
- const [currentPage, setCurrentPage] = React6.useState(1);
2779
+ const [internalPage, setInternalPage] = React6.useState(1);
2780
+ const currentPage = options.page ?? internalPage;
2780
2781
  const [totalPages, setTotalPages] = React6.useState(1);
2781
2782
  const fetchIdRef = React6.useRef(0);
2782
2783
  const displayParts = React6.useMemo(
@@ -2925,7 +2926,7 @@ function useSpareParts(options) {
2925
2926
  userKey
2926
2927
  ]);
2927
2928
  const goToPage = React6.useCallback((page) => {
2928
- setCurrentPage((prev) => {
2929
+ setInternalPage((prev) => {
2929
2930
  if (page >= 1 && (totalPages <= 1 || page <= totalPages)) return page;
2930
2931
  return prev;
2931
2932
  });
@@ -2944,9 +2945,9 @@ function useSpareParts(options) {
2944
2945
  var inflightFetches4 = /* @__PURE__ */ new Set();
2945
2946
  var ROOT_MACHINE_FIELDS = `
2946
2947
  id
2947
- name(language: $language) { language value }
2948
- description(language: $language) { language value }
2949
- slug(language: $language) { language value }
2948
+ name { language value }
2949
+ description { language value }
2950
+ slug { language value }
2950
2951
  media {
2951
2952
  images {
2952
2953
  items { imageVariants(input: $imageVariantFilters) { url } }
@@ -8549,6 +8550,7 @@ function getMachineImageUrl(machine) {
8549
8550
  }
8550
8551
  function MachineCard(props) {
8551
8552
  const { machine, href, language } = props;
8553
+ const Wrapper = href ? "a" : "div";
8552
8554
  const showImage = props.showImage ?? true;
8553
8555
  const showDescription = props.showDescription ?? false;
8554
8556
  const name = propellerV2CoreUi.getLocalizedValue(machine.name, language);
@@ -8560,10 +8562,9 @@ function MachineCard(props) {
8560
8562
  {
8561
8563
  className: cn(`propeller-machine-card group relative flex h-full flex-col overflow-hidden rounded-lg border border-border bg-background ${props.className ?? ""}`),
8562
8564
  children: /* @__PURE__ */ jsxRuntime.jsxs(
8563
- "a",
8565
+ Wrapper,
8564
8566
  {
8565
- href,
8566
- onClick: handleClick,
8567
+ ...href ? { href, onClick: handleClick } : {},
8567
8568
  className: "flex h-full flex-col focus:outline-none focus-visible:ring-2 focus-visible:ring-primary",
8568
8569
  children: [
8569
8570
  showImage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-machine-card__image aspect-square w-full overflow-hidden bg-background-subtle", children: imageUrl ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -10468,7 +10469,7 @@ function MachineGrid(rawProps) {
10468
10469
  // node query threads through useSpareParts.
10469
10470
  imageVariantFilters: configuration?.imageVariantFiltersMedium
10470
10471
  });
10471
- const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages, goToPage } = useSpareParts({
10472
+ const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages } = useSpareParts({
10472
10473
  graphqlClient: props.graphqlClient,
10473
10474
  // Idle at the root (no slug).
10474
10475
  slug: isRoot ? void 0 : currentSlug,
@@ -10484,6 +10485,10 @@ function MachineGrid(rawProps) {
10484
10485
  sortField,
10485
10486
  sortOrder,
10486
10487
  pageSize: offset,
10488
+ // The hook takes the controlled page directly now. It used to own its own
10489
+ // counter, so the only way to drive it from URL state was the effect
10490
+ // below — undiscoverable unless you read this file (PWP-995b).
10491
+ page: listing.page,
10487
10492
  configuration: rawProps.configuration ?? props.configuration,
10488
10493
  onFiltersChange: setGridFilters,
10489
10494
  onPriceBoundsChange: (min, max) => {
@@ -10493,9 +10498,6 @@ function MachineGrid(rawProps) {
10493
10498
  onItemsFoundChange: setItemsFound,
10494
10499
  onMachineChange: setMachine
10495
10500
  });
10496
- React6.useEffect(() => {
10497
- goToPage(listing.page);
10498
- }, [listing.page, goToPage]);
10499
10501
  const machineName = machine ? propellerV2CoreUi.getLocalizedValue(machine.name, machineLanguage) : slugToLabel(currentSlug);
10500
10502
  const partProducts = React6.useMemo(
10501
10503
  () => displayParts.map((p) => p.product).filter(Boolean),
@@ -10552,11 +10554,10 @@ function MachineGrid(rawProps) {
10552
10554
  );
10553
10555
  const childSlugHref = (child) => {
10554
10556
  const slug = propellerV2CoreUi.getLocalizedValue(child.slug, machineLanguage);
10555
- if (!slug) return null;
10556
- return `${currentPath}/${slug}`;
10557
+ return slug ? `${currentPath}/${slug}` : void 0;
10557
10558
  };
10558
10559
  if (isRoot) {
10559
- const cards = rootMachines.map((m) => ({ machine: m, href: childSlugHref(m) })).filter((c) => c.href !== null);
10560
+ const cards = rootMachines.map((m) => ({ machine: m, href: childSlugHref(m) }));
10560
10561
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rawProps.className, children: [
10561
10562
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "propeller-grid-title mb-6 text-2xl font-semibold", children: rawProps.rootTitle ?? "Machines" }),
10562
10563
  rootLoading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: propellerV2CoreUi.getLabel(rawProps.machineCardLabels, "loading", "Loading\u2026") }) : cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: propellerV2CoreUi.getLabel(rawProps.machineCardLabels, "noMachines", "No machines found.") }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: cards.map(({ machine: m, href }) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -10588,7 +10589,6 @@ function MachineGrid(rawProps) {
10588
10589
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "propeller-grid-title mb-6 text-2xl font-semibold", children: machineName }),
10589
10590
  childMachines.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-machine-children mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: childMachines.map((child) => {
10590
10591
  const href = childSlugHref(child);
10591
- if (!href) return null;
10592
10592
  return /* @__PURE__ */ jsxRuntime.jsx(
10593
10593
  MachineCard,
10594
10594
  {
@@ -10666,6 +10666,12 @@ function MachineGrid(rawProps) {
10666
10666
  columns: viewMode === "list" ? 1 : 3,
10667
10667
  showAvailability: rawProps.showAvailability ?? false,
10668
10668
  showStock: rawProps.showStock ?? true,
10669
+ productCardComponent: rawProps.productCardComponent,
10670
+ productCardLabels: rawProps.productCardLabels,
10671
+ addToCartLabels: rawProps.addToCartLabels,
10672
+ stockLabels: rawProps.stockLabels,
10673
+ priceLabels: rawProps.priceLabels,
10674
+ labels: rawProps.labels,
10669
10675
  belowName: (product) => {
10670
10676
  const qty = quantityBySku.get(product.sku);
10671
10677
  if (!qty) return null;