@fayz-ai/plugin-inventory 0.8.0 → 0.8.3

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.ts CHANGED
@@ -20,6 +20,14 @@ export interface InventoryPluginOptions {
20
20
  recipes?: boolean;
21
21
  stockLocations?: boolean;
22
22
  batchTracking?: boolean;
23
+ /**
24
+ * The product catalogue screens (list + form). Defaults on. Turn OFF when the
25
+ * host already owns product registration elsewhere — an e-commerce app
26
+ * manages products in the shop plugin, and two competing product CRUDs is
27
+ * how the same product ends up entered twice, differently.
28
+ * Stock entry/exit/history stay available either way.
29
+ */
30
+ products?: boolean;
23
31
  };
24
32
  labels?: Partial<InventoryPluginLabels>;
25
33
  productTypes?: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAazD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;KACxB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IACjC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAChE;AAqDD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CAqFtF;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAazD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IACjC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAChE;AAsDD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CA2HtF;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React2, { useState, useMemo, useEffect } from 'react';
2
2
  import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, useSaveBar, Breadcrumb, DataTable, DashboardCanvas, KpiCard, Card, CardHeader, CardTitle, CardContent } from '@fayz-ai/ui';
3
- import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, CrudListView, CrudFormPage, formatCurrency } from '@fayz-ai/saas';
3
+ import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, CrudListView, useLimitGuard, CrudFormPage, formatCurrency, PermissionGate, invalidateLimit } from '@fayz-ai/saas';
4
4
  import { registerTranslations, createSafeDataProvider, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
  import { Package, Ruler, MapPin, Check, ChevronRight, ArrowDownRight, ArrowUpRight, RefreshCw, ArrowRightLeft, Trash2, Plus, BookOpen, Layers, Clock } from 'lucide-react';
@@ -167,7 +167,8 @@ function ProductListView({ onNew, onEdit }) {
167
167
  onFacetChange: (_field, value) => setTypeFilter(value),
168
168
  onNew,
169
169
  addLabel: t2("inventory.productList.newProduct"),
170
- onRowClick: (row) => onEdit?.(row.id)
170
+ onRowClick: (row) => onEdit?.(row.id),
171
+ feature: "inventory"
171
172
  }
172
173
  );
173
174
  }
@@ -176,6 +177,7 @@ function ProductCrudForm({ editId, onSaved }) {
176
177
  const { productTypes, currency } = useInventoryConfig();
177
178
  const provider = useInventoryProvider();
178
179
  const createProduct = useInventoryStore((s) => s.createProduct);
180
+ const guardProducts = useLimitGuard("products");
179
181
  const isEdit = !!editId;
180
182
  const [initialData, setInitialData] = React2.useState(isEdit ? null : {});
181
183
  const [headerName, setHeaderName] = React2.useState("");
@@ -227,7 +229,9 @@ function ProductCrudForm({ editId, onSaved }) {
227
229
  if (isEdit && editId) {
228
230
  await provider.updateProduct(editId, payload);
229
231
  } else {
232
+ if (await guardProducts() === "blocked") return;
230
233
  await createProduct(payload);
234
+ invalidateLimit("products");
231
235
  }
232
236
  onSaved?.();
233
237
  }
@@ -803,11 +807,11 @@ function RecipesView({ onNew, onView }) {
803
807
  {
804
808
  title: t2("inventory.recipes.title"),
805
809
  subtitle: t2("inventory.recipes.productionFormulas", { count: String(recipes.length) }),
806
- actions: onNew && /* @__PURE__ */ jsxs("button", { onClick: onNew, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
810
+ actions: onNew && /* @__PURE__ */ jsx(PermissionGate, { feature: "inventory", action: "create", children: /* @__PURE__ */ jsxs("button", { onClick: onNew, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
807
811
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
808
812
  " ",
809
813
  t2("inventory.recipes.newRecipe")
810
- ] })
814
+ ] }) })
811
815
  }
812
816
  ),
813
817
  recipesLoading ? /* @__PURE__ */ jsx(RecipeSkeleton, {}) : recipes.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center rounded-lg border-2 border-dashed border-muted", children: [
