@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.
Files changed (97) 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/sentry-logger.js +82 -0
  26. package/.medusa/server/src/public/settings.js +17 -0
  27. package/.medusa/server/src/public/shop-master-data.js +95 -0
  28. package/.medusa/server/src/utils/discover-complete-cart-participants.js +9 -2
  29. package/.medusa/server/src/utils/discover-plugin-features.js +47 -9
  30. package/.medusa/server/src/workflows/hooks/complete-cart-participants.js +28 -16
  31. package/README.md +416 -12
  32. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +6 -0
  33. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.js +66 -0
  34. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.d.ts +2 -0
  35. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.js +9 -0
  36. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.d.ts +22 -0
  37. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.js +18 -0
  38. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +11 -0
  39. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.js +19 -0
  40. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +12 -0
  41. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.js +34 -0
  42. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +5 -0
  43. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +16 -0
  44. package/dist/admin-components/features/plugin-features/components/PluginHubCard.d.ts +7 -0
  45. package/dist/admin-components/features/plugin-features/components/PluginHubCard.js +21 -0
  46. package/dist/admin-components/features/plugin-features/components/PluginPageShell.d.ts +13 -0
  47. package/dist/admin-components/features/plugin-features/components/PluginPageShell.js +20 -0
  48. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +7 -0
  49. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +39 -0
  50. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -0
  51. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -0
  52. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +114 -0
  53. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.js +40 -0
  54. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +392 -0
  55. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.js +128 -0
  56. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +93 -0
  57. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.js +44 -0
  58. package/dist/admin-components/features/plugin-features/lib/action-runs.d.ts +19 -0
  59. package/dist/admin-components/features/plugin-features/lib/action-runs.js +74 -0
  60. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +363 -0
  61. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +9 -0
  62. package/dist/admin-components/features/plugin-features/lib/plugin-paths.d.ts +4 -0
  63. package/dist/admin-components/features/plugin-features/lib/plugin-paths.js +4 -0
  64. package/dist/admin-components/features/plugin-features/lib/settings-draft.d.ts +28 -0
  65. package/dist/admin-components/features/plugin-features/lib/settings-draft.js +86 -0
  66. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.d.ts +7 -0
  67. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.js +7 -0
  68. package/dist/admin-components/i18n/texts.d.ts +90 -0
  69. package/dist/admin-components/i18n/texts.js +90 -0
  70. package/dist/admin-components/index.d.ts +14 -0
  71. package/dist/admin-components/index.js +14 -0
  72. package/dist/admin-components/lib/errors.d.ts +6 -0
  73. package/dist/admin-components/lib/errors.js +27 -0
  74. package/dist/admin-components/lib/sdk.d.ts +2 -0
  75. package/dist/admin-components/lib/sdk.js +13 -0
  76. package/dist/public/action-progress.d.ts +25 -0
  77. package/dist/public/action-progress.js +66 -0
  78. package/dist/public/descriptor.d.ts +3 -2
  79. package/dist/public/descriptor.js +198 -1
  80. package/dist/public/effective-features.d.ts +2 -1
  81. package/dist/public/effective-features.js +8 -4
  82. package/dist/public/enforcement.d.ts +7 -0
  83. package/dist/public/enforcement.js +17 -7
  84. package/dist/public/index.d.ts +5 -2
  85. package/dist/public/index.js +9 -1
  86. package/dist/public/module-access.d.ts +3 -0
  87. package/dist/public/module-access.js +13 -0
  88. package/dist/public/sentry-logger.d.ts +11 -0
  89. package/dist/public/sentry-logger.js +81 -0
  90. package/dist/public/settings.d.ts +11 -0
  91. package/dist/public/settings.js +16 -0
  92. package/dist/public/shop-master-data.d.ts +25 -0
  93. package/dist/public/shop-master-data.js +94 -0
  94. package/dist/public/types.d.ts +97 -1
  95. package/dist/utils/discover-plugin-features.d.ts +28 -0
  96. package/dist/utils/discover-plugin-features.js +96 -0
  97. package/package.json +20 -4
@@ -5,6 +5,8 @@ const ui = require("@medusajs/ui");
5
5
  const reactQuery = require("@tanstack/react-query");
6
6
  const pluginFeatures = require("@hartl-services/medusa-food-supplements-contracts/paths/plugin-features");
7
7
  const Medusa = require("@medusajs/js-sdk");
8
+ const reactRouterDom = require("react-router-dom");
9
+ const react = require("react");
8
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
9
11
  const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
