@hartl-services/medusa-base 2.6.1 → 2.8.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.
Files changed (151) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
  4. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
  5. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
  6. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
  7. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
  8. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
  9. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
  10. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
  11. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
  12. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
  13. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
  14. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
  15. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
  16. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
  17. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
  18. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +77 -26
  19. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
  20. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
  21. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
  22. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
  23. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +40 -5
  24. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +160 -20
  25. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +40 -5
  26. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
  27. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
  28. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +160 -20
  29. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
  30. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
  31. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
  32. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
  33. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +47 -0
  34. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +70 -0
  35. package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
  36. package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
  37. package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
  38. package/.medusa/server/src/admin/admin-components/index.js +11 -0
  39. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
  40. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
  41. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
  42. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
  43. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
  44. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
  45. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
  46. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
  47. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
  48. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
  49. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
  50. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
  51. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
  52. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
  53. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
  54. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
  55. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
  56. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
  57. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
  58. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
  59. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
  60. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
  61. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
  62. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
  63. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
  64. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
  65. package/.medusa/server/src/admin/index.js +3155 -2039
  66. package/.medusa/server/src/admin/index.mjs +3158 -2042
  67. package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
  68. package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
  69. package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
  70. package/.medusa/server/src/api/code-account-password-routes.js +92 -0
  71. package/.medusa/server/src/api/middlewares.js +82 -17
  72. package/.medusa/server/src/api/password-policy.js +55 -10
  73. package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
  74. package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
  75. package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
  76. package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
  77. package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
  78. package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
  79. package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
  80. package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
  81. package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
  82. package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
  83. package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
  84. package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
  85. package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
  86. package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
  87. package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
  88. package/.medusa/server/src/documents/plugin-features.js +8 -2
  89. package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
  90. package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
  91. package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
  92. package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
  93. package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
  94. package/.medusa/server/src/email/domain/business-keys.js +15 -2
  95. package/.medusa/server/src/email/plugin-features.js +8 -3
  96. package/.medusa/server/src/email/rendering/default-templates.js +2 -1
  97. package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
  98. package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
  99. package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
  100. package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
  101. package/.medusa/server/src/email/templates/render.js +2 -1
  102. package/.medusa/server/src/email/templates/sample-data.js +2 -1
  103. package/.medusa/server/src/email/templates/validate.js +8 -3
  104. package/.medusa/server/src/email/types/email-kinds.js +4 -2
  105. package/.medusa/server/src/email/types/template-data.js +9 -1
  106. package/.medusa/server/src/modules/customer-auth/index.js +14 -0
  107. package/.medusa/server/src/modules/customer-auth/service.js +35 -0
  108. package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
  109. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +30 -6
  110. package/.medusa/server/src/modules/plugin-features/service.js +29 -3
  111. package/.medusa/server/src/providers/email-otp/index.js +8 -0
  112. package/.medusa/server/src/providers/email-otp/service.js +96 -0
  113. package/.medusa/server/src/public/descriptor.js +138 -5
  114. package/.medusa/server/src/public/effective-features.js +22 -1
  115. package/.medusa/server/src/public/index.js +4 -2
  116. package/.medusa/server/src/public/plugin-icons.js +28 -0
  117. package/.medusa/server/src/public/shop-master-data.js +33 -14
  118. package/.medusa/server/src/shared/lib/dates.js +8 -2
  119. package/.medusa/server/src/shared/lib/index.js +2 -1
  120. package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
  121. package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
  122. package/README.md +436 -87
  123. package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
  124. package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
  125. package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
  126. package/dist/customer-auth/domain/auth-methods.js +31 -0
  127. package/dist/customer-auth/plugin-features.d.ts +2 -0
  128. package/dist/customer-auth/plugin-features.js +79 -0
  129. package/dist/email/customer-verification-snapshot.d.ts +3 -15
  130. package/dist/email/customer-verification-snapshot.js +19 -107
  131. package/dist/email/types/email-kinds.d.ts +4 -3
  132. package/dist/email/types/email-kinds.js +3 -1
  133. package/dist/email/types/template-data.d.ts +8 -0
  134. package/dist/email/types/template-data.js +8 -0
  135. package/dist/public/descriptor.js +137 -4
  136. package/dist/public/effective-features.d.ts +10 -0
  137. package/dist/public/effective-features.js +21 -0
  138. package/dist/public/index.d.ts +2 -1
  139. package/dist/public/index.js +3 -1
  140. package/dist/public/plugin-icons.d.ts +7 -0
  141. package/dist/public/plugin-icons.js +27 -0
  142. package/dist/public/shop-master-data.js +32 -13
  143. package/dist/public/types.d.ts +74 -2
  144. package/dist/shared/lib/dates.d.ts +2 -0
  145. package/dist/shared/lib/dates.js +7 -1
  146. package/dist/shared/lib/index.d.ts +1 -0
  147. package/dist/shared/lib/index.js +1 -0
  148. package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
  149. package/dist/shared/lib/refreshing-snapshot.js +121 -0
  150. package/dist/shop-config/build-food-shop-config.js +37 -0
  151. package/package.json +8 -3
