@gorgo/medusa-feed-yandex
Version:
A Medusa plugin that generates a product feed in YML (Yandex Market Language) format
1,358 lines • 50.8 kB
JavaScript
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import { defineRouteConfig } from "@medusajs/admin-sdk";
import { EllipsisHorizontal, SquareGreenSolid, SquareRedSolid, InformationCircleSolid, Rss, Pencil, Folder, Trash } from "@medusajs/icons";
import { DropdownMenu, IconButton, clx, Heading, Text, StatusBadge, Button, Tooltip, createDataTableColumnHelper, Badge, useDataTable, DataTable, FocusModal, Label, Input, Select, Container, Switch, useToggleState, toast, Drawer, Prompt } from "@medusajs/ui";
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
import React, { useState, useMemo, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next, useTranslation } from "react-i18next";
import Medusa from "@medusajs/js-sdk";
import { formatDistance, sub, format } from "date-fns";
import { enUS, ru as ru$1 } from "date-fns/locale";
const SingleColumnLayout = ({ children }) => {
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-3", children });
};
const TwoColumnLayout = ({
firstCol,
secondCol
}) => {
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3 xl:flex-row xl:gap-x-4 xl:items-start", children: [
/* @__PURE__ */ jsx("div", { className: "flex flex-col w-full gap-y-3", children: firstCol }),
/* @__PURE__ */ jsx("div", { className: "flex flex-col w-full xl:max-w-[440px] gap-y-3 xl:mt-0", children: secondCol })
] });
};
const $schema$1 = "./$schema.json";
const general$1 = {
active: "Active",
inactive: "Inactive",
error: "Error",
success: "Success",
of: "of",
results: "results",
pages: "pages",
next: "Next",
prev: "Previous"
};
const actions$1 = {
create: "Create",
edit: "Edit",
"delete": "Delete",
deleteFile: "Delete file",
save: "Save",
cancel: "Cancel",
launchNow: "Launch now"
};
const dateTime$1 = {
days: "d",
hours: "h",
minutes: "min"
};
const feeds$1 = {
domain: "Feeds",
subtitle: "Create feeds for scheduled data exports",
create: {
title: "Create feed",
description: "Create and manage feed settings."
},
fields: {
title: "Title",
fileName: "File name",
filePath: "File path",
lastExport: "Last export",
status: "Status",
schedule: "Export interval",
id: "ID",
feedUrl: "Feed link",
created: "Created",
updated: "Updated"
},
edit: {
title: "Edit feed",
description: "Update feed configuration."
},
toasts: {
exportLaunched: "Feed successfully launched",
categoriesSaved: "Categories saved successfully",
categoriesSaveFailed: "Failed to save categories"
},
prompts: {
deleteFeed: {
title: "Are you sure?",
description: "You are about to delete the feed. This action cannot be undone."
},
deleteFeedFile: {
title: "Are you sure?",
description: "You are about to delete the feed file. This action cannot be undone."
}
},
tooltips: {
schedule: "Set the feed export interval"
},
activityContainer: {
title: "Activity",
subtitle: "If unchecked, the feed will not be exported on schedule."
}
};
const settings$1 = {
shop: {
title: "Shop",
subtitle: "Shop data for the feed file",
fields: {
name: "Shop name",
company: "Company",
url: "Shop URL",
platform: "Platform"
},
edit: {
title: "Edit shop",
description: "Update shop data"
}
},
categories: {
title: "Product categories",
subtitle: "Select product categories for export"
}
};
const en = {
$schema: $schema$1,
general: general$1,
actions: actions$1,
dateTime: dateTime$1,
feeds: feeds$1,
settings: settings$1
};
const $schema = "./$schema.json";
const general = {
active: "Активен",
inactive: "Неактивен",
error: "Ошибка",
success: "Успешно",
of: "из",
results: "результатов",
pages: "страниц",
next: "Далее",
prev: "Назад"
};
const actions = {
create: "Создать",
edit: "Редактировать",
"delete": "Удалить",
deleteFile: "Удалить файл",
save: "Сохранить",
cancel: "Отмена",
launchNow: "Запустить сейчас"
};
const dateTime = {
days: "д",
hours: "ч",
minutes: "мин"
};
const feeds = {
domain: "Фиды",
subtitle: "Создавайте фиды для выгрузки данных по расписанию",
create: {
title: "Создать фид",
description: "Создавайте и управляйте настройками фида."
},
fields: {
title: "Название",
fileName: "Имя файла",
filePath: "Путь к файлу",
lastExport: "Последний экспорт",
status: "Статус",
schedule: "Интервал экспорта",
id: "ID",
feedUrl: "Ссылка на фид",
created: "Создано",
updated: "Обновлено"
},
edit: {
title: "Редактировать фид",
description: "Обновить конфигурацию фида."
},
toasts: {
exportLaunched: "Фид успешно запущен",
categoriesSaved: "Категории успешно сохранены",
categoriesSaveFailed: "Не получучилось сохранить категории"
},
prompts: {
deleteFeed: {
title: "Вы уверены?",
description: "Вы собираетесь удалить фид. Это действие нельзя отменить."
},
deleteFeedFile: {
title: "Вы уверены?",
description: "Вы собираетесь удалить файл фида. Это действие нельзя отменить."
}
},
tooltips: {
schedule: "Установите интервал для экспорта фида"
},
activityContainer: {
title: "Активность",
subtitle: "Если флаг снят, фид не будет экспортироваться по расписанию."
}
};
const settings = {
shop: {
title: "Магазин",
subtitle: "Данные магазина для файла фида",
fields: {
name: "Название магазина",
company: "Компания",
url: "URL магазина",
platform: "Платформа"
},
edit: {
title: "Редактировать магазин",
description: "Обновить данные магазина"
}
},
categories: {
title: "Категории товаров",
subtitle: "Выберите категории товаров для экспорта"
}
};
const ru = {
$schema,
general,
actions,
dateTime,
feeds,
settings
};
const i18nTranslations0 = {
en: {
translation: en
},
ru: {
translation: ru
}
};
const defaultI18nOptions = {
debug: process.env.NODE_ENV === "development",
fallbackLng: "en",
interpolation: {
escapeValue: false
},
detection: {
caches: ["cookie", "localStorage", "header"],
lookupCookie: "lng",
lookupLocalStorage: "lng",
order: ["cookie", "localStorage", "header"]
},
resources: i18nTranslations0,
supportedLngs: Object.keys(i18nTranslations0)
};
const i18nInstance = i18n.createInstance();
if (!i18n.isInitialized) {
i18nInstance.use(
new LanguageDetector(null, {
lookupCookie: "lng",
lookupLocalStorage: "lng"
})
).use(initReactI18next).init(defaultI18nOptions);
}
const I18n = () => null;
const sdk = new Medusa({
baseUrl: __BACKEND_URL__ || "/",
auth: {
type: "session"
}
});
const ActionMenu = ({ groups }) => {
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
/* @__PURE__ */ jsx(DropdownMenu.Content, { children: groups.map((group, groupIdx) => {
if (!group.actions.length) return null;
const commonItemClasses = "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2";
const isLastGroup = groupIdx === groups.length - 1;
return /* @__PURE__ */ jsxs(DropdownMenu.Group, { children: [
group.actions.map((action, actionIdx) => {
const itemClasses = clx(commonItemClasses, {
"[&_svg]:text-ui-fg-disabled": action.disabled
});
if (action.onClick) {
return /* @__PURE__ */ jsxs(
DropdownMenu.Item,
{
disabled: action.disabled,
onClick: (e) => {
e.stopPropagation();
action.onClick();
},
className: itemClasses,
children: [
action.icon,
/* @__PURE__ */ jsx("span", { children: action.label })
]
},
actionIdx
);
}
return /* @__PURE__ */ jsx(
DropdownMenu.Item,
{
asChild: true,
disabled: action.disabled,
className: itemClasses,
children: /* @__PURE__ */ jsxs(Link, { to: action.to, onClick: (e) => e.stopPropagation(), children: [
action.icon,
/* @__PURE__ */ jsx("span", { children: action.label })
] })
},
actionIdx
);
}),
!isLastGroup && /* @__PURE__ */ jsx(DropdownMenu.Separator, {})
] }, groupIdx);
}) })
] });
};
const Header = ({
title,
subtitle,
status,
actions: actions2 = []
}) => {
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx(Heading, { level: "h2", children: title }),
subtitle && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: subtitle })
] }),
(status || actions2.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-x-2", children: [
status && /* @__PURE__ */ jsx(StatusBadge, { color: status.color, children: status.text }),
actions2.map((action, idx) => {
switch (action.type) {
case "button":
return action.link ? /* @__PURE__ */ jsx(Link, { ...action.link, children: /* @__PURE__ */ jsx(
Button,
{
...action.props,
size: action.props.size || "small"
}
) }, idx) : /* @__PURE__ */ jsx(
Button,
{
...action.props,
size: action.props.size || "small"
},
idx
);
case "action-menu":
return /* @__PURE__ */ jsx(ActionMenu, { ...action.props }, idx);
case "custom":
return /* @__PURE__ */ jsx(React.Fragment, { children: action.children }, idx);
default:
return null;
}
})
] })
] });
};
const PlaceholderCell = () => {
return /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center", children: /* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "-" }) });
};
const languages = [
{
code: "en",
date_locale: enUS
},
{
code: "ru",
date_locale: ru$1
}
];
const useDate = () => {
var _a;
const { i18n: i18n2 } = useTranslation();
const locale = ((_a = languages.find((l) => l.code === i18n2.language)) == null ? void 0 : _a.date_locale) || enUS;
const getFullDate = ({
date,
includeTime = false
}) => {
const ensuredDate = new Date(date);
if (isNaN(ensuredDate.getTime())) {
return "";
}
const timeFormat = includeTime ? "p" : "";
return format(ensuredDate, `PP ${timeFormat}`, {
locale
});
};
function getRelativeDate(date) {
const now = /* @__PURE__ */ new Date();
return formatDistance(sub(new Date(date), { minutes: 0 }), now, {
addSuffix: true,
locale
});
}
return {
getFullDate,
getRelativeDate
};
};
const DateCell = ({ date, mode = "full" }) => {
const { getFullDate, getRelativeDate } = useDate();
if (!date) {
return /* @__PURE__ */ jsx(PlaceholderCell, {});
}
let displayDate;
switch (mode) {
case "full":
displayDate = getFullDate({ date, includeTime: false });
break;
case "relative":
displayDate = getRelativeDate(date);
break;
}
return /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center overflow-hidden", children: /* @__PURE__ */ jsx(
Tooltip,
{
className: "z-10",
content: /* @__PURE__ */ jsx("span", { className: "text-pretty", children: `${getFullDate({
date,
includeTime: true
})}` }),
children: /* @__PURE__ */ jsx("span", { className: "truncate", children: displayDate })
}
) });
};
const scheduleIntervals = [
10,
30,
60,
120,
180,
360,
720,
1440
];
const fileExtension = ".xml";
const getScheduleLabel = (minutes) => {
if (minutes === void 0 || minutes < 0) {
return "-";
}
const hours = Math.floor(minutes / 60);
switch (true) {
case hours == 0:
return `${minutes} ${i18nInstance.t("dateTime.minutes")}`;
case (hours >= 1 && hours < 24):
return `${hours} ${i18nInstance.t("dateTime.hours")}`;
case hours >= 24:
return `${Math.floor(hours / 24)} ${i18nInstance.t("dateTime.days")}`;
}
};
const PAGE_SIZE = 20;
const FeedListTable = ({
stateModal,
openModal
}) => {
const columnHelper = createDataTableColumnHelper();
const navigate = useNavigate();
const limit = PAGE_SIZE;
const [pagination, setPagination] = useState({
pageSize: limit,
pageIndex: 0
});
const offset = useMemo(() => pagination.pageIndex * limit, [pagination]);
const { data, isLoading } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/feeds`, {
query: { limit, offset }
}),
queryKey: [["feeds"]]
});
const columns = [
columnHelper.accessor("title", { header: i18nInstance.t("feeds.fields.title") }),
columnHelper.accessor("file_name", {
header: i18nInstance.t("feeds.fields.fileName"),
cell: ({ row }) => {
return row.original.file_name + fileExtension;
}
}),
columnHelper.accessor("file_path", {
header: i18nInstance.t("feeds.fields.feedUrl"),
cell: ({ row }) => {
const filePath = row.original.file_path;
const id = row.original.id;
const fileName = row.original.file_name;
if (!filePath || !id || !fileName) {
return /* @__PURE__ */ jsx(PlaceholderCell, {});
}
try {
const fullLink = `${window.location.origin}/feeds/${id}/${fileName}${fileExtension}`;
return /* @__PURE__ */ jsx("a", { href: fullLink, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(Badge, { size: "xsmall", children: /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-interactive", children: fullLink }) }) });
} catch {
return /* @__PURE__ */ jsx(PlaceholderCell, {});
}
}
}),
columnHelper.accessor("last_export_at", {
header: i18nInstance.t("feeds.fields.lastExport"),
cell: ({ getValue }) => {
const rawDate = getValue();
return /* @__PURE__ */ jsx(DateCell, { date: rawDate, mode: "relative" });
}
}),
columnHelper.accessor("is_active", {
header: i18nInstance.t("feeds.fields.status"),
cell: ({ getValue }) => {
const isActive = getValue();
if (isActive) {
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx(SquareGreenSolid, {}),
/* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "whitespace-pre-line text-pretty", children: i18nInstance.t("general.active") })
] });
} else {
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx(SquareRedSolid, {}),
/* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", className: "whitespace-pre-line text-pretty", children: i18nInstance.t("general.inactive") })
] });
}
}
}),
columnHelper.accessor("schedule", {
header: i18nInstance.t("feeds.fields.schedule"),
cell: ({ getValue }) => {
const value = getValue();
return /* @__PURE__ */ jsx(Badge, { size: "2xsmall", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", leading: "compact", children: getScheduleLabel(value) }) });
}
})
];
const table = useDataTable({
columns,
data: (data == null ? void 0 : data.feeds) || [],
getRowId: (row) => row.id,
rowCount: (data == null ? void 0 : data.count) || 0,
isLoading,
pagination: {
state: pagination,
onPaginationChange: setPagination
},
onRowClick(_, row) {
navigate(`${row.id}`);
}
});
return /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
/* @__PURE__ */ jsx(
Header,
{
title: i18nInstance.t("feeds.domain"),
actions: [
{
type: "button",
props: {
children: i18nInstance.t("actions.create"),
variant: "secondary",
onClick: () => openModal()
}
}
]
},
stateModal ? "create-open" : "create-closed"
),
/* @__PURE__ */ jsx(DataTable.Table, {}),
/* @__PURE__ */ jsx(DataTable.Pagination, {})
] });
};
const FeedCreateModal = ({
stateModal,
closeModal
}) => {
const [title, setTitle] = useState("");
const [fileName, setFileName] = useState("");
const [schedule, setSchedule] = useState(scheduleIntervals[1]);
const [isActive, setIsActive] = useState(false);
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: async (newFeed) => {
return sdk.client.fetch(`/admin/feeds`, {
method: "POST",
body: newFeed,
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [["feeds"]]
});
setTitle("");
setFileName("");
setSchedule(scheduleIntervals[1]);
setIsActive(false);
},
onError: (error) => {
console.error("Error creating feed:", error);
}
});
const saveFeed = () => {
const newFeed = [{
title,
file_name: fileName,
is_active: isActive,
schedule: Number(schedule)
}];
mutate({ feeds: newFeed });
closeModal();
};
return /* @__PURE__ */ jsx(FocusModal, { open: stateModal, onOpenChange: (open) => {
if (!open) closeModal();
}, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
/* @__PURE__ */ jsx(FocusModal.Header, {}),
/* @__PURE__ */ jsx(FocusModal.Body, { className: "flex flex-col items-center py-16", children: /* @__PURE__ */ jsxs("div", { className: "flex w-full max-w-lg flex-col gap-y-8", children: [
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
/* @__PURE__ */ jsx(Heading, { children: i18nInstance.t("feeds.create.title") }),
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: i18nInstance.t("feeds.create.description") })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "title", size: "small", children: i18nInstance.t("feeds.fields.title") }),
/* @__PURE__ */ jsx(Input, { id: "title", value: title, onChange: (e) => setTitle(e.target.value) })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "file_name", size: "small", children: i18nInstance.t("feeds.fields.fileName") }),
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
/* @__PURE__ */ jsx(Input, { id: "file_name", value: fileName, onChange: (e) => setFileName(e.target.value) }),
/* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-0 z-10 flex w-12 items-center justify-center border-l", children: /* @__PURE__ */ jsx("p", { className: "font-medium font-sans txt-compact-small text-ui-fg-muted", children: fileExtension }) })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "schedule_selector", size: "small", children: i18nInstance.t("feeds.fields.schedule") }),
/* @__PURE__ */ jsx(Tooltip, { content: i18nInstance.t("feeds.tooltips.schedule"), children: /* @__PURE__ */ jsx(InformationCircleSolid, { className: "text-ui-fg-subtle" }) })
] }),
/* @__PURE__ */ jsxs(Select, { onValueChange: (v) => setSchedule(Number(v)), value: String(schedule), children: [
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, {}) }),
/* @__PURE__ */ jsx(Select.Content, { sideOffset: 100, children: scheduleIntervals.map((value) => /* @__PURE__ */ jsx(Select.Item, { value: String(value), children: getScheduleLabel(value) }, value)) })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "is-active-switch", size: "small", children: i18nInstance.t("feeds.activityContainer.title") }),
/* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsxs("div", { className: "flex gap-x-4", children: [
/* @__PURE__ */ jsx(Switch, { id: "is-active-switch", checked: isActive, onCheckedChange: () => setIsActive((prev) => !prev) }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
/* @__PURE__ */ jsx(Label, { size: "small", htmlFor: "is-active-switch", children: i18nInstance.t("general.active") }),
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: i18nInstance.t("feeds.activityContainer.subtitle") })
] })
] }) })
] })
] }) }),
/* @__PURE__ */ jsxs(FocusModal.Footer, { children: [
/* @__PURE__ */ jsx(FocusModal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: i18nInstance.t("actions.cancel") }) }),
/* @__PURE__ */ jsx(Button, { onClick: saveFeed, children: i18nInstance.t("actions.save") })
] })
] }) });
};
const FeedsPage = () => {
const [stateModal, openModal, closeModal] = useToggleState();
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(I18n, {}),
/* @__PURE__ */ jsx(SingleColumnLayout, { children: /* @__PURE__ */ jsxs(Container, { className: "p-0", children: [
/* @__PURE__ */ jsx(FeedListTable, { stateModal, openModal }),
/* @__PURE__ */ jsx(FeedCreateModal, { stateModal, closeModal })
] }) })
] });
};
const config = defineRouteConfig({
label: i18nInstance.t("feeds.domain"),
icon: Rss
});
const handle$1 = {
breadcrumb: () => i18nInstance.t("feeds.domain")
};
const SectionRow = ({ title, value, actions: actions2, className }) => {
const isSimpleText = typeof value === "string" || value === null || value === void 0;
const hasActions = Boolean(actions2);
const containerClasses = clx(
"text-ui-fg-subtle grid items-center px-6 py-4",
hasActions ? "grid-cols-[1fr_1fr_28px]" : "grid-cols-2"
);
const textClasses = clx(
className,
isSimpleText ? "whitespace-pre-line text-pretty" : "flex flex-wrap gap-1"
);
return /* @__PURE__ */ jsxs("div", { className: containerClasses, children: [
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", leading: "compact", children: title }),
isSimpleText ? /* @__PURE__ */ jsx(
Text,
{
size: "small",
leading: "compact",
className: textClasses,
children: value ?? "-"
}
) : /* @__PURE__ */ jsx("div", { className: textClasses, children: value }),
hasActions && /* @__PURE__ */ jsx("div", { children: actions2 })
] });
};
const FeedGeneralSection = () => {
const { id } = useParams();
const navigate = useNavigate();
const [deleteFeedOpen, openDeleteFeed, closeDeleteFeed] = useToggleState();
const [deleteFeedFileOpen, openDeleteFeedFile, closeDeleteFeedFile] = useToggleState();
const [editOpen, openEdit, closeEdit] = useToggleState();
const [title, setTitle] = useState("");
const [fileName, setFileName] = useState("");
const [schedule, setSchedule] = useState();
const [isActive, setIsActive] = useState(true);
const { getFullDate, getRelativeDate } = useDate();
const { data, isError, error } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/feeds/${id}`),
queryKey: ["feed", id]
});
if (isError) {
throw error;
}
useEffect(() => {
if (data == null ? void 0 : data.feed) {
setTitle(data.feed.title);
setFileName(data.feed.file_name);
setIsActive(data.feed.is_active);
setSchedule(String(data.feed.schedule));
}
}, [data]);
const feed = data == null ? void 0 : data.feed;
const queryClient = useQueryClient();
const { mutate: updateFeedMutate } = useMutation({
mutationFn: async (updatedFeed) => {
return sdk.client.fetch(`/admin/feeds/${updatedFeed.id}`, {
method: "PATCH",
body: updatedFeed,
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
},
onError: (error2) => {
console.error("Error updating feed:", error2);
}
});
const { mutate: deleteFeedMutate } = useMutation({
mutationFn: async (feedId) => {
return sdk.client.fetch(`/admin/feeds/${feedId.ids[0]}`, {
method: "DELETE",
body: feedId,
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
},
onError: (error2) => {
console.error("Error deleting feed:", error2);
}
});
const { mutate: deleteFeedFileMutate } = useMutation({
mutationFn: async (feedId) => {
return sdk.client.fetch(`/admin/feeds/${feedId}/delete-file`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
},
onError: (error2) => {
console.error("Error deleting file:", error2);
}
});
const { mutate: launchFeedMutate } = useMutation({
mutationFn: async (feedId) => {
return sdk.client.fetch(`/admin/feeds/${feedId}/launch`, {
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
toast.success(i18nInstance.t("general.success"), {
description: i18nInstance.t("feeds.toasts.exportLaunched")
});
},
onError: (error2) => {
console.error("Error launching feed:", error2);
}
});
const saveFeedSettings = () => {
const updatedFeed = {
id,
title,
file_name: fileName,
is_active: isActive,
schedule: Number(schedule)
};
updateFeedMutate(updatedFeed);
closeEdit();
};
const deleteFeed = () => {
const deletedFeed = {
ids: [id]
};
deleteFeedMutate(deletedFeed);
navigate(`../`);
};
const deleteFeedFile = () => {
deleteFeedFileMutate(id);
closeDeleteFeedFile();
};
const launchFeed = async () => {
await launchFeedMutate(id);
};
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
/* @__PURE__ */ jsx(
Header,
{
title: feed == null ? void 0 : feed.title,
status: {
color: (feed == null ? void 0 : feed.is_active) ? "green" : "red",
text: (feed == null ? void 0 : feed.is_active) ? i18nInstance.t("general.active") : i18nInstance.t("general.inactive")
},
actions: [
{
type: "button",
props: {
children: i18nInstance.t("actions.launchNow"),
variant: "secondary",
onClick: () => {
launchFeed();
}
}
},
{
type: "action-menu",
props: {
groups: [
{
actions: [
{
icon: /* @__PURE__ */ jsx(Pencil, {}),
label: i18nInstance.t("actions.edit"),
onClick: () => openEdit()
},
{
icon: /* @__PURE__ */ jsx(Folder, {}),
label: i18nInstance.t("actions.deleteFile"),
onClick: () => openDeleteFeedFile()
},
{
icon: /* @__PURE__ */ jsx(Trash, {}),
label: i18nInstance.t("actions.delete"),
onClick: () => openDeleteFeed()
}
]
}
]
}
}
]
},
`${editOpen ? "edit-open" : "edit-closed"}-${deleteFeedOpen ? "delete-feed-open" : "delete-feed-closed"}-${deleteFeedFileOpen ? "delete-feed-file-open" : "delete-feed-file-closed"}`
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.id"),
value: (feed == null ? void 0 : feed.id) || "-"
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.fileName"),
value: (feed == null ? void 0 : feed.file_name) + fileExtension || "-",
className: "break-all"
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.feedUrl"),
value: (feed == null ? void 0 : feed.file_path) && (feed == null ? void 0 : feed.id) && (feed == null ? void 0 : feed.file_name) ? (() => {
const feedViewUrl = `${window.location.origin}/feeds/${feed.id}/${feed.file_name}${fileExtension}`;
return /* @__PURE__ */ jsx("a", { href: feedViewUrl, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(Badge, { size: "base", className: "h-full", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-interactive break-all", children: feedViewUrl }) }) });
})() : ""
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.filePath"),
value: (feed == null ? void 0 : feed.file_path) ? /* @__PURE__ */ jsx("a", { href: feed.file_path, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(Badge, { size: "base", className: "h-full", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-interactive break-all", children: feed.file_path }) }) }) : ""
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.schedule"),
value: (feed == null ? void 0 : feed.schedule) ? (() => {
const interval = scheduleIntervals.find(
(value) => value === feed.schedule
);
return /* @__PURE__ */ jsx(Badge, { size: "2xsmall", children: /* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", children: getScheduleLabel(interval || feed.schedule) }) });
})() : ""
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.lastExport"),
value: (feed == null ? void 0 : feed.last_export_at) ? /* @__PURE__ */ jsx(
Tooltip,
{
className: "z-10",
content: /* @__PURE__ */ jsx("span", { className: "text-pretty", children: `${getFullDate({
date: feed.last_export_at,
includeTime: true
})}` }),
children: /* @__PURE__ */ jsx(
Text,
{
size: "small",
leading: "compact",
className: "whitespace-pre-line text-pretty",
children: getRelativeDate(feed.last_export_at)
}
)
}
) : ""
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.created"),
value: (feed == null ? void 0 : feed.created_at) ? getFullDate({
date: feed.created_at,
includeTime: true
}) : ""
}
),
/* @__PURE__ */ jsx(
SectionRow,
{
title: i18nInstance.t("feeds.fields.updated"),
value: (feed == null ? void 0 : feed.updated_at) ? getFullDate({
date: feed.updated_at,
includeTime: true
}) : ""
}
),
/* @__PURE__ */ jsx(Drawer, { open: editOpen, onOpenChange: (open) => {
if (!open) closeEdit();
}, children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
/* @__PURE__ */ jsx(Drawer.Header, { children: /* @__PURE__ */ jsx(Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsx(Heading, { children: i18nInstance.t("feeds.edit.title") }) }) }),
/* @__PURE__ */ jsx(Drawer.Body, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4", children: [
/* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsxs("div", { className: "flex gap-x-4", children: [
/* @__PURE__ */ jsx(Switch, { id: "is-active-switch", checked: isActive, onCheckedChange: () => setIsActive((prev) => !prev) }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
/* @__PURE__ */ jsx(Label, { size: "small", htmlFor: "is-active-switch", children: i18nInstance.t("general.active") }),
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: i18nInstance.t("feeds.activityContainer.subtitle") })
] })
] }) }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "title", size: "small", children: i18nInstance.t("feeds.fields.title") }),
/* @__PURE__ */ jsx(Input, { id: "title", value: title, onChange: (e) => setTitle(e.target.value) })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { size: "small", htmlFor: "feed-file-name-input", children: i18nInstance.t("feeds.fields.fileName") }),
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
/* @__PURE__ */ jsx(Input, { className: "pr-14", id: "feed-file-name-input", value: fileName, onChange: (e) => setFileName(e.target.value) }),
/* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-0 z-10 flex w-12 items-center justify-center border-l", children: /* @__PURE__ */ jsx("p", { className: "font-medium font-sans txt-compact-small text-ui-fg-muted", children: fileExtension }) })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { size: "small", htmlFor: "schedule-selector", children: i18nInstance.t("feeds.fields.schedule") }),
/* @__PURE__ */ jsxs(Select, { value: schedule, onValueChange: setSchedule, children: [
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, {}) }),
/* @__PURE__ */ jsx(Select.Content, { sideOffset: 100, children: scheduleIntervals.map((value) => /* @__PURE__ */ jsx(Select.Item, { value: String(value), children: getScheduleLabel(value) }, value)) })
] })
] })
] }) }),
/* @__PURE__ */ jsx(Drawer.Footer, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-x-2", children: [
/* @__PURE__ */ jsx(Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", children: i18nInstance.t("actions.cancel") }) }),
/* @__PURE__ */ jsx(Button, { size: "small", type: "submit", onClick: saveFeedSettings, children: i18nInstance.t("actions.save") })
] }) })
] }) }),
/* @__PURE__ */ jsx(Prompt, { open: deleteFeedOpen, onOpenChange: (open) => {
if (!open) closeDeleteFeed();
}, children: /* @__PURE__ */ jsxs(Prompt.Content, { children: [
/* @__PURE__ */ jsxs(Prompt.Header, { children: [
/* @__PURE__ */ jsx(Prompt.Title, { children: i18nInstance.t("feeds.prompts.deleteFeed.title") }),
/* @__PURE__ */ jsx(Prompt.Description, { children: i18nInstance.t("feeds.prompts.deleteFeed.description") })
] }),
/* @__PURE__ */ jsxs(Prompt.Footer, { children: [
/* @__PURE__ */ jsx(Prompt.Cancel, { children: i18nInstance.t("actions.cancel") }),
/* @__PURE__ */ jsx(Prompt.Action, { onClick: () => deleteFeed(), children: i18nInstance.t("actions.delete") })
] })
] }) }),
/* @__PURE__ */ jsx(Prompt, { open: deleteFeedFileOpen, onOpenChange: (open) => {
if (!open) closeDeleteFeedFile();
}, children: /* @__PURE__ */ jsxs(Prompt.Content, { children: [
/* @__PURE__ */ jsxs(Prompt.Header, { children: [
/* @__PURE__ */ jsx(Prompt.Title, { children: i18nInstance.t("feeds.prompts.deleteFeedFile.title") }),
/* @__PURE__ */ jsx(Prompt.Description, { children: i18nInstance.t("feeds.prompts.deleteFeedFile.description") })
] }),
/* @__PURE__ */ jsxs(Prompt.Footer, { children: [
/* @__PURE__ */ jsx(Prompt.Cancel, { children: i18nInstance.t("actions.cancel") }),
/* @__PURE__ */ jsx(Prompt.Action, { onClick: () => deleteFeedFile(), children: i18nInstance.t("actions.delete") })
] })
] }) })
] });
};
function buildCategoryTree(categories) {
const map = /* @__PURE__ */ new Map();
const roots = [];
categories.forEach((cat) => {
map.set(cat.id, { ...cat, children: [] });
});
categories.forEach((cat) => {
const node = map.get(cat.id);
if (cat.parent_category_id && map.has(cat.parent_category_id)) {
map.get(cat.parent_category_id).children.push(node);
} else {
roots.push(node);
}
});
return roots;
}
function flattenCategoryTree(tree, level = 0) {
const result = [];
for (const node of tree) {
result.push({
...node,
name: `${" ".repeat(level)}${node.name}`
});
if (node.children.length) {
result.push(...flattenCategoryTree(node.children, level + 1));
}
}
return result;
}
const ProductCategoriesSection = () => {
var _a, _b;
const { id } = useParams();
const [rowSelection, setRowSelection] = useState({});
const { data: feedData, isError, error } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/feeds/${id}`),
queryKey: ["feed", id]
});
if (isError) {
throw error;
}
const feed = feedData == null ? void 0 : feedData.feed;
const selectedIds = ((_b = (_a = feed == null ? void 0 : feed.settings) == null ? void 0 : _a.categories) == null ? void 0 : _b.map((c) => c.id)) ?? [];
const columnHelper = createDataTableColumnHelper();
const columns = [
columnHelper.select(),
columnHelper.accessor("name", {
header: "Name",
cell: ({ getValue }) => /* @__PURE__ */ jsx(Text, { className: "whitespace-pre h-12 flex items-center", children: getValue() })
})
];
const { data: categoriesData, isLoading } = useQuery({
queryKey: ["categories"],
queryFn: async () => {
const { product_categories } = await sdk.admin.productCategory.list({
is_active: true,
fields: "id,name,rank,parent_category_id"
});
const tree = buildCategoryTree(product_categories);
return flattenCategoryTree(tree);
}
});
useEffect(() => {
if (categoriesData && selectedIds.length > 0) {
setRowSelection(
Object.fromEntries(selectedIds.map((id2) => [id2, true]))
);
}
}, [categoriesData, feedData]);
const isModified = useMemo(() => {
return JSON.stringify(rowSelection) !== JSON.stringify(Object.fromEntries(selectedIds.map((id2) => [id2, true])));
}, [rowSelection, selectedIds]);
const table = useDataTable({
columns,
data: categoriesData || [],
getRowId: (row) => row.id,
isLoading,
rowSelection: {
state: rowSelection,
onRowSelectionChange: setRowSelection
}
});
const selectedCategories = useMemo(() => {
if (!categoriesData) return [];
return categoriesData.filter((cat) => rowSelection[cat.id]).map((cat) => ({
id: cat.id,
parentId: cat.parent_category_id || void 0,
value: cat.name.trim()
}));
}, [rowSelection, categoriesData]);
const queryClient = useQueryClient();
const { mutate: updateFeedMutate } = useMutation({
mutationFn: async (updatedFeed) => {
return sdk.client.fetch(`/admin/feeds/${updatedFeed.id}`, {
method: "PATCH",
body: updatedFeed,
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", feed == null ? void 0 : feed.id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
},
onError: (error2) => {
console.error("Error updating feed:", error2);
}
});
const saveFeedCategories = (selectedCategories2) => {
const updatedFeed = {
id: feed == null ? void 0 : feed.id,
settings: { categories: selectedCategories2 }
};
updateFeedMutate(updatedFeed);
};
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
/* @__PURE__ */ jsx(
Header,
{
title: i18nInstance.t("settings.categories.title"),
subtitle: i18nInstance.t("settings.categories.subtitle"),
actions: [
{
type: "button",
props: {
children: i18nInstance.t("actions.save"),
variant: "secondary",
onClick: () => {
try {
saveFeedCategories(selectedCategories);
toast.success(i18nInstance.t("general.success"), {
description: i18nInstance.t("feeds.toasts.categoriesSaved")
});
} catch (e) {
console.error(e);
toast.error(i18nInstance.t("general.error"), {
description: i18nInstance.t("feeds.toasts.categoriesSaveFailed")
});
}
},
disabled: !isModified
}
}
]
}
),
/* @__PURE__ */ jsx(DataTable.Table, {})
] }) }) });
};
const ShopSettingsSection = () => {
var _a, _b, _c;
const { id } = useParams();
const [editShopOpen, openEditShop, closeEditShop] = useToggleState();
const [shopName, setShopName] = useState("");
const [shopCompany, setShopCompany] = useState("");
const [shopUrl, setShopUrl] = useState("");
const { data, isError, error } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/feeds/${id}`),
queryKey: ["feed", id]
});
if (isError) {
throw error;
}
useEffect(() => {
var _a2, _b2, _c2;
if (data == null ? void 0 : data.feed) {
setShopName((_a2 = data.feed.settings) == null ? void 0 : _a2.name);
setShopCompany((_b2 = data.feed.settings) == null ? void 0 : _b2.company);
setShopUrl((_c2 = data.feed.settings) == null ? void 0 : _c2.url);
}
}, [data]);
const feed = data == null ? void 0 : data.feed;
const queryClient = useQueryClient();
const { mutate: updateFeedMutate } = useMutation({
mutationFn: async (updatedFeed) => {
return sdk.client.fetch(`/admin/feeds/${updatedFeed.id}`, {
method: "PATCH",
body: updatedFeed,
headers: {
"Content-Type": "application/json"
}
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["feed", feed == null ? void 0 : feed.id]
});
queryClient.invalidateQueries({ queryKey: [["feeds"]] });
},
onError: (error2) => {
console.error("Error updating feed:", error2);
}
});
const saveShopSettings = () => {
const updatedFeed = {
id: feed == null ? void 0 : feed.id,
settings: {
name: shopName,
company: shopCompany,
url: shopUrl,
platform: "Medusa"
}
};
updateFeedMutate(updatedFeed);
closeEditShop();
};
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
/* @__PURE__ */ jsx(
Header,
{
title: i18nInstance.t("settings.shop.title"),
subtitle: i18nInstance.t("settings.shop.subtitle"),
actions: [
{
type: "action-menu",
props: {
groups: [
{
actions: [
{
icon: /* @__PURE__ */ jsx(Pencil, {}),
label: i18nInstance.t("actions.edit"),
onClick: () => openEditShop()
}
]
}
]
}
}
]
},
`${editShopOpen ? "edit-shop-open" : "edit-shop-closed"}`
),
/* @__PURE__ */ jsx(SectionRow, { title: i18nInstance.t("settings.shop.fields.name"), value: ((_a = feed == null ? void 0 : feed.settings) == null ? void 0 : _a.name) || "-" }),
/* @__PURE__ */ jsx(SectionRow, { title: i18nInstance.t("settings.shop.fields.company"), value: ((_b = feed == null ? void 0 : feed.settings) == null ? void 0 : _b.company) || "-" }),
/* @__PURE__ */ jsx(SectionRow, { title: i18nInstance.t("settings.shop.fields.url"), value: ((_c = feed == null ? void 0 : feed.settings) == null ? void 0 : _c.url) || "-" }),
/* @__PURE__ */ jsx(SectionRow, { title: i18nInstance.t("settings.shop.fields.platform"), value: "Medusa" }),
/* @__PURE__ */ jsx(Drawer, { open: editShopOpen, onOpenChange: (open) => {
if (!open) closeEditShop();
}, children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
/* @__PURE__ */ jsx(Drawer.Header, { children: /* @__PURE__ */ jsx(Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsx(Heading, { children: i18nInstance.t("feeds.edit.title") }) }) }),
/* @__PURE__ */ jsx(Drawer.Body, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "shop-name", size: "small", children: i18nInstance.t("settings.shop.fields.name") }),
/* @__PURE__ */ jsx(Input, { id: "shop-name", value: shopName, onChange: (e) => setShopName(e.target.value) })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { size: "small", htmlFor: "shop-company", children: i18nInstance.t("settings.shop.fields.company") }),
/* @__PURE__ */ jsx(Input, { id: "shop-company", value: shopCompany, onChange: (e) => setShopCompany(e.target.value) })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsx(Label, { htmlFor: "shop-url", size: "small", children: i18nInstance.t("settings.shop.fields.url") }),
/* @__PURE__ */ jsx(Input, { id: "shop-url", value: shopUrl, onChange: (e) => setShopUrl(e.target.value) })
] })
] }) }),
/* @__PURE__ */ jsx(Drawer.Footer, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-x-2", children: [
/* @__PURE__ */ jsx(Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", children: i18nInstance.t("actions.cancel") }) }),
/* @__PURE__ */ jsx(Button, { size: "small", type: "submit", onClick: saveShopSettings, children: i18nInstance.t("actions.save") })
] }) })
] }) })
] });
};
const FeedDetailsPage = () => /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(I18n, {}),
/* @__PURE__ */ jsx(
TwoColumnLayout,
{
firstCol: /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(FeedGeneralSection, {}),
/* @__PURE__ */ jsx(ProductCategoriesSection, {})
] }),
secondCol: /* @__PURE__ */ jsx(ShopSettingsSection, {})
}
)
] });
const Breadcrumb = (props) => {
const { feed } = props.data || {};
if (!feed)
return null;
return /* @__PURE__ */ jsx("span", { children: feed.title });
};
const loader = async ({ params }) => {
const { id } = params;
const response = await sdk.client.fetch(`/admin/feeds/${id}`);
return response;
};
const handle = {
breadcrumb: (match) => /* @__PURE__ */ jsx(Breadcrumb, { ...match })
};
const widgetModule = { widgets: [] };
const routeModule = {
routes: [
{
Component: FeedsPage,
path: "/settings/feeds",
handle: handle$1
},
{
Component: FeedDetailsPage,
path: "/settings/feeds/:id",
handle,
loader
}
]
};
const menuItemModule = {
menuItems: [
{
label: config.label,
icon: config.icon,
path: "/settings/feeds",
nested: void 0,
rank: void 0,
translationNs: void 0
}
]
};
const formModule = { customFields: {} };
const displayModule = {
displays: {}
};
const i18nModule = { resources: i18nTranslations0 };
const plugin = {
widgetModule,
routeModule,
menuItemModule,
formModule,
displayModule,
i18nModule
};
exp