10
12
  const texts = {
@@ -13,26 +15,80 @@ const texts = {
13
15
  },
14
16
  page: {
15
17
  title: "Hartl Services Plugins",
16
- description: "Plugins und einzelne Funktionen ein- oder ausschalten. Änderungen gelten sofort; abgeschaltete Funktionen sind im Shop nicht erreichbar."
18
+ description: "Plugins ein- oder ausschalten und öffnen, um Funktionen, Einstellungen und Aktionen zu verwalten. Änderungen gelten sofort; abgeschaltete Funktionen sind im Shop nicht erreichbar."
17
19
  },
18
20
  states: {
19
- loading: "Funktionsschalter werden geladen…",
20
- error: "Funktionsschalter konnten nicht geladen werden.",
21
+ loading: "Plugins werden geladen…",
22
+ error: "Plugins konnten nicht geladen werden.",
21
23
  empty: "Keine Plugins mit Funktionsschaltern installiert.",
22
- retry: "Erneut laden"
24
+ retry: "Erneut laden",
25
+ notFound: "Dieses Plugin ist nicht installiert."
23
26
  },
24
27
  plugin: {
25
- switchLabel: (label) => `${label} aktiv`
28
+ switchLabel: (label) => `${label} aktiv`,
29
+ open: "Öffnen",
30
+ openLabel: (label) => `${label} öffnen`
31
+ },
32
+ chips: {
33
+ enabled: "aktiv",
34
+ disabled: "aus",
35
+ configured: "konfiguriert",
36
+ notConfigured: (labels) => `nicht konfiguriert: ${labels}`
37
+ },
38
+ detail: {
39
+ back: "Zurück zu Hartl Services Plugins"
40
+ },
41
+ sections: {
42
+ features: "Funktionen",
43
+ settings: "Einstellungen",
44
+ actions: "Aktionen"
26
45
  },
27
46
  hints: {
28
47
  pluginDisabled: "Das Plugin ist ausgeschaltet. Alle Funktionen sind inaktiv, ihre Einstellungen bleiben erhalten.",
29
48
  parentDisabled: (parentLabel) => `Inaktiv, solange „${parentLabel}“ ausgeschaltet ist.`,
30
49
  saving: "Wird gespeichert…"
31
50
  },
51
+ settings: {
52
+ missing: (labels) => `Nicht konfiguriert: ${labels}`,
53
+ save: "Einstellungen speichern",
54
+ secretPlaceholder: "gesetzt",
55
+ secretClear: "Löschen",
56
+ secretClearPending: "Wird beim Speichern gelöscht; danach gilt der Wert aus der Konfiguration, falls vorhanden.",
57
+ secretClearUndo: "Nicht löschen",
58
+ secretClearLabel: (label) => `${label} löschen`,
59
+ secretClearUndoLabel: (label) => `${label} nicht löschen`,
60
+ invalidates: (labels) => `Beim Ändern muss ${labels} neu eingegeben werden.`,
61
+ required: "Pflichtfeld",
62
+ invalidNumber: "Bitte eine gültige Zahl eingeben.",
63
+ outOfRange: "Der Wert liegt außerhalb des erlaubten Bereichs.",
64
+ numberMin: (min) => `Mindestens ${min}.`,
65
+ numberMax: (max) => `Höchstens ${max}.`,
66
+ numberRange: (min, max) => `Zwischen ${min} und ${max}.`,
67
+ selectPlaceholder: "Bitte auswählen",
68
+ clearHint: "Ein leeres Feld speichern entfernt den gespeicherten Wert (dann gilt Konfiguration oder Standard).",
69
+ source: {
70
+ stored: "gespeichert",
71
+ config: "aus Konfiguration",
72
+ default: "Standard",
73
+ none: "nicht gesetzt"
74
+ }
75
+ },
76
+ actions: {
77
+ confirmTitle: (label) => `${label}?`,
78
+ confirm: "Ausführen",
79
+ cancel: "Abbrechen",
80
+ runningSince: (time) => `läuft seit ${time}`,
81
+ lastCompleted: (time) => `Zuletzt erfolgreich: ${time}`,
82
+ lastFailed: (time) => `Zuletzt fehlgeschlagen: ${time}`
83
+ },
32
84
  toasts: {
33
85
  saved: "Einstellung wurde gespeichert.",
34
86
  saveFailed: "Einstellung konnte nicht gespeichert werden.",
35
- conflict: "Die Einstellungen wurden zwischenzeitlich geändert und neu geladen."
87
+ conflict: "Die Einstellungen wurden zwischenzeitlich geändert und neu geladen.",
88
+ actionCompleted: (label) => `„${label}“ wurde ausgeführt.`,
89
+ actionStarted: (label) => `„${label}“ wurde gestartet und läuft im Hintergrund.`,
90
+ actionFailed: (label) => `„${label}“ ist fehlgeschlagen.`,
91
+ actionAlreadyRunning: (label) => `„${label}“ läuft bereits; der Fortschritt wird angezeigt.`
36
92
  },
37
93
  errors: {
38
94
  unknown: "Unbekannter Fehler"
@@ -61,62 +117,679 @@ const getErrorMessage = (error) => {
61
117
  return void 0;
62
118
  };
63
119
  const sdk = new Medusa__default.default({
64
- baseUrl: __BACKEND_URL__ ?? "/",
120
+ baseUrl: (typeof __BACKEND_URL__ === "undefined" ? void 0 : __BACKEND_URL__) ?? "/",
65
121
  auth: {
66
- type: __AUTH_TYPE__ ?? "session",
67
- jwtTokenStorageKey: __JWT_TOKEN_STORAGE_KEY__ || void 0
122
+ type: (typeof __AUTH_TYPE__ === "undefined" ? void 0 : __AUTH_TYPE__) ?? "session",
123
+ jwtTokenStorageKey: (typeof __JWT_TOKEN_STORAGE_KEY__ === "undefined" ? void 0 : __JWT_TOKEN_STORAGE_KEY__) || void 0
68
124
  }
69
125
  });
70
126
  const listPluginFeatures = () => sdk.client.fetch(
71
127
  pluginFeatures.pluginFeaturesAdminPaths.get.list,
72
128
  { method: "GET" }
73
129
  );
130
+ const retrievePluginFeatures = (pluginKey) => sdk.client.fetch(
131
+ pluginFeatures.pluginFeaturesAdminPaths.get.retrieve(pluginKey),
132
+ { method: "GET" }
133
+ );
74
134
  const updatePluginFeatures = (payload) => sdk.client.fetch(
75
135
  pluginFeatures.pluginFeaturesAdminPaths.post.update,
76
136
  { method: "POST", body: payload }
77
137
  );
138
+ const updatePluginSettings = (pluginKey, payload) => sdk.client.fetch(
139
+ pluginFeatures.pluginFeaturesAdminPaths.post.updateSettings(pluginKey),
140
+ { method: "POST", body: payload }
141
+ );
142
+ const runPluginAction = (pluginKey, actionKey) => sdk.client.fetch(
143
+ pluginFeatures.pluginFeaturesAdminPaths.post.runAction(pluginKey, actionKey),
144
+ { method: "POST", body: {} }
145
+ );
146
+ const listPluginActionRuns = (pluginKey, actionKey, limit = 1) => sdk.client.fetch(
147
+ pluginFeatures.pluginFeaturesAdminPaths.get.actionRuns(pluginKey, actionKey),
148
+ { method: "GET", query: { limit } }
149
+ );
78
150
  const pluginFeatureKeys = {
79
151
  all: ["plugin-features"],
80
- list: () => [...pluginFeatureKeys.all, "list"]
152
+ list: () => [...pluginFeatureKeys.all, "list"],
153
+ details: () => [...pluginFeatureKeys.all, "detail"],
154
+ detail: (pluginKey) => [...pluginFeatureKeys.details(), pluginKey],
155
+ actionRuns: (pluginKey, actionKey) => [...pluginFeatureKeys.all, "action-runs", pluginKey, actionKey]
81
156
  };
82
157
  const isConflict = (error) => typeof error === "object" && error !== null && "status" in error && error.status === 409;
158
+ const storePlugin = (queryClient, plugin2) => {
159
+ queryClient.setQueryData(
160
+ pluginFeatureKeys.list(),
161
+ (current) => current && {
162
+ plugins: current.plugins.map(
163
+ (entry) => entry.plugin_key === plugin2.plugin_key ? plugin2 : entry
164
+ )
165
+ }
166
+ );
167
+ queryClient.setQueryData(
168
+ pluginFeatureKeys.detail(plugin2.plugin_key),
169
+ { plugin: plugin2 }
170
+ );
171
+ };
172
+ const onSaveError = async (queryClient, error) => {
173
+ if (isConflict(error)) {
174
+ ui.toast.error(texts.toasts.conflict);
175
+ } else {
176
+ ui.toast.error(texts.toasts.saveFailed, {
177
+ description: getErrorMessage(error) ?? texts.errors.unknown
178
+ });
179
+ }
180
+ await queryClient.invalidateQueries({ queryKey: pluginFeatureKeys.all });
181
+ };
83
182
  function usePluginFeatures() {
84
183
  return reactQuery.useQuery({
85
184
  queryKey: pluginFeatureKeys.list(),
86
185
  queryFn: listPluginFeatures
87
186
  });
88
187
  }
188
+ function usePluginFeature(pluginKey) {
189
+ const queryClient = reactQuery.useQueryClient();
190
+ return reactQuery.useQuery({
191
+ queryKey: pluginFeatureKeys.detail(pluginKey ?? ""),
192
+ queryFn: () => retrievePluginFeatures(pluginKey),
193
+ enabled: Boolean(pluginKey),
194
+ placeholderData: () => {
195
+ const plugin2 = queryClient.getQueryData(pluginFeatureKeys.list())?.plugins.find((entry) => entry.plugin_key === pluginKey);
196
+ return plugin2 ? { plugin: plugin2 } : void 0;
197
+ }
198
+ });
199
+ }
89
200
  function useUpdatePluginFeatures() {
90
201
  const queryClient = reactQuery.useQueryClient();
91
202
  return reactQuery.useMutation({
92
203
  mutationFn: updatePluginFeatures,
93
204
  onSuccess: ({ plugin: plugin2 }) => {
205
+ storePlugin(queryClient, plugin2);
206
+ ui.toast.success(texts.toasts.saved);
207
+ },
208
+ onError: (error) => onSaveError(queryClient, error)
209
+ });
210
+ }
211
+ function useUpdatePluginSettings(pluginKey) {
212
+ const queryClient = reactQuery.useQueryClient();
213
+ return reactQuery.useMutation({
214
+ mutationFn: (payload) => updatePluginSettings(pluginKey, payload),
215
+ onSuccess: ({ plugin: plugin2 }) => {
216
+ storePlugin(queryClient, plugin2);
217
+ ui.toast.success(texts.toasts.saved);
218
+ },
219
+ onError: (error) => onSaveError(queryClient, error)
220
+ });
221
+ }
222
+ const PLUGIN_ACTION_RUN_POLL_MS = 2e3;
223
+ function useLatestPluginActionRun(pluginKey, actionKey) {
224
+ return reactQuery.useQuery({
225
+ queryKey: pluginFeatureKeys.actionRuns(pluginKey, actionKey),
226
+ queryFn: () => listPluginActionRuns(pluginKey, actionKey, 1),
227
+ select: (data) => data.runs[0],
228
+ refetchInterval: (query) => query.state.data?.runs[0]?.status === "running" ? PLUGIN_ACTION_RUN_POLL_MS : false
229
+ });
230
+ }
231
+ function useRunPluginAction(pluginKey) {
232
+ const queryClient = reactQuery.useQueryClient();
233
+ return reactQuery.useMutation({
234
+ mutationFn: (action) => runPluginAction(pluginKey, action.key),
235
+ onSuccess: ({ action: run }, action) => {
94
236
  queryClient.setQueryData(
95
- pluginFeatureKeys.list(),
96
- (current) => current && {
97
- plugins: current.plugins.map(
98
- (entry) => entry.plugin_key === plugin2.plugin_key ? plugin2 : entry
99
- )
100
- }
237
+ pluginFeatureKeys.actionRuns(pluginKey, action.key),
238
+ { runs: [run.run] }
101
239
  );
102
- ui.toast.success(texts.toasts.saved);
240
+ if (run.status === "started") {
241
+ ui.toast.info(texts.toasts.actionStarted(action.label));
242
+ } else {
243
+ ui.toast.success(texts.toasts.actionCompleted(action.label));
244
+ }
103
245
  },
104
- onError: async (error) => {
246
+ onError: async (error, action) => {
105
247
  if (isConflict(error)) {
106
- ui.toast.error(texts.toasts.conflict);
248
+ ui.toast.info(texts.toasts.actionAlreadyRunning(action.label));
107
249
  } else {
108
- ui.toast.error(texts.toasts.saveFailed, {
250
+ ui.toast.error(texts.toasts.actionFailed(action.label), {
109
251
  description: getErrorMessage(error) ?? texts.errors.unknown
110
252
  });
111
253
  }
112
- await queryClient.invalidateQueries({ queryKey: pluginFeatureKeys.list() });
254
+ await queryClient.invalidateQueries({
255
+ queryKey: pluginFeatureKeys.actionRuns(pluginKey, action.key)
256
+ });
113
257
  }
114
258
  });
115
259
  }
260
+ const PLUGIN_HUB_PATH = "/settings/hartl-services";
261
+ const pluginDetailPath = (pluginKey) => `${PLUGIN_HUB_PATH}/plugin/${encodeURIComponent(pluginKey)}`;
262
+ const initialEntry = (setting) => {
263
+ if (setting.type === "secret") {
264
+ return "";
265
+ }
266
+ if (setting.type === "boolean") {
267
+ return setting.value === true;
268
+ }
269
+ return setting.value === null ? "" : String(setting.value);
270
+ };
271
+ const initialSettingsDraft = (settings) => Object.fromEntries(
272
+ settings.map((setting) => [setting.key, initialEntry(setting)])
273
+ );
274
+ const outsideBounds = (setting, value) => setting.min !== null && value < setting.min || setting.max !== null && value > setting.max;
275
+ const numberEntryValue = (entry) => {
276
+ const value = Number(entry.trim());
277
+ return Number.isFinite(value) ? value : null;
278
+ };
279
+ const invalidDraftKeys = (settings, draft, unparsable = []) => settings.filter((setting) => {
280
+ if (setting.type !== "number") {
281
+ return false;
282
+ }
283
+ if (unparsable.includes(setting.key)) {
284
+ return true;
285
+ }
286
+ const entry = draft[setting.key];
287
+ if (typeof entry !== "string" || entry.trim() === "") {
288
+ return false;
289
+ }
290
+ const value = numberEntryValue(entry);
291
+ return value === null || outsideBounds(setting, value);
292
+ }).map((setting) => setting.key);
293
+ const changedSettingValues = (settings, draft, unparsable = []) => {
294
+ const initial = initialSettingsDraft(settings);
295
+ const values = {};
296
+ for (const setting of settings) {
297
+ const entry = draft[setting.key];
298
+ if (entry === void 0 || entry === initial[setting.key]) {
299
+ continue;
300
+ }
301
+ if (setting.type === "secret") {
302
+ if (entry === null) {
303
+ if (setting.source === "stored") {
304
+ values[setting.key] = null;
305
+ }
306
+ } else if (typeof entry === "string" && entry !== "") {
307
+ values[setting.key] = entry;
308
+ }
309
+ continue;
310
+ }
311
+ if (setting.type === "boolean") {
312
+ values[setting.key] = entry === true;
313
+ continue;
314
+ }
315
+ if (unparsable.includes(setting.key)) {
316
+ continue;
317
+ }
318
+ const text = typeof entry === "string" ? entry.trim() : "";
319
+ if (text === "") {
320
+ if (setting.source === "stored") {
321
+ values[setting.key] = null;
322
+ }
323
+ } else if (setting.type === "number") {
324
+ const value = numberEntryValue(text);
325
+ if (value !== null) {
326
+ values[setting.key] = value;
327
+ }
328
+ } else {
329
+ values[setting.key] = text;
330
+ }
331
+ }
332
+ return values;
333
+ };
334
+ function usePluginSettingsForm(plugin2) {
335
+ const mutation = useUpdatePluginSettings(plugin2.plugin_key);
336
+ const [draft, setDraft] = react.useState(
337
+ () => initialSettingsDraft(plugin2.settings)
338
+ );
339
+ const [unparsable, setUnparsable] = react.useState([]);
340
+ react.useEffect(() => {
341
+ setDraft(initialSettingsDraft(plugin2.settings));
342
+ setUnparsable([]);
343
+ }, [plugin2.settings, plugin2.settings_version]);
344
+ const changes = react.useMemo(
345
+ () => changedSettingValues(plugin2.settings, draft, unparsable),
346
+ [plugin2.settings, draft, unparsable]
347
+ );
348
+ const invalid = react.useMemo(
349
+ () => invalidDraftKeys(plugin2.settings, draft, unparsable),
350
+ [plugin2.settings, draft, unparsable]
351
+ );
352
+ const dirty = Object.keys(changes).length > 0;
353
+ const setValue = (key, value, badInput = false) => {
354
+ setDraft((current) => ({ ...current, [key]: value }));
355
+ setUnparsable(
356
+ (current) => badInput ? current.includes(key) ? current : [...current, key] : current.filter((entry) => entry !== key)
357
+ );
358
+ };
359
+ const submit = (event) => {
360
+ event.preventDefault();
361
+ if (!dirty || invalid.length > 0 || mutation.isPending) {
362
+ return;
363
+ }
364
+ mutation.mutate({
365
+ expected_version: plugin2.settings_version,
366
+ values: changes
367
+ });
368
+ };
369
+ return {
370
+ draft,
371
+ setValue,
372
+ dirty,
373
+ invalid,
374
+ submit,
375
+ mutation
376
+ };
377
+ }
378
+ const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
379
+ const missingLabels = (plugin2) => plugin2.missing.map(
380
+ (key) => plugin2.settings.find((setting) => setting.key === key)?.label ?? key
381
+ ).join(", ");
382
+ const invalidatesLabels = (plugin2, setting) => setting.invalidates.map(
383
+ (key) => plugin2.settings.find((candidate) => candidate.key === key)?.label ?? key
384
+ ).join(", ");
385
+ const boundsText = (setting) => setting.min !== null && setting.max !== null ? texts.settings.numberRange(setting.min, setting.max) : setting.min !== null ? texts.settings.numberMin(setting.min) : setting.max !== null ? texts.settings.numberMax(setting.max) : void 0;
386
+ const SettingField = ({
387
+ pluginKey,
388
+ setting,
389
+ invalidatesLabels: invalidatesLabels2,
390
+ value,
391
+ invalid,
392
+ disabled,
393
+ onChange
394
+ }) => {
395
+ const id = fieldId(pluginKey, setting.key);
396
+ const hintId = `${id}-hint`;
397
+ const clearPending = setting.type === "secret" && value === null;
398
+ const bounds = setting.type === "number" ? boundsText(setting) : void 0;
399
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2", children: [
400
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
401
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: id, size: "small", weight: "plus", children: [
402
+ setting.label,
403
+ setting.required && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-ui-fg-muted", children: [
404
+ " (",
405
+ texts.settings.required,
406
+ ")"
407
+ ] })
408
+ ] }),
409
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted shrink-0", children: texts.settings.source[setting.source] })
410
+ ] }),
411
+ setting.type === "boolean" ? /* @__PURE__ */ jsxRuntime.jsx(
412
+ ui.Switch,
413
+ {
414
+ id,
415
+ "aria-describedby": hintId,
416
+ checked: value === true,
417
+ disabled,
418
+ onCheckedChange: (checked) => onChange(checked === true)
419
+ }
420
+ ) : setting.type === "secret" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
421
+ /* @__PURE__ */ jsxRuntime.jsx(
422
+ ui.Input,
423
+ {
424
+ id,
425
+ "aria-describedby": hintId,
426
+ className: "flex-1",
427
+ type: "password",
428
+ autoComplete: "new-password",
429
+ placeholder: setting.is_set ? texts.settings.secretPlaceholder : "",
430
+ value: typeof value === "string" ? value : "",
431
+ disabled: disabled || clearPending,
432
+ onChange: (event) => onChange(event.target.value)
433
+ }
434
+ ),
435
+ setting.source === "stored" && /* @__PURE__ */ jsxRuntime.jsx(
436
+ ui.Button,
437
+ {
438
+ type: "button",
439
+ size: "small",
440
+ variant: "secondary",
441
+ disabled,
442
+ "aria-label": clearPending ? texts.settings.secretClearUndoLabel(setting.label) : texts.settings.secretClearLabel(setting.label),
443
+ onClick: () => onChange(clearPending ? "" : null),
444
+ children: clearPending ? texts.settings.secretClearUndo : texts.settings.secretClear
445
+ }
446
+ )
447
+ ] }) : setting.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
448
+ ui.Select,
449
+ {
450
+ value: typeof value === "string" ? value : "",
451
+ disabled,
452
+ onValueChange: (next) => onChange(next),
453
+ children: [
454
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { id, "aria-describedby": hintId, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: texts.settings.selectPlaceholder }) }),
455
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: setting.options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: option.value, children: option.label }, option.value)) })
456
+ ]
457
+ }
458
+ ) : setting.type === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
459
+ ui.Input,
460
+ {
461
+ id,
462
+ "aria-describedby": hintId,
463
+ "aria-invalid": invalid,
464
+ type: "number",
465
+ step: "any",
466
+ min: setting.min ?? void 0,
467
+ max: setting.max ?? void 0,
468
+ value: typeof value === "string" ? value : "",
469
+ disabled,
470
+ onChange: (event) => onChange(event.target.value, event.target.validity.badInput)
471
+ }
472
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
473
+ ui.Input,
474
+ {
475
+ id,
476
+ "aria-describedby": hintId,
477
+ "aria-invalid": invalid,
478
+ type: setting.type === "url" ? "url" : "text",
479
+ value: typeof value === "string" ? value : "",
480
+ disabled,
481
+ onChange: (event) => onChange(event.target.value)
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: hintId, className: "grid gap-1", children: [
485
+ setting.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description }),
486
+ clearPending && /* @__PURE__ */ jsxRuntime.jsx(ui.Hint, { variant: "info", children: texts.settings.secretClearPending }),
487
+ invalidatesLabels2 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels2) }),
488
+ bounds && !invalid && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: bounds }),
489
+ invalid && /* @__PURE__ */ jsxRuntime.jsx(ui.Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber })
490
+ ] })
491
+ ] });
492
+ };
493
+ const PluginSettingsForm = ({ plugin: plugin2 }) => {
494
+ const form = usePluginSettingsForm(plugin2);
495
+ const pending = form.mutation.isPending;
496
+ const hasTextFields = plugin2.settings.some(
497
+ (setting) => setting.type !== "boolean" && setting.type !== "secret" && setting.type !== "select"
498
+ );
499
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", "aria-busy": pending, children: [
500
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
501
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: texts.sections.settings }),
502
+ hasTextFields && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.clearHint })
503
+ ] }),
504
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: form.submit, className: "grid gap-5 px-6 py-5", children: [
505
+ !plugin2.configured && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin2)) }),
506
+ plugin2.settings.map((setting) => /* @__PURE__ */ jsxRuntime.jsx(
507
+ SettingField,
508
+ {
509
+ pluginKey: plugin2.plugin_key,
510
+ setting,
511
+ invalidatesLabels: invalidatesLabels(plugin2, setting),
512
+ value: form.draft[setting.key],
513
+ invalid: form.invalid.includes(setting.key),
514
+ disabled: pending,
515
+ onChange: (value, badInput) => form.setValue(setting.key, value, badInput)
516
+ },
517
+ setting.key
518
+ )),
519
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
520
+ ui.Button,
521
+ {
522
+ type: "submit",
523
+ size: "small",
524
+ isLoading: pending,
525
+ disabled: !form.dirty || form.invalid.length > 0 || pending,
526
+ children: texts.settings.save
527
+ }
528
+ ) })
529
+ ] })
530
+ ] });
531
+ };
532
+ const PluginStatusChips = ({ plugin: plugin2 }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
533
+ plugin2.switchable && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: plugin2.enabled ? "green" : "grey", children: plugin2.enabled ? texts.chips.enabled : texts.chips.disabled }),
534
+ plugin2.settings.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: plugin2.configured ? "green" : "orange", children: plugin2.configured ? texts.chips.configured : texts.chips.notConfigured(missingLabels(plugin2)) })
535
+ ] });
536
+ const PluginHubCard = ({ plugin: plugin2 }) => {
537
+ const mutation = useUpdatePluginFeatures();
538
+ const pending = mutation.isPending;
539
+ const toggle = (enabled) => {
540
+ mutation.mutate({
541
+ plugin_key: plugin2.plugin_key,
542
+ expected_version: plugin2.version,
543
+ enabled,
544
+ features: Object.fromEntries(
545
+ plugin2.features.map((feature) => [feature.key, feature.enabled])
546
+ )
547
+ });
548
+ };
549
+ return /* @__PURE__ */ jsxRuntime.jsxs(
550
+ ui.Container,
551
+ {
552
+ className: "flex flex-wrap items-start justify-between gap-4 px-6 py-4",
553
+ "aria-busy": pending,
554
+ children: [
555
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
556
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: plugin2.label }),
557
+ plugin2.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
558
+ /* @__PURE__ */ jsxRuntime.jsx(PluginStatusChips, { plugin: plugin2 }),
559
+ pending && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving })
560
+ ] }),
561
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [
562
+ plugin2.switchable && /* @__PURE__ */ jsxRuntime.jsx(
563
+ ui.Switch,
564
+ {
565
+ "aria-label": texts.plugin.switchLabel(plugin2.label),
566
+ checked: plugin2.enabled,
567
+ disabled: pending,
568
+ onCheckedChange: (checked) => toggle(checked === true)
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
572
+ reactRouterDom.Link,
573
+ {
574
+ to: plugin2.admin_page ?? pluginDetailPath(plugin2.plugin_key),
575
+ "aria-label": texts.plugin.openLabel(plugin2.label),
576
+ children: texts.plugin.open
577
+ }
578
+ ) })
579
+ ] })
580
+ ]
581
+ }
582
+ );
583
+ };
584
+ const PluginFeaturesSettingsPage = () => {
585
+ const query = usePluginFeatures();
586
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
587
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "px-6 py-4", children: [
588
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: texts.page.title }),
589
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.page.description })
590
+ ] }),
591
+ query.isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) }) : query.isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [
592
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: texts.states.error }),
593
+ /* @__PURE__ */ jsxRuntime.jsx(
594
+ ui.Button,
595
+ {
596
+ size: "small",
597
+ variant: "secondary",
598
+ isLoading: query.isFetching,
599
+ onClick: () => void query.refetch(),
600
+ children: texts.states.retry
601
+ }
602
+ )
603
+ ] }) : query.data.plugins.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.empty }) }) : [
604
+ ...query.data.plugins.filter((plugin2) => !plugin2.switchable),
605
+ ...query.data.plugins.filter((plugin2) => plugin2.switchable)
606
+ ].map((plugin2) => /* @__PURE__ */ jsxRuntime.jsx(PluginHubCard, { plugin: plugin2 }, plugin2.plugin_key))
607
+ ] });
608
+ };
609
+ const config = adminSdk.defineRouteConfig({
610
+ label: texts.menu.label
611
+ });
612
+ const SUMMARY_MAX_LENGTH = 200;
613
+ const cut = (text) => text.length > SUMMARY_MAX_LENGTH ? `${text.slice(0, SUMMARY_MAX_LENGTH - 1)}…` : text;
614
+ const isPrimitive = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
615
+ const summarizeActionResult = (result) => {
616
+ if (result === null || result === void 0) {
617
+ return null;
618
+ }
619
+ if (isPrimitive(result)) {
620
+ return cut(String(result));
621
+ }
622
+ if (typeof result !== "object" || Array.isArray(result)) {
623
+ return null;
624
+ }
625
+ const record = result;
626
+ for (const key of ["summary", "message"]) {
627
+ const value = record[key];
628
+ if (typeof value === "string" && value.trim()) {
629
+ return cut(value.trim());
630
+ }
631
+ }
632
+ const parts = Object.entries(record).filter(
633
+ (entry) => isPrimitive(entry[1])
634
+ ).map(([key, value]) => `${key}: ${String(value)}`);
635
+ return parts.length > 0 ? cut(parts.join(", ")) : null;
636
+ };
637
+ const progressPercent = (progress) => {
638
+ if (!progress || progress.total === null || progress.total <= 0) {
639
+ return null;
640
+ }
641
+ return Math.max(
642
+ 0,
643
+ Math.min(100, Math.round(progress.current / progress.total * 100))
644
+ );
645
+ };
646
+ const progressCount = (progress) => {
647
+ if (!progress) {
648
+ return null;
649
+ }
650
+ return progress.total === null ? String(progress.current) : `${progress.current} / ${progress.total}`;
651
+ };
652
+ const formatRunTime = (iso) => {
653
+ if (!iso) {
654
+ return "";
655
+ }
656
+ const date = new Date(iso);
657
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleString("de-AT", { dateStyle: "short", timeStyle: "short" });
658
+ };
659
+ const runTransition = (previous, current) => {
660
+ if (!previous || !current || previous.id !== current.id || previous.status !== "running" || current.status === "running") {
661
+ return null;
662
+ }
663
+ return current.status;
664
+ };
665
+ const RunProgress = ({ run }) => {
666
+ const percent = progressPercent(run.progress);
667
+ const count = progressCount(run.progress);
668
+ const label = run.progress?.label;
669
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-col gap-y-1", role: "status", children: [
670
+ (label || count) && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: [label, count].filter(Boolean).join(" · ") }),
671
+ percent !== null && /* @__PURE__ */ jsxRuntime.jsx(
672
+ "div",
673
+ {
674
+ className: "bg-ui-border-base w-full overflow-hidden rounded-full",
675
+ style: { height: 6 },
676
+ role: "progressbar",
677
+ "aria-valuemin": 0,
678
+ "aria-valuemax": 100,
679
+ "aria-valuenow": percent,
680
+ children: /* @__PURE__ */ jsxRuntime.jsx(
681
+ "div",
682
+ {
683
+ className: "bg-ui-fg-interactive h-full rounded-full",
684
+ style: { width: `${percent}%`, height: "100%" }
685
+ }
686
+ )
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: texts.actions.runningSince(formatRunTime(run.started_at)) })
690
+ ] });
691
+ };
692
+ const RunStatusLine = ({ run }) => {
693
+ if (run.status === "completed") {
694
+ const summary = summarizeActionResult(run.result);
695
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1", children: [
696
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: texts.actions.lastCompleted(
697
+ formatRunTime(run.finished_at ?? run.started_at)
698
+ ) }),
699
+ summary && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: summary })
700
+ ] });
701
+ }
702
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1", children: [
703
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: texts.actions.lastFailed(
704
+ formatRunTime(run.finished_at ?? run.started_at)
705
+ ) }),
706
+ run.message && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: run.message })
707
+ ] });
708
+ };
709
+ const PluginActionRow = ({
710
+ pluginKey,
711
+ action,
712
+ starting,
713
+ blocked,
714
+ onRun
715
+ }) => {
716
+ const { data: run } = useLatestPluginActionRun(pluginKey, action.key);
717
+ const previous = react.useRef(void 0);
718
+ const running = run?.status === "running";
719
+ react.useEffect(() => {
720
+ const transition = runTransition(previous.current, run);
721
+ previous.current = run;
722
+ if (transition === "completed") {
723
+ ui.toast.success(texts.toasts.actionCompleted(action.label), {
724
+ description: summarizeActionResult(run?.result) ?? void 0
725
+ });
726
+ } else if (transition === "failed") {
727
+ ui.toast.error(texts.toasts.actionFailed(action.label), {
728
+ description: run?.message ?? texts.errors.unknown
729
+ });
730
+ }
731
+ }, [run, action.label]);
732
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [
733
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
734
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: action.label }),
735
+ action.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: action.description }),
736
+ run && (running ? /* @__PURE__ */ jsxRuntime.jsx(RunProgress, { run }) : /* @__PURE__ */ jsxRuntime.jsx(RunStatusLine, { run }))
737
+ ] }),
738
+ /* @__PURE__ */ jsxRuntime.jsx(
739
+ ui.Button,
740
+ {
741
+ size: "small",
742
+ variant: "secondary",
743
+ className: "shrink-0",
744
+ isLoading: starting || running,
745
+ disabled: blocked || starting || running,
746
+ onClick: () => onRun(action),
747
+ children: action.label
748
+ }
749
+ )
750
+ ] });
751
+ };
752
+ const PluginActionsSection = ({ plugin: plugin2 }) => {
753
+ const mutation = useRunPluginAction(plugin2.plugin_key);
754
+ const prompt = ui.usePrompt();
755
+ const startingKey = mutation.isPending ? mutation.variables?.key : void 0;
756
+ const run = async (action) => {
757
+ if (mutation.isPending) {
758
+ return;
759
+ }
760
+ if (action.confirm) {
761
+ const confirmed = await prompt({
762
+ title: texts.actions.confirmTitle(action.label),
763
+ description: action.confirm,
764
+ confirmText: texts.actions.confirm,
765
+ cancelText: texts.actions.cancel,
766
+ variant: "confirmation"
767
+ });
768
+ if (!confirmed) {
769
+ return;
770
+ }
771
+ }
772
+ mutation.mutate(action);
773
+ };
774
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: texts.sections.actions }) }),
776
+ plugin2.actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
777
+ PluginActionRow,
778
+ {
779
+ pluginKey: plugin2.plugin_key,
780
+ action,
781
+ starting: startingKey === action.key,
782
+ blocked: mutation.isPending && startingKey !== action.key,
783
+ onRun: (entry) => void run(entry)
784
+ },
785
+ action.key
786
+ ))
787
+ ] });
788
+ };
116
789
  const configuredFeatures = (plugin2) => Object.fromEntries(
117
790
  plugin2.features.map((feature) => [feature.key, feature.enabled])
118
791
  );