@@ -1,6 +1,7 @@
1
1
  type ImageSettingInputProps = {
2
2
  id: string;
3
- hintId: string;
3
+ /** Id of the element describing the field; `undefined` when there is none. */
4
+ describedBy: string | undefined;
4
5
  label: string;
5
6
  /** Public URL of the current image; `""` = none. */
6
7
  value: string;
@@ -11,5 +12,5 @@ type ImageSettingInputProps = {
11
12
  * Uploads an image to the public File Module storage and hands its URL to
12
13
  * the settings draft; the form stores it on save like any other value.
13
14
  */
14
- export declare const ImageSettingInput: ({ id, hintId, label, value, disabled, onChange, }: ImageSettingInputProps) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const ImageSettingInput: ({ id, describedBy, label, value, disabled, onChange, }: ImageSettingInputProps) => import("react/jsx-runtime").JSX.Element;
15
16
  export {};
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Hint, Text } from "@medusajs/ui";
2
+ import { Button, Hint } from "@medusajs/ui";
3
3
  import { useMutation } from "@tanstack/react-query";
4
4
  import { useState } from "react";
5
5
  import { texts } from "../../../i18n/texts";
6
6
  import { validateFile } from "../../../shared/lib/file-validation";
7
7
  import { sdk } from "../../../shared/lib/sdk";
8
- import { FilePickerButton } from "../../../shared/ui/FilePickerButton";
8
+ import { FileDropField } from "../../../shared/ui/FileDropField";
9
9
  /** Largest image an `image` setting accepts in the Admin. */
10
10
  const SETTING_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
11
11
  const SETTING_IMAGE_OPTIONS = {
@@ -22,7 +22,7 @@ const SETTING_IMAGE_OPTIONS = {
22
22
  * Uploads an image to the public File Module storage and hands its URL to
23
23
  * the settings draft; the form stores it on save like any other value.
24
24
  */
25
- export const ImageSettingInput = ({ id, hintId, label, value, disabled, onChange, }) => {
25
+ export const ImageSettingInput = ({ id, describedBy, label, value, disabled, onChange, }) => {
26
26
  const [error, setError] = useState(null);
27
27
  const upload = useMutation({
28
28
  mutationFn: async (file) => {
@@ -44,5 +44,5 @@ export const ImageSettingInput = ({ id, hintId, label, value, disabled, onChange
44
44
  upload.mutate(file);
45
45
  }
46
46
  };
47
- return (_jsxs("div", { className: "grid gap-2", children: [value && (_jsx("div", { className: "bg-ui-bg-subtle flex items-center justify-center rounded-md border p-4", children: _jsx("img", { src: value, alt: texts.settings.imagePreviewAlt(label), className: "max-h-24 max-w-full object-contain" }) })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FilePickerButton, { id: id, "aria-describedby": hintId, accept: "image/png,image/jpeg", label: value ? texts.settings.imageReplace : texts.settings.imageUpload, isLoading: upload.isPending, disabled: busy, onFileSelected: selectFile }), value && (_jsx(Button, { type: "button", size: "small", variant: "transparent", disabled: busy, onClick: () => onChange(""), children: texts.settings.imageRemove })), !value && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.imageNone }))] }), error && _jsx(Hint, { variant: "error", children: error })] }));
47
+ return (_jsxs("div", { className: "grid gap-2", children: [value && (_jsx("div", { className: "bg-ui-bg-subtle flex items-center justify-center rounded-md border p-4", children: _jsx("img", { src: value, alt: texts.settings.imagePreviewAlt(label), className: "max-h-24 max-w-full object-contain" }) })), _jsx(FileDropField, { id: id, "aria-describedby": describedBy, accept: "image/png,image/jpeg", label: value ? texts.settings.imageReplace : texts.settings.imageUpload, text: value ? undefined : texts.settings.imageNone, isLoading: upload.isPending, disabled: busy, onFileSelected: selectFile }), value && (_jsx("div", { children: _jsx(Button, { type: "button", size: "small", variant: "transparent", disabled: busy, onClick: () => onChange(""), children: texts.settings.imageRemove }) })), error && _jsx(Hint, { variant: "error", children: error })] }));
48
48
  };
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Container, Text, toast, usePrompt } from "@medusajs/ui";
2
+ import { Button, Text, toast, usePrompt } from "@medusajs/ui";
3
3
  import { useEffect, useRef } from "react";
4
4
  import { texts } from "../../../i18n/texts";
5
- import { SectionHeader } from "../../../shared/ui/SectionHeader";
5
+ import { AdminSection } from "../../../shared/ui/AdminSection";
6
6
  import { useLatestPluginActionRun, useRunPluginAction, } from "../hooks/usePluginFeatures";
7
7
  import { formatRunTime, progressCount, progressPercent, runTransition, summarizeActionResult, } from "../lib/action-runs";
8
8
  /** Progress of a running run: label, "current / total", bar and start time. */
@@ -39,7 +39,7 @@ const PluginActionRow = ({ pluginKey, action, starting, blocked, onRun, }) => {
39
39
  });
40
40
  }
41
41
  }, [run, action.label]);
42
- return (_jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "small", weight: "plus", children: action.label }), action.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: action.description })), run && (running ? _jsx(RunProgress, { run: run }) : _jsx(RunStatusLine, { run: run }))] }), _jsx(Button, { size: "small", variant: "secondary", className: "shrink-0", isLoading: starting || running, disabled: blocked || starting || running, onClick: () => onRun(action), children: action.label })] }));
42
+ return (_jsxs("div", { className: "flex items-center gap-4 px-6 py-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "small", leading: "compact", weight: "plus", children: action.label }), action.description && (_jsx(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: action.description })), run && (running ? _jsx(RunProgress, { run: run }) : _jsx(RunStatusLine, { run: run }))] }), _jsx(Button, { size: "small", variant: "secondary", className: "shrink-0", isLoading: starting || running, disabled: blocked || starting || running, onClick: () => onRun(action), children: action.label })] }));
43
43
  };
44
44
  export const PluginActionsSection = ({ plugin }) => {
45
45
  const mutation = useRunPluginAction(plugin.plugin_key);
@@ -63,5 +63,5 @@ export const PluginActionsSection = ({ plugin }) => {
63
63
  }
64
64
  mutation.mutate(action);
65
65
  };
66
- return (_jsxs(Container, { className: "divide-y p-0", children: [_jsx(SectionHeader, { title: texts.sections.actions }), plugin.actions.map((action) => (_jsx(PluginActionRow, { pluginKey: plugin.plugin_key, action: action, starting: startingKey === action.key, blocked: mutation.isPending && startingKey !== action.key, onRun: (entry) => void run(entry) }, action.key)))] }));
66
+ return (_jsx(AdminSection, { title: texts.sections.actions, children: plugin.actions.map((action) => (_jsx(PluginActionRow, { pluginKey: plugin.plugin_key, action: action, starting: startingKey === action.key, blocked: mutation.isPending && startingKey !== action.key, onRun: (entry) => void run(entry) }, action.key))) }));
67
67
  };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The plugin's label as a breadcrumb text. A route `handle` function cannot
3
+ * call hooks, so the label is read by this element, which the handle returns.
4
+ * Falls back to the plugin key until the plugin is loaded.
5
+ */
6
+ export declare const PluginLabelCrumb: ({ pluginKey }: {
7
+ pluginKey: string;
8
+ }) => import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Breadcrumb of a plugin's own page that lives in another plugin than the
11
+ * base's hub: "Plugins" (linked to the hub) and the plugin's label. The
12
+ * Admin only adds parent crumbs for routes of the same plugin, so the base's
13
+ * own pages need just `PluginLabelCrumb`. Use as the route handle's
14
+ * `breadcrumb`: `export const handle = { breadcrumb: () => pluginBreadcrumb("affiliate") }`.
15
+ */
16
+ export declare const pluginBreadcrumb: (pluginKey: string) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { texts } from "../../../i18n/texts";
3
+ import { BreadcrumbTrail } from "../../../shared/ui/BreadcrumbTrail";
4
+ import { usePluginFeature } from "../hooks/usePluginFeatures";
5
+ import { PLUGIN_HUB_PATH } from "../lib/plugin-paths";
6
+ /**
7
+ * The plugin's label as a breadcrumb text. A route `handle` function cannot
8
+ * call hooks, so the label is read by this element, which the handle returns.
9
+ * Falls back to the plugin key until the plugin is loaded.
10
+ */
11
+ export const PluginLabelCrumb = ({ pluginKey }) => {
12
+ const label = usePluginFeature(pluginKey).data?.plugin.label;
13
+ return _jsx(_Fragment, { children: label ?? pluginKey });
14
+ };
15
+ /**
16
+ * Breadcrumb of a plugin's own page that lives in another plugin than the
17
+ * base's hub: "Plugins" (linked to the hub) and the plugin's label. The
18
+ * Admin only adds parent crumbs for routes of the same plugin, so the base's
19
+ * own pages need just `PluginLabelCrumb`. Use as the route handle's
20
+ * `breadcrumb`: `export const handle = { breadcrumb: () => pluginBreadcrumb("affiliate") }`.
21
+ */
22
+ export const pluginBreadcrumb = (pluginKey) => (_jsx(BreadcrumbTrail, { items: [
23
+ { label: texts.breadcrumbs.hub, to: PLUGIN_HUB_PATH },
24
+ { label: _jsx(PluginLabelCrumb, { pluginKey: pluginKey }) },
25
+ ] }));
@@ -5,5 +5,5 @@ import { PluginPageShell } from "./PluginPageShell";
5
5
  /** The base's generic page of a plugin without an own `admin_page`. */
6
6
  export const PluginDetailPage = () => {
7
7
  const { plugin_key: pluginKey = "" } = useParams();
8
- return (_jsx(PluginPageShell, { pluginKey: pluginKey, children: _jsx(PluginFeatureSections, { pluginKey: pluginKey }) }));
8
+ return (_jsx(PluginPageShell, { pluginKey: pluginKey, variant: "header", children: _jsx(PluginFeatureSections, { pluginKey: pluginKey }) }));
9
9
  };
@@ -2,10 +2,11 @@ type PluginFeatureSectionsProps = {
2
2
  pluginKey: string;
3
3
  };
4
4
  /**
5
- * The base's sections of one plugin: Funktionen (unless the plugin has
6
- * neither a switch nor features), Einstellungen (when the plugin declares
7
- * settings) and Aktionen (when it declares actions). Renders
8
- * nothing until the plugin is loaded; `PluginPageShell` shows that state.
5
+ * The base's sections of one plugin: warnings (required settings missing,
6
+ * every top-level feature off), Funktionen (unless the plugin has neither a
7
+ * switch nor features), Einstellungen (when the plugin declares settings) and
8
+ * Aktionen (when it declares actions). Renders nothing until the plugin is
9
+ * loaded; `PluginPageShell` shows that state.
9
10
  */
10
11
  export declare const PluginFeatureSections: ({ pluginKey }: PluginFeatureSectionsProps) => import("react/jsx-runtime").JSX.Element | null;
11
12
  export {};
@@ -1,19 +1,22 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert } from "@medusajs/ui";
2
3
  import { texts } from "../../../i18n/texts";
