@propeller-commerce/propeller-v2-react-ui 0.22.0 → 0.23.1

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,60 @@ 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.23.1] - 2026-09-24
12
+
13
+ ### Fixed
14
+
15
+ - **The language fallback never ran, so a machine listed in one language still
16
+ opened empty.** 0.23.0 added the fallback but only advanced it on a *thrown*
17
+ error. The API answers a wrong-language slug with a **partial** response —
18
+ `machine: null` alongside `SPARE_PARTS_MACHINE_NOT_FOUND_ERROR` — and the
19
+ SDK's `runOperation` returns that data rather than throwing unless the client
20
+ opts into `throwOnPartialErrors`, which no boilerplate does. So `getMachine`
21
+ resolved to `null`, the first candidate language was accepted as a hit, the
22
+ remaining languages were never tried and `notFound` was never set: one
23
+ request in the tree language per page, and the same empty listing as before.
24
+ A `null` machine now counts as "not found in this language" and the loop
25
+ continues, so the fallback reaches the language the slug was authored in and
26
+ a slug that resolves nowhere sets `notFound`. (PWP-993)
27
+
28
+ ## [0.23.0] - 2026-09-24
29
+
30
+ ### Fixed
31
+
32
+ - **A machine listed in one language could not be opened.** 0.21.0 stopped the
33
+ listing from narrowing to a single language, so an NL-only installation
34
+ appears in an EN tree and is linked by its NL slug — but `machine(slug:)`
35
+ resolves a slug **only** in the language it was authored in, so following
36
+ that link asked for `machine(slug: "<NL slug>", language: "EN")` and got
37
+ `SPARE_PARTS_MACHINE_NOT_FOUND_ERROR`. The page then showed a title derived
38
+ from the slug, no parts and no error: the bug moved one click deeper instead
39
+ of going away. `useSpareParts` now tries the tree language, then the
40
+ storefront language, then any locales given in the new `machineLanguages`
41
+ option, and upper-cases each (the API matches case exactly — `nl` misses
42
+ where `NL` hits). A slug that resolves in none of them sets the new
43
+ `notFound` flag, and `MachineGrid` renders "This machine could not be found."
44
+ instead of an empty listing. A fully-translated shop still costs exactly one
45
+ request, and a non-"not found" failure is never retried or reported as a
46
+ missing machine. (PWP-993)
47
+
48
+ - **"Qty in machine" and "Search parts…" stayed English in a translated shop.**
49
+ Both were read from `toolbarLabels`, which is forwarded verbatim to
50
+ `GridToolbar` — so they belonged to no toolbar dictionary, and a shop that
51
+ translated its `GridToolbar` keys properly still got English. They now come
52
+ from `machineCardLabels` (the grid's own label bag, alongside `loading`,
53
+ `noMachines` and the new `machineNotFound`), falling back to `toolbarLabels`
54
+ so hosts that already set them keep working. (PWP-995a)
55
+
56
+ ### Added
57
+
58
+ - `MachineGrid.machineLanguages` / `useSpareParts.machineLanguages` — the
59
+ locales to try when a machine slug does not resolve in the tree language.
60
+ - `useSpareParts().notFound` — the slug resolved in none of the candidate
61
+ languages, as distinct from "resolved but has no parts".
62
+ - `machineCardLabels.machineNotFound` — copy for that state.
63
+
64
+
11
65
  ## [0.22.0] - 2026-09-24
12
66
 
13
67
  ### Fixed
package/dist/index.cjs CHANGED
@@ -2782,6 +2782,41 @@ function useQuickOrder(options) {
2782
2782
  );
2783
2783
  return { submitting, error, searchProducts, submit };
2784
2784
  }
2785
+
2786
+ // src/composables/shared/utils/machineLanguage.ts
2787
+ var MACHINE_NOT_FOUND = "SPARE_PARTS_MACHINE_NOT_FOUND_ERROR";
2788
+ function isMachineNotFound(error) {
2789
+ const entries = error?.errors;
2790
+ if (!Array.isArray(entries)) return false;
2791
+ return entries.some(
2792
+ (e) => e?.extensions?.code === MACHINE_NOT_FOUND || /no machine found for slug and language/i.test(e?.message ?? "")
2793
+ );
2794
+ }
2795
+ function machineLanguageCandidates(machineLanguage, language, extra) {
2796
+ const ordered = [machineLanguage, language, ...extra ?? []];
2797
+ const seen = /* @__PURE__ */ new Set();
2798
+ const out = [];
2799
+ for (const raw of ordered) {
2800
+ const value = raw?.trim().toUpperCase();
2801
+ if (!value || seen.has(value)) continue;
2802
+ seen.add(value);
2803
+ out.push(value);
2804
+ }
2805
+ return out;
2806
+ }
2807
+ async function resolveMachineAcrossLanguages(candidates, fetchOne) {
2808
+ for (const language of candidates) {
2809
+ try {
2810
+ const machine = await fetchOne(language);
2811
+ if (machine !== null && machine !== void 0) return { machine, language };
2812
+ } catch (error) {
2813
+ if (!isMachineNotFound(error)) throw error;
2814
+ }
2815
+ }
2816
+ return null;
2817
+ }
2818
+
2819
+ // src/composables/react/useSpareParts.ts
2785
2820
  var inflightFetches3 = /* @__PURE__ */ new Set();