119
- const PluginFeaturesCard = ({ plugin: plugin2 }) => {
792
+ const PluginFeaturesCard = ({ plugin: plugin2, title }) => {
120
793
  const mutation = useUpdatePluginFeatures();
121
794
  const pending = mutation.isPending;
122
795
  const labels = new Map(
@@ -135,12 +808,12 @@ const PluginFeaturesCard = ({ plugin: plugin2 }) => {
135
808
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", "aria-busy": pending, children: [
136
809
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-4", children: [
137
810
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
138
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: plugin2.label }),
139
- plugin2.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
811
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: title ?? plugin2.label }),
812
+ title === void 0 && plugin2.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: plugin2.description }),
140
813
  !plugin2.enabled && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.pluginDisabled }),
141
814
  pending && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mt-1", children: texts.hints.saving })
142
815
  ] }),
143
- /* @__PURE__ */ jsxRuntime.jsx(
816
+ plugin2.switchable && /* @__PURE__ */ jsxRuntime.jsx(
144
817
  ui.Switch,
145
818
  {
146
819
  id: pluginSwitchId,
@@ -200,14 +873,41 @@ const PluginFeaturesCard = ({ plugin: plugin2 }) => {
200
873
  })
201
874
  ] });
202
875
  };
203
- const PluginFeaturesSettingsPage = () => {
204
- const query = usePluginFeatures();
876
+ const PluginFeatureSections = ({ pluginKey }) => {
877
+ const plugin2 = usePluginFeature(pluginKey).data?.plugin;
878
+ if (!plugin2) {
879
+ return null;
880
+ }
881
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
882
+ (plugin2.switchable || plugin2.features.length > 0) && /* @__PURE__ */ jsxRuntime.jsx(PluginFeaturesCard, { plugin: plugin2, title: texts.sections.features }),
883
+ plugin2.settings.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(PluginSettingsForm, { plugin: plugin2 }),
884
+ plugin2.actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(PluginActionsSection, { plugin: plugin2 })
885
+ ] });
886
+ };
887
+ const isNotFound = (error) => typeof error === "object" && error !== null && error.status === 404;
888
+ const PluginPageShell = ({ pluginKey, children }) => {
889
+ const query = usePluginFeature(pluginKey);
890
+ const plugin2 = query.data?.plugin;
205
891
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3", children: [
206
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "px-6 py-4", children: [
207
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: texts.page.title }),
208
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.page.description })
209
- ] }),
210
- query.isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) }) : query.isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [
892
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsxs(
893
+ reactRouterDom.Link,
894
+ {
895
+ to: PLUGIN_HUB_PATH,
896
+ className: "text-ui-fg-subtle hover:text-ui-fg-base txt-compact-small-plus",
897
+ children: [
898
+ "← ",
899
+ texts.detail.back
900
+ ]
901
+ }
902
+ ) }),
903
+ plugin2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
904
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "px-6 py-4", children: [
905
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: plugin2.label }),
906
+ plugin2.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: plugin2.description }),
907
+ /* @__PURE__ */ jsxRuntime.jsx(PluginStatusChips, { plugin: plugin2 })
908
+ ] }),
909
+ children
910
+ ] }) : query.isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.loading }) }) : isNotFound(query.error) ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.notFound }) }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-wrap items-center justify-between gap-3", children: [
211
911
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: texts.states.error }),
212
912
  /* @__PURE__ */ jsxRuntime.jsx(
213
913
  ui.Button,
@@ -219,12 +919,13 @@ const PluginFeaturesSettingsPage = () => {
219
919
  children: texts.states.retry
220
920
  }
221
921
  )