3
4
  import { usePluginFeature } from "../hooks/usePluginFeatures";
5
+ import { allTopLevelFeaturesOff, isUnconfigured, missingLabels } from "../lib/plugin-view";
4
6
  import { PluginActionsSection } from "./PluginActionsSection";
5
7
  import { PluginFeaturesCard } from "./PluginFeaturesCard";
6
8
  import { PluginSettingsForm } from "./PluginSettingsForm";
7
9
  /**
8
- * The base's sections of one plugin: Funktionen (unless the plugin has
9
- * neither a switch nor features), Einstellungen (when the plugin declares
10
- * settings) and Aktionen (when it declares actions). Renders
11
- * nothing until the plugin is loaded; `PluginPageShell` shows that state.
10
+ * The base's sections of one plugin: warnings (required settings missing,
11
+ * every top-level feature off), Funktionen (unless the plugin has neither a
12
+ * switch nor features), Einstellungen (when the plugin declares settings) and
13
+ * Aktionen (when it declares actions). Renders nothing until the plugin is
14
+ * loaded; `PluginPageShell` shows that state.
12
15
  */
13
16
  export const PluginFeatureSections = ({ pluginKey }) => {
14
17
  const plugin = usePluginFeature(pluginKey).data?.plugin;
15
18
  if (!plugin) {
16
19
  return null;
17
20
  }
18
- return (_jsxs(_Fragment, { children: [(plugin.switchable || plugin.features.length > 0) && (_jsx(PluginFeaturesCard, { plugin: plugin, title: texts.sections.features })), plugin.settings.length > 0 && _jsx(PluginSettingsForm, { plugin: plugin }), plugin.actions.length > 0 && _jsx(PluginActionsSection, { plugin: plugin })] }));
21
+ return (_jsxs(_Fragment, { children: [isUnconfigured(plugin) && (_jsx(Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin)) })), plugin.all_off_warning && allTopLevelFeaturesOff(plugin) && (_jsx(Alert, { variant: "warning", children: plugin.all_off_warning })), (plugin.switchable || plugin.features.length > 0) && (_jsx(PluginFeaturesCard, { plugin: plugin })), plugin.settings.length > 0 && _jsx(PluginSettingsForm, { plugin: plugin }), plugin.actions.length > 0 && _jsx(PluginActionsSection, { plugin: plugin })] }));
19
22
  };
@@ -1,12 +1,12 @@
1
1
  import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
2
2
  type PluginFeaturesCardProps = {
3
3
  plugin: AdminPluginFeatures;
4
- /** Heading of the card; the plugin label when omitted. */
5
- title?: string;
6
4
  };
7
5
  /**
8
6
  * Plugin switch (unless the plugin is not switchable) plus the feature
9
- * switches of one plugin; saves immediately.
7
+ * switches of one plugin; saves immediately. A plugin without features gets
8
+ * just its switch row; otherwise the switch is the first row of the features
9
+ * section, and the feature rows are dimmed and locked while the plugin is off.
10
10
  */
11
- export declare const PluginFeaturesCard: ({ plugin, title }: PluginFeaturesCardProps) => import("react/jsx-runtime").JSX.Element;
11
+ export declare const PluginFeaturesCard: ({ plugin }: PluginFeaturesCardProps) => import("react/jsx-runtime").JSX.Element;
12
12
  export {};
@@ -1,34 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Container, Heading, Label, Switch, Text } from "@medusajs/ui";
3
2
  import { texts } from "../../../i18n/texts";