@@ -866,6 +870,7 @@ function RecipeFormView({ onSaved }) {
866
870
  const t2 = useTranslation();
867
871
  const provider = useInventoryProvider();
868
872
  const createRecipe = useInventoryStore((s) => s.createRecipe);
873
+ const guardRecipes = useLimitGuard("recipes");
869
874
  const [name, setName] = useState("");
870
875
  const [description, setDescription] = useState("");
871
876
  const [productId, setProductId] = useState("");
@@ -889,6 +894,7 @@ function RecipeFormView({ onSaved }) {
889
894
  toast.error(t2("common.formIncomplete"));
890
895
  return;
891
896
  }
897
+ if (await guardRecipes() === "blocked") return;
892
898
  setSaving(true);
893
899
  try {
894
900
  const recipe = await createRecipe({
@@ -906,6 +912,7 @@ function RecipeFormView({ onSaved }) {
906
912
  notes: i.notes || void 0
907
913
  }))
908
914
  });
915
+ invalidateLimit("recipes");
909
916
  onSaved?.(recipe.id);
910
917
  } finally {
911
918
  setSaving(false);
@@ -1216,28 +1223,31 @@ function RecipeDetailView({ recipeId, onBack }) {
1216
1223
  ] })
1217
1224
  ] });
1218
1225
  }
1226
+ var INVENTORY_DEFAULTS = {
1227
+ lowStockAlerts: true,
1228
+ requireReason: true,
1229
+ autoDeduct: false,
1230
+ requireSku: false,
1231
+ allowNegative: false,
1232
+ lowStockEmail: false,
1233
+ expiryWarnings: true
1234
+ };
1219
1235
  function InventoryGeneralSettings() {
1220
1236
  const t2 = useTranslation();
1237
+ const s = useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
1221
1238
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1222
1239
  /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
1223
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: true, onChange: () => {
1224
- } }),
1225
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: true, onChange: () => {
1226
- } }),
1227
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: false, onChange: () => {
1228
- } })
1240
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
1241
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
1242
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
1229
1243
  ] }),
1230
1244
  /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
1231
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: false, onChange: () => {
1232
- } }),
1233
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: false, onChange: () => {
1234
- } })
1245
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
1246
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
1235
1247
  ] }),
1236
1248
  /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
1237
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: false, onChange: () => {
1238
- } }),
1239
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: true, onChange: () => {
1240
- } })
1249
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
1250
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
1241
1251
  ] })
1242
1252
  ] });
1243
1253
  }
