@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.
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +77 -26
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +40 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +160 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +40 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +160 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +47 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +70 -0
- package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
- package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
- package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/index.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
- package/.medusa/server/src/admin/index.js +3155 -2039
- package/.medusa/server/src/admin/index.mjs +3158 -2042
- package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
- package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
- package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
- package/.medusa/server/src/api/code-account-password-routes.js +92 -0
- package/.medusa/server/src/api/middlewares.js +82 -17
- package/.medusa/server/src/api/password-policy.js +55 -10
- package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
- package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
- package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
- package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
- package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
- package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
- package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
- package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
- package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
- package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
- package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
- package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
- package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
- package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
- package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
- package/.medusa/server/src/documents/plugin-features.js +8 -2
- package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
- package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
- package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
- package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
- package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
- package/.medusa/server/src/email/domain/business-keys.js +15 -2
- package/.medusa/server/src/email/plugin-features.js +8 -3
- package/.medusa/server/src/email/rendering/default-templates.js +2 -1
- package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
- package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
- package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
- package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
- package/.medusa/server/src/email/templates/render.js +2 -1
- package/.medusa/server/src/email/templates/sample-data.js +2 -1
- package/.medusa/server/src/email/templates/validate.js +8 -3
- package/.medusa/server/src/email/types/email-kinds.js +4 -2
- package/.medusa/server/src/email/types/template-data.js +9 -1
- package/.medusa/server/src/modules/customer-auth/index.js +14 -0
- package/.medusa/server/src/modules/customer-auth/service.js +35 -0
- package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +30 -6
- package/.medusa/server/src/modules/plugin-features/service.js +29 -3
- package/.medusa/server/src/providers/email-otp/index.js +8 -0
- package/.medusa/server/src/providers/email-otp/service.js +96 -0
- package/.medusa/server/src/public/descriptor.js +138 -5
- package/.medusa/server/src/public/effective-features.js +22 -1
- package/.medusa/server/src/public/index.js +4 -2
- package/.medusa/server/src/public/plugin-icons.js +28 -0
- package/.medusa/server/src/public/shop-master-data.js +33 -14
- package/.medusa/server/src/shared/lib/dates.js +8 -2
- package/.medusa/server/src/shared/lib/index.js +2 -1
- package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
- package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
- package/README.md +436 -87
- package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
- package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
- package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
- package/dist/customer-auth/domain/auth-methods.js +31 -0
- package/dist/customer-auth/plugin-features.d.ts +2 -0
- package/dist/customer-auth/plugin-features.js +79 -0
- package/dist/email/customer-verification-snapshot.d.ts +3 -15
- package/dist/email/customer-verification-snapshot.js +19 -107
- package/dist/email/types/email-kinds.d.ts +4 -3
- package/dist/email/types/email-kinds.js +3 -1
- package/dist/email/types/template-data.d.ts +8 -0
- package/dist/email/types/template-data.js +8 -0
- package/dist/public/descriptor.js +137 -4
- package/dist/public/effective-features.d.ts +10 -0
- package/dist/public/effective-features.js +21 -0
- package/dist/public/index.d.ts +2 -1
- package/dist/public/index.js +3 -1
- package/dist/public/plugin-icons.d.ts +7 -0
- package/dist/public/plugin-icons.js +27 -0
- package/dist/public/shop-master-data.js +32 -13
- package/dist/public/types.d.ts +74 -2
- package/dist/shared/lib/dates.d.ts +2 -0
- package/dist/shared/lib/dates.js +7 -1
- package/dist/shared/lib/index.d.ts +1 -0
- package/dist/shared/lib/index.js +1 -0
- package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
- package/dist/shared/lib/refreshing-snapshot.js +121 -0
- package/dist/shop-config/build-food-shop-config.js +37 -0
- package/package.json +8 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
type ImageSettingInputProps = {
|
|
2
2
|
id: string;
|
|
3
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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,
|
|
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" }) })),
|
|
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,
|
|
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 {
|
|
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-
|
|
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 (
|
|
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:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* nothing until the plugin is
|
|
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:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* nothing until the plugin is
|
|
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
|
|
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
|
|
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 {
|
|
5
|
-
|
|
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
|
|
11
|
-
const
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
3
|
-
* (
|
|
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:
|
|
9
|
-
* (
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
5
|
+
import { PluginIcon } from "../../../shared/ui/PluginIcon";
|
|
6
|
+
import { usePluginSwitch } from "../hooks/usePluginSwitch";
|
|
6
7
|
import { pluginDetailPath } from "../lib/plugin-paths";
|
|
7
|
-
import {
|
|
8
|
-
/**
|
|
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
|
|
11
|
-
|
|
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:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
|
47
|
-
const hasTextFields = plugin.settings.some((setting) => setting.type
|
|
48
|
-
setting.type
|
|
49
|
-
setting.type
|
|
50
|
-
setting.type
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
};
|