4
- import { useUpdatePluginFeatures } from "../hooks/usePluginFeatures";
5
- const configuredFeatures = (plugin) => Object.fromEntries(plugin.features.map((feature) => [feature.key, feature.enabled]));
3
+ import { AdminSection } from "../../../shared/ui/AdminSection";
4
+ import { SwitchRow } from "../../../shared/ui/SwitchRow";
5
+ import { usePluginSwitch } from "../hooks/usePluginSwitch";
6
+ import { disabledParent, isLastRequiredFeature } from "../lib/plugin-view";
6
7
  /**
7
8
  * Plugin switch (unless the plugin is not switchable) plus the feature
8
- * switches of one plugin; saves immediately.
9
+ * switches of one plugin; saves immediately. A plugin without features gets
10
+ * just its switch row; otherwise the switch is the first row of the features
11
+ * section, and the feature rows are dimmed and locked while the plugin is off.
9
12
  */
10
- export const PluginFeaturesCard = ({ plugin, title }) => {
11
- const mutation = useUpdatePluginFeatures();
12
- const pending = mutation.isPending;
13
- const labels = new Map(plugin.features.map((feature) => [feature.key, feature.label]));
14
- const configured = configuredFeatures(plugin);
13
+ export const PluginFeaturesCard = ({ plugin }) => {
14
+ const { pending, setEnabled, setFeature } = usePluginSwitch(plugin);
15
15
  const pluginSwitchId = `plugin-features-${plugin.plugin_key}`;
16
- const save = (enabled, features) => {
17
- mutation.mutate({
18
- plugin_key: plugin.plugin_key,
19
- expected_version: plugin.version,
20
- enabled,
21
- features,
22
- });
23
- };
24
- return (_jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [_jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { level: "h2", children: title ?? plugin.label }), title === undefined && plugin.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin.description })), !plugin.enabled && (_jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.pluginDisabled })), pending && (_jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving }))] }), plugin.switchable && (_jsx(Switch, { id: pluginSwitchId, "aria-label": texts.plugin.switchLabel(plugin.label), className: "shrink-0", checked: plugin.enabled, disabled: pending, onCheckedChange: (checked) => save(checked === true, configured) }))] }), plugin.features.map((feature) => {
25
- const switchId = `${pluginSwitchId}-${feature.key}`;
26
- const parent = plugin.features.find((candidate) => candidate.key === feature.parent_key);
27
- const parentOff = parent !== undefined && !parent.enabled;
28
- const locked = pending || !plugin.enabled || parentOff;
29
- return (_jsxs("div", { className: `flex items-start justify-between gap-4 py-3 pr-6 ${feature.parent_key ? "pl-12" : "pl-6"}`, children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Label, { htmlFor: switchId, size: "small", weight: "plus", className: locked ? "text-ui-fg-disabled" : undefined, children: feature.label }), feature.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: feature.description })), plugin.enabled && parentOff && feature.parent_key && (_jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.parentDisabled(labels.get(feature.parent_key) ?? feature.parent_key) }))] }), _jsx(Switch, { id: switchId, className: "shrink-0", checked: feature.enabled, disabled: locked, onCheckedChange: (checked) => save(plugin.enabled, {
30
- ...configured,
31
- [feature.key]: checked === true,
32
- }) })] }, feature.key));
33
- })] }));
16
+ const labels = new Map(plugin.features.map((feature) => [feature.key, feature.label]));
17
+ const pluginRow = plugin.switchable ? (_jsx(SwitchRow, { id: pluginSwitchId, label: texts.plugin.switchRow, description: plugin.disable_hint, hint: plugin.enabled ? undefined : texts.hints.pluginDisabled, checked: plugin.enabled, disabled: pending, onCheckedChange: setEnabled, className: plugin.features.length > 0 ? "bg-ui-bg-subtle" : "py-4" })) : null;
18
+ if (plugin.features.length === 0) {
19
+ return (_jsx(AdminSection, { "aria-busy": pending, children: pluginRow }));
20
+ }
21
+ return (_jsxs(AdminSection, { "aria-busy": pending, title: plugin.features_heading ?? texts.sections.features, note: pending ? texts.hints.saving : plugin.features_note, children: [pluginRow, _jsx("div", { className: `divide-y transition-opacity ${plugin.enabled ? "" : "opacity-50"}`, children: plugin.features.map((feature) => {
22
+ const parent = disabledParent(plugin.features, feature);
23
+ const lastRequired = isLastRequiredFeature(plugin, feature);
24
+ const locked = pending || !plugin.enabled || parent !== undefined || lastRequired;
25
+ return (_jsx(SwitchRow, { id: `${pluginSwitchId}-${feature.key}`, label: feature.label, description: feature.description, hint: plugin.enabled && parent
26
+ ? texts.hints.parentDisabled(labels.get(parent.key) ?? parent.key)
27
+ : lastRequired
28
+ ? texts.hints.lastRequiredFeature
29
+ : undefined, indent: feature.parent_key !== null, checked: feature.enabled, disabled: locked, onCheckedChange: (checked) => setFeature(feature.key, checked), className: plugin.enabled && parent ? "opacity-60" : undefined }, feature.key));
30
+ }) })] }));
34
31
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Hub: one card per installed plugin. Plugins that cannot be switched off
3
- * (the shop master data) come first; the rest keep the server's order.
2
+ * Hub: title, the shop master data banner and one card per installed plugin
3
+ * (plugins that cannot be switched off first, then the server's order).
4
4
  */
5
5
  export declare const PluginFeaturesSettingsPage: () => import("react/jsx-runtime").JSX.Element;
@@ -1,17 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Container, Text } from "@medusajs/ui";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, Container, Heading, Text } from "@medusajs/ui";
3
3
  import { texts } from "../../../i18n/texts";
4
- import { SectionHeader } from "../../../shared/ui/SectionHeader";
5
4
  import { usePluginFeatures } from "../hooks/usePluginFeatures";
5
+ import { hubOrder } from "../lib/plugin-view";
6
6
  import { PluginHubCard } from "./PluginHubCard";
7
+ import { SHOP_PLUGIN_KEY, ShopMasterDataBanner } from "./ShopMasterDataBanner";
7
8
  /**
8
- * Hub: one card per installed plugin. Plugins that cannot be switched off
9
- * (the shop master data) come first; the rest keep the server's order.
9
+ * Hub: title, the shop master data banner and one card per installed plugin
10
+ * (plugins that cannot be switched off first, then the server's order).
10
11
  */