2786
2821
  var STOREFRONT_STATUSES = [
2787
2822
  propellerSdkV2.ProductStatus.A,
@@ -2798,6 +2833,8 @@ function useSpareParts(options) {
2798
2833
  const [itemsFound, setItemsFound] = React6.useState(0);
2799
2834
  const [internalLoading, setInternalLoading] = React6.useState(false);
2800
2835
  const [internalPage, setInternalPage] = React6.useState(1);
2836
+ const [notFound, setNotFound] = React6.useState(false);
2837
+ const machineLanguagesKey = (options.machineLanguages ?? []).join(",");
2801
2838
  const currentPage = options.page ?? internalPage;
2802
2839
  const [totalPages, setTotalPages] = React6.useState(1);
2803
2840
  const fetchIdRef = React6.useRef(0);
@@ -2863,17 +2900,35 @@ function useSpareParts(options) {
2863
2900
  const filterAvailableAttributeInput = {
2864
2901
  isSearchable: true
2865
2902
  };
2866
- const machine = await service.getMachine({
2867
- slug: options.slug,
2868
- // The machine tree's language, NOT the parts' — see `machineLanguage`.
2869
- language: options.machineLanguage ?? options.language,
2870
- sparePartsMachineProductSearchInput,
2871
- filterAvailableAttributeInput,
2872
- priceCalculateProductInput,
2873
- imageSearchFilters: options.configuration?.imageSearchFiltersGrid,
2874
- imageVariantFilters: options.configuration?.imageVariantFiltersMedium
2875
- });
2903
+ const resolved = await resolveMachineAcrossLanguages(
2904
+ machineLanguageCandidates(
2905
+ options.machineLanguage,
2906
+ options.language,
2907
+ options.machineLanguages
2908
+ ),
2909
+ (language) => service.getMachine({
2910
+ slug: options.slug,
2911
+ // The machine tree's language, NOT the parts' — see `machineLanguage`.
2912
+ language,
2913
+ sparePartsMachineProductSearchInput,
2914
+ filterAvailableAttributeInput,
2915
+ priceCalculateProductInput,
2916
+ imageSearchFilters: options.configuration?.imageSearchFiltersGrid,
2917
+ imageVariantFilters: options.configuration?.imageVariantFiltersMedium
2918
+ })
2919
+ );
2876
2920
  if (thisId !== fetchIdRef.current) return;
2921
+ if (!resolved) {
2922
+ setNotFound(true);
2923
+ setInternalParts([]);
2924
+ setChildMachines([]);
2925
+ setItemsFound(0);
2926
+ options.onItemsFoundChange?.(0);
2927
+ setTotalPages(1);
2928
+ return;
2929
+ }
2930
+ setNotFound(false);
2931
+ const machine = resolved.machine;
2877
2932
  const partsResponse = machine?.sparePartProducts;
2878
2933
  const items = partsResponse?.items ?? [];
2879
2934
  setInternalParts(items);
@@ -2915,6 +2970,7 @@ function useSpareParts(options) {
2915
2970
  options.term,
2916
2971
  options.language,
2917
2972
  options.machineLanguage,
2973
+ machineLanguagesKey,
2918
2974
  options.taxZone,
2919
2975
  options.companyId,
2920
2976
  options.textFilters,
@@ -2935,6 +2991,7 @@ function useSpareParts(options) {
2935
2991
  options.term,
2936
2992
  options.language,
2937
2993
  options.machineLanguage,
2994
+ machineLanguagesKey,
2938
2995
  options.companyId,
2939
2996
  // eslint-disable-next-line react-hooks/exhaustive-deps
2940
2997
  JSON.stringify(options.textFilters ?? []),
@@ -2960,7 +3017,8 @@ function useSpareParts(options) {
2960
3017
  currentPage,
2961
3018
  totalPages,
2962
3019
  fetchParts,
2963
- goToPage
3020
+ goToPage,
3021
+ notFound: !isControlled && notFound
2964
3022
  };
2965
3023
  }
2966
3024
  var inflightFetches4 = /* @__PURE__ */ new Set();
@@ -10504,13 +10562,14 @@ function MachineGrid(rawProps) {
10504
10562
  // node query threads through useSpareParts.
10505
10563
  imageVariantFilters: configuration?.imageVariantFiltersMedium
10506
10564
  });
10507
- const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages } = useSpareParts({
10565
+ const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages, notFound } = useSpareParts({
10508
10566
  graphqlClient: props.graphqlClient,
10509
10567
  // Idle at the root (no slug).
10510
10568
  slug: isRoot ? void 0 : currentSlug,
10511
10569
  term: term || void 0,
10512
10570
  language,
10513
10571
  machineLanguage,
10572
+ machineLanguages: rawProps.machineLanguages,
10514
10573
  taxZone: props.taxZone,
10515
10574
  user: props.user,
10516
10575
  companyId: props.companyId,
@@ -10534,6 +10593,7 @@ function MachineGrid(rawProps) {
10534
10593
  onMachineChange: setMachine
10535
10594
  });
10536
10595
  const machineName = machine ? propellerV2CoreUi.getLocalizedValue(machine.name, machineLanguage) : slugToLabel(currentSlug);
10596
+ const machineLabel = (key, fallback) => rawProps.machineCardLabels?.[key] ?? rawProps.toolbarLabels?.[key] ?? fallback;
10537
10597
  const partProducts = React6.useMemo(
10538
10598
  () => displayParts.map((p) => p.product).filter(Boolean),
10539
10599
  [displayParts]
@@ -10608,6 +10668,12 @@ function MachineGrid(rawProps) {
10608
10668
  ] });
10609
10669
  }
10610
10670
  const hasParts = itemsFound > 0 || partProducts.length > 0;
10671
+ if (notFound && !partsLoading) {
10672
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rawProps.className, children: [
10673
+ /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Breadcrumb", className: "propeller-breadcrumbs mb-6", children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "flex flex-wrap items-center gap-2 text-sm text-foreground-subtle", children: /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: basePath, className: "hover:text-primary", children: rawProps.rootTitle ?? "Machines" }) }) }) }),
10674
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: machineLabel("machineNotFound", "This machine could not be found.") })
10675
+ ] });
10676
+ }
10611
10677
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rawProps.className, children: [
10612
10678
  /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Breadcrumb", className: "propeller-breadcrumbs mb-6", children: /* @__PURE__ */ jsxRuntime.jsxs("ol", { className: "flex flex-wrap items-center gap-2 text-sm text-foreground-subtle", children: [
10613
10679
  /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: basePath, className: "hover:text-primary", children: rawProps.rootTitle ?? "Machines" }) }),
