@hartl-services/medusa-base 0.3.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.
Files changed (94) hide show
  1. package/.medusa/server/src/admin/index.js +741 -36
  2. package/.medusa/server/src/admin/index.mjs +743 -38
  3. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/route.js +31 -0
  4. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/runs/route.js +16 -0
  5. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/route.js +14 -0
  6. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/settings/route.js +22 -0
  7. package/.medusa/server/src/api/admin/plugin-features/route.js +6 -4
  8. package/.medusa/server/src/api/middlewares.js +22 -8
  9. package/.medusa/server/src/api/plugin-actions.js +221 -0
  10. package/.medusa/server/src/api/plugin-features-http.js +67 -8
  11. package/.medusa/server/src/modules/plugin-features/domain/plugin-action-runs.js +92 -0
  12. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +213 -0
  13. package/.medusa/server/src/modules/plugin-features/domain/secret-box.js +56 -0
  14. package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928205031.js +16 -0
  15. package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928215428.js +17 -0
  16. package/.medusa/server/src/modules/plugin-features/models/plugin-action-run.js +43 -0
  17. package/.medusa/server/src/modules/plugin-features/models/plugin-setting-state.js +32 -0
  18. package/.medusa/server/src/modules/plugin-features/service.js +419 -17
  19. package/.medusa/server/src/public/action-progress.js +67 -0
  20. package/.medusa/server/src/public/descriptor.js +199 -2
  21. package/.medusa/server/src/public/effective-features.js +9 -5
  22. package/.medusa/server/src/public/enforcement.js +18 -8
  23. package/.medusa/server/src/public/index.js +10 -2
  24. package/.medusa/server/src/public/module-access.js +14 -0
  25. package/.medusa/server/src/public/settings.js +17 -0
  26. package/.medusa/server/src/public/shop-master-data.js +95 -0
  27. package/.medusa/server/src/utils/discover-complete-cart-participants.js +9 -2
  28. package/.medusa/server/src/utils/discover-plugin-features.js +47 -9
  29. package/.medusa/server/src/workflows/hooks/complete-cart-participants.js +28 -16
  30. package/README.md +393 -12
  31. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +6 -0
  32. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.js +66 -0
  33. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.d.ts +2 -0
  34. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.js +9 -0
  35. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.d.ts +22 -0
  36. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.js +18 -0
  37. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +11 -0
  38. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.js +19 -0
  39. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +12 -0
  40. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.js +34 -0
  41. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +5 -0
  42. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +16 -0
  43. package/dist/admin-components/features/plugin-features/components/PluginHubCard.d.ts +7 -0
  44. package/dist/admin-components/features/plugin-features/components/PluginHubCard.js +21 -0
  45. package/dist/admin-components/features/plugin-features/components/PluginPageShell.d.ts +13 -0
  46. package/dist/admin-components/features/plugin-features/components/PluginPageShell.js +20 -0
  47. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +7 -0
  48. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +39 -0
  49. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -0
  50. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -0
  51. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +114 -0
  52. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.js +40 -0
  53. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +392 -0
  54. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.js +128 -0
  55. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +93 -0
  56. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.js +44 -0
  57. package/dist/admin-components/features/plugin-features/lib/action-runs.d.ts +19 -0
  58. package/dist/admin-components/features/plugin-features/lib/action-runs.js +74 -0
  59. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +363 -0
  60. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +9 -0
  61. package/dist/admin-components/features/plugin-features/lib/plugin-paths.d.ts +4 -0
  62. package/dist/admin-components/features/plugin-features/lib/plugin-paths.js +4 -0
  63. package/dist/admin-components/features/plugin-features/lib/settings-draft.d.ts +28 -0
  64. package/dist/admin-components/features/plugin-features/lib/settings-draft.js +86 -0
  65. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.d.ts +7 -0
  66. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.js +7 -0
  67. package/dist/admin-components/i18n/texts.d.ts +90 -0
  68. package/dist/admin-components/i18n/texts.js +90 -0
  69. package/dist/admin-components/index.d.ts +14 -0
  70. package/dist/admin-components/index.js +14 -0
  71. package/dist/admin-components/lib/errors.d.ts +6 -0
  72. package/dist/admin-components/lib/errors.js +27 -0
  73. package/dist/admin-components/lib/sdk.d.ts +2 -0
  74. package/dist/admin-components/lib/sdk.js +13 -0
  75. package/dist/public/action-progress.d.ts +25 -0
  76. package/dist/public/action-progress.js +66 -0
  77. package/dist/public/descriptor.d.ts +3 -2
  78. package/dist/public/descriptor.js +198 -1
  79. package/dist/public/effective-features.d.ts +2 -1
  80. package/dist/public/effective-features.js +8 -4
  81. package/dist/public/enforcement.d.ts +7 -0
  82. package/dist/public/enforcement.js +17 -7
  83. package/dist/public/index.d.ts +5 -2
  84. package/dist/public/index.js +9 -1
  85. package/dist/public/module-access.d.ts +3 -0
  86. package/dist/public/module-access.js +13 -0
  87. package/dist/public/settings.d.ts +11 -0
  88. package/dist/public/settings.js +16 -0
  89. package/dist/public/shop-master-data.d.ts +25 -0
  90. package/dist/public/shop-master-data.js +94 -0
  91. package/dist/public/types.d.ts +97 -1
  92. package/dist/utils/discover-plugin-features.d.ts +28 -0
  93. package/dist/utils/discover-plugin-features.js +96 -0
  94. package/package.json +7 -3