11
12
  export const PluginFeaturesSettingsPage = () => {
12
13
  const query = usePluginFeatures();
13
- return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [_jsx(Container, { className: "p-0", children: _jsx(SectionHeader, { description: texts.page.description, level: "h1", title: texts.page.title }) }), query.isPending ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) })) : query.isError ? (_jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }), _jsx(Button, { size: "small", variant: "secondary", isLoading: query.isFetching, onClick: () => void query.refetch(), children: texts.states.retry })] })) : query.data.plugins.length === 0 ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.empty }) })) : ([
14
- ...query.data.plugins.filter((plugin) => !plugin.switchable),
15
- ...query.data.plugins.filter((plugin) => plugin.switchable),
16
- ].map((plugin) => (_jsx(PluginHubCard, { plugin: plugin }, plugin.plugin_key))))] }));
14
+ const shop = query.data?.plugins.find((plugin) => plugin.plugin_key === SHOP_PLUGIN_KEY);
15
+ const plugins = hubOrder(query.data?.plugins.filter((plugin) => plugin.plugin_key !== SHOP_PLUGIN_KEY) ?? []);
16
+ return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [_jsx(Container, { children: _jsxs("div", { className: "flex flex-col gap-y-1", children: [_jsx(Heading, { level: "h1", children: texts.page.title }), _jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.page.description })] }) }), query.isPending ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) })) : query.isError ? (_jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }), _jsx(Button, { size: "small", variant: "secondary", isLoading: query.isFetching, onClick: () => void query.refetch(), children: texts.states.retry })] })) : query.data.plugins.length === 0 ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.empty }) })) : (_jsxs(_Fragment, { children: [shop && _jsx(ShopMasterDataBanner, { plugin: shop }), _jsxs("div", { className: "flex items-center justify-between gap-x-4 px-1 pt-3", children: [_jsx(Heading, { level: "h2", children: texts.page.pluginsHeading }), _jsx(Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: texts.page.installed(plugins.length) })] }), _jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(min(100%,320px),1fr))] gap-3", children: plugins.map((plugin) => (_jsx(PluginHubCard, { plugin: plugin }, plugin.plugin_key))) })] }))] }));
17
17
  };
@@ -2,6 +2,10 @@ import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplement
2
2
  type PluginHubCardProps = {
3
3
  plugin: AdminPluginFeatures;
4
4
  };
5
- /** One plugin on the hub: status, plugin switch and the link to its page. */
5
+ /**
6
+ * One plugin on the hub: icon, name, plugin switch (switchable plugins only),
7
+ * summary, "Nicht konfiguriert" while required settings are missing, and the
8
+ * link to its page. Switching off asks for confirmation.
9
+ */
6
10
  export declare const PluginHubCard: ({ plugin }: PluginHubCardProps) => import("react/jsx-runtime").JSX.Element;
7
11
  export {};
@@ -1,21 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Container, Heading, Switch, Text } from "@medusajs/ui";
2
+ import { Button, Container, Heading, IconBadge, StatusBadge, Switch, Text, } from "@medusajs/ui";
3
3
  import { Link } from "react-router-dom";
4
4
  import { texts } from "../../../i18n/texts";
5
- import { useUpdatePluginFeatures } from "../hooks/usePluginFeatures";
5
+ import { PluginIcon } from "../../../shared/ui/PluginIcon";
6
+ import { usePluginSwitch } from "../hooks/usePluginSwitch";
6
7
  import { pluginDetailPath } from "../lib/plugin-paths";
7
- import { PluginStatusChips } from "./PluginStatusChips";
8
- /** One plugin on the hub: status, plugin switch and the link to its page. */
8
+ import { isUnconfigured } from "../lib/plugin-view";
9
+ /**
10
+ * One plugin on the hub: icon, name, plugin switch (switchable plugins only),
11
+ * summary, "Nicht konfiguriert" while required settings are missing, and the
12
+ * link to its page. Switching off asks for confirmation.
13
+ */
9
14
  export const PluginHubCard = ({ plugin }) => {
10
- const mutation = useUpdatePluginFeatures();
11
- const pending = mutation.isPending;
12
- const toggle = (enabled) => {
13
- mutation.mutate({
14
- plugin_key: plugin.plugin_key,
15
- expected_version: plugin.version,
16
- enabled,
17
- features: Object.fromEntries(plugin.features.map((feature) => [feature.key, feature.enabled])),
18
- });
19
- };
20
- return (_jsxs(Container, { className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4", "aria-busy": pending, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Heading, { level: "h2", children: plugin.label }), plugin.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin.description })), _jsx(PluginStatusChips, { plugin: plugin }), pending && (_jsx(Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [plugin.switchable && (_jsx(Switch, { "aria-label": texts.plugin.switchLabel(plugin.label), checked: plugin.enabled, disabled: pending, onCheckedChange: (checked) => toggle(checked === true) })), _jsx(Button, { size: "small", variant: "secondary", asChild: true, children: _jsx(Link, { to: plugin.admin_page ?? pluginDetailPath(plugin.plugin_key), "aria-label": texts.plugin.openLabel(plugin.label), children: texts.plugin.open }) })] })] }));
15
+ const { pending, setEnabled } = usePluginSwitch(plugin);
16
+ return (_jsx(Container, { "aria-busy": pending, children: _jsxs("div", { className: "flex min-h-[132px] flex-col gap-y-3", children: [_jsxs("div", { className: "flex items-center gap-x-3", children: [_jsx(IconBadge, { size: "large", children: _jsx(PluginIcon, { name: plugin.icon }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(Heading, { level: "h3", children: plugin.label }) }), plugin.switchable && (_jsx(Switch, { "aria-label": texts.plugin.switchLabel(plugin.label), checked: plugin.enabled, disabled: pending, onCheckedChange: (checked) => setEnabled(checked === true) }))] }), _jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: plugin.summary ?? plugin.description }), _jsxs("div", { className: "mt-auto flex items-center justify-between gap-x-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2", children: [isUnconfigured(plugin) && (_jsx(StatusBadge, { color: "orange", children: texts.chips.notConfigured })), pending && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.hints.saving }))] }), _jsx(Button, { size: "small", variant: "secondary", asChild: true, children: _jsx(Link, { to: plugin.admin_page ?? pluginDetailPath(plugin.plugin_key), "aria-label": texts.plugin.openLabel(plugin.label), className: "ml-auto", children: texts.plugin.open }) })] })] }) }));
21
17
  };
@@ -1,13 +1,19 @@
1
1
  import type { ReactNode } from "react";
2
2
  type PluginPageShellProps = {
3
3
  pluginKey: string;
4
+ /**
5
+ * `back-link` (default): back link to the hub above a plain title card.
6
+ * `header`: `AdminPageHeader` with the plugin's icon, summary and status
7
+ * badges and no back link; the route's `handle` carries the breadcrumb.
8
+ */
9
+ variant?: "back-link" | "header";
4
10
  /** Rendered below the header once the plugin is loaded. */
5
11
  children?: ReactNode;
6
12
  };
7
13
  /**
8
- * Frame of a plugin's page: back link to the hub, the plugin's label,
9
- * description and status, then `children`. Shows loading, not-installed and
10
- * error states instead of `children`.
14
+ * Frame of a plugin's page: the plugin's label, description and status, then
15
+ * `children`. Shows loading, not-installed and error states instead of
16
+ * `children`.
11
17
  */