@@ -10660,7 +10726,9 @@ function MachineGrid(rawProps) {
10660
10726
  {
10661
10727
  defaultValue: term,
10662
10728
  onSubmit: submitSearch,
10663
- labels: rawProps.toolbarLabels
10729
+ searchLabel: machineLabel("searchParts", "Search parts\u2026"),
10730
+ searchAriaLabel: machineLabel("searchParts", "Search parts"),
10731
+ buttonLabel: propellerV2CoreUi.getLabel(rawProps.toolbarLabels, "search", "Search")
10664
10732
  },
10665
10733
  term
10666
10734
  ),
@@ -10711,7 +10779,7 @@ function MachineGrid(rawProps) {
10711
10779
  const qty = quantityBySku.get(product.sku);
10712
10780
  if (!qty) return null;
10713
10781
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-spare-part__quantity text-sm text-foreground-subtle", children: [
10714
- rawProps.toolbarLabels?.quantityInMachine ?? "Qty in machine",
10782
+ machineLabel("quantityInMachine", "Qty in machine"),
10715
10783
  ": ",
10716
10784
  qty
10717
10785
  ] });
@@ -10743,8 +10811,8 @@ function MachinePartsSearch(props) {
10743
10811
  onKeyDown: (e) => {
10744
10812
  if (e.key === "Enter") submit();
10745
10813
  },
10746
- placeholder: props.labels?.searchParts ?? "Search parts\u2026",
10747
- "aria-label": props.labels?.searchParts ?? "Search parts",
10814
+ placeholder: props.searchLabel,
10815
+ "aria-label": props.searchAriaLabel,
10748
10816
  className: "w-full rounded border border-border bg-background px-3 py-2"
10749
10817
  }
10750
10818
  ),
@@ -10754,7 +10822,7 @@ function MachinePartsSearch(props) {
10754
10822
  type: "button",
10755
10823
  onClick: submit,
10756
10824
  className: "rounded bg-primary px-4 py-2 text-primary-foreground",
10757
- children: props.labels?.search ?? "Search"
10825
+ children: props.buttonLabel
10758
10826
  }
10759
10827
  )
10760
10828
  ] });