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