@hartl-services/medusa-base 0.2.0 → 0.4.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.
- package/.medusa/server/src/admin/index.js +741 -36
- package/.medusa/server/src/admin/index.mjs +743 -38
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/route.js +31 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/runs/route.js +16 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/route.js +14 -0
- package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/settings/route.js +22 -0
- package/.medusa/server/src/api/admin/plugin-features/route.js +6 -4
- package/.medusa/server/src/api/middlewares.js +22 -8
- package/.medusa/server/src/api/plugin-actions.js +221 -0
- package/.medusa/server/src/api/plugin-features-http.js +67 -8
- package/.medusa/server/src/modules/plugin-features/domain/plugin-action-runs.js +92 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +213 -0
- package/.medusa/server/src/modules/plugin-features/domain/secret-box.js +56 -0
- package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928205031.js +16 -0
- package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928215428.js +17 -0
- package/.medusa/server/src/modules/plugin-features/models/plugin-action-run.js +43 -0
- package/.medusa/server/src/modules/plugin-features/models/plugin-setting-state.js +32 -0
- package/.medusa/server/src/modules/plugin-features/service.js +419 -17
- package/.medusa/server/src/public/action-progress.js +67 -0
- package/.medusa/server/src/public/descriptor.js +199 -2
- package/.medusa/server/src/public/effective-features.js +9 -5
- package/.medusa/server/src/public/enforcement.js +18 -8
- package/.medusa/server/src/public/index.js +10 -2
- package/.medusa/server/src/public/module-access.js +14 -0
- package/.medusa/server/src/public/sentry-logger.js +82 -0
- package/.medusa/server/src/public/settings.js +17 -0
- package/.medusa/server/src/public/shop-master-data.js +95 -0
- package/.medusa/server/src/utils/discover-complete-cart-participants.js +9 -2
- package/.medusa/server/src/utils/discover-plugin-features.js +47 -9
- package/.medusa/server/src/workflows/hooks/complete-cart-participants.js +28 -16
- package/README.md +416 -12
- package/dist/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +6 -0
- package/dist/admin-components/features/plugin-features/components/PluginActionsSection.js +66 -0
- package/dist/admin-components/features/plugin-features/components/PluginDetailPage.d.ts +2 -0
- package/dist/admin-components/features/plugin-features/components/PluginDetailPage.js +9 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.d.ts +22 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.js +18 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +11 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.js +19 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +12 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.js +34 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +5 -0
- package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +16 -0
- package/dist/admin-components/features/plugin-features/components/PluginHubCard.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/components/PluginHubCard.js +21 -0
- package/dist/admin-components/features/plugin-features/components/PluginPageShell.d.ts +13 -0
- package/dist/admin-components/features/plugin-features/components/PluginPageShell.js +20 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +39 -0
- package/dist/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -0
- package/dist/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +114 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.js +40 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +392 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.js +128 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +93 -0
- package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.js +44 -0
- package/dist/admin-components/features/plugin-features/lib/action-runs.d.ts +19 -0
- package/dist/admin-components/features/plugin-features/lib/action-runs.js +74 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +363 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +9 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-paths.d.ts +4 -0
- package/dist/admin-components/features/plugin-features/lib/plugin-paths.js +4 -0
- package/dist/admin-components/features/plugin-features/lib/settings-draft.d.ts +28 -0
- package/dist/admin-components/features/plugin-features/lib/settings-draft.js +86 -0
- package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.d.ts +7 -0
- package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.js +7 -0
- package/dist/admin-components/i18n/texts.d.ts +90 -0
- package/dist/admin-components/i18n/texts.js +90 -0
- package/dist/admin-components/index.d.ts +14 -0
- package/dist/admin-components/index.js +14 -0
- package/dist/admin-components/lib/errors.d.ts +6 -0
- package/dist/admin-components/lib/errors.js +27 -0
- package/dist/admin-components/lib/sdk.d.ts +2 -0
- package/dist/admin-components/lib/sdk.js +13 -0
- package/dist/public/action-progress.d.ts +25 -0
- package/dist/public/action-progress.js +66 -0
- package/dist/public/descriptor.d.ts +3 -2
- package/dist/public/descriptor.js +198 -1
- package/dist/public/effective-features.d.ts +2 -1
- package/dist/public/effective-features.js +8 -4
- package/dist/public/enforcement.d.ts +7 -0
- package/dist/public/enforcement.js +17 -7
- package/dist/public/index.d.ts +5 -2
- package/dist/public/index.js +9 -1
- package/dist/public/module-access.d.ts +3 -0
- package/dist/public/module-access.js +13 -0
- package/dist/public/sentry-logger.d.ts +11 -0
- package/dist/public/sentry-logger.js +81 -0
- package/dist/public/settings.d.ts +11 -0
- package/dist/public/settings.js +16 -0
- package/dist/public/shop-master-data.d.ts +25 -0
- package/dist/public/shop-master-data.js +94 -0
- package/dist/public/types.d.ts +97 -1
- package/dist/utils/discover-plugin-features.d.ts +28 -0
- package/dist/utils/discover-plugin-features.js +96 -0
- package/package.json +20 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
type PluginFeaturesCardProps = {
|
|
3
|
+
plugin: AdminPluginFeatures;
|
|
4
|
+
/** Heading of the card; the plugin label when omitted. */
|
|
5
|
+
title?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Plugin switch (unless the plugin is not switchable) plus the feature
|
|
9
|
+
* switches of one plugin; saves immediately.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PluginFeaturesCard: ({ plugin, title }: PluginFeaturesCardProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Container, Heading, Label, Switch, Text } from "@medusajs/ui";
|
|
3
|
+
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]));
|
|
6
|
+
/**
|
|
7
|
+
* Plugin switch (unless the plugin is not switchable) plus the feature
|
|
8
|
+
* switches of one plugin; saves immediately.
|
|
9
|
+
*/
|
|
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);
|
|
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
|
+
})] }));
|
|
34
|
+
};
|
package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Container, Heading, Text } from "@medusajs/ui";
|
|
3
|
+
import { texts } from "../../../i18n/texts";
|
|
4
|
+
import { usePluginFeatures } from "../hooks/usePluginFeatures";
|
|
5
|
+
import { PluginHubCard } from "./PluginHubCard";
|
|
6
|
+
/**
|
|
7
|
+
* Hub: one card per installed plugin. Plugins that cannot be switched off
|
|
8
|
+
* (the shop master data) come first; the rest keep the server's order.
|
|
9
|
+
*/
|
|
10
|
+
export const PluginFeaturesSettingsPage = () => {
|
|
11
|
+
const query = usePluginFeatures();
|
|
12
|
+
return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [_jsxs(Container, { className: "px-6 py-4", children: [_jsx(Heading, { level: "h1", children: texts.page.title }), _jsx(Text, { 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 }) })) : ([
|
|
13
|
+
...query.data.plugins.filter((plugin) => !plugin.switchable),
|
|
14
|
+
...query.data.plugins.filter((plugin) => plugin.switchable),
|
|
15
|
+
].map((plugin) => (_jsx(PluginHubCard, { plugin: plugin }, plugin.plugin_key))))] }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
type PluginHubCardProps = {
|
|
3
|
+
plugin: AdminPluginFeatures;
|
|
4
|
+
};
|
|
5
|
+
/** One plugin on the hub: status, plugin switch and the link to its page. */
|
|
6
|
+
export declare const PluginHubCard: ({ plugin }: PluginHubCardProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Container, Heading, Switch, Text } from "@medusajs/ui";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { texts } from "../../../i18n/texts";
|
|
5
|
+
import { useUpdatePluginFeatures } from "../hooks/usePluginFeatures";
|
|
6
|
+
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. */
|
|
9
|
+
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 }) })] })] }));
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
type PluginPageShellProps = {
|
|
3
|
+
pluginKey: string;
|
|
4
|
+
/** Rendered below the header once the plugin is loaded. */
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
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`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PluginPageShell: ({ pluginKey, children }: PluginPageShellProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Container, Heading, Text } from "@medusajs/ui";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { texts } from "../../../i18n/texts";
|
|
5
|
+
import { usePluginFeature } from "../hooks/usePluginFeatures";
|
|
6
|
+
import { PLUGIN_HUB_PATH } from "../lib/plugin-paths";
|
|
7
|
+
import { PluginStatusChips } from "./PluginStatusChips";
|
|
8
|
+
const isNotFound = (error) => typeof error === "object" &&
|
|
9
|
+
error !== null &&
|
|
10
|
+
error.status === 404;
|
|
11
|
+
/**
|
|
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`.
|
|
15
|
+
*/
|
|
16
|
+
export const PluginPageShell = ({ pluginKey, children }) => {
|
|
17
|
+
const query = usePluginFeature(pluginKey);
|
|
18
|
+
const plugin = query.data?.plugin;
|
|
19
|
+
return (_jsxs("div", { className: "flex flex-col gap-y-3", children: [_jsx("div", { children: _jsxs(Link, { to: PLUGIN_HUB_PATH, className: "text-ui-fg-subtle hover:text-ui-fg-base txt-compact-small-plus", children: ["\u2190 ", texts.detail.back] }) }), 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 })] }))] }));
|
|
20
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
type PluginSettingsFormProps = {
|
|
3
|
+
plugin: AdminPluginFeatures;
|
|
4
|
+
};
|
|
5
|
+
export declare const missingLabels: (plugin: AdminPluginFeatures) => string;
|
|
6
|
+
export declare const PluginSettingsForm: ({ plugin }: PluginSettingsFormProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Button, Container, Heading, Hint, Input, Label, Select, Switch, Text, } from "@medusajs/ui";
|
|
3
|
+
import { texts } from "../../../i18n/texts";
|
|
4
|
+
import { usePluginSettingsForm } from "../hooks/usePluginSettingsForm";
|
|
5
|
+
const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
|
|
6
|
+
export const missingLabels = (plugin) => plugin.missing
|
|
7
|
+
.map((key) => plugin.settings.find((setting) => setting.key === key)?.label ?? key)
|
|
8
|
+
.join(", ");
|
|
9
|
+
/** "API-Key, Passwort" for the secrets a change of `setting` deletes. */
|
|
10
|
+
const invalidatesLabels = (plugin, setting) => setting.invalidates
|
|
11
|
+
.map((key) => plugin.settings.find((candidate) => candidate.key === key)?.label ?? key)
|
|
12
|
+
.join(", ");
|
|
13
|
+
/** "Zwischen 0 und 100." etc.; `undefined` without bounds. */
|
|
14
|
+
const boundsText = (setting) => setting.min !== null && setting.max !== null
|
|
15
|
+
? texts.settings.numberRange(setting.min, setting.max)
|
|
16
|
+
: setting.min !== null
|
|
17
|
+
? texts.settings.numberMin(setting.min)
|
|
18
|
+
: setting.max !== null
|
|
19
|
+
? texts.settings.numberMax(setting.max)
|
|
20
|
+
: undefined;
|
|
21
|
+
const SettingField = ({ pluginKey, setting, invalidatesLabels, value, invalid, disabled, onChange, }) => {
|
|
22
|
+
const id = fieldId(pluginKey, setting.key);
|
|
23
|
+
const hintId = `${id}-hint`;
|
|
24
|
+
const clearPending = setting.type === "secret" && value === null;
|
|
25
|
+
const bounds = setting.type === "number" ? boundsText(setting) : undefined;
|
|
26
|
+
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
|
|
27
|
+
? texts.settings.secretClearUndoLabel(setting.label)
|
|
28
|
+
: texts.settings.secretClearLabel(setting.label), onClick: () => onChange(clearPending ? "" : null), children: clearPending
|
|
29
|
+
? texts.settings.secretClearUndo
|
|
30
|
+
: 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 === "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: bounds ?? texts.settings.invalidNumber }))] })] }));
|
|
31
|
+
};
|
|
32
|
+
export const PluginSettingsForm = ({ plugin }) => {
|
|
33
|
+
const form = usePluginSettingsForm(plugin);
|
|
34
|
+
const pending = form.mutation.isPending;
|
|
35
|
+
const hasTextFields = plugin.settings.some((setting) => setting.type !== "boolean" &&
|
|
36
|
+
setting.type !== "secret" &&
|
|
37
|
+
setting.type !== "select");
|
|
38
|
+
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)) })), plugin.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))), _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 }) })] })] }));
|
|
39
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
/** aktiv/aus (switchable plugins only), plus konfiguriert/nicht konfiguriert when the plugin has settings. */
|
|
3
|
+
export declare const PluginStatusChips: ({ plugin }: {
|
|
4
|
+
plugin: AdminPluginFeatures;
|
|
5
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { StatusBadge } from "@medusajs/ui";
|
|
3
|
+
import { texts } from "../../../i18n/texts";
|
|
4
|
+
import { missingLabels } from "./PluginSettingsForm";
|
|
5
|
+
/** aktiv/aus (switchable plugins only), plus konfiguriert/nicht konfiguriert when the plugin has settings. */
|
|
6
|
+
export const PluginStatusChips = ({ plugin }) => (_jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [plugin.switchable && (_jsx(StatusBadge, { color: plugin.enabled ? "green" : "grey", children: plugin.enabled ? texts.chips.enabled : texts.chips.disabled })), plugin.settings.length > 0 && (_jsx(StatusBadge, { color: plugin.configured ? "green" : "orange", children: plugin.configured
|
|
7
|
+
? texts.chips.configured
|
|
8
|
+
: texts.chips.notConfigured(missingLabels(plugin)) }))] }));
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { AdminPluginFeaturesListResponse } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
export type PluginFeatureEnabledState = {
|
|
3
|
+
/** `false` while loading and on error, so gated UI never flashes. */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
isPending: boolean;
|
|
6
|
+
isError: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Widgets re-read the toggles at most every 30 s (hub saves update at once). */
|
|
9
|
+
export declare const PLUGIN_FEATURE_ENABLED_STALE_TIME = 30000;
|
|
10
|
+
/**
|
|
11
|
+
* Like the server's `isPluginFeatureEnabled`: the plugin switch and every
|
|
12
|
+
* given key must be effective; an unknown plugin or key is `false`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isPluginFeatureEnabledIn: (data: AdminPluginFeaturesListResponse, pluginKey: string, featureKeys: readonly string[]) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Query options shared with the hub's list (`GET /admin/plugin-features`,
|
|
17
|
+
* same key), so the hub's toggle mutations update gated widgets too.
|
|
18
|
+
*/
|
|
19
|
+
export declare const pluginFeatureEnabledQueryOptions: (pluginKey: string, featureKeys: readonly string[]) => {
|
|
20
|
+
queryKey: readonly ["plugin-features", "list"];
|
|
21
|
+
queryFn: () => Promise<{
|
|
22
|
+
plugins: {
|
|
23
|
+
plugin_key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
description: string | null;
|
|
26
|
+
default_enabled: boolean;
|
|
27
|
+
switchable: boolean;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
version: number;
|
|
30
|
+
features: {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
description: string | null;
|
|
34
|
+
parent_key: string | null;
|
|
35
|
+
default_enabled: boolean;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
effective_enabled: boolean;
|
|
38
|
+
}[];
|
|
39
|
+
settings: ({
|
|
40
|
+
type: "secret";
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
description: string | null;
|
|
44
|
+
required: boolean;
|
|
45
|
+
is_set: boolean;
|
|
46
|
+
source: "default" | "stored" | "config" | "none";
|
|
47
|
+
invalidates: string[];
|
|
48
|
+
} | {
|
|
49
|
+
type: "select";
|
|
50
|
+
options: {
|
|
51
|
+
value: string;
|
|
52
|
+
label: string;
|
|
53
|
+
}[];
|
|
54
|
+
default: string | null;
|
|
55
|
+
value: string | null;
|
|
56
|
+
key: string;
|
|
57
|
+
label: string;
|
|
58
|
+
description: string | null;
|
|
59
|
+
required: boolean;
|
|
60
|
+
is_set: boolean;
|
|
61
|
+
source: "default" | "stored" | "config" | "none";
|
|
62
|
+
invalidates: string[];
|
|
63
|
+
} | {
|
|
64
|
+
type: "number";
|
|
65
|
+
min: number | null;
|
|
66
|
+
max: number | null;
|
|
67
|
+
default: number | null;
|
|
68
|
+
value: number | null;
|
|
69
|
+
key: string;
|
|
70
|
+
label: string;
|
|
71
|
+
description: string | null;
|
|
72
|
+
required: boolean;
|
|
73
|
+
is_set: boolean;
|
|
74
|
+
source: "default" | "stored" | "config" | "none";
|
|
75
|
+
invalidates: string[];
|
|
76
|
+
} | {
|
|
77
|
+
type: "string" | "boolean" | "url";
|
|
78
|
+
default: string | number | boolean | null;
|
|
79
|
+
value: string | number | boolean | null;
|
|
80
|
+
key: string;
|
|
81
|
+
label: string;
|
|
82
|
+
description: string | null;
|
|
83
|
+
required: boolean;
|
|
84
|
+
is_set: boolean;
|
|
85
|
+
source: "default" | "stored" | "config" | "none";
|
|
86
|
+
invalidates: string[];
|
|
87
|
+
})[];
|
|
88
|
+
configured: boolean;
|
|
89
|
+
missing: string[];
|
|
90
|
+
settings_version: number;
|
|
91
|
+
actions: {
|
|
92
|
+
key: string;
|
|
93
|
+
label: string;
|
|
94
|
+
description: string | null;
|
|
95
|
+
mode: "sync" | "background";
|
|
96
|
+
confirm: string | null;
|
|
97
|
+
}[];
|
|
98
|
+
admin_page: string | null;
|
|
99
|
+
}[];
|
|
100
|
+
}>;
|
|
101
|
+
staleTime: number;
|
|
102
|
+
select: (data: AdminPluginFeaturesListResponse) => boolean;
|
|
103
|
+
};
|
|
104
|
+
export declare const toPluginFeatureEnabledState: (result: {
|
|
105
|
+
data: boolean | undefined;
|
|
106
|
+
isPending: boolean;
|
|
107
|
+
isError: boolean;
|
|
108
|
+
}) => PluginFeatureEnabledState;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a plugin (and optionally some of its features) is effectively on,
|
|
111
|
+
* for hiding widgets and pages of a disabled feature. No keys = plugin
|
|
112
|
+
* switch only; several keys = all must be on.
|
|
113
|
+
*/
|
|
114
|
+
export declare function usePluginFeatureEnabled(pluginKey: string, ...featureKeys: string[]): PluginFeatureEnabledState;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { listPluginFeatures } from "../lib/plugin-features-admin-client";
|
|
3
|
+
import { pluginFeatureKeys } from "../queries/pluginFeatureKeys";
|
|
4
|
+
/** Widgets re-read the toggles at most every 30 s (hub saves update at once). */
|
|
5
|
+
export const PLUGIN_FEATURE_ENABLED_STALE_TIME = 30_000;
|
|
6
|
+
/**
|
|
7
|
+
* Like the server's `isPluginFeatureEnabled`: the plugin switch and every
|
|
8
|
+
* given key must be effective; an unknown plugin or key is `false`.
|
|
9
|
+
*/
|
|
10
|
+
export const isPluginFeatureEnabledIn = (data, pluginKey, featureKeys) => {
|
|
11
|
+
const plugin = data.plugins.find((entry) => entry.plugin_key === pluginKey);
|
|
12
|
+
if (!plugin?.enabled) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return featureKeys.every((key) => plugin.features.find((feature) => feature.key === key)
|
|
16
|
+
?.effective_enabled === true);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Query options shared with the hub's list (`GET /admin/plugin-features`,
|
|
20
|
+
* same key), so the hub's toggle mutations update gated widgets too.
|
|
21
|
+
*/
|
|
22
|
+
export const pluginFeatureEnabledQueryOptions = (pluginKey, featureKeys) => ({
|
|
23
|
+
queryKey: pluginFeatureKeys.list(),
|
|
24
|
+
queryFn: listPluginFeatures,
|
|
25
|
+
staleTime: PLUGIN_FEATURE_ENABLED_STALE_TIME,
|
|
26
|
+
select: (data) => isPluginFeatureEnabledIn(data, pluginKey, featureKeys),
|
|
27
|
+
});
|
|
28
|
+
export const toPluginFeatureEnabledState = (result) => ({
|
|
29
|
+
enabled: !result.isPending && !result.isError && result.data === true,
|
|
30
|
+
isPending: result.isPending,
|
|
31
|
+
isError: result.isError,
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Whether a plugin (and optionally some of its features) is effectively on,
|
|
35
|
+
* for hiding widgets and pages of a disabled feature. No keys = plugin
|
|
36
|
+
* switch only; several keys = all must be on.
|
|
37
|
+
*/
|
|
38
|
+
export function usePluginFeatureEnabled(pluginKey, ...featureKeys) {
|
|
39
|
+
return toPluginFeatureEnabledState(useQuery(pluginFeatureEnabledQueryOptions(pluginKey, featureKeys)));
|
|
40
|
+
}
|