12
- export declare const PluginPageShell: ({ pluginKey, children }: PluginPageShellProps) => import("react/jsx-runtime").JSX.Element;
18
+ export declare const PluginPageShell: ({ pluginKey, variant, children, }: PluginPageShellProps) => import("react/jsx-runtime").JSX.Element;
13
19
  export {};
@@ -1,20 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Button, Container, Heading, Text } from "@medusajs/ui";
3
3
  import { texts } from "../../../i18n/texts";
4
+ import { AdminPageHeader } from "../../../shared/ui/AdminPageHeader";
4
5
  import { BackLink } from "../../../shared/ui/BackLink";
6
+ import { InfoTooltip } from "../../../shared/ui/InfoTooltip";
7
+ import { PluginIcon } from "../../../shared/ui/PluginIcon";
5
8
  import { usePluginFeature } from "../hooks/usePluginFeatures";
6
9
  import { PLUGIN_HUB_PATH } from "../lib/plugin-paths";
10
+ import { headerInfoText } from "../lib/plugin-view";
7
11
  import { PluginStatusChips } from "./PluginStatusChips";
8
12
  const isNotFound = (error) => typeof error === "object" &&
9
13
  error !== null &&
10
14
  error.status === 404;
11
15
  /**
12
- * Frame of a plugin's page: back link to the hub, the plugin's label,
13
- * description and status, then `children`. Shows loading, not-installed and
14
- * error states instead of `children`.
16
+ * Frame of a plugin's page: the plugin's label, description and status, then
17
+ * `children`. Shows loading, not-installed and error states instead of
18
+ * `children`.
15
19
  */
16
- export const PluginPageShell = ({ pluginKey, children }) => {
20
+ export const PluginPageShell = ({ pluginKey, variant = "back-link", children, }) => {
17
21
  const query = usePluginFeature(pluginKey);
18
22
  const plugin = query.data?.plugin;
19
- return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [_jsx(BackLink, { label: texts.detail.back, to: PLUGIN_HUB_PATH }), plugin ? (_jsxs(_Fragment, { children: [_jsxs(Container, { className: "px-6 py-4", children: [_jsx(Heading, { level: "h1", children: plugin.label }), plugin.description && (_jsx(Text, { className: "text-ui-fg-subtle", children: plugin.description })), _jsx(PluginStatusChips, { plugin: plugin })] }), children] })) : query.isPending ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) })) : isNotFound(query.error) ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.notFound }) })) : (_jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }), _jsx(Button, { size: "small", variant: "secondary", isLoading: query.isFetching, onClick: () => void query.refetch(), children: texts.states.retry })] }))] }));
23
+ const info = plugin ? headerInfoText(plugin) : null;
24
+ return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [variant === "back-link" && (_jsx(BackLink, { label: texts.detail.back, to: PLUGIN_HUB_PATH })), plugin ? (_jsxs(_Fragment, { children: [variant === "header" ? (_jsx(AdminPageHeader, { icon: _jsx(PluginIcon, { name: plugin.icon }), title: plugin.label, description: plugin.summary ?? plugin.description, info: info ? (_jsx(InfoTooltip, { label: texts.info.label(plugin.label), children: info })) : undefined, badges: _jsx(PluginStatusChips, { plugin: plugin }) })) : (_jsxs(Container, { className: "px-6 py-4", children: [_jsx(Heading, { level: "h1", children: plugin.label }), plugin.description && (_jsx(Text, { className: "text-ui-fg-subtle", children: plugin.description })), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: _jsx(PluginStatusChips, { plugin: plugin }) })] })), children] })) : query.isPending ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) })) : isNotFound(query.error) ? (_jsx(Container, { children: _jsx(Text, { className: "text-ui-fg-subtle", children: texts.states.notFound }) })) : (_jsxs(Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Text, { className: "text-ui-fg-error", children: texts.states.error }), _jsx(Button, { size: "small", variant: "secondary", isLoading: query.isFetching, onClick: () => void query.refetch(), children: texts.states.retry })] }))] }));
20
25
  };
@@ -2,6 +2,5 @@ import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplement
2
2
  type PluginSettingsFormProps = {
3
3
  plugin: AdminPluginFeatures;
4
4
  };
5
- export declare const missingLabels: (plugin: AdminPluginFeatures) => string;
6
5
  export declare const PluginSettingsForm: ({ plugin }: PluginSettingsFormProps) => import("react/jsx-runtime").JSX.Element;
7
6
  export {};
@@ -1,54 +1,105 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Alert, Button, Container, Heading, Hint, Input, Label, Select, Switch, Text, } from "@medusajs/ui";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, Heading, Hint, Input, Label, Select, Text } from "@medusajs/ui";
3
3
  import { texts } from "../../../i18n/texts";
4
- import { groupSettings, isHexColor, normalizeColorInput, } from "../lib/settings-draft";
4
+ import { AdminSection } from "../../../shared/ui/AdminSection";
5
+ import { InfoTooltip } from "../../../shared/ui/InfoTooltip";
6
+ import { SwitchRow } from "../../../shared/ui/SwitchRow";
7
+ import { invalidatedByLabels, invalidatesLabels, isGroupDimmed, settingInfoText, showsBoundsLine, showsSourceHint, } from "../lib/plugin-view";
8
+ import { isHexColor, normalizeColorInput } from "../lib/settings-draft";
9
+ import { dateFromInputValue, inputValueFromDate } from "../../../shared/lib/date-input";
10
+ import { AdminDatePicker } from "../../../shared/ui/AdminDatePicker";
11
+ import { buildSettingGroups, isGroupVisible, } from "../lib/settings-layout";
5
12
  import { usePluginSettingsForm } from "../hooks/usePluginSettingsForm";
6
13
  import { ImageSettingInput } from "./ImageSettingInput";
7
14
  const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
8
- export const missingLabels = (plugin) => plugin.missing
9
- .map((key) => plugin.settings.find((setting) => setting.key === key)?.label ?? key)
10
- .join(", ");
11
- /** "API-Key, Passwort" for the secrets a change of `setting` deletes. */
12
- const invalidatesLabels = (plugin, setting) => setting.invalidates
13
- .map((key) => plugin.settings.find((candidate) => candidate.key === key)?.label ?? key)
14
- .join(", ");
15
- /** "Zwischen 0 und 100." etc.; `undefined` without bounds. */
16
- const boundsText = (setting) => setting.min !== null && setting.max !== null
15
+ const rangeText = (setting) => setting.min !== null && setting.max !== null
17
16
  ? texts.settings.numberRange(setting.min, setting.max)
18
17
  : setting.min !== null
19
18
  ? texts.settings.numberMin(setting.min)
20
19
  : setting.max !== null
