В этом руководстве вы узнаете, как добавить на страницу интеграции в Medusa Admin собственную секцию на виджете.
Модуль генерирует секцию для каждой группы параметров в дескрипторе, по одному элементу управления на параметр. Этого хватает для скалярных настроек, например ключа, переключателя или выбора из списка. Для параметра со списком записей, например нескольких вебхуков или подключений служб доставки, подходящего элемента управления нет, поэтому для него нужна собственная секция.
Собственная секция строится как обычный admin-виджет Medusa на . Модуль отрисовывает его на странице интеграции рядом со сгенерированными секциями, и администратор магазина может перемещать его, как любую другую секцию.
Схема показывает, как страница, из Medusa, ваш виджет и API Admin участвуют в работе, от загрузки страницы до сохранения параметра виджета:
Страница загружает интеграцию, затем отрисовывает composer с префиксом зоны и данными своих секций. Composer ставит ваш виджет в зону, для которой он зарегистрирован, и передаёт ему . Когда виджет сохраняет параметр, он отправляет в тот же API-роут Admin, из которого читает страница, а модуль проверяет значения и записывает изменение с автором запроса. В шагах ниже вы построите каждую часть этой схемы.
Провайдер доставки ApiShip хранит подключения служб доставки в параметре типа и редактирует их в собственном виджете . В шагах ниже вы построите такой же виджет, только проще, со списком адресов вебхуков.
Объявите в дескрипторе параметр, которым управляет виджет, и не включайте его в , чтобы его не показывала ни одна сгенерированная секция:
providers/integration-my/services/my-integration.ts1const descriptor = defineIntegration({2 // ...3 options: {4 // ...5 webhooks: {6 type: "json",7 default: [],8 label: "my.webhooks.title",9 validate: (value, { addIssue }) => {10 if (!Array.isArray(value) || !value.every((url) => typeof url === "string")) {11 addIssue({ message: "Expected a list of URLs" })12 }13 },14 },15 },16})
Значения типа модуль проверяет только через собственный параметра, поэтому корректность списка обеспечивает правило выше. Параметр по-прежнему входит в дескриптор, поэтому его значение проходит валидацию, может быть помечено , отдаётся во время выполнения вместе с остальными и попадает в историю изменений.
На странице интеграции модуль предоставляет две зоны внедрения, названные по провайдера:
Medusa собирает виджет и в собственной зоне, но типы знают только её зоны. Объявите свои, дополнив в файле в корне плагина:
index.d.ts1export {}23declare module "@medusajs/admin-shared" {4 interface InjectionZoneRegistry {5 "gorgo.integration.my": true,6 "gorgo.integration.my.side": true7 }8}
Затем опубликуйте этот файл вместе с плагином и укажите его как типы плагина в :
package.json1{2 "files": [3 ".medusa/server",4 "index.d.ts"5 ],6 "exports": {7 ".": {8 "types": "./index.d.ts"9 }10 }11}
Остальные записи оставьте как есть. Так объявление попадёт в проекты, которые установят ваш плагин. TypeScript-проект самой Admin по умолчанию включает только , поэтому файл в корне плагина он пока не видит. Добавьте объявление в его в файле :
src/admin/tsconfig.json1{2 "include": [".", "../../index.d.ts"]3}
Остальную часть файла, например , оставьте как есть. После этого примет , а проект, который установит ваш плагин, увидит те же зоны. Полный список зон смотрите в справочнике Зоны виджетов в Admin.
Создайте виджет в директории плагина. Он читает и записывает параметр через API-роуты Admin самого модуля:
src/admin/widgets/my-integration-webhooks.tsx1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Button, Container, Heading, Input, Text, toast } from "@medusajs/ui"3import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"4import { useState } from "react"5import { useTranslation } from "react-i18next"6import type { IntegrationSectionData } from "@gorgo/medusa-integration"7import { sdk } from "../lib/sdk"89type IntegrationResponse = { integration: { values: Record<string, unknown> } | null }1011const MyWebhooksWidget = ({ data }: { data: IntegrationSectionData }) => {12 const { t } = useTranslation()13 const queryClient = useQueryClient()14 const queryKey = ["my-webhooks", data.providerId]15 const [url, setUrl] = useState("")1617 const { data: stored } = useQuery({18 queryKey,19 queryFn: () => sdk.client.fetch<IntegrationResponse>(`/admin/integrations/${data.providerId}`),20 })21 const webhooks = (stored?.integration?.values.webhooks as string[] | undefined) ?? []2223 const save = useMutation({24 mutationFn: (next: string[]) =>25 sdk.client.fetch(`/admin/integrations/${data.providerId}`, {26 method: "POST",27 body: { values: { webhooks: next } },28 }),29 onSuccess: () => {30 queryClient.invalidateQueries({ queryKey })31 setUrl("")32 },33 onError: (error: Error) => toast.error(error.message),34 })3536 return (37 <Container className="divide-y p-0">38 <div className="px-6 py-4">39 <Heading level="h2">{t("my.webhooks.title")}</Heading>40 </div>41 {webhooks.map((webhook) => (42 <Text key={webhook} size="small" className="px-6 py-4">43 {webhook}44 </Text>45 ))}46 <div className="flex gap-x-2 px-6 py-4">47 <Input value={url} onChange={(event) => setUrl(event.target.value)} />48 <Button size="small" variant="secondary" onClick={() => save.mutate([...webhooks, url])}>49 {t("my.webhooks.add")}50 </Button>51 </div>52 </Container>53 )54}5556export const config = defineWidgetConfig({57 zone: "gorgo.integration.my",58})5960export default MyWebhooksWidget
Виджет получает от страницы типа :
приходит из собственного запроса страницы и не обновляется после сохранения в виджете. То же относится к остальным данным страницы, например к , значку статуса и разделу История изменений. Они показывают состояние до сохранения, пока страница не запросит их снова, потому что сохранение в виджете не сбрасывает запросы страницы. Поэтому виджет хранит свою копию интеграции под своим ключом запроса и обновляет её после каждого сохранения. возвращает актуальный рядом со значениями, поэтому, если виджету нужна полнота настройки после сохранения, берите её из этой копии.
В запросе нет , поэтому модуль записывает ровно те отправленные параметры, которые объявлены в дескрипторе, и отбрасывает остальные ключи. Он проверяет значения, шифрует секреты, отправляет событие , которое сбрасывает кэш параметров, и записывает изменение в историю с текущим пользователем Admin в качестве автора.
здесь клиент JS SDK из , настроенный так же, как в руководствах Medusa по кастомизации панели администратора.
Medusa загружает расширения Admin плагина, его виджеты и переводы, только для пакетов, указанных в файла приложения. Кроме записи Модуля интеграций, добавьте сам пакет вашего плагина как . Без неё провайдер продолжит работать, но виджет не загрузится, а i18n-ключи будут показаны в Admin как есть.
Секреты не попадают в браузер ни через , ни через . Если виджету нужен секрет или операции над отдельными записями со своими проверками, добавьте собственный API-роут Admin и запускайте из него . Передайте в автора запроса, как показано на странице История изменений настроек, чтобы история сохранила правильного автора.
Виджет передаёт в этот роут , поэтому роут должен проверить, что принадлежит вашему провайдеру, до того как что-либо прочитает или запишет. записывает объявленные параметры любого зарегистрированного провайдера, так что непроверенный ключ позволяет запросом изменить интеграцию другого провайдера. Как выполнить проверку, читайте в шаге 4 руководства «Миграция существующего провайдера Medusa на Модуль интеграций».
Метки виджета задаются i18n-ключами, как и в дескрипторе. Где находятся их переводы, читайте в руководстве Локализация интеграции.