@@ -0,0 +1,392 @@
1
+ export declare function usePluginFeatures(): import("@tanstack/react-query").UseQueryResult<{
2
+ plugins: {
3
+ plugin_key: string;
4
+ label: string;
5
+ description: string | null;
6
+ default_enabled: boolean;
7
+ switchable: boolean;
8
+ enabled: boolean;
9
+ version: number;
10
+ features: {
11
+ key: string;
12
+ label: string;
13
+ description: string | null;
14
+ parent_key: string | null;
15
+ default_enabled: boolean;
16
+ enabled: boolean;
17
+ effective_enabled: boolean;
18
+ }[];
19
+ settings: ({
20
+ type: "secret";
21
+ key: string;
22
+ label: string;
23
+ description: string | null;
24
+ required: boolean;
25
+ is_set: boolean;
26
+ source: "default" | "stored" | "config" | "none";
27
+ invalidates: string[];
28
+ } | {
29
+ type: "select";
30
+ options: {
31
+ value: string;
32
+ label: string;
33
+ }[];
34
+ default: string | null;
35
+ value: string | null;
36
+ key: string;
37
+ label: string;
38
+ description: string | null;
39
+ required: boolean;
40
+ is_set: boolean;
41
+ source: "default" | "stored" | "config" | "none";
42
+ invalidates: string[];
43
+ } | {
44
+ type: "number";
45
+ min: number | null;
46
+ max: number | null;
47
+ default: number | null;
48
+ value: number | null;
49
+ key: string;
50
+ label: string;
51
+ description: string | null;
52
+ required: boolean;
53
+ is_set: boolean;
54
+ source: "default" | "stored" | "config" | "none";
55
+ invalidates: string[];
56
+ } | {
57
+ type: "string" | "boolean" | "url";
58
+ default: string | number | boolean | null;
59
+ value: string | number | boolean | null;
60
+ key: string;
61
+ label: string;
62
+ description: string | null;
63
+ required: boolean;
64
+ is_set: boolean;
65
+ source: "default" | "stored" | "config" | "none";
66
+ invalidates: string[];
67
+ })[];
68
+ configured: boolean;
69
+ missing: string[];
70
+ settings_version: number;
71
+ actions: {
72
+ key: string;
73
+ label: string;
74
+ description: string | null;
75
+ mode: "sync" | "background";
76
+ confirm: string | null;
77
+ }[];
78
+ admin_page: string | null;
79
+ }[];
80
+ }, Error>;
81
+ /**
82
+ * One plugin (`GET /admin/plugin-features/:plugin_key`). Shows the entry from
83
+ * the already loaded list right away and fetches the plugin itself, so a deep
84
+ * link works without the hub. Toggle, settings and action mutations of the
85
+ * base update this cache; invalidate `pluginFeatureKeys.detail(pluginKey)`
86
+ * after an own mutation that changes what the base reports.
87
+ */
88
+ export declare function usePluginFeature(pluginKey: string | undefined): import("@tanstack/react-query").UseQueryResult<{
89
+ plugin: {
90
+ plugin_key: string;
91
+ label: string;
92
+ description: string | null;
93
+ default_enabled: boolean;
94
+ switchable: boolean;
95
+ enabled: boolean;
96
+ version: number;
97
+ features: {
98
+ key: string;
99
+ label: string;
100
+ description: string | null;
101
+ parent_key: string | null;
102
+ default_enabled: boolean;
103
+ enabled: boolean;
104
+ effective_enabled: boolean;
105
+ }[];
106
+ settings: ({
107
+ type: "secret";
108
+ key: string;
109
+ label: string;
110
+ description: string | null;
111
+ required: boolean;
112
+ is_set: boolean;
113
+ source: "default" | "stored" | "config" | "none";
114
+ invalidates: string[];
115
+ } | {
116
+ type: "select";
117
+ options: {
118
+ value: string;
119
+ label: string;
120
+ }[];
121
+ default: string | null;
122
+ value: string | null;
123
+ key: string;
124
+ label: string;
125
+ description: string | null;
126
+ required: boolean;
127
+ is_set: boolean;
128
+ source: "default" | "stored" | "config" | "none";
129
+ invalidates: string[];
130
+ } | {
131
+ type: "number";
132
+ min: number | null;
133
+ max: number | null;
134
+ default: number | null;
135
+ value: number | null;
136
+ key: string;
137
+ label: string;
138
+ description: string | null;
139
+ required: boolean;
140
+ is_set: boolean;
141
+ source: "default" | "stored" | "config" | "none";
142
+ invalidates: string[];
143
+ } | {
144
+ type: "string" | "boolean" | "url";
145
+ default: string | number | boolean | null;
146
+ value: string | number | boolean | null;
147
+ key: string;
148
+ label: string;
149
+ description: string | null;
150
+ required: boolean;
151
+ is_set: boolean;
152
+ source: "default" | "stored" | "config" | "none";
153
+ invalidates: string[];
154
+ })[];
155
+ configured: boolean;
156
+ missing: string[];
157
+ settings_version: number;
158
+ actions: {
159
+ key: string;
160
+ label: string;
161
+ description: string | null;
162
+ mode: "sync" | "background";
163
+ confirm: string | null;
164
+ }[];
165
+ admin_page: string | null;
166
+ };
167
+ }, Error>;
168
+ /** One instance per plugin card, so pending state is scoped to that plugin. */
169
+ export declare function useUpdatePluginFeatures(): import("@tanstack/react-query").UseMutationResult<{
170
+ plugin: {
171
+ plugin_key: string;
172
+ label: string;
173
+ description: string | null;
174
+ default_enabled: boolean;
175
+ switchable: boolean;
176
+ enabled: boolean;
177
+ version: number;
178
+ features: {
179
+ key: string;
180
+ label: string;
181
+ description: string | null;
182
+ parent_key: string | null;
183
+ default_enabled: boolean;
184
+ enabled: boolean;
185
+ effective_enabled: boolean;
186
+ }[];
187
+ settings: ({
188
+ type: "secret";
189
+ key: string;
190
+ label: string;
191
+ description: string | null;
192
+ required: boolean;
193
+ is_set: boolean;
194
+ source: "default" | "stored" | "config" | "none";
195
+ invalidates: string[];
196
+ } | {
197
+ type: "select";
198
+ options: {
199
+ value: string;
200
+ label: string;
201
+ }[];
202
+ default: string | null;
203
+ value: string | null;
204
+ key: string;
205
+ label: string;
206
+ description: string | null;
207
+ required: boolean;
208
+ is_set: boolean;
209
+ source: "default" | "stored" | "config" | "none";
210
+ invalidates: string[];
211
+ } | {
212
+ type: "number";
213
+ min: number | null;
214
+ max: number | null;
215
+ default: number | null;
216
+ value: number | null;
217
+ key: string;
218
+ label: string;
219
+ description: string | null;
220
+ required: boolean;
221
+ is_set: boolean;
222
+ source: "default" | "stored" | "config" | "none";
223
+ invalidates: string[];
224
+ } | {
225
+ type: "string" | "boolean" | "url";
226
+ default: string | number | boolean | null;
227
+ value: string | number | boolean | null;
228
+ key: string;
229
+ label: string;
230
+ description: string | null;
231
+ required: boolean;
232
+ is_set: boolean;
233
+ source: "default" | "stored" | "config" | "none";
234
+ invalidates: string[];
235
+ })[];
236
+ configured: boolean;
237
+ missing: string[];
238
+ settings_version: number;
239
+ actions: {
240
+ key: string;
241
+ label: string;
242
+ description: string | null;
243
+ mode: "sync" | "background";
244
+ confirm: string | null;
245
+ }[];
246
+ admin_page: string | null;
247
+ };
248
+ }, Error, {
249
+ plugin_key: string;
250
+ expected_version: number;
251
+ enabled: boolean;
252
+ features: Record<string, boolean>;
253
+ }, unknown>;
254
+ export declare function useUpdatePluginSettings(pluginKey: string): import("@tanstack/react-query").UseMutationResult<{
255
+ plugin: {
256
+ plugin_key: string;
257
+ label: string;
258
+ description: string | null;
259
+ default_enabled: boolean;
260
+ switchable: boolean;
261
+ enabled: boolean;
262
+ version: number;
263
+ features: {
264
+ key: string;
265
+ label: string;
266
+ description: string | null;
267
+ parent_key: string | null;
268
+ default_enabled: boolean;
269
+ enabled: boolean;
270
+ effective_enabled: boolean;
271
+ }[];
272
+ settings: ({
273
+ type: "secret";
274
+ key: string;
275
+ label: string;
276
+ description: string | null;
277
+ required: boolean;
278
+ is_set: boolean;
279
+ source: "default" | "stored" | "config" | "none";
280
+ invalidates: string[];
281
+ } | {
282
+ type: "select";
283
+ options: {
284
+ value: string;
285
+ label: string;
286
+ }[];
287
+ default: string | null;
288
+ value: string | null;
289
+ key: string;
290
+ label: string;
291
+ description: string | null;
292
+ required: boolean;
293
+ is_set: boolean;
294
+ source: "default" | "stored" | "config" | "none";
295
+ invalidates: string[];
296
+ } | {
297
+ type: "number";
298
+ min: number | null;
299
+ max: number | null;
300
+ default: number | null;
301
+ value: number | null;
302
+ key: string;
303
+ label: string;
304
+ description: string | null;
305
+ required: boolean;
306
+ is_set: boolean;
307
+ source: "default" | "stored" | "config" | "none";
308
+ invalidates: string[];
309
+ } | {
310
+ type: "string" | "boolean" | "url";
311
+ default: string | number | boolean | null;
312
+ value: string | number | boolean | null;
313
+ key: string;
314
+ label: string;
315
+ description: string | null;
316
+ required: boolean;
317
+ is_set: boolean;
318
+ source: "default" | "stored" | "config" | "none";
319
+ invalidates: string[];
320
+ })[];
321
+ configured: boolean;
322
+ missing: string[];
323
+ settings_version: number;
324
+ actions: {
325
+ key: string;
326
+ label: string;
327
+ description: string | null;
328
+ mode: "sync" | "background";
329
+ confirm: string | null;
330
+ }[];
331
+ admin_page: string | null;
332
+ };
333
+ }, Error, {
334
+ expected_version: number;
335
+ values: Record<string, string | number | boolean | null>;
336
+ }, unknown>;
337
+ /** Poll interval while the latest run of an action is `running`. */
338
+ export declare const PLUGIN_ACTION_RUN_POLL_MS = 2000;
339
+ /**
340
+ * Latest run of one action. Polls every 2 s while it is `running` and stops
341
+ * when it finished.
342
+ */
343
+ export declare function useLatestPluginActionRun(pluginKey: string, actionKey: string): import("@tanstack/react-query").UseQueryResult<{
344
+ id: string;
345
+ plugin_key: string;
346
+ action_key: string;
347
+ status: "failed" | "completed" | "running";
348
+ started_at: string;
349
+ finished_at: string | null;
350
+ triggered_by: string | null;
351
+ message: string | null;
352
+ progress: {
353
+ current: number;
354
+ total: number | null;
355
+ label: string | null;
356
+ } | null;
357
+ result: unknown;
358
+ }, Error>;
359
+ /**
360
+ * Runs one action. The recorded run goes into the action's run query, so a
361
+ * background run is polled right away. 409 (already running) shows a hint
362
+ * and loads the running run.
363
+ */
364
+ export declare function useRunPluginAction(pluginKey: string): import("@tanstack/react-query").UseMutationResult<{
365
+ action: {
366
+ key: string;
367
+ status: "completed" | "started";
368
+ run: {
369
+ id: string;
370
+ plugin_key: string;
371
+ action_key: string;
372
+ status: "failed" | "completed" | "running";
373
+ started_at: string;
374
+ finished_at: string | null;
375
+ triggered_by: string | null;
376
+ message: string | null;
377
+ progress: {
378
+ current: number;
379
+ total: number | null;
380
+ label: string | null;
381
+ } | null;
382
+ result: unknown;
383
+ };
384
+ result?: unknown;
385
+ };
386
+ }, Error, {
387
+ key: string;
388
+ label: string;
389
+ description: string | null;
390
+ mode: "sync" | "background";
391
+ confirm: string | null;
392
+ }, unknown>;
@@ -0,0 +1,128 @@
1
+ import { toast } from "@medusajs/ui";
2
+ import { useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
3
+ import { texts } from "../../../i18n/texts";
4
+ import { getErrorMessage } from "../../../lib/errors";
5
+ import { listPluginActionRuns, listPluginFeatures, retrievePluginFeatures, runPluginAction, updatePluginFeatures, updatePluginSettings, } from "../lib/plugin-features-admin-client";
6
+ import { pluginFeatureKeys } from "../queries/pluginFeatureKeys";
7
+ const isConflict = (error) => typeof error === "object" &&
8
+ error !== null &&
9
+ "status" in error &&
10
+ error.status === 409;
11
+ /** Writes a server response into the list and the plugin's detail cache. */
12
+ const storePlugin = (queryClient, plugin) => {
13
+ queryClient.setQueryData(pluginFeatureKeys.list(), (current) => current && {
14
+ plugins: current.plugins.map((entry) => entry.plugin_key === plugin.plugin_key ? plugin : entry),
15
+ });
16
+ queryClient.setQueryData(pluginFeatureKeys.detail(plugin.plugin_key), { plugin });
17
+ };
18
+ /** Toast for a failed save; reloads the server state either way. */
19
+ const onSaveError = async (queryClient, error) => {
20
+ if (isConflict(error)) {
21
+ toast.error(texts.toasts.conflict);
22
+ }
23
+ else {
24
+ toast.error(texts.toasts.saveFailed, {
25
+ description: getErrorMessage(error) ?? texts.errors.unknown,
26
+ });
27
+ }
28
+ await queryClient.invalidateQueries({ queryKey: pluginFeatureKeys.all });
29
+ };
30
+ export function usePluginFeatures() {
31
+ return useQuery({
32
+ queryKey: pluginFeatureKeys.list(),
33
+ queryFn: listPluginFeatures,
34
+ });
35
+ }
36
+ /**
37
+ * One plugin (`GET /admin/plugin-features/:plugin_key`). Shows the entry from
38
+ * the already loaded list right away and fetches the plugin itself, so a deep
39
+ * link works without the hub. Toggle, settings and action mutations of the
40
+ * base update this cache; invalidate `pluginFeatureKeys.detail(pluginKey)`
41
+ * after an own mutation that changes what the base reports.
42
+ */
43
+ export function usePluginFeature(pluginKey) {
44
+ const queryClient = useQueryClient();
45
+ return useQuery({
46
+ queryKey: pluginFeatureKeys.detail(pluginKey ?? ""),
47
+ queryFn: () => retrievePluginFeatures(pluginKey),
48
+ enabled: Boolean(pluginKey),
49
+ placeholderData: () => {
50
+ const plugin = queryClient
51
+ .getQueryData(pluginFeatureKeys.list())
52
+ ?.plugins.find((entry) => entry.plugin_key === pluginKey);
53
+ return plugin ? { plugin } : undefined;
54
+ },
55
+ });
56
+ }
57
+ /** One instance per plugin card, so pending state is scoped to that plugin. */
58
+ export function useUpdatePluginFeatures() {
59
+ const queryClient = useQueryClient();
60
+ return useMutation({
61
+ mutationFn: updatePluginFeatures,
62
+ onSuccess: ({ plugin }) => {
63
+ storePlugin(queryClient, plugin);
64
+ toast.success(texts.toasts.saved);
65
+ },
66
+ onError: (error) => onSaveError(queryClient, error),
67
+ });
68
+ }
69
+ export function useUpdatePluginSettings(pluginKey) {
70
+ const queryClient = useQueryClient();
71
+ return useMutation({
72
+ mutationFn: (payload) => updatePluginSettings(pluginKey, payload),
73
+ onSuccess: ({ plugin }) => {
74
+ storePlugin(queryClient, plugin);
75
+ toast.success(texts.toasts.saved);
76
+ },
77
+ onError: (error) => onSaveError(queryClient, error),
78
+ });
79
+ }
80
+ /** Poll interval while the latest run of an action is `running`. */
81
+ export const PLUGIN_ACTION_RUN_POLL_MS = 2000;
82
+ /**
83
+ * Latest run of one action. Polls every 2 s while it is `running` and stops
84
+ * when it finished.
85
+ */
86
+ export function useLatestPluginActionRun(pluginKey, actionKey) {
87
+ return useQuery({
88
+ queryKey: pluginFeatureKeys.actionRuns(pluginKey, actionKey),
89
+ queryFn: () => listPluginActionRuns(pluginKey, actionKey, 1),
90
+ select: (data) => data.runs[0],
91
+ refetchInterval: (query) => query.state.data?.runs[0]?.status === "running"
92
+ ? PLUGIN_ACTION_RUN_POLL_MS
93
+ : false,
94
+ });
95
+ }
96
+ /**
97
+ * Runs one action. The recorded run goes into the action's run query, so a
98
+ * background run is polled right away. 409 (already running) shows a hint
99
+ * and loads the running run.
100
+ */
101
+ export function useRunPluginAction(pluginKey) {
102
+ const queryClient = useQueryClient();
103
+ return useMutation({
104
+ mutationFn: (action) => runPluginAction(pluginKey, action.key),
105
+ onSuccess: ({ action: run }, action) => {
106
+ queryClient.setQueryData(pluginFeatureKeys.actionRuns(pluginKey, action.key), { runs: [run.run] });
107
+ if (run.status === "started") {
108
+ toast.info(texts.toasts.actionStarted(action.label));
109
+ }
110
+ else {
111
+ toast.success(texts.toasts.actionCompleted(action.label));
112
+ }
113
+ },
114
+ onError: async (error, action) => {
115
+ if (isConflict(error)) {
116
+ toast.info(texts.toasts.actionAlreadyRunning(action.label));
117
+ }
118
+ else {
119
+ toast.error(texts.toasts.actionFailed(action.label), {
120
+ description: getErrorMessage(error) ?? texts.errors.unknown,
121
+ });
122
+ }
123
+ await queryClient.invalidateQueries({
124
+ queryKey: pluginFeatureKeys.actionRuns(pluginKey, action.key),
125
+ });
126
+ },
127
+ });
128
+ }
@@ -0,0 +1,93 @@
1
+ import type { AdminPluginFeatures } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
2
+ import type { FormEvent } from "react";
3
+ import { type SettingsDraft } from "../lib/settings-draft";
4
+ export declare function usePluginSettingsForm(plugin: AdminPluginFeatures): {
5
+ draft: SettingsDraft;
6
+ setValue: (key: string, value: string | boolean | null, badInput?: boolean) => void;
7
+ dirty: boolean;
8
+ invalid: string[];
9
+ submit: (event: FormEvent<HTMLFormElement>) => void;
10
+ mutation: import("@tanstack/react-query").UseMutationResult<{
11
+ plugin: {
12
+ plugin_key: string;
13
+ label: string;
14
+ description: string | null;
15
+ default_enabled: boolean;
16
+ switchable: boolean;
17
+ enabled: boolean;
18
+ version: number;
19
+ features: {
20
+ key: string;
21
+ label: string;
22
+ description: string | null;
23
+ parent_key: string | null;
24
+ default_enabled: boolean;
25
+ enabled: boolean;
26
+ effective_enabled: boolean;
27
+ }[];
28
+ settings: ({
29
+ type: "secret";
30
+ key: string;
31
+ label: string;
32
+ description: string | null;
33
+ required: boolean;
34
+ is_set: boolean;
35
+ source: "default" | "stored" | "config" | "none";
36
+ invalidates: string[];
37
+ } | {
38
+ type: "select";
39
+ options: {
40
+ value: string;
41
+ label: string;
42
+ }[];
43
+ default: string | null;
44
+ value: string | null;
45
+ key: string;
46
+ label: string;
47
+ description: string | null;
48
+ required: boolean;
49
+ is_set: boolean;
50
+ source: "default" | "stored" | "config" | "none";
51
+ invalidates: string[];
52
+ } | {
53
+ type: "number";
54
+ min: number | null;
55
+ max: number | null;
56
+ default: number | null;
57
+ value: number | null;
58
+ key: string;
59
+ label: string;
60
+ description: string | null;
61
+ required: boolean;
62
+ is_set: boolean;
63
+ source: "default" | "stored" | "config" | "none";
64
+ invalidates: string[];
65
+ } | {
66
+ type: "string" | "boolean" | "url";
67
+ default: string | number | boolean | null;
68
+ value: string | number | boolean | null;
69
+ key: string;
70
+ label: string;
71
+ description: string | null;
72
+ required: boolean;
73
+ is_set: boolean;
74
+ source: "default" | "stored" | "config" | "none";
75
+ invalidates: string[];
76
+ })[];
77
+ configured: boolean;
78
+ missing: string[];
79
+ settings_version: number;
80
+ actions: {
81
+ key: string;
82
+ label: string;
83
+ description: string | null;
84
+ mode: "sync" | "background";
85
+ confirm: string | null;
86
+ }[];
87
+ admin_page: string | null;
88
+ };
89
+ }, Error, {
90
+ expected_version: number;
91
+ values: Record<string, string | number | boolean | null>;
92
+ }, unknown>;
93
+ };
@@ -0,0 +1,44 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { changedSettingValues, initialSettingsDraft, invalidDraftKeys, } from "../lib/settings-draft";
3
+ import { useUpdatePluginSettings } from "./usePluginFeatures";
4
+ export function usePluginSettingsForm(plugin) {
5
+ const mutation = useUpdatePluginSettings(plugin.plugin_key);
6
+ const [draft, setDraft] = useState(() => initialSettingsDraft(plugin.settings));
7
+ // Number inputs the browser could not parse (they report "").
8
+ const [unparsable, setUnparsable] = useState([]);
9
+ // New server data (save, conflict reload, refetch) resets the form.
10
+ useEffect(() => {
11
+ setDraft(initialSettingsDraft(plugin.settings));
12
+ setUnparsable([]);
13
+ }, [plugin.settings, plugin.settings_version]);
14
+ const changes = useMemo(() => changedSettingValues(plugin.settings, draft, unparsable), [plugin.settings, draft, unparsable]);
15
+ const invalid = useMemo(() => invalidDraftKeys(plugin.settings, draft, unparsable), [plugin.settings, draft, unparsable]);
16
+ const dirty = Object.keys(changes).length > 0;
17
+ /** `badInput`: the browser could not parse the typed number. */
18
+ const setValue = (key, value, badInput = false) => {
19
+ setDraft((current) => ({ ...current, [key]: value }));
20
+ setUnparsable((current) => badInput
21
+ ? current.includes(key)
22
+ ? current
23
+ : [...current, key]
24
+ : current.filter((entry) => entry !== key));
25
+ };
26
+ const submit = (event) => {
27
+ event.preventDefault();
28
+ if (!dirty || invalid.length > 0 || mutation.isPending) {
29
+ return;
30
+ }
31
+ mutation.mutate({
32
+ expected_version: plugin.settings_version,
33
+ values: changes,
34
+ });
35
+ };
36
+ return {
37
+ draft,
38
+ setValue,
39
+ dirty,
40
+ invalid,
41
+ submit,
42
+ mutation,
43
+ };
44
+ }
@@ -0,0 +1,19 @@
1
+ import type { PluginActionRun, PluginActionRunProgress } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-admin";
2
+ /**
3
+ * One line for a finished run's result: a string result, else its `summary`
4
+ * or `message`, else its primitive top-level fields (`key: value, …`).
5
+ * `null` when nothing readable is there.
6
+ */
7
+ export declare const summarizeActionResult: (result: unknown) => string | null;
8
+ /** Share done in percent (0–100); `null` when the total is unknown. */
9
+ export declare const progressPercent: (progress: PluginActionRunProgress | null) => number | null;
10
+ /** "3 / 10" or "3" (unknown total); `null` without progress. */
11
+ export declare const progressCount: (progress: PluginActionRunProgress | null) => string | null;
12
+ /** Short German date and time. */
13
+ export declare const formatRunTime: (iso: string | null) => string;
14
+ export type RunTransition = "completed" | "failed" | null;
15
+ /**
16
+ * Whether the latest run just finished while this page watched it: the same
17
+ * run was `running` before and is `completed`/`failed` now.
18
+ */
19
+ export declare const runTransition: (previous: Pick<PluginActionRun, "id" | "status"> | undefined, current: Pick<PluginActionRun, "id" | "status"> | undefined) => RunTransition;