21
20
  ? texts.settings.numberMax(setting.max)
22
21
  : undefined;
23
- const ColorSettingInput = ({ id, hintId, label, value, invalid, disabled, onChange, }) => {
22
+ /** "Zwischen 0 und 100. Ganze Zahl." etc.; `undefined` without bounds or integer. */
23
+ const boundsText = (setting) => {
24
+ const range = rangeText(setting);
25
+ return setting.integer
26
+ ? [range, texts.settings.numberInteger].filter(Boolean).join(" ")
27
+ : range;
28
+ };
29
+ /** Label text with the " *" marker of a required setting. */
30
+ const labelText = (setting) => setting.required ? `${setting.label} *` : setting.label;
31
+ const ColorSettingInput = ({ id, hintId, label, value, invalid, required, disabled, onChange, }) => {
24
32
  const hasColor = isHexColor(value.trim());
25
- return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, className: "flex-1 font-mono", type: "text", autoComplete: "off", spellCheck: false, placeholder: texts.settings.colorPlaceholder, value: value, disabled: disabled, onChange: (event) => onChange(normalizeColorInput(event.target.value)) }), _jsx("input", { type: "color", "aria-label": texts.settings.colorPickLabel(label),
33
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, "aria-required": required || undefined, className: "flex-1 font-mono", type: "text", autoComplete: "off", spellCheck: false, placeholder: texts.settings.colorPlaceholder, value: value, disabled: disabled, onChange: (event) => onChange(normalizeColorInput(event.target.value)) }), _jsx("input", { type: "color", "aria-label": texts.settings.colorPickLabel(label),
26
34
  // An empty value shows the browser's black; dim it so it does not
27
35
  // read as a chosen color.
28
36
  className: `h-8 w-10 shrink-0 cursor-pointer rounded-md border border-ui-border-base bg-ui-bg-field p-1 disabled:cursor-not-allowed ${hasColor ? "" : "opacity-30"}`, value: hasColor ? value.trim() : "#000000", disabled: disabled, onChange: (event) => onChange(normalizeColorInput(event.target.value)) })] }));
29
37
  };
30
- const SettingField = ({ pluginKey, setting, invalidatesLabels, value, invalid, disabled, onChange, }) => {
38
+ /** A non-boolean setting: label (with source hint), control and hints. */
39
+ const SettingField = ({ pluginKey, setting, invalidatedBy, invalidates, value, invalid, disabled, onChange, }) => {
31
40
  const id = fieldId(pluginKey, setting.key);
41
+ const labelId = `${id}-label`;
32
42
  const hintId = `${id}-hint`;
33
43
  const clearPending = setting.type === "secret" && value === null;
34
44
  const bounds = setting.type === "number" ? boundsText(setting) : undefined;
35
- return (_jsxs("div", { className: "grid gap-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs(Label, { htmlFor: id, size: "small", weight: "plus", children: [setting.label, setting.required && (_jsxs("span", { className: "text-ui-fg-muted", children: [" (", texts.settings.required, ")"] }))] }), _jsx(Text, { size: "xsmall", className: "text-ui-fg-muted shrink-0", children: texts.settings.source[setting.source] })] }), setting.type === "boolean" ? (_jsx(Switch, { id: id, "aria-describedby": hintId, checked: value === true, disabled: disabled, onCheckedChange: (checked) => onChange(checked === true) })) : setting.type === "secret" ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: id, "aria-describedby": hintId, className: "flex-1", type: "password", autoComplete: "new-password", placeholder: setting.is_set ? texts.settings.secretPlaceholder : "", value: typeof value === "string" ? value : "", disabled: disabled || clearPending, onChange: (event) => onChange(event.target.value) }), setting.source === "stored" && (_jsx(Button, { type: "button", size: "small", variant: "secondary", disabled: disabled, "aria-label": clearPending
45
+ const hint = setting.hint ?? setting.description;
46
+ const info = settingInfoText(setting);
47
+ const showBounds = Boolean(bounds) && showsBoundsLine(setting);
48
+ const required = setting.required;
49
+ const hasHints = Boolean(hint) ||
50
+ clearPending ||
51
+ invalidatedBy.length > 0 ||
52
+ invalidates.length > 0 ||
53
+ showBounds ||
54
+ invalid;
55
+ const describedBy = hasHints ? hintId : undefined;
56
+ const placeholder = setting.placeholder ?? undefined;
57
+ return (_jsxs("div", { className: "flex flex-col gap-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-x-1", children: [_jsx(Label, { id: labelId, htmlFor: id, size: "small", weight: "plus", children: labelText(setting) }), info && (_jsx(InfoTooltip, { label: texts.info.label(setting.label), children: info }))] }), showsSourceHint(setting.source) && (_jsx(Text, { size: "xsmall", className: "text-ui-fg-muted shrink-0", children: texts.settings.source[setting.source] }))] }), setting.type === "secret" ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-required": required || undefined, className: "flex-1", type: "password", autoComplete: "new-password", placeholder: setting.is_set ? texts.settings.secretPlaceholder : placeholder, value: typeof value === "string" ? value : "", disabled: disabled || clearPending, onChange: (event) => onChange(event.target.value) }), setting.source === "stored" && (_jsx(Button, { type: "button", size: "small", variant: "secondary", disabled: disabled, "aria-label": clearPending
36
58
  ? texts.settings.secretClearUndoLabel(setting.label)
37
59
  : texts.settings.secretClearLabel(setting.label), onClick: () => onChange(clearPending ? "" : null), children: clearPending
38
60
  ? texts.settings.secretClearUndo
39
- : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": hintId, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "image" ? (_jsx(ImageSettingInput, { id: id, hintId: hintId, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "color" ? (_jsx(ColorSettingInput, { id: id, hintId: hintId, label: setting.label, value: typeof value === "string" ? value : "", invalid: invalid, disabled: disabled, onChange: (next) => onChange(next) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: "number", step: "any", min: setting.min ?? undefined, max: setting.max ?? undefined, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: setting.type === "url" ? "url" : "text", value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), _jsxs("div", { id: hintId, className: "grid gap-1", children: [setting.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatesLabels && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels) })), bounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: setting.type === "color"
61
+ : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": describedBy, "aria-required": required || undefined, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "image" ? (_jsx(ImageSettingInput, { id: id, describedBy: describedBy, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "color" ? (_jsx(ColorSettingInput, { id: id, hintId: describedBy, label: setting.label, value: typeof value === "string" ? value : "", invalid: invalid, required: required, disabled: disabled, onChange: (next) => onChange(next) })) : setting.type === "date" ? (_jsx(AdminDatePicker, { id: id, "aria-labelledby": labelId, "aria-describedby": describedBy, isDisabled: disabled, value: dateFromInputValue(typeof value === "string" ? value : ""), onChange: (date) => onChange(inputValueFromDate(date)) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: "number", step: setting.integer ? 1 : "any", min: setting.min ?? undefined, max: setting.max ?? undefined, placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": required || undefined, type: setting.type === "url" ? "url" : "text", placeholder: placeholder, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), hasHints && (_jsxs("div", { id: hintId, className: "flex flex-col gap-y-1", children: [hint && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: hint })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatedBy.length > 0 && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidatedBy(invalidatedBy) })), invalidates.length > 0 && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.invalidates(invalidates.join(", ")) })), showBounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: setting.type === "color"
40
62
  ? texts.settings.invalidColor