222
- ] }) : query.data.plugins.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: texts.states.empty }) }) : query.data.plugins.map((plugin2) => /* @__PURE__ */ jsxRuntime.jsx(PluginFeaturesCard, { plugin: plugin2 }, plugin2.plugin_key))
922
+ ] })
223
923
  ] });
224
924
  };
225
- const config = adminSdk.defineRouteConfig({
226
- label: texts.menu.label
227
- });
925
+ const PluginDetailPage = () => {
926
+ const { plugin_key: pluginKey = "" } = reactRouterDom.useParams();
927
+ return /* @__PURE__ */ jsxRuntime.jsx(PluginPageShell, { pluginKey, children: /* @__PURE__ */ jsxRuntime.jsx(PluginFeatureSections, { pluginKey }) });
928
+ };
228
929
  const widgetModule = { widgets: [] };
229
930
  const routeModule = {
230
931
  routes: [
@@ -232,6 +933,10 @@ const routeModule = {
232
933
  Component: PluginFeaturesSettingsPage,
233
934
  path: "/settings/hartl-services",
234
935
  handle: { label: config.label, translationNs: config.translationNs }
936
+ },
937
+ {
938
+ Component: PluginDetailPage,
939
+ path: "/settings/hartl-services/plugin/:plugin_key"
235
940
  }
236
941
  ]
237
942
  };