@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/dist/index.d.cts CHANGED
@@ -1681,6 +1681,15 @@ interface UseSparePartsOptions {
1681
1681
  * parts list need different languages. Set this when they diverge.
1682
1682
  */
1683
1683
  machineLanguage?: string;
1684
+ /**
1685
+ * Extra languages to try when the slug does not resolve in `machineLanguage`.
1686
+ *
1687
+ * A slug resolves only in the language it was authored in, so a tree that is
1688
+ * only half-translated has machines reachable by an NL slug but not an EN
1689
+ * one. `machineLanguage` and `language` are always tried first; list the
1690
+ * shop's other locales here to cover the rest. Order is the try order.
1691
+ */
1692
+ machineLanguages?: string[];
1684
1693
  /** Tax zone for price calculation. */
1685
1694
  taxZone?: string;
1686
1695
  /** Active user — drives `userId` scoping and contact/customer pricing. */
@@ -1749,6 +1758,14 @@ interface UseSparePartsReturn {
1749
1758
  fetchParts: () => Promise<void>;
1750
1759
  /** Navigate to a page. */
1751
1760
  goToPage: (page: number) => void;
1761
+ /**
1762
+ * The slug resolved in none of the candidate languages.
1763
+ *
1764
+ * Distinct from "resolved but has no parts": this node does not exist, and a
1765
+ * host that renders the usual empty listing for it shows a page built
1766
+ * entirely from the URL. Always `false` in controlled mode.
1767
+ */
1768
+ notFound: boolean;
1752
1769
  }
1753
1770
  /**
1754
1771
  * useSpareParts — fetch and paginate a machine node's spare-parts list.
@@ -4292,6 +4309,16 @@ interface MachineGridProps {
4292
4309
  rootTitle?: string;
4293
4310
  /** Language the machine tree is authored in (usually EN). Defaults to `'EN'`. */
4294
4311
  machineLanguage?: string;
4312
+ /**
4313
+ * Other languages the tree may be authored in, tried in order when a slug
4314
+ * does not resolve in `machineLanguage`.
4315
+ *
4316
+ * A slug resolves only in its own language, so a half-translated tree lists a
4317
+ * machine by its NL slug and then cannot open it with `language: 'EN'`. Pass
4318
+ * the shop's locales here; `machineLanguage` and the storefront `language`
4319
+ * are always tried first (PWP-993).
4320
+ */
4321
+ machineLanguages?: string[];
4295
4322
  listing: MachineListingState;
4296
4323
  onListingChange: (next: MachineListingState) => void;
4297
4324
  graphqlClient?: GraphQLClient;
@@ -4331,9 +4358,19 @@ interface MachineGridProps {
4331
4358
  filtersLabels?: Record<string, string>;
4332
4359
  toolbarLabels?: Record<string, string>;
4333
4360
  /**
4334
- * Labels for the machine side of the grid. Keys read here: `loading` and
4335
- * `noMachines`. The same object is passed to each `MachineCard` as its
4336
- * `labels`, which reads `viewMachine` — so all three keys belong in it.
4361
+ * Labels for the machine side of the grid.
4362
+ *
4363
+ * Keys read here: `loading`, `noMachines`, `machineNotFound`,
4364
+ * `quantityInMachine` and `searchParts`. The same object is passed to each
4365
+ * `MachineCard` as its `labels`, which reads `viewMachine` — so all six keys
4366
+ * belong in it.
4367
+ *
4368
+ * `quantityInMachine` and `searchParts` used to be read from `toolbarLabels`,
4369
+ * which is forwarded verbatim to `GridToolbar`: a shop that translated its
4370
+ * toolbar dictionary properly still got "Qty in machine" and "Search parts…"
4371
+ * in English, because those keys belong to no toolbar (PWP-995a). They are
4372
+ * still read from `toolbarLabels` as a fallback so existing hosts keep
4373
+ * working.
4337
4374
  */
4338
4375
  machineCardLabels?: Record<string, string>;
4339
4376
  /**
package/dist/index.d.ts CHANGED
@@ -1681,6 +1681,15 @@ interface UseSparePartsOptions {
1681
1681
  * parts list need different languages. Set this when they diverge.
1682
1682
  */
1683
1683
  machineLanguage?: string;
1684
+ /**
1685
+ * Extra languages to try when the slug does not resolve in `machineLanguage`.
1686
+ *
1687
+ * A slug resolves only in the language it was authored in, so a tree that is
1688
+ * only half-translated has machines reachable by an NL slug but not an EN
1689
+ * one. `machineLanguage` and `language` are always tried first; list the
1690
+ * shop's other locales here to cover the rest. Order is the try order.
1691
+ */
1692
+ machineLanguages?: string[];
1684
1693
  /** Tax zone for price calculation. */
1685
1694
  taxZone?: string;
1686
1695
  /** Active user — drives `userId` scoping and contact/customer pricing. */
@@ -1749,6 +1758,14 @@ interface UseSparePartsReturn {
1749
1758
  fetchParts: () => Promise<void>;
1750
1759
  /** Navigate to a page. */
1751
1760
  goToPage: (page: number) => void;
1761
+ /**
1762
+ * The slug resolved in none of the candidate languages.
1763
+ *
1764
+ * Distinct from "resolved but has no parts": this node does not exist, and a
1765
+ * host that renders the usual empty listing for it shows a page built
1766
+ * entirely from the URL. Always `false` in controlled mode.
1767
+ */
1768
+ notFound: boolean;
1752
1769
  }
1753
1770
  /**
1754
1771
  * useSpareParts — fetch and paginate a machine node's spare-parts list.
@@ -4292,6 +4309,16 @@ interface MachineGridProps {
4292
4309
  rootTitle?: string;
4293
4310
  /** Language the machine tree is authored in (usually EN). Defaults to `'EN'`. */
4294
4311
  machineLanguage?: string;
4312
+ /**
4313
+ * Other languages the tree may be authored in, tried in order when a slug
4314
+ * does not resolve in `machineLanguage`.
4315
+ *
4316
+ * A slug resolves only in its own language, so a half-translated tree lists a
4317
+ * machine by its NL slug and then cannot open it with `language: 'EN'`. Pass
4318
+ * the shop's locales here; `machineLanguage` and the storefront `language`
4319
+ * are always tried first (PWP-993).
4320
+ */
4321
+ machineLanguages?: string[];
4295
4322
  listing: MachineListingState;
4296
4323
  onListingChange: (next: MachineListingState) => void;
4297
4324
  graphqlClient?: GraphQLClient;
@@ -4331,9 +4358,19 @@ interface MachineGridProps {
4331
4358
  filtersLabels?: Record<string, string>;
4332
4359
  toolbarLabels?: Record<string, string>;
4333
4360
  /**
4334
- * Labels for the machine side of the grid. Keys read here: `loading` and
4335
- * `noMachines`. The same object is passed to each `MachineCard` as its
4336
- * `labels`, which reads `viewMachine` — so all three keys belong in it.
4361
+ * Labels for the machine side of the grid.
4362
+ *
4363
+ * Keys read here: `loading`, `noMachines`, `machineNotFound`,
4364
+ * `quantityInMachine` and `searchParts`. The same object is passed to each
4365
+ * `MachineCard` as its `labels`, which reads `viewMachine` — so all six keys
4366
+ * belong in it.
4367
+ *
4368
+ * `quantityInMachine` and `searchParts` used to be read from `toolbarLabels`,
4369
+ * which is forwarded verbatim to `GridToolbar`: a shop that translated its
4370
+ * toolbar dictionary properly still got "Qty in machine" and "Search parts…"
4371
+ * in English, because those keys belong to no toolbar (PWP-995a). They are
4372
+ * still read from `toolbarLabels` as a fallback so existing hosts keep
4373
+ * working.
4337
4374
  */
4338
4375
  machineCardLabels?: Record<string, string>;
4339
4376
  /**
package/dist/index.js CHANGED
@@ -2762,6 +2762,40 @@ function useQuickOrder(options) {
2762
2762
  );
2763
2763
  return { submitting, error, searchProducts, submit };
2764
2764
  }
2765
+
2766
+ // src/composables/shared/utils/machineLanguage.ts
2767
+ var MACHINE_NOT_FOUND = "SPARE_PARTS_MACHINE_NOT_FOUND_ERROR";
2768
+ function isMachineNotFound(error) {
2769
+ const entries = error?.errors;
2770
+ if (!Array.isArray(entries)) return false;
2771
+ return entries.some(
2772
+ (e) => e?.extensions?.code === MACHINE_NOT_FOUND || /no machine found for slug and language/i.test(e?.message ?? "")
2773
+ );
2774
+ }
2775
+ function machineLanguageCandidates(machineLanguage, language, extra) {
2776
+ const ordered = [machineLanguage, language, ...extra ?? []];
2777
+ const seen = /* @__PURE__ */ new Set();
2778
+ const out = [];
2779
+ for (const raw of ordered) {
2780
+ const value = raw?.trim().toUpperCase();
2781
+ if (!value || seen.has(value)) continue;
2782
+ seen.add(value);
2783
+ out.push(value);
2784
+ }
2785
+ return out;
2786
+ }
2787
+ async function resolveMachineAcrossLanguages(candidates, fetchOne) {
2788
+ for (const language of candidates) {
2789
+ try {
2790
+ return { machine: await fetchOne(language), language };
2791
+ } catch (error) {
2792
+ if (!isMachineNotFound(error)) throw error;
2793
+ }
2794
+ }
2795
+ return null;
2796
+ }
2797
+
2798
+ // src/composables/react/useSpareParts.ts
2765
2799
  var inflightFetches3 = /* @__PURE__ */ new Set();
2766
2800
  var STOREFRONT_STATUSES = [
2767
2801
  ProductStatus.A,
@@ -2778,6 +2812,8 @@ function useSpareParts(options) {
2778
2812
  const [itemsFound, setItemsFound] = useState(0);
2779
2813
  const [internalLoading, setInternalLoading] = useState(false);
2780
2814
  const [internalPage, setInternalPage] = useState(1);
2815
+ const [notFound, setNotFound] = useState(false);
2816
+ const machineLanguagesKey = (options.machineLanguages ?? []).join(",");
2781
2817
  const currentPage = options.page ?? internalPage;
2782
2818
  const [totalPages, setTotalPages] = useState(1);
2783
2819
  const fetchIdRef = useRef(0);
@@ -2843,17 +2879,35 @@ function useSpareParts(options) {
2843
2879
  const filterAvailableAttributeInput = {
2844
2880
  isSearchable: true
2845
2881
  };
2846
- const machine = await service.getMachine({
2847
- slug: options.slug,
2848
- // The machine tree's language, NOT the parts' — see `machineLanguage`.
2849
- language: options.machineLanguage ?? options.language,
2850
- sparePartsMachineProductSearchInput,
2851
- filterAvailableAttributeInput,
2852
- priceCalculateProductInput,
2853
- imageSearchFilters: options.configuration?.imageSearchFiltersGrid,
2854
- imageVariantFilters: options.configuration?.imageVariantFiltersMedium
2855
- });
2882
+ const resolved = await resolveMachineAcrossLanguages(
2883
+ machineLanguageCandidates(
2884
+ options.machineLanguage,
2885
+ options.language,
2886
+ options.machineLanguages
2887
+ ),
2888
+ (language) => service.getMachine({
2889
+ slug: options.slug,
2890
+ // The machine tree's language, NOT the parts' — see `machineLanguage`.
2891
+ language,
2892
+ sparePartsMachineProductSearchInput,
2893
+ filterAvailableAttributeInput,
2894
+ priceCalculateProductInput,
2895
+ imageSearchFilters: options.configuration?.imageSearchFiltersGrid,
2896
+ imageVariantFilters: options.configuration?.imageVariantFiltersMedium
2897
+ })
2898
+ );
2856
2899
  if (thisId !== fetchIdRef.current) return;
2900
+ if (!resolved) {
2901
+ setNotFound(true);
2902
+ setInternalParts([]);
2903
+ setChildMachines([]);
2904
+ setItemsFound(0);
2905
+ options.onItemsFoundChange?.(0);
2906
+ setTotalPages(1);
2907
+ return;
2908
+ }
2909
+ setNotFound(false);
2910
+ const machine = resolved.machine;
2857
2911
  const partsResponse = machine?.sparePartProducts;
2858
2912
  const items = partsResponse?.items ?? [];
2859
2913
  setInternalParts(items);
@@ -2895,6 +2949,7 @@ function useSpareParts(options) {
2895
2949
  options.term,
2896
2950
  options.language,
2897
2951
  options.machineLanguage,
2952
+ machineLanguagesKey,
2898
2953
  options.taxZone,
2899
2954
  options.companyId,
2900
2955
  options.textFilters,
@@ -2915,6 +2970,7 @@ function useSpareParts(options) {
2915
2970
  options.term,
2916
2971
  options.language,
2917
2972
  options.machineLanguage,
2973
+ machineLanguagesKey,
2918
2974
  options.companyId,
2919
2975
  // eslint-disable-next-line react-hooks/exhaustive-deps
2920
2976
  JSON.stringify(options.textFilters ?? []),
@@ -2940,7 +2996,8 @@ function useSpareParts(options) {
2940
2996
  currentPage,
2941
2997
  totalPages,
2942
2998
  fetchParts,
2943
- goToPage
2999
+ goToPage,
3000
+ notFound: !isControlled && notFound
2944
3001
  };
2945
3002
  }
2946
3003
  var inflightFetches4 = /* @__PURE__ */ new Set();
@@ -10484,13 +10541,14 @@ function MachineGrid(rawProps) {
10484
10541
  // node query threads through useSpareParts.
10485
10542
  imageVariantFilters: configuration?.imageVariantFiltersMedium
10486
10543
  });
10487
- const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages } = useSpareParts({
10544
+ const { displayParts, childMachines, isLoading: partsLoading, currentPage, totalPages, notFound } = useSpareParts({
10488
10545
  graphqlClient: props.graphqlClient,
10489
10546
  // Idle at the root (no slug).
10490
10547
  slug: isRoot ? void 0 : currentSlug,
10491
10548
  term: term || void 0,
10492
10549
  language,
10493
10550
  machineLanguage,
10551
+ machineLanguages: rawProps.machineLanguages,
10494
10552
  taxZone: props.taxZone,
10495
10553
  user: props.user,
10496
10554
  companyId: props.companyId,
@@ -10514,6 +10572,7 @@ function MachineGrid(rawProps) {
10514
10572
  onMachineChange: setMachine
10515
10573
  });
10516
10574
  const machineName = machine ? getLocalizedValue(machine.name, machineLanguage) : slugToLabel(currentSlug);
10575
+ const machineLabel = (key, fallback) => rawProps.machineCardLabels?.[key] ?? rawProps.toolbarLabels?.[key] ?? fallback;
10517
10576
  const partProducts = useMemo(
10518
10577
  () => displayParts.map((p) => p.product).filter(Boolean),
10519
10578
  [displayParts]
@@ -10588,6 +10647,12 @@ function MachineGrid(rawProps) {
10588
10647
  ] });
10589
10648
  }
10590
10649
  const hasParts = itemsFound > 0 || partProducts.length > 0;
10650
+ if (notFound && !partsLoading) {
10651
+ return /* @__PURE__ */ jsxs("div", { className: rawProps.className, children: [
10652
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: "propeller-breadcrumbs mb-6", children: /* @__PURE__ */ jsx("ol", { className: "flex flex-wrap items-center gap-2 text-sm text-foreground-subtle", children: /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", { href: basePath, className: "hover:text-primary", children: rawProps.rootTitle ?? "Machines" }) }) }) }),
10653
+ /* @__PURE__ */ jsx("p", { className: "py-12 text-center text-foreground-subtle", children: machineLabel("machineNotFound", "This machine could not be found.") })
10654
+ ] });
10655
+ }
10591
10656
  return /* @__PURE__ */ jsxs("div", { className: rawProps.className, children: [
10592
10657
  /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: "propeller-breadcrumbs mb-6", children: /* @__PURE__ */ jsxs("ol", { className: "flex flex-wrap items-center gap-2 text-sm text-foreground-subtle", children: [
10593
10658
  /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", { href: basePath, className: "hover:text-primary", children: rawProps.rootTitle ?? "Machines" }) }),
@@ -10640,7 +10705,9 @@ function MachineGrid(rawProps) {
10640
10705
  {
10641
10706
  defaultValue: term,
10642
10707
  onSubmit: submitSearch,
10643
- labels: rawProps.toolbarLabels
10708
+ searchLabel: machineLabel("searchParts", "Search parts\u2026"),
10709
+ searchAriaLabel: machineLabel("searchParts", "Search parts"),
10710
+ buttonLabel: getLabel(rawProps.toolbarLabels, "search", "Search")
10644
10711
  },
10645
10712
  term
10646
10713
  ),
@@ -10691,7 +10758,7 @@ function MachineGrid(rawProps) {
10691
10758
  const qty = quantityBySku.get(product.sku);
10692
10759
  if (!qty) return null;
10693
10760
  return /* @__PURE__ */ jsxs("span", { className: "propeller-spare-part__quantity text-sm text-foreground-subtle", children: [
10694
- rawProps.toolbarLabels?.quantityInMachine ?? "Qty in machine",
10761
+ machineLabel("quantityInMachine", "Qty in machine"),
10695
10762
  ": ",
10696
10763
  qty
10697
10764
  ] });
@@ -10723,8 +10790,8 @@ function MachinePartsSearch(props) {
10723
10790
  onKeyDown: (e) => {
10724
10791
  if (e.key === "Enter") submit();
10725
10792
  },
10726
- placeholder: props.labels?.searchParts ?? "Search parts\u2026",
10727
- "aria-label": props.labels?.searchParts ?? "Search parts",
10793
+ placeholder: props.searchLabel,
10794
+ "aria-label": props.searchAriaLabel,
10728
10795
  className: "w-full rounded border border-border bg-background px-3 py-2"
10729
10796
  }
10730
10797
  ),
@@ -10734,7 +10801,7 @@ function MachinePartsSearch(props) {
10734
10801
  type: "button",
10735
10802
  onClick: submit,
10736
10803
  className: "rounded bg-primary px-4 py-2 text-primary-foreground",
10737
- children: props.labels?.search ?? "Search"
10804
+ children: props.buttonLabel
10738
10805
  }
10739
10806
  )
10740
10807
  ] });