@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
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Medusa Base
|
|
2
2
|
|
|
3
3
|
Medusa v2.19 base plugin for Hartl Services shops. Every shop installs it. It
|
|
4
|
-
provides per-plugin feature toggles: a generic
|
|
5
|
-
**Settings → Hartl Services Plugins
|
|
6
|
-
helpers that plugins use to
|
|
4
|
+
provides per-plugin feature toggles, runtime settings and actions: a generic
|
|
5
|
+
Admin hub under **Settings → Hartl Services Plugins** with one page per
|
|
6
|
+
plugin, a Store endpoint for storefronts and helpers that plugins use to
|
|
7
|
+
enforce their toggles and read their settings.
|
|
7
8
|
|
|
8
9
|
## What it does
|
|
9
10
|
|
|
@@ -16,6 +17,19 @@ helpers that plugins use to enforce their toggles.
|
|
|
16
17
|
values to the Admin.
|
|
17
18
|
- Exports `definePluginFeatures`, `requirePluginFeature` and
|
|
18
19
|
`isPluginFeatureEnabled` from `@hartl-services/medusa-base` for plugins.
|
|
20
|
+
- Emits `plugin-features.updated.v1` after every saved Admin toggle update
|
|
21
|
+
(see "Toggle-change event").
|
|
22
|
+
- Stores runtime settings that plugins declare (secrets encrypted), resolves
|
|
23
|
+
them with the medusa-config fallback and exports `getPluginSettings` (see
|
|
24
|
+
"Runtime settings").
|
|
25
|
+
- Runs actions that plugins declare as workflow ids, records every run with
|
|
26
|
+
its progress and outcome and rejects a second start while one runs (see
|
|
27
|
+
"Actions").
|
|
28
|
+
- Holds the shop's master data (Firmenangaben) as its own settings and
|
|
29
|
+
exports `getShopMasterData` (see "Shop-Stammdaten").
|
|
30
|
+
- Exports Admin components from `@hartl-services/medusa-base/admin-components`
|
|
31
|
+
so a plugin composes the base's sections into its own page and hides
|
|
32
|
+
widgets of disabled features (see "Admin components for plugin pages").
|
|
19
33
|
- Owns the `completeCartWorkflow` hooks `validate` and `orderCreated` and runs
|
|
20
34
|
the workflows that plugins declare for them (see "Complete-cart hook
|
|
21
35
|
composition").
|
|
@@ -32,13 +46,21 @@ plugins: [
|
|
|
32
46
|
// First in the list: other plugins read their toggles through it.
|
|
33
47
|
{
|
|
34
48
|
resolve: "@hartl-services/medusa-base",
|
|
35
|
-
options: {
|
|
49
|
+
options: {
|
|
50
|
+
// Optional: key material for secret settings (see "Secrets").
|
|
51
|
+
settingsSecret: process.env.PLUGIN_SETTINGS_SECRET,
|
|
52
|
+
},
|
|
36
53
|
},
|
|
37
54
|
// … all other plugins
|
|
38
55
|
]
|
|
39
56
|
```
|
|
40
57
|
|
|
41
|
-
The
|
|
58
|
+
The only option is `settingsSecret` (`BasePluginOptions`, optional): the key
|
|
59
|
+
material for stored secret settings. Set it from an environment variable of
|
|
60
|
+
the shop; unset or empty, the base uses `projectConfig.http.cookieSecret`.
|
|
61
|
+
Run `medusa db:migrate` after installing it (and
|
|
62
|
+
after upgrading: 0.4.0 adds the tables `plugin_setting_state` and
|
|
63
|
+
`plugin_action_run`).
|
|
42
64
|
|
|
43
65
|
Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
|
|
44
66
|
`@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`)
|
|
@@ -53,8 +75,17 @@ dashboard uses.
|
|
|
53
75
|
| `GET` | `/store/plugin-features/:plugin_key` | publishable key | `StorePluginFeaturesResponse`, 404 for an unknown plugin |
|
|
54
76
|
| `GET` | `/admin/plugin-features` | Admin user | `AdminPluginFeaturesListResponse` |
|
|
55
77
|
| `POST` | `/admin/plugin-features` | Admin user | `AdminPluginFeaturesResponse` |
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
| `GET` | `/admin/plugin-features/:plugin_key` | Admin user | `AdminPluginFeaturesResponse`, 404 for an unknown plugin |
|
|
79
|
+
| `GET` | `/admin/plugin-features/:plugin_key/settings` | Admin user | `AdminPluginSettingsResponse` |
|
|
80
|
+
| `POST` | `/admin/plugin-features/:plugin_key/settings` | Admin user | `AdminPluginFeaturesResponse` |
|
|
81
|
+
| `POST` | `/admin/plugin-features/:plugin_key/actions/:action_key` | Admin user | `AdminPluginActionRunResponse` (200 sync, 202 background), 409 `AdminPluginActionConflictResponse` while it runs |
|
|
82
|
+
| `GET` | `/admin/plugin-features/:plugin_key/actions/:action_key/runs` | Admin user | `AdminPluginActionRunsResponse` (`?limit=`, default 1, max 20) |
|
|
83
|
+
|
|
84
|
+
Every authenticated Admin user may change toggles and settings and run
|
|
85
|
+
actions (no RBAC by design). Admin plugin DTOs carry the toggles plus
|
|
86
|
+
`settings` (with `source`, never a secret's value), `configured`, `missing`,
|
|
87
|
+
`settings_version`, `actions`, `admin_page` (the plugin's own page or
|
|
88
|
+
`null`) and `switchable`, so the list is one round trip for the hub.
|
|
58
89
|
`POST` takes `{ plugin_key, expected_version, enabled, features }`; `features`
|
|
59
90
|
holds configured values for declared keys, missing keys keep their default and
|
|
60
91
|
an unknown key is 400. An unknown `plugin_key` is 404, a stale
|
|
@@ -108,6 +139,11 @@ Keys match `^[a-z][a-z0-9_]{0,62}$`, feature keys are unique, a `parent_key`
|
|
|
108
139
|
must exist and only one nesting level is allowed. `definePluginFeatures`
|
|
109
140
|
throws at import time otherwise.
|
|
110
141
|
|
|
142
|
+
`switchable: false` declares a plugin that cannot be switched off (the
|
|
143
|
+
base's own shop master data): the Admin shows no plugin switch, `enabled` is
|
|
144
|
+
always `true` whatever is stored, and `POST /admin/plugin-features` with
|
|
145
|
+
`enabled: false` is 400. It cannot be combined with `default_enabled: false`.
|
|
146
|
+
|
|
111
147
|
### Gating routes
|
|
112
148
|
|
|
113
149
|
Put gate entries at the start of the plugin's `routes` array. An entry without
|
|
@@ -148,6 +184,43 @@ A read failure throws, so the subscriber or job is retried. A missing base
|
|
|
148
184
|
plugin fails with `UNEXPECTED_STATE` ("@hartl-services/medusa-base ist nicht in
|
|
149
185
|
medusa-config registriert.").
|
|
150
186
|
|
|
187
|
+
To check another plugin without importing it, use its plugin key:
|
|
188
|
+
`isPluginFeatureEnabledByKey(container, "documents", "invoices")`. It finds
|
|
189
|
+
the descriptor among the registered modules; an unknown or uninstalled plugin
|
|
190
|
+
key (and an undeclared feature key) is `false`.
|
|
191
|
+
|
|
192
|
+
### Toggle-change event
|
|
193
|
+
|
|
194
|
+
After a successful `POST /admin/plugin-features` the base emits
|
|
195
|
+
`PLUGIN_FEATURES_UPDATED_EVENT` (`plugin-features.updated.v1`) through the
|
|
196
|
+
event bus. The payload (`PluginFeaturesUpdatedEventSchema`) carries the
|
|
197
|
+
plugin's effective values after the update:
|
|
198
|
+
`{ plugin_key, enabled, features: { [key]: effective }, version }`. The event
|
|
199
|
+
is emitted after the write committed; a failed emit is only logged and does
|
|
200
|
+
not undo the write. Direct service calls (`updatePluginFeatures`) emit
|
|
201
|
+
nothing.
|
|
202
|
+
|
|
203
|
+
A plugin that must react right away (instead of on its next job run)
|
|
204
|
+
subscribes to it and filters on its own `plugin_key`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import {
|
|
208
|
+
PLUGIN_FEATURES_UPDATED_EVENT,
|
|
209
|
+
PluginFeaturesUpdatedEventSchema,
|
|
210
|
+
} from "@hartl-services/medusa-food-supplements-contracts/events/plugin-features"
|
|
211
|
+
|
|
212
|
+
export default async function onToggleChange({ event, container }: SubscriberArgs<unknown>) {
|
|
213
|
+
const payload = PluginFeaturesUpdatedEventSchema.parse(event.data)
|
|
214
|
+
if (payload.plugin_key !== demoPluginFeatures.plugin_key) return
|
|
215
|
+
// re-read the toggles and reconcile
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const config: SubscriberConfig = { event: PLUGIN_FEATURES_UPDATED_EVENT }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Events can arrive late or twice; re-read the current toggles instead of
|
|
222
|
+
trusting the payload for anything that writes.
|
|
223
|
+
|
|
151
224
|
### Never on the cart path
|
|
152
225
|
|
|
153
226
|
Code on the cart and checkout hot path (cart middlewares, cart workflow hooks,
|
|
@@ -156,6 +229,310 @@ fail-safe and independent of the database state of this plugin. Toggles
|
|
|
156
229
|
control visibility and reachability of features, not the integrity of
|
|
157
230
|
existing data.
|
|
158
231
|
|
|
232
|
+
## Runtime settings
|
|
233
|
+
|
|
234
|
+
Configuration a plugin reads at RUN time (for example an external system's
|
|
235
|
+
URL and API key) is declared in the descriptor and edited on the plugin's
|
|
236
|
+
Admin page. Boot-time provider configuration (SMTP, S3, Sentry, Redis) and
|
|
237
|
+
function-valued options stay in `medusa-config.ts`.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
export const demoPluginFeatures = definePluginFeatures({
|
|
241
|
+
plugin_key: "demo",
|
|
242
|
+
label: "Demo-Plugin",
|
|
243
|
+
features: [],
|
|
244
|
+
settings: [
|
|
245
|
+
{ key: "api_url", label: "API-URL", type: "url", required: true },
|
|
246
|
+
{ key: "api_key", label: "API-Key", type: "secret", required: true },
|
|
247
|
+
{ key: "collection", label: "Collection", type: "string", default: "users" },
|
|
248
|
+
{ key: "batch_size", label: "Batchgröße", type: "number", min: 1, max: 500, default: 100 },
|
|
249
|
+
{
|
|
250
|
+
key: "mode",
|
|
251
|
+
label: "Modus",
|
|
252
|
+
type: "select",
|
|
253
|
+
options: [
|
|
254
|
+
{ value: "live", label: "Live" },
|
|
255
|
+
{ value: "test", label: "Test" },
|
|
256
|
+
],
|
|
257
|
+
default: "test",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
263
|
+
readonly pluginFeatures = demoPluginFeatures
|
|
264
|
+
// Values the plugin received from medusa-config; shown as "aus Konfiguration".
|
|
265
|
+
readonly pluginSettingsFallback: PluginSettingsFallback<"api_url" | "api_key">
|
|
266
|
+
|
|
267
|
+
constructor(cradle: unknown, options?: DemoOptions) {
|
|
268
|
+
super(...arguments)
|
|
269
|
+
this.pluginSettingsFallback = { api_url: options?.apiUrl, api_key: options?.apiKey }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- Types: `string`, `url` (http/https), `number` (finite, optional inclusive
|
|
275
|
+
`min`/`max`), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
|
|
276
|
+
non-empty, unique values; the stored value must be one of them). Keys
|
|
277
|
+
follow the feature key format and are unique. A `secret` has no default;
|
|
278
|
+
any other default must match its type, options and bounds. `options` exist
|
|
279
|
+
only on `select`, `min`/`max` only on `number`. `required` defaults to
|
|
280
|
+
`false`. `invalidates` (optional) lists secret settings of the same plugin
|
|
281
|
+
that a change of this setting deletes, e.g. `invalidates: ["api_key"]` on
|
|
282
|
+
`api_url` so a stored key is never sent to a new host. `definePluginFeatures`
|
|
283
|
+
throws at import time otherwise (also for an `invalidates` entry that is not
|
|
284
|
+
a secret of the plugin). The Admin renders a `select` as a dropdown, gives
|
|
285
|
+
number inputs their bounds and shows "Beim Ändern muss … neu eingegeben
|
|
286
|
+
werden" under a setting with `invalidates`.
|
|
287
|
+
- Precedence per key: stored in the Admin > `pluginSettingsFallback`
|
|
288
|
+
(medusa-config) > declared default > not set. A value that does not fit the
|
|
289
|
+
declared type is skipped. A plugin is `configured` when every required
|
|
290
|
+
setting has a value; `missing` lists the others. A plugin must handle "nicht
|
|
291
|
+
konfiguriert" at run time (log and skip) instead of failing at boot.
|
|
292
|
+
- Server-side code reads the resolved values with secrets DECRYPTED:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { getPluginSettings } from "@hartl-services/medusa-base"
|
|
296
|
+
|
|
297
|
+
const { values, configured, missing } = await getPluginSettings(
|
|
298
|
+
container, demoPluginFeatures, fallback
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
A module service has no access to other modules through its own container;
|
|
303
|
+
it passes the global `container` from `@medusajs/framework`.
|
|
304
|
+
- Updates (`POST …/settings` with `{ expected_version, values }`) merge
|
|
305
|
+
`values` into the stored row in one transaction: `null` clears a stored
|
|
306
|
+
value (the setting falls back to medusa-config or its default), a secret
|
|
307
|
+
that is not sent keeps its stored value, keys the plugin no longer declares
|
|
308
|
+
are dropped. When a setting with `invalidates` gets a different stored
|
|
309
|
+
value (also when it is cleared), the listed secrets are deleted in the same
|
|
310
|
+
transaction unless the update sets them anew; they appear in
|
|
311
|
+
`changed_keys`. Unknown keys and values of the wrong type, outside `min`/`max`
|
|
312
|
+
or not among a select's options answer 400 naming the key, a stale `expected_version` 409. The response is the full
|
|
313
|
+
`AdminPluginFeaturesResponse`.
|
|
314
|
+
- After a saved update the base emits `PLUGIN_SETTINGS_UPDATED_EVENT`
|
|
315
|
+
(`plugin-settings.updated.v1`) with `{ plugin_key, version, changed_keys }`
|
|
316
|
+
(no values). Direct service calls emit nothing.
|
|
317
|
+
|
|
318
|
+
### Secrets
|
|
319
|
+
|
|
320
|
+
Secrets are stored AES-256-GCM encrypted (`{ enc: "<base64 iv|tag|ciphertext>" }`
|
|
321
|
+
in `plugin_setting_state.values`) with a key derived by HKDF-SHA256 from the
|
|
322
|
+
plugin option `settingsSecret`, else `projectConfig.http.cookieSecret`. The
|
|
323
|
+
ciphertext is bound to `<plugin_key>:<setting key>` (AES-GCM additional
|
|
324
|
+
authenticated data), so a stored value cannot be copied to another setting or
|
|
325
|
+
plugin. The Admin never receives a secret's value, only whether it is set.
|
|
326
|
+
**Changing the key material (`settingsSecret`, or `COOKIE_SECRET` without it)
|
|
327
|
+
invalidates stored secrets:** they then count as "nicht gesetzt" (a warning is
|
|
328
|
+
logged once per secret) and must be entered again; a medusa-config fallback
|
|
329
|
+
applies in the meantime. Secrets saved by an unpublished development build
|
|
330
|
+
before the key binding existed count as "nicht gesetzt" the same way and must
|
|
331
|
+
be entered again.
|
|
332
|
+
|
|
333
|
+
## Actions
|
|
334
|
+
|
|
335
|
+
A plugin declares actions as registered workflow ids; the base runs them from
|
|
336
|
+
the plugin's Admin page:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
actions: [
|
|
340
|
+
{
|
|
341
|
+
key: "sync_now",
|
|
342
|
+
label: "Jetzt synchronisieren",
|
|
343
|
+
workflow_id: "demo-sync-catalog",
|
|
344
|
+
mode: "background", // or "sync"
|
|
345
|
+
confirm: "Den ganzen Katalog jetzt abgleichen?", // optional prompt
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The workflow receives `PluginActionWorkflowInput`
|
|
351
|
+
(`{ plugin_key, action_key, triggered_by, run_id }`; `triggered_by` is the
|
|
352
|
+
Admin user id, `run_id` the recorded run).
|
|
353
|
+
|
|
354
|
+
- Every start is recorded in `plugin_action_run` (`running` → `completed` |
|
|
355
|
+
`failed`, `started_at`, `finished_at`, `triggered_by`, `message` of a
|
|
356
|
+
failure, `progress`, `result` when it is JSON and at most 4 KB).
|
|
357
|
+
- While a run of the same action is `running`, a start answers 409
|
|
358
|
+
(`{ type: "conflict", message: "Diese Aktion läuft bereits.", run }`) and
|
|
359
|
+
starts nothing. A partial unique index enforces this also for concurrent
|
|
360
|
+
requests.
|
|
361
|
+
- `sync`: the request waits for the workflow and answers
|
|
362
|
+
`{ action: { key, status: "completed", result?, run } }`. A failure goes
|
|
363
|
+
through Medusa's error pipeline (a `MedusaError` keeps its status); the run
|
|
364
|
+
is recorded as failed.
|
|
365
|
+
- `background`: the run is started without waiting and the request answers
|
|
366
|
+
202 `{ action: { key, status: "started", run } }`. The outcome is recorded
|
|
367
|
+
on the run when the workflow settles; a failure is also logged.
|
|
368
|
+
- A workflow that is still running when the engine returns (async steps,
|
|
369
|
+
e.g. a step retry scheduled with `retryInterval`) keeps its run `running`
|
|
370
|
+
(a second start stays 409) until the engine reports its finish; the run is
|
|
371
|
+
then recorded like a background run. A `sync` action answers 202 `started`
|
|
372
|
+
in that case.
|
|
373
|
+
- Both emit `PLUGIN_ACTION_FINISHED_EVENT` (`plugin-action.finished.v1`,
|
|
374
|
+
`{ plugin_key, action_key, run_id, status: "completed" | "failed", message? }`)
|
|
375
|
+
when the run ends.
|
|
376
|
+
- `GET …/actions/:action_key/runs?limit=1` returns the latest runs, newest
|
|
377
|
+
first. The Admin polls it every 2 s while the latest run is `running`.
|
|
378
|
+
- Lost runs: a `running` run without start or progress for one hour (the
|
|
379
|
+
process restarted, the workflow was lost) is listed as `failed` with
|
|
380
|
+
"Abgebrochen (Neustart)" (listing never writes) and persisted as such the
|
|
381
|
+
next time the action is started. A run that reports progress regularly is
|
|
382
|
+
never affected.
|
|
383
|
+
- An unknown action is 404 ("Aktion unbekannt"); a declared action whose
|
|
384
|
+
workflow is not registered is 500 (`UNEXPECTED_STATE` naming the workflow,
|
|
385
|
+
no run is recorded).
|
|
386
|
+
|
|
387
|
+
### Reporting progress
|
|
388
|
+
|
|
389
|
+
A long-running action reports progress from its workflow; the Admin shows it
|
|
390
|
+
as a bar ("current / total") with the label:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import {
|
|
394
|
+
reportPluginActionProgress,
|
|
395
|
+
type PluginActionWorkflowInput,
|
|
396
|
+
} from "@hartl-services/medusa-base"
|
|
397
|
+
|
|
398
|
+
const syncPageStep = createStep(
|
|
399
|
+
"demo-sync-page",
|
|
400
|
+
async (input: PluginActionWorkflowInput & { page: number }, { container }) => {
|
|
401
|
+
// … one page of work
|
|
402
|
+
await reportPluginActionProgress(container, input.run_id, {
|
|
403
|
+
current: input.page,
|
|
404
|
+
total: 12, // or null when unknown
|
|
405
|
+
label: `Produkte: Seite ${input.page}`,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`current` and `total` must be non-negative integers, the label is cut to 200
|
|
412
|
+
characters. Without a `run_id` (the same workflow run from a script) the
|
|
413
|
+
helper does nothing; invalid progress, an unknown or finished run and a
|
|
414
|
+
failed write are logged as a warning and dropped, and never fail the action.
|
|
415
|
+
|
|
416
|
+
## Shop-Stammdaten
|
|
417
|
+
|
|
418
|
+
The base declares its own descriptor on its module service
|
|
419
|
+
(`plugin_key: "shop"`, label "Shop-Stammdaten", `switchable: false`, no
|
|
420
|
+
features). The hub shows it as the first card; its generic page edits the
|
|
421
|
+
settings like any plugin's:
|
|
422
|
+
|
|
423
|
+
| Key | Type | Required |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `legal_name` (Firmenname) | string | yes |
|
|
426
|
+
| `address_line1`, `address_line2` | string | line 1 |
|
|
427
|
+
| `postal_code`, `city` | string | yes |
|
|
428
|
+
| `country_code` | select: EU-27 plus `ch`, `gb`, `no`, `is`, `li` (lowercase ISO-2, German labels) | yes |
|
|
429
|
+
| `vat_number` (UID-Nummer) | string | yes |
|
|
430
|
+
| `email`, `phone` | string | no |
|
|
431
|
+
| `website` | url | no |
|
|
432
|
+
| `iban`, `bic` | string | no |
|
|
433
|
+
| `company_register_number` (Firmenbuchnummer), `company_register_court` (Firmenbuchgericht) | string | no |
|
|
434
|
+
|
|
435
|
+
Server-side code reads them with a plugin's own medusa-config values as
|
|
436
|
+
fallback (stored in the Admin wins per key):
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { getShopMasterData } from "@hartl-services/medusa-base"
|
|
440
|
+
|
|
441
|
+
const { values, configured, missing } = await getShopMasterData(container, {
|
|
442
|
+
legal_name: options.merchant?.legalName,
|
|
443
|
+
})
|
|
444
|
+
if (!configured) {
|
|
445
|
+
throw new MedusaError(
|
|
446
|
+
MedusaError.Types.UNEXPECTED_STATE,
|
|
447
|
+
`Shop-Stammdaten unvollständig: ${missing.join(", ")}`
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
The base only checks presence and type; what counts as a usable value
|
|
453
|
+
(for example a placeholder UID) is the consuming plugin's rule.
|
|
454
|
+
|
|
455
|
+
## Admin pages
|
|
456
|
+
|
|
457
|
+
- `/settings/hartl-services` (sidebar entry "Hartl Services Plugins") is the
|
|
458
|
+
hub: one card per plugin with status chips, the plugin switch (not for
|
|
459
|
+
`switchable: false`) and "Öffnen"; non-switchable plugins (Shop-Stammdaten)
|
|
460
|
+
come first.
|
|
461
|
+
- The Aktionen section shows per action the progress of a running run
|
|
462
|
+
("läuft seit …", bar, label; the button is disabled) and the outcome of the
|
|
463
|
+
last finished run; a finished run it watched and a second start ("läuft
|
|
464
|
+
bereits") are announced as toasts.
|
|
465
|
+
- "Öffnen" leads to the plugin's ONE page: its `admin_page` when declared,
|
|
466
|
+
otherwise the base's generic page
|
|
467
|
+
`/settings/hartl-services/plugin/<plugin_key>` (Funktionen, Einstellungen,
|
|
468
|
+
Aktionen). The static `plugin` segment keeps it from colliding with
|
|
469
|
+
plugin-owned routes.
|
|
470
|
+
- A plugin that needs more than the generic sections (its own forms, lists)
|
|
471
|
+
declares `admin_page: "/settings/hartl-services/<plugin-slug>"` and composes
|
|
472
|
+
that page from the base's components (below). Nested settings routes get no
|
|
473
|
+
sidebar entry, so the hub is the only way in. `definePluginFeatures`
|
|
474
|
+
rejects a path outside `/settings/hartl-services/` or under
|
|
475
|
+
`/settings/hartl-services/plugin/`.
|
|
476
|
+
|
|
477
|
+
### Admin components for plugin pages
|
|
478
|
+
|
|
479
|
+
`@hartl-services/medusa-base/admin-components` (ESM for the Admin's vite
|
|
480
|
+
build, with types) exports:
|
|
481
|
+
|
|
482
|
+
| Export | Purpose |
|
|
483
|
+
| --- | --- |
|
|
484
|
+
| `PluginPageShell({ pluginKey, children })` | Back link to the hub, the plugin's label, description and status chips, then `children`; loading, "nicht installiert" and error states instead of `children` |
|
|
485
|
+
| `PluginFeatureSections({ pluginKey })` | Funktionen, Einstellungen, Aktionen exactly as on the generic page |
|
|
486
|
+
| `usePluginFeature(pluginKey)` | Query of `GET /admin/plugin-features/:plugin_key` |
|
|
487
|
+
| `pluginFeatureKeys` | Query key factory (`all`, `list()`, `detail(key)`) for invalidation after an own mutation |
|
|
488
|
+
| `usePluginFeatureEnabled(pluginKey, ...featureKeys)` | `{ enabled, isPending, isError }` from the shared list query (30 s stale time); plugin switch and every key must be effective; `enabled` is `false` while loading and on error |
|
|
489
|
+
| `PluginFeatureGate({ pluginKey, featureKeys?, fallback? })` | Renders `children` only while enabled, otherwise `fallback` (default nothing) |
|
|
490
|
+
| `PluginFeatureDisabledNotice({ pluginKey, featureLabel? })` | "Diese Funktion ist unter Einstellungen → Hartl Services Plugins ausgeschaltet." with a link to the hub |
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
// src/admin/routes/settings/hartl-services/demo/page.tsx
|
|
494
|
+
import {
|
|
495
|
+
PluginFeatureSections,
|
|
496
|
+
PluginPageShell,
|
|
497
|
+
} from "@hartl-services/medusa-base/admin-components"
|
|
498
|
+
|
|
499
|
+
import { DemoMappingSection } from "../../../../features/demo/DemoMappingSection"
|
|
500
|
+
|
|
501
|
+
const DemoPage = () => (
|
|
502
|
+
<PluginPageShell pluginKey="demo">
|
|
503
|
+
<PluginFeatureSections pluginKey="demo" />
|
|
504
|
+
<DemoMappingSection />
|
|
505
|
+
</PluginPageShell>
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
export default DemoPage
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Widgets and pages of a disabled feature disappear instead of showing a 404:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
// Widget: renders nothing while the feature is off (or still loading).
|
|
515
|
+
const DemoOrderWidget = () => (
|
|
516
|
+
<PluginFeatureGate pluginKey="demo" featureKeys={["order_sync"]}>
|
|
517
|
+
<DemoOrderPanel />
|
|
518
|
+
</PluginFeatureGate>
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
// Full page with a sidebar entry that cannot be hidden: explain instead.
|
|
522
|
+
const DemoReportsPage = () => {
|
|
523
|
+
const { enabled, isPending } = usePluginFeatureEnabled("demo", "reports")
|
|
524
|
+
if (isPending) return null
|
|
525
|
+
return enabled ? <DemoReports /> : (
|
|
526
|
+
<PluginFeatureDisabledNotice pluginKey="demo" featureLabel="Berichte" />
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The consuming plugin lists `@hartl-services/medusa-base` in its
|
|
532
|
+
`dependencies` or `peerDependencies`, so its admin build keeps the import
|
|
533
|
+
external and the shop's Admin resolves one shared copy. The components use
|
|
534
|
+
the host Admin's React, React Query, React Router and `@medusajs/ui`.
|
|
535
|
+
|
|
159
536
|
## Semantics
|
|
160
537
|
|
|
161
538
|
- No stored row: every value is its declared default, `version` is 0.
|
|
@@ -204,9 +581,10 @@ class MyModuleService extends MedusaService({ /* … */ }) {
|
|
|
204
581
|
finished transaction.
|
|
205
582
|
- The base discovers participants like feature descriptors (every module in
|
|
206
583
|
`medusa-config` that is registered in the container). Fail closed: an
|
|
207
|
-
invalid declaration (non-string or blank `workflow_id`,
|
|
208
|
-
|
|
209
|
-
`UNEXPECTED_STATE` and blocks
|
|
584
|
+
invalid declaration (non-string or blank `workflow_id`, a `workflow_id` that
|
|
585
|
+
is not a registered workflow, non-finite `order`, unknown hook name, the
|
|
586
|
+
same workflow twice for one hook) is `UNEXPECTED_STATE` and blocks
|
|
587
|
+
checkout, naming the module key (and the workflow id).
|
|
210
588
|
|
|
211
589
|
**Ordering.** Ascending `order`, then module key, then workflow id. In use:
|
|
212
590
|
Affiliate 100 (`affiliate-validate-complete-cart`,
|
|
@@ -218,9 +596,12 @@ Affiliate 100 (`affiliate-validate-complete-cart`,
|
|
|
218
596
|
- `validate` participants run one after another without their own transaction
|
|
219
597
|
id (they write nothing). The first error stops the chain and is rethrown
|
|
220
598
|
unchanged.
|
|
599
|
+
- Participants run and are cancelled with an explicitly built context, like
|
|
600
|
+
Medusa's `runAsStep`: the parent's shared transaction context plus its
|
|
601
|
+
event group (`preventReleaseEvents`), never the step's parent linkage
|
|
602
|
+
(`parentStepIdempotencyKey`), so complete-cart may itself run nested.
|
|
221
603
|
- `orderCreated` participants run one after another through the workflow
|
|
222
|
-
engine under the complete-cart transaction id,
|
|
223
|
-
(`preventReleaseEvents`). If participant *k* fails, the base cancels
|
|
604
|
+
engine under the complete-cart transaction id. If participant *k* fails, the base cancels
|
|
224
605
|
participants *k-1 … 0* in reverse order, logs every failed cancellation
|
|
225
606
|
(module key, workflow id, cart, order, transaction), and rethrows the
|
|
226
607
|
original error.
|
|
@@ -230,3 +611,26 @@ Affiliate 100 (`affiliate-validate-complete-cart`,
|
|
|
230
611
|
instead of silently leaving partial state.
|
|
231
612
|
- The base never swallows a participant error and never reads toggles here.
|
|
232
613
|
|
|
614
|
+
|
|
615
|
+
## Sentry logger wrapper
|
|
616
|
+
|
|
617
|
+
`@hartl-services/medusa-base/sentry-logger` exports `withSentryErrorReporting(logger)`,
|
|
618
|
+
formerly published as `@hartl-services/medusa-sentry-logger`. Medusa's HTTP
|
|
619
|
+
error handler catches thrown route and workflow errors and only logs them, so
|
|
620
|
+
Sentry's default Node integrations never see them; wrapping the logger reports
|
|
621
|
+
every `logger.error(...)` and every `Error` passed to `logger.warn(...)` (at
|
|
622
|
+
level `warning`). It is a plain library import for `medusa-config.ts`, not a
|
|
623
|
+
Medusa plugin feature, and needs `@sentry/node` (an optional peer dependency):
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
import { logger } from "@medusajs/framework/logger";
|
|
627
|
+
import { withSentryErrorReporting } from "@hartl-services/medusa-base/sentry-logger";
|
|
628
|
+
|
|
629
|
+
module.exports = defineConfig({
|
|
630
|
+
logger: withSentryErrorReporting(logger),
|
|
631
|
+
// ...
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Initialize Sentry in `instrumentation.ts` as usual; without a `SENTRY_DSN` the
|
|
636
|
+
wrapper is inert.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
|
|
2
|
+
type PluginActionsSectionProps = {
|
|
3
|
+
plugin: AdminPluginFeatures;
|
|
4
|
+
};
|
|
5
|
+
export declare const PluginActionsSection: ({ plugin }: PluginActionsSectionProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Container, Heading, Text, toast, usePrompt } from "@medusajs/ui";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { texts } from "../../../i18n/texts";
|
|
5
|
+
import { useLatestPluginActionRun, useRunPluginAction, } from "../hooks/usePluginFeatures";
|
|
6
|
+
import { formatRunTime, progressCount, progressPercent, runTransition, summarizeActionResult, } from "../lib/action-runs";
|
|
7
|
+
/** Progress of a running run: label, "current / total", bar and start time. */
|
|
8
|
+
const RunProgress = ({ run }) => {
|
|
9
|
+
const percent = progressPercent(run.progress);
|
|
10
|
+
const count = progressCount(run.progress);
|
|
11
|
+
const label = run.progress?.label;
|
|
12
|
+
return (_jsxs("div", { className: "mt-2 flex flex-col gap-y-1", role: "status", children: [(label || count) && (_jsx(Text, { size: "small", children: [label, count].filter(Boolean).join(" · ") })), percent !== null && (_jsx("div", { className: "bg-ui-border-base w-full overflow-hidden rounded-full", style: { height: 6 }, role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": percent, children: _jsx("div", { className: "bg-ui-fg-interactive h-full rounded-full", style: { width: `${percent}%`, height: "100%" } }) })), _jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.actions.runningSince(formatRunTime(run.started_at)) })] }));
|
|
13
|
+
};
|
|
14
|
+
/** Outcome of the last finished run. */
|
|
15
|
+
const RunStatusLine = ({ run }) => {
|
|
16
|
+
if (run.status === "completed") {
|
|
17
|
+
const summary = summarizeActionResult(run.result);
|
|
18
|
+
return (_jsxs("div", { className: "mt-1", children: [_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.actions.lastCompleted(formatRunTime(run.finished_at ?? run.started_at)) }), summary && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: summary }))] }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs("div", { className: "mt-1", children: [_jsx(Text, { size: "small", className: "text-ui-fg-error", children: texts.actions.lastFailed(formatRunTime(run.finished_at ?? run.started_at)) }), run.message && (_jsx(Text, { size: "small", className: "text-ui-fg-error", children: run.message }))] }));
|
|
21
|
+
};
|
|
22
|
+
const PluginActionRow = ({ pluginKey, action, starting, blocked, onRun, }) => {
|
|
23
|
+
const { data: run } = useLatestPluginActionRun(pluginKey, action.key);
|
|
24
|
+
const previous = useRef(undefined);
|
|
25
|
+
const running = run?.status === "running";
|
|
26
|
+
// Toast when a run this page watched while running has finished.
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const transition = runTransition(previous.current, run);
|
|
29
|
+
previous.current = run;
|
|
30
|
+
if (transition === "completed") {
|
|
31
|
+
toast.success(texts.toasts.actionCompleted(action.label), {
|
|
32
|
+
description: summarizeActionResult(run?.result) ?? undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else if (transition === "failed") {
|
|
36
|
+
toast.error(texts.toasts.actionFailed(action.label), {
|
|
37
|
+
description: run?.message ?? texts.errors.unknown,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}, [run, action.label]);
|
|
41
|
+
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
|
+
};
|
|
43
|
+
export const PluginActionsSection = ({ plugin }) => {
|
|
44
|
+
const mutation = useRunPluginAction(plugin.plugin_key);
|
|
45
|
+
const prompt = usePrompt();
|
|
46
|
+
const startingKey = mutation.isPending ? mutation.variables?.key : undefined;
|
|
47
|
+
const run = async (action) => {
|
|
48
|
+
if (mutation.isPending) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (action.confirm) {
|
|
52
|
+
const confirmed = await prompt({
|
|
53
|
+
title: texts.actions.confirmTitle(action.label),
|
|
54
|
+
description: action.confirm,
|
|
55
|
+
confirmText: texts.actions.confirm,
|
|
56
|
+
cancelText: texts.actions.cancel,
|
|
57
|
+
variant: "confirmation",
|
|
58
|
+
});
|
|
59
|
+
if (!confirmed) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
mutation.mutate(action);
|
|
64
|
+
};
|
|
65
|
+
return (_jsxs(Container, { className: "divide-y p-0", children: [_jsx("div", { className: "px-6 py-4", children: _jsx(Heading, { level: "h2", children: 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
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { PluginFeatureSections } from "./PluginFeatureSections";
|
|
4
|
+
import { PluginPageShell } from "./PluginPageShell";
|
|
5
|
+
/** The base's generic page of a plugin without an own `admin_page`. */
|
|
6
|
+
export const PluginDetailPage = () => {
|
|
7
|
+
const { plugin_key: pluginKey = "" } = useParams();
|
|
8
|
+
return (_jsx(PluginPageShell, { pluginKey: pluginKey, children: _jsx(PluginFeatureSections, { pluginKey: pluginKey }) }));
|
|
9
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
type PluginFeatureGateProps = {
|
|
3
|
+
pluginKey: string;
|
|
4
|
+
/** All must be on; none = plugin switch only. */
|
|
5
|
+
featureKeys?: readonly string[];
|
|
6
|
+
/** Rendered while loading, on error and when disabled; `null` by default. */
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
/** Renders `children` only while the plugin and all `featureKeys` are on. */
|
|
11
|
+
export declare const PluginFeatureGate: ({ pluginKey, featureKeys, fallback, children, }: PluginFeatureGateProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
type PluginFeatureDisabledNoticeProps = {
|
|
13
|
+
pluginKey: string;
|
|
14
|
+
/** Named in the text when given. */
|
|
15
|
+
featureLabel?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* For a full page whose sidebar entry cannot be hidden: says the feature is
|
|
19
|
+
* off and links to the hub.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PluginFeatureDisabledNotice: ({ pluginKey, featureLabel, }: PluginFeatureDisabledNoticeProps) => import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Container, Text } from "@medusajs/ui";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { texts } from "../../../i18n/texts";
|
|
5
|
+
import { usePluginFeatureEnabled } from "../hooks/usePluginFeatureEnabled";
|
|
6
|
+
import { PLUGIN_HUB_PATH } from "../lib/plugin-paths";
|
|
7
|
+
/** Renders `children` only while the plugin and all `featureKeys` are on. */
|
|
8
|
+
export const PluginFeatureGate = ({ pluginKey, featureKeys = [], fallback = null, children, }) => {
|
|
9
|
+
const { enabled } = usePluginFeatureEnabled(pluginKey, ...featureKeys);
|
|
10
|
+
return _jsx(_Fragment, { children: enabled ? children : fallback });
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* For a full page whose sidebar entry cannot be hidden: says the feature is
|
|
14
|
+
* off and links to the hub.
|
|
15
|
+
*/
|
|
16
|
+
export const PluginFeatureDisabledNotice = ({ pluginKey, featureLabel, }) => (_jsxs(Container, { className: "flex flex-col gap-2 px-6 py-4", "data-plugin-key": pluginKey, children: [_jsx(Text, { className: "text-ui-fg-subtle", children: featureLabel
|
|
17
|
+
? texts.gate.disabledNamed(featureLabel)
|
|
18
|
+
: texts.gate.disabled }), _jsx("div", { children: _jsx(Link, { to: PLUGIN_HUB_PATH, className: "text-ui-fg-interactive hover:text-ui-fg-interactive-hover txt-compact-small-plus", children: texts.gate.openHub }) })] }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type PluginFeatureSectionsProps = {
|
|
2
|
+
pluginKey: string;
|
|
3
|
+
};
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PluginFeatureSections: ({ pluginKey }: PluginFeatureSectionsProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { texts } from "../../../i18n/texts";
|
|
3
|
+
import { usePluginFeature } from "../hooks/usePluginFeatures";
|
|
4
|
+
import { PluginActionsSection } from "./PluginActionsSection";
|
|
5
|
+
import { PluginFeaturesCard } from "./PluginFeaturesCard";
|
|
6
|
+
import { PluginSettingsForm } from "./PluginSettingsForm";
|
|
7
|
+
/**
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export const PluginFeatureSections = ({ pluginKey }) => {
|
|
14
|
+
const plugin = usePluginFeature(pluginKey).data?.plugin;
|
|
15
|
+
if (!plugin) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
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 })] }));
|
|
19
|
+
};
|