@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.
- 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/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 +393 -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/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 +7 -3
|
@@ -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
|
|
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: "
|
|
20
|
-
error: "
|
|
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.
|
|
96
|
-
|
|
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
|
-
|
|
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.
|
|
248
|
+
ui.toast.info(texts.toasts.actionAlreadyRunning(action.label));
|
|
107
249
|
} else {
|
|
108
|
-
ui.toast.error(texts.toasts.
|
|
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({
|
|
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
|
|
204
|
-
const
|
|
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.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
] })
|
|
922
|
+
] })
|
|
223
923
|
] });
|
|
224
924
|
};
|
|
225
|
-
const
|
|
226
|
-
|
|
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
|
};
|