41
- : (bounds ?? texts.settings.invalidNumber) }))] })] }));
63
+ : (bounds ?? texts.settings.invalidNumber) }))] }))] }));
42
64
  };
43
65
  export const PluginSettingsForm = ({ plugin }) => {
44
66
  const form = usePluginSettingsForm(plugin);
45
67
  const pending = form.mutation.isPending;
46
- const sections = groupSettings(plugin.settings);
47
- const hasTextFields = plugin.settings.some((setting) => setting.type !== "boolean" &&
48
- setting.type !== "secret" &&
49
- setting.type !== "select" &&
50
- setting.type !== "image");
51
- return (_jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [_jsxs("div", { className: "px-6 py-4", children: [_jsx(Heading, { level: "h2", children: texts.sections.settings }), hasTextFields && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.clearHint }))] }), _jsxs("form", { onSubmit: form.submit, className: "grid gap-5 px-6 py-5", children: [!plugin.configured && (_jsx(Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin)) })), sections.map((section, index) => (_jsxs("div", { className: section.group !== null && index > 0
52
- ? "grid gap-5 border-t border-ui-border-base pt-5"
53
- : "grid gap-5", children: [section.group !== null && (_jsx(Heading, { level: "h3", children: section.group })), section.settings.map((setting) => (_jsx(SettingField, { pluginKey: plugin.plugin_key, setting: setting, invalidatesLabels: invalidatesLabels(plugin, setting), value: form.draft[setting.key], invalid: form.invalid.includes(setting.key), disabled: pending, onChange: (value, badInput) => form.setValue(setting.key, value, badInput) }, setting.key)))] }, section.settings[0].key))), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", size: "small", isLoading: pending, disabled: !form.dirty || form.invalid.length > 0 || pending, children: texts.settings.save }) })] })] }));
68
+ const groups = buildSettingGroups(plugin.settings, plugin.setting_groups, texts.sections.settings).filter((group) => isGroupVisible(group, form.draft));
69
+ const hasTextFields = plugin.settings.some((setting) => setting.type === "string" ||
70
+ setting.type === "url" ||
71
+ setting.type === "number" ||
72
+ setting.type === "color" ||
73
+ setting.type === "date");
74
+ const renderField = (setting) => (_jsx(SettingField, { pluginKey: plugin.plugin_key, setting: setting, invalidatedBy: setting.type === "secret"
75
+ ? invalidatedByLabels(plugin.settings, setting.key)
76
+ : [], invalidates: invalidatesLabels(plugin.settings, setting), value: form.draft[setting.key], invalid: form.invalid.includes(setting.key), disabled: pending, onChange: (value, badInput) => form.setValue(setting.key, value, badInput) }, setting.key));
77
+ // `dimmed`: the group's required feature is off; the rows stay editable.
78
+ const renderBlock = (block, dimmed) => {
79
+ const dim = dimmed ? "opacity-50 transition-opacity" : "transition-opacity";
80
+ const heading = block.heading ? (_jsx(Heading, { level: "h3", className: "px-6 pt-5", children: block.heading })) : null;
81
+ if (block.kind === "switch") {
82
+ const { setting } = block;
83
+ const row = (_jsx(SwitchRow, { id: fieldId(plugin.plugin_key, setting.key), label: labelText(setting), info: settingInfoText(setting) ? (_jsx(InfoTooltip, { label: texts.info.label(setting.label), children: settingInfoText(setting) })) : undefined, description: setting.hint ?? setting.description, hint: showsSourceHint(setting.source)
84
+ ? texts.settings.source[setting.source]
85
+ : undefined, checked: form.draft[setting.key] === true, disabled: pending, onCheckedChange: (checked) => form.setValue(setting.key, checked) }));
86
+ return heading ? (_jsxs("div", { className: dim, children: [heading, row] }, setting.key)) : (_jsx("div", { className: dim, children: row }, setting.key));
87
+ }
88
+ const first = block.rows[0];
89
+ const key = first.kind === "full" ? first.setting.key : first.settings[0].key;
90
+ return (_jsxs("div", { className: `flex flex-col gap-y-5 px-6 py-5 ${dim}`, children: [block.heading ? _jsx(Heading, { level: "h3", children: block.heading }) : null, block.rows.map((row) => row.kind === "full" ? (renderField(row.setting)) : (_jsx("div", {
91
+ // A lone half setting stays half width (never stretched to the full row).
92
+ className: `grid grid-cols-[repeat(auto-fit,minmax(min(100%,280px),1fr))] gap-4 ${row.settings.length === 1 ? "md:max-w-[calc(50%-0.5rem)]" : ""}`, children: row.settings.map(renderField) }, row.settings[0].key)))] }, key));
93
+ };
94
+ const footer = (_jsxs(_Fragment, { children: [hasTextFields && (_jsx(Text, { size: "small", className: "text-ui-fg-muted mr-auto", children: texts.settings.clearHint })), _jsx(Button, { type: "submit", size: "small", isLoading: pending, disabled: !form.dirty || form.invalid.length > 0 || pending, children: texts.settings.save })] }));
95
+ // One card: the save button is its footer row. Several cards: one save row
96
+ // under all of them (the form saves every changed value at once).
97
+ const single = groups.length === 1;
98
+ return (_jsxs("form", { onSubmit: form.submit, className: "flex flex-col gap-y-3", "aria-busy": pending, children: [groups.map((group) => {
99
+ const dimmed = isGroupDimmed(plugin.features, group.requiresFeature);
100
+ const legend = group.settings.some((setting) => setting.required)
101
+ ? texts.settings.requiredLegend
102
+ : null;
103
+ return (_jsx(AdminSection, { title: group.title, description: group.description && legend ? (_jsxs(_Fragment, { children: [group.description, _jsx("span", { className: "block", children: legend })] })) : ((group.description ?? legend)), note: group.note, footer: single ? footer : undefined, children: group.blocks.map((block) => renderBlock(block, dimmed)) }, group.name ?? ""));
104
+ }), !single && (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-x-4 gap-y-2", children: footer }))] }));
54
105
  };