@@ -1279,7 +1289,7 @@ function InventoryOnboarding({ onComplete }) {
1279
1289
  function buildNav(config, view, navigate, t2) {
1280
1290
  const items = [
1281
1291
  { id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
1282
- {
1292
+ ...config.modules.products ? [{
1283
1293
  id: "products",
1284
1294
  label: t2("inventory.nav.products"),
1285
1295
  icon: "Package",
@@ -1288,7 +1298,7 @@ function buildNav(config, view, navigate, t2) {
1288
1298
  { id: "products-new", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
1289
1299
  { id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list", onClick: () => navigate("products-list") }
1290
1300
  ]
1291
- },
1301
+ }] : [],
1292
1302
  {
1293
1303
  id: "stock",
1294
1304
  label: t2("inventory.nav.stock"),
@@ -1339,13 +1349,15 @@ function InventoryPage({ config, provider, store, registries }) {
1339
1349
  const nav = buildNav(config, view, navigate, t2);
1340
1350
  const quickActions = useMemo(() => {
1341
1351
  const actions = [
1342
- {
1352
+ // Dropped with the catalogue module: offering "new product" while the
1353
+ // screen is gone would dead-end the user.
1354
+ ...config.modules.products ? [{
1343
1355
  id: "new-product",
1344
1356
  label: t2("inventory.quickActions.newProduct"),
1345
1357
  icon: "Package",
1346
1358
  description: t2("inventory.quickActions.newProductDesc"),
1347
1359
  action: () => navigate("products-new")
1348
- },
1360
+ }] : [],
1349
1361
  {
1350
1362
  id: "stock-entry",
1351
1363
  label: t2("inventory.quickActions.stockEntry"),
@@ -1382,9 +1394,13 @@ function InventoryPage({ config, provider, store, registries }) {
1382
1394
  ] }) }) });
1383
1395
  }
1384
1396
  const renderView = createViewRouter([
1385
- { id: "products-list", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1386
- { id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1387
- { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) },
1397
+ // Routes go with the module, not just the nav entry: leaving them mounted
1398
+ // keeps a second product form reachable by URL.
1399
+ ...config.modules.products ? [
1400
+ { id: "products-list", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1401
+ { id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1402
+ { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) }
1403
+ ] : [],
1388
1404
  { id: "stock-entry", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
1389
1405
  { id: "stock-exit", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
1390
1406
  { id: "stock-history", render: () => /* @__PURE__ */ jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
@@ -2741,6 +2757,7 @@ var DEFAULT_PRODUCT_TYPES = [
2741
2757
  function resolveConfig(options) {
2742
2758
  return {
2743
2759
  modules: {
2760
+ products: options?.modules?.products !== false,
2744
2761
  recipes: options?.modules?.recipes !== false,
2745
2762
  stockLocations: options?.modules?.stockLocations !== false,
2746
2763
  batchTracking: options?.modules?.batchTracking ?? false
@@ -2774,6 +2791,40 @@ function createInventoryPlugin(options) {
2774
2791
  { id: "inventory", label: config.labels.pageTitle, group: config.labels.pageTitle },
2775
2792
  ...config.modules.recipes ? [{ id: "inventory.recipes", label: config.labels.recipes ?? "Recipes", group: config.labels.pageTitle }] : []
2776
2793
  ],
2794
+ queryEntities: [
2795
+ {
2796
+ key: "inventory:products",
2797
+ writable: true,
2798
+ entity: {
2799
+ name: "Product",
2800
+ namePlural: "Products",
2801
+ icon: "Package",
2802
+ limitKey: "products",
2803
+ permission: { feature: "inventory", action: "read" },
2804
+ fields: [
2805
+ { key: "name", label: "Name", type: "text", required: true, searchable: true },
2806
+ { key: "kind", label: "Kind (sale/ingredient/asset)", type: "text" },
2807
+ { key: "price", label: "Price", type: "number" },
2808
+ { key: "cost", label: "Cost", type: "number" },
2809
+ { key: "sku", label: "SKU", type: "text", searchable: true },
2810
+ { key: "isActive", label: "Active", type: "boolean" },
2811
+ { key: "createdAt", label: "Created at", type: "text" }
2812
+ ],
2813
+ data: {
2814
+ table: "products",
2815
+ tenantScoped: true,
2816
+ archetype: "product",
2817
+ archetypeKind: "sale",
2818
+ searchColumns: ["name", "sku"],
2819
+ defaults: { kind: "sale", status: "active", is_active: true }
2820
+ }
2821
+ }
2822
+ }
2823
+ ],
2824
+ declaredLimits: [
2825
+ { key: "products", label: "Products", table: "products" },
2826
+ ...config.modules.recipes ? [{ key: "recipes", label: config.labels.recipes ?? "Recipes", table: "plg_inventory_recipes" }] : []
2827
+ ],
2777
2828
  navigation: [
2778
2829
  {
2779
2830
  section: options?.navSection ?? "main",
@@ -2788,7 +2839,11 @@ function createInventoryPlugin(options) {
2788
2839
  {
2789
2840
  path: "/inventory",
2790
2841
  component: PageComponent,
2791
- permission: { feature: "inventory", action: "read" }
2842
+ permission: { feature: "inventory", action: "read" },
2843
+ // Products live on this hand-built page, not a CRUD route, so nothing
2844
+ // would register them and a product the assistant just created would
2845
+ // resolve to no page at all.
2846
+ entityRoutes: ["product:sale", "product"]
2792
2847
  }
2793
2848
  ],
2794
2849
  widgets: [],