UNPKG

medusa-plugin-tolgee

Version:

Medusa V2 Plugin to easily add translations to your models with Tolgee

569 lines (568 loc) 25.3 kB
"use strict"; const adminSdk = require("@medusajs/admin-sdk"); const jsxRuntime = require("react/jsx-runtime"); const reactQuery = require("@tanstack/react-query"); const react$1 = require("@tolgee/react"); const tools = require("@tolgee/web/tools"); const ui = require("@medusajs/ui"); const Medusa = require("@medusajs/js-sdk"); const react = require("react"); const reactRouterDom = require("react-router-dom"); const icons = require("@medusajs/icons"); const reactI18next = require("react-i18next"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa); const formatKeyName = (keyName) => { const parts = keyName.split("."); if (parts.length > 1) { return parts[1].charAt(0).toUpperCase() + parts[1].slice(1); } return keyName; }; const sdk = new Medusa__default.default({ baseUrl: __BACKEND_URL__ ?? "/", auth: { type: "session" } }); const Container = (props) => { return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { ...props, className: ui.clx( "divide-y p-0", props.className ) }); }; const ActionMenu = ({ groups }) => { return /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu, { children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", children: /* @__PURE__ */ jsxRuntime.jsx(icons.EllipsisHorizontal, {}) }) }), /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Content, { children: groups.map((group, index) => { if (!group.actions.length) { return null; } const isLast = index === groups.length - 1; return /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu.Group, { children: [ group.actions.map((action, index2) => { if (action.onClick) { return /* @__PURE__ */ jsxRuntime.jsxs( ui.DropdownMenu.Item, { disabled: action.disabled, onClick: (e) => { e.stopPropagation(); action.onClick(); }, className: ui.clx( "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", { "[&_svg]:text-ui-fg-disabled": action.disabled } ), children: [ action.icon, /* @__PURE__ */ jsxRuntime.jsx("span", { children: action.label }) ] }, index2 ); } return /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx( ui.DropdownMenu.Item, { className: ui.clx( "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", { "[&_svg]:text-ui-fg-disabled": action.disabled } ), asChild: true, disabled: action.disabled, children: /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Link, { to: action.to, onClick: (e) => e.stopPropagation(), children: [ action.icon, /* @__PURE__ */ jsxRuntime.jsx("span", { children: action.label }) ] }) } ) }, index2); }), !isLast && /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Separator, {}) ] }, index); }) }) ] }); }; const Header = ({ title, subtitle, actions = [] }) => { return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title }), subtitle && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: subtitle }) ] }), actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center gap-x-2", children: actions.map((action, index) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [ action.type === "button" && /* @__PURE__ */ jsxRuntime.jsx( ui.Button, { ...action.props, size: action.props.size || "small", children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ action.props.children, action.link && /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { ...action.link }) ] }) } ), action.type === "action-menu" && /* @__PURE__ */ jsxRuntime.jsx(ActionMenu, { ...action.props }), action.type === "custom" && action.children ] }, index)) }) ] }); }; const SectionRow = ({ title, value, actions }) => { const isValueString = typeof value === "string" || !value; return /* @__PURE__ */ jsxRuntime.jsxs( "div", { className: ui.clx( `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`, { "grid-cols-[1fr_1fr_28px]": !!actions } ), children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", leading: "compact", children: title }), isValueString ? /* @__PURE__ */ jsxRuntime.jsx( ui.Text, { size: "small", leading: "compact", className: "whitespace-pre-line text-pretty", children: value ?? "-" } ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: value }), actions && /* @__PURE__ */ jsxRuntime.jsx("div", { children: actions }) ] } ); }; const TranslationManagement = ({ id, slug, availableLanguages }) => { const { t: adminT } = reactI18next.useTranslation("tolgee"); const { t } = react$1.useTranslate(id); const tolgee = react$1.useTolgee(); const client = reactQuery.useQueryClient(); const handleLanguageChange = async (lang) => { if (!tolgee) return; await tolgee.changeLanguage(lang); }; const { mutateAsync: syncTranslation, isPending: syncing } = reactQuery.useMutation({ mutationFn: async () => { await sdk.client.fetch(`/admin/tolgee/translation/${slug}`, { method: "post" }); }, onSuccess: () => { ui.toast.success("Success", { description: "Translations sync successful." }); client.invalidateQueries({ queryKey: ["tolgee-translations", slug] }); tolgee == null ? void 0 : tolgee.addPlugin(tools.InContextTools()); }, onError: (error) => { console.error(`Failed to sync translations for ${slug}:`, error); ui.toast.error("Error", { description: "Failed to sync translations." }); }, mutationKey: ["syncTranslation"] }); const { mutateAsync: addTranslation, isPending: adding } = reactQuery.useMutation({ mutationFn: () => sdk.client.fetch(`/admin/tolgee/translation/${slug}/${id}`, { method: "post" }), onSuccess: () => { ui.toast.success("Success", { description: "Translation created." }); client.invalidateQueries({ queryKey: ["tolgee-translations", slug, id] }); tolgee == null ? void 0 : tolgee.addPlugin(tools.InContextTools()); }, onError: (error) => { console.error(`Failed to create translation for ${slug}(${id}):`, error.message); ui.toast.error("Error", { description: "Failed to create translation." }); }, mutationKey: ["addTranslation"] }); const { data: { keyNames = [] } = {}, isLoading } = reactQuery.useQuery({ queryFn: async () => await sdk.client.fetch(`/admin/tolgee/translation/${slug}/${id}`), queryKey: ["tolgee-translations", slug, id] }); const syncAllAction = { type: "button", props: { children: adminT("widget.syncAll"), onClick: () => syncTranslation(), isLoading: syncing, variant: "secondary" } }; const addTranslationAction = { type: "button", props: { children: adminT("widget.add"), onClick: () => addTranslation(), isLoading: adding, variant: "secondary" } }; return /* @__PURE__ */ jsxRuntime.jsxs(Container, { children: [ /* @__PURE__ */ jsxRuntime.jsx( Header, { title: adminT("widget.title"), subtitle: (keyNames == null ? void 0 : keyNames.length) > 0 ? adminT("widget.subtitle") : adminT("widget.subtitleEmpty"), actions: (keyNames == null ? void 0 : keyNames.length) <= 0 ? [addTranslationAction, syncAllAction] : [{ type: "custom", children: /* @__PURE__ */ jsxRuntime.jsxs( ui.Select, { onValueChange: handleLanguageChange, value: tolgee.getLanguage(), children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: adminT("widget.selectLanguage") }) }), /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: availableLanguages.map((item) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: item.tag, children: item.label }, item.tag)) }) ] } ) }] } ), isLoading ? /* @__PURE__ */ jsxRuntime.jsx(SectionRow, { title: adminT("loading") }) : keyNames.map( (keyName) => /* @__PURE__ */ jsxRuntime.jsx( SectionRow, { title: formatKeyName(keyName), value: t(keyName, adminT("widget.notTranslated")) }, keyName ) ) ] }); }; const $schema$1 = "./$schema.json"; const shippingOptionsList$1 = { title: "Translations", empty: "No translations present yet." }; const productOptionsList$1 = { title: "Options translations", empty: "No translations present yet." }; const widget$1 = { title: "Translations", subtitle: "To translate, ALT+click on the value.", subtitleEmpty: "No translations present yet.", notTranslated: "Not translated (press ALT + click here)", add: "Add", syncAll: "Sync all", selectLanguage: "Select a language" }; const loading$1 = "Loading..."; const en = { $schema: $schema$1, shippingOptionsList: shippingOptionsList$1, productOptionsList: productOptionsList$1, widget: widget$1, loading: loading$1 }; const $schema = "./$schema.json"; const shippingOptionsList = { title: "Traduzioni", empty: "Non sono ancora presenti traduzioni." }; const productOptionsList = { title: "Traduzioni opzioni", empty: "Non sono ancora presenti traduzioni." }; const widget = { title: "Traduzioni", subtitle: "Per tradurre, premi ALT+click sul valore.", subtitleEmpty: "Non sono ancora presenti traduzioni.", notTranslated: "Non tradotto (premi ALT + click qui)", add: "Aggiungi", syncAll: "Sincronizza tutto", selectLanguage: "Seleziona lingua" }; const loading = "Caricamento..."; const it = { $schema, shippingOptionsList, productOptionsList, widget, loading }; const translations = { en, it }; const PluginI18n = ({ children }) => { const { i18n } = reactI18next.useTranslation(); react.useEffect(() => { if (!i18n.isInitialized) return; if (i18n.hasResourceBundle("en", "tolgee")) return; for (const [lng, resource] of Object.entries(translations)) i18n.addResourceBundle(lng, "tolgee", resource); }, []); return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ " ", children, " " ] }); }; const TranslationWidget = (slug) => ({ data: { id } }) => { const { options, tolgeeInstance } = useTolgeeFromOptions(); return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(PluginI18n, { children: options && tolgeeInstance ? /* @__PURE__ */ jsxRuntime.jsx(react$1.TolgeeProvider, { tolgee: tolgeeInstance, fallback: "Loading...", children: /* @__PURE__ */ jsxRuntime.jsx( TranslationManagement, { id, slug, availableLanguages: options.availableLanguages || [] } ) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { children: "Loading..." }) }) }); }; const useTolgeeFromOptions = () => { const { data: { options, tolgeeInstance } = {} } = reactQuery.useQuery({ queryFn: () => sdk.client.fetch("/admin/tolgee/options").then((options2) => { const languages = options2.availableLanguages.map((lang) => lang.tag); const tolgeeInstance2 = react$1.Tolgee().use(react$1.FormatSimple()).init({ language: options2.defaultLanguage, apiUrl: options2.apiUrl, apiKey: options2.apiKey, availableLanguages: languages, observerOptions: { highlightColor: "rgba(0,0,0,0.7)" } }); tolgeeInstance2.addPlugin(tools.InContextTools()); return { options: options2, tolgeeInstance: tolgeeInstance2 }; }), queryKey: ["tolgee-options"] }); return { options, tolgeeInstance }; }; adminSdk.defineWidgetConfig({ zone: "product_category.details.after" }); const WidgetComponent0 = TranslationWidget("product_category"); adminSdk.defineWidgetConfig({ zone: "product_collection.details.after" }); const WidgetComponent1 = TranslationWidget("product_collection"); const ShippingOptionCard = ({ labelKey, descriptionKey }) => { return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shadow-elevation-card-rest bg-ui-bg-component transition-fg hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed group-focus-visible:shadow-borders-interactive-with-active rounded-md px-4 py-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 justify-between", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-start", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: labelKey }), /* @__PURE__ */ jsxRuntime.jsx( ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: descriptionKey } ) ] }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-7 items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(icons.TriangleRightMini, { className: "text-ui-fg-muted" }) }) ] }) }) }); }; const ProductOptionWidget = TranslationWidget("product_option"); const ProductOptionValueWidget = TranslationWidget("product_option_value"); const ProductOptionsInProductWidget = ({ data }) => { const { options } = data; const { t } = reactI18next.useTranslation("tolgee"); const isEmpty = !(options == null ? void 0 : options.length); return /* @__PURE__ */ jsxRuntime.jsx(PluginI18n, { children: /* @__PURE__ */ jsxRuntime.jsxs(Container, { children: [ /* @__PURE__ */ jsxRuntime.jsx(Header, { title: t("productOptionsList.title"), subtitle: isEmpty ? t("productOptionsList.empty") : void 0 }), !isEmpty && OptionsGrid(options, t("widget.title")) ] }) }); }; adminSdk.defineWidgetConfig({ zone: "product.details.side.after" }); function OptionsGrid(options, title) { return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 grid gap-4 grid-cols-1 sm:grid-cols-2", children: options == null ? void 0 : options.map((option) => { var _a; return /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer, { modal: false, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("button", { children: /* @__PURE__ */ jsxRuntime.jsx(ShippingOptionCard, { labelKey: option.title, descriptionKey: "" }) }) }), /* @__PURE__ */ jsxRuntime.jsxs( ui.Drawer.Content, { onInteractOutside: (e) => e.preventDefault(), className: "bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-ui-code-bg-base flex items-center justify-between px-6 py-4", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-4", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { className: "text-ui-contrast-fg-primary", children: title }) }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { className: "sr-only", children: `Drawer with translations for product option ${option.title}` }) ] }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Kbd, { className: "bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary", children: "esc" }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( ui.IconButton, { size: "small", variant: "transparent", className: "text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active", children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) } ) }) ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { className: "flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3", children: /* @__PURE__ */ jsxRuntime.jsxs( react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-full flex-col" }), children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { className: "text-ui-contrast-fg-primary ps-2 pb-4", children: "Option title" }), /* @__PURE__ */ jsxRuntime.jsx(ProductOptionWidget, { data: option }, option.id), /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { className: "h-8" }), /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { className: "text-ui-contrast-fg-primary ps-2 pb-4", children: "Option values:" }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-y-2", children: (_a = option.values) == null ? void 0 : _a.map((value) => /* @__PURE__ */ jsxRuntime.jsx(ProductOptionValueWidget, { data: value }, value.id)) }) ] } ) }) }) ] } ) ] }, option.id); }) }); } adminSdk.defineWidgetConfig({ zone: "product_tag.details.after" }); const WidgetComponent3 = TranslationWidget("product_tag"); adminSdk.defineWidgetConfig({ zone: "product_type.details.after" }); const WidgetComponent4 = TranslationWidget("product_type"); adminSdk.defineWidgetConfig({ zone: "product_variant.details.after" }); const WidgetComponent5 = TranslationWidget("product_variant"); adminSdk.defineWidgetConfig({ zone: "product.details.after" }); const WidgetComponent6 = TranslationWidget("product"); const ShippingOptionWidget = TranslationWidget("shipping_option"); const SOWidget = ({ data }) => { var _a; const { id } = data; const { t } = reactI18next.useTranslation("tolgee"); const { data: { stock_location } = {}, isLoading } = reactQuery.useQuery({ queryFn: () => sdk.admin.stockLocation.retrieve(id, { fields: "name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile,*fulfillment_providers" }), queryKey: ["shipping_options"] }); const shipping_options = ((_a = stock_location == null ? void 0 : stock_location.fulfillment_sets) == null ? void 0 : _a.flatMap((fs) => fs.service_zones.flatMap((sz) => sz.shipping_options.map((option) => ({ fset_type: `${fs.type[0].toUpperCase()}${fs.type.slice(1)}`, zone_name: sz.name, option }))))) ?? []; const isEmpty = !(shipping_options == null ? void 0 : shipping_options.length); return /* @__PURE__ */ jsxRuntime.jsx(PluginI18n, { children: /* @__PURE__ */ jsxRuntime.jsxs(Container, { children: [ /* @__PURE__ */ jsxRuntime.jsx(Header, { title: t("shippingOptionsList.title"), subtitle: isEmpty ? t("shippingOptionsList.empty") : void 0 }), isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: "Loading..." }) : !isEmpty && ShippingOptionsGrid(shipping_options, t("widget.title")) ] }) }); }; adminSdk.defineWidgetConfig({ zone: "location.details.side.after" }); function ShippingOptionsGrid(shipping_options, title) { return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 grid gap-4 grid-cols-1 sm:grid-cols-2", children: shipping_options == null ? void 0 : shipping_options.map(({ option, zone_name, fset_type }) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer, { modal: false, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("button", { children: /* @__PURE__ */ jsxRuntime.jsx(ShippingOptionCard, { labelKey: option.name, descriptionKey: `${fset_type} - ${zone_name}` }) }) }), /* @__PURE__ */ jsxRuntime.jsxs( ui.Drawer.Content, { onInteractOutside: (e) => e.preventDefault(), className: "bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-ui-code-bg-base flex items-center justify-between px-6 py-4", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-4", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { className: "text-ui-contrast-fg-primary", children: title }) }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { className: "sr-only", children: `Drawer with translations for shipping option ${option.name}` }) ] }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Kbd, { className: "bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary", children: "esc" }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( ui.IconButton, { size: "small", variant: "transparent", className: "text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active", children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) } ) }) ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { className: "flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3", children: /* @__PURE__ */ jsxRuntime.jsx( react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-full flex-col" }), children: /* @__PURE__ */ jsxRuntime.jsx(ShippingOptionWidget, { data: option }) } ) }) }) ] } ) ] }, option.id)) }); } const widgetModule = { widgets: [ { Component: WidgetComponent0, zone: ["product_category.details.after"] }, { Component: WidgetComponent1, zone: ["product_collection.details.after"] }, { Component: ProductOptionsInProductWidget, zone: ["product.details.side.after"] }, { Component: WidgetComponent3, zone: ["product_tag.details.after"] }, { Component: WidgetComponent4, zone: ["product_type.details.after"] }, { Component: WidgetComponent5, zone: ["product_variant.details.after"] }, { Component: WidgetComponent6, zone: ["product.details.after"] }, { Component: SOWidget, zone: ["location.details.side.after"] } ] }; const routeModule = { routes: [] }; const menuItemModule = { menuItems: [] }; const formModule = { customFields: {} }; const displayModule = { displays: {} }; const plugin = { widgetModule, routeModule, menuItemModule, formModule, displayModule }; module.exports = plugin;