configuration-management 0.1.4 → 0.1.8
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/README.md +21 -9
- package/package.json +8 -4
- package/scripts/build-web.js +58 -0
- package/scripts/{setup-app-config.js → setup.js} +27 -19
- package/web/dist/index.css +294 -0
- package/web/dist/index.js +2717 -0
- package/web/index.js +2 -3
- package/web/src/index.js +2 -0
- package/web/styles.css +326 -1
|
@@ -0,0 +1,2717 @@
|
|
|
1
|
+
// web/src/components/App.js
|
|
2
|
+
import React2 from "react";
|
|
3
|
+
import { Provider, lightTheme } from "@adobe/react-spectrum";
|
|
4
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
5
|
+
import { Route, Routes, HashRouter } from "react-router-dom";
|
|
6
|
+
|
|
7
|
+
// web/src/components/ExtensionRegistration.js
|
|
8
|
+
import { register } from "@adobe/uix-guest";
|
|
9
|
+
|
|
10
|
+
// web/src/components/MainPage.js
|
|
11
|
+
import { View as View3 } from "@adobe/react-spectrum";
|
|
12
|
+
import { attach } from "@adobe/uix-guest";
|
|
13
|
+
import { useEffect as useEffect6 } from "react";
|
|
14
|
+
import { useLocation as useLocation2 } from "react-router-dom";
|
|
15
|
+
|
|
16
|
+
// web/src/settings.js
|
|
17
|
+
var DEFAULT_ACTION_KEYS = {
|
|
18
|
+
commerceRestGet: "ConfigurationManagement/commerce-rest-get",
|
|
19
|
+
systemConfigList: "ConfigurationManagement/system-config-list",
|
|
20
|
+
systemConfigSave: "ConfigurationManagement/system-config-save",
|
|
21
|
+
systemConfigSchema: "ConfigurationManagement/system-config-schema",
|
|
22
|
+
exportConfig: "ConfigurationManagement/export-config",
|
|
23
|
+
importConfig: "ConfigurationManagement/import-config",
|
|
24
|
+
syncStoreMappings: "ConfigurationManagement/sync-store-mappings-from-commerce"
|
|
25
|
+
};
|
|
26
|
+
var extensionId = "ConfigurationManagement";
|
|
27
|
+
var actionUrls = {};
|
|
28
|
+
var actionKeys = { ...DEFAULT_ACTION_KEYS };
|
|
29
|
+
function getExtensionId() {
|
|
30
|
+
return extensionId;
|
|
31
|
+
}
|
|
32
|
+
function getActionKey(name) {
|
|
33
|
+
return actionKeys[name] || name;
|
|
34
|
+
}
|
|
35
|
+
function getActionUrl(actionKey) {
|
|
36
|
+
return actionUrls[actionKey];
|
|
37
|
+
}
|
|
38
|
+
function configureWeb({
|
|
39
|
+
extensionId: nextExtensionId,
|
|
40
|
+
actionUrls: nextActionUrls,
|
|
41
|
+
actionKeys: nextActionKeys
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (nextExtensionId != null) {
|
|
44
|
+
extensionId = String(nextExtensionId);
|
|
45
|
+
}
|
|
46
|
+
if (nextActionUrls) {
|
|
47
|
+
actionUrls = { ...nextActionUrls };
|
|
48
|
+
}
|
|
49
|
+
if (nextActionKeys) {
|
|
50
|
+
actionKeys = { ...actionKeys, ...nextActionKeys };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// web/src/components/AppSectionNav.js
|
|
55
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
56
|
+
import Settings from "@spectrum-icons/workflow/Settings";
|
|
57
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
58
|
+
var NAV_ITEMS = [
|
|
59
|
+
{ key: "/", label: "System Configurations", Icon: Settings }
|
|
60
|
+
];
|
|
61
|
+
function AppSectionNav() {
|
|
62
|
+
const navigate = useNavigate();
|
|
63
|
+
const location = useLocation();
|
|
64
|
+
const activeKey = NAV_ITEMS.some((it) => it.key === location.pathname) ? location.pathname : "/";
|
|
65
|
+
return /* @__PURE__ */ jsx("div", { className: "sm-tab-bar", children: /* @__PURE__ */ jsx("div", { className: "sm-tab-bar__track", role: "tablist", "aria-label": "Application sections", children: NAV_ITEMS.map(({ key, label, Icon }) => {
|
|
66
|
+
const active = key === activeKey;
|
|
67
|
+
return /* @__PURE__ */ jsxs(
|
|
68
|
+
"button",
|
|
69
|
+
{
|
|
70
|
+
type: "button",
|
|
71
|
+
role: "tab",
|
|
72
|
+
"aria-selected": active,
|
|
73
|
+
className: `sm-tab${active ? " is-active" : ""}`,
|
|
74
|
+
onClick: () => {
|
|
75
|
+
if (!active) navigate(key);
|
|
76
|
+
},
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx("span", { className: "sm-tab__icon", children: /* @__PURE__ */ jsx(Icon, { size: "XS" }) }),
|
|
79
|
+
label
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
key
|
|
83
|
+
);
|
|
84
|
+
}) }) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// web/src/components/SystemConfig.js
|
|
88
|
+
import { useState as useState5, useMemo as useMemo3, useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
89
|
+
import { Link } from "react-router-dom";
|
|
90
|
+
import {
|
|
91
|
+
View as View2,
|
|
92
|
+
Flex as Flex2,
|
|
93
|
+
Heading as Heading2,
|
|
94
|
+
Text as Text2,
|
|
95
|
+
Button as Button2,
|
|
96
|
+
ActionButton as ActionButton2,
|
|
97
|
+
TooltipTrigger,
|
|
98
|
+
Tooltip,
|
|
99
|
+
TextField as TextField2,
|
|
100
|
+
TextArea,
|
|
101
|
+
NumberField,
|
|
102
|
+
Switch as Switch2,
|
|
103
|
+
Checkbox as Checkbox2,
|
|
104
|
+
Picker as Picker2,
|
|
105
|
+
Item as Item2,
|
|
106
|
+
Section,
|
|
107
|
+
ProgressCircle as ProgressCircle2,
|
|
108
|
+
ProgressBar,
|
|
109
|
+
Divider as Divider2,
|
|
110
|
+
Well as Well2
|
|
111
|
+
} from "@adobe/react-spectrum";
|
|
112
|
+
import Settings2 from "@spectrum-icons/workflow/Settings";
|
|
113
|
+
import Globe from "@spectrum-icons/workflow/Globe";
|
|
114
|
+
import Refresh from "@spectrum-icons/workflow/Refresh";
|
|
115
|
+
import Edit from "@spectrum-icons/workflow/Edit";
|
|
116
|
+
import CloudUpload from "@spectrum-icons/workflow/UploadToCloud";
|
|
117
|
+
import LockClosed from "@spectrum-icons/workflow/LockClosed";
|
|
118
|
+
import Back from "@spectrum-icons/workflow/Back";
|
|
119
|
+
import ChevronDown from "@spectrum-icons/workflow/ChevronDown";
|
|
120
|
+
import ChevronRight from "@spectrum-icons/workflow/ChevronRight";
|
|
121
|
+
|
|
122
|
+
// web/src/hooks/useSystemConfig.js
|
|
123
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
124
|
+
|
|
125
|
+
// web/src/utils.js
|
|
126
|
+
async function callAction(props, action, operation, body = {}) {
|
|
127
|
+
var _a;
|
|
128
|
+
const url = getActionUrl(action);
|
|
129
|
+
if (!url) {
|
|
130
|
+
throw new Error(`Action ${action} is not configured. Call configureWeb({ actionUrls }) with deploy-time URLs.`);
|
|
131
|
+
}
|
|
132
|
+
const res = await fetch(url, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
"x-gw-ims-org-id": props.ims && props.ims.org || "",
|
|
137
|
+
authorization: `Bearer ${props.ims && props.ims.token || ""}`
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
operation,
|
|
141
|
+
...body
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
const text = await res.text();
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(text);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
throw new Error(`Invalid response from ${action}: ${text.slice(0, 200)}`);
|
|
150
|
+
}
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
const msg = (parsed == null ? void 0 : parsed.error) || ((_a = parsed == null ? void 0 : parsed.body) == null ? void 0 : _a.error) || (parsed == null ? void 0 : parsed.message) || `Action ${action} failed with HTTP ${res.status}`;
|
|
153
|
+
const err = new Error(typeof msg === "string" ? msg : JSON.stringify(msg));
|
|
154
|
+
err.status = res.status;
|
|
155
|
+
err.response = parsed;
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// web/src/schema/systemConfigSchema.js
|
|
162
|
+
var FIELD_TYPES = ["text", "textarea", "password", "number", "select", "boolean"];
|
|
163
|
+
var SCOPES = ["default", "websites", "stores"];
|
|
164
|
+
var SENSITIVE_FIELD_TYPES = /* @__PURE__ */ new Set(["password"]);
|
|
165
|
+
function emptySchema() {
|
|
166
|
+
return { sections: [] };
|
|
167
|
+
}
|
|
168
|
+
function getFieldPath(sectionId, groupId, fieldId) {
|
|
169
|
+
return `${sectionId}/${groupId}/${fieldId}`;
|
|
170
|
+
}
|
|
171
|
+
function isFieldSensitive(field) {
|
|
172
|
+
return !!(field == null ? void 0 : field.sensitive) || SENSITIVE_FIELD_TYPES.has(field == null ? void 0 : field.type);
|
|
173
|
+
}
|
|
174
|
+
function isFieldVisibleAtScope(field, scope) {
|
|
175
|
+
const allowed = (field == null ? void 0 : field.showIn) || ["default"];
|
|
176
|
+
return allowed.includes(scope);
|
|
177
|
+
}
|
|
178
|
+
function flattenFields(schema) {
|
|
179
|
+
const out = [];
|
|
180
|
+
if (!schema || !Array.isArray(schema.sections)) return out;
|
|
181
|
+
for (const section of schema.sections) {
|
|
182
|
+
if (!Array.isArray(section.groups)) continue;
|
|
183
|
+
for (const group of section.groups) {
|
|
184
|
+
if (!Array.isArray(group.fields)) continue;
|
|
185
|
+
for (const field of group.fields) {
|
|
186
|
+
out.push({
|
|
187
|
+
section,
|
|
188
|
+
group,
|
|
189
|
+
field,
|
|
190
|
+
path: getFieldPath(section.id, group.id, field.id),
|
|
191
|
+
sensitive: isFieldSensitive(field)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function coerceDefault(field) {
|
|
199
|
+
var _a;
|
|
200
|
+
switch (field == null ? void 0 : field.type) {
|
|
201
|
+
case "boolean":
|
|
202
|
+
return !!field.default;
|
|
203
|
+
case "number":
|
|
204
|
+
return typeof field.default === "number" ? field.default : Number(field.default) || 0;
|
|
205
|
+
default:
|
|
206
|
+
return (_a = field == null ? void 0 : field.default) != null ? _a : "";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// web/src/utils/storeMappingsFromCommerceRest.js
|
|
211
|
+
function localeToLanguageCode(locale) {
|
|
212
|
+
if (locale == null || locale === "") return null;
|
|
213
|
+
const s = String(locale).trim();
|
|
214
|
+
const head = s.split(/[-_]/u)[0];
|
|
215
|
+
if (head && /^[a-zA-Z]{2,8}$/.test(head)) return head.toLowerCase();
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function inferLanguageFromStoreCode(code) {
|
|
219
|
+
if (typeof code !== "string") return null;
|
|
220
|
+
const m = /^([a-z]{2})[-_]/i.exec(code);
|
|
221
|
+
return m ? m[1].toLowerCase() : null;
|
|
222
|
+
}
|
|
223
|
+
function buildStoreMappingsFromCommercePayload(websitesRaw, storeViewsRaw, storeConfigsRaw) {
|
|
224
|
+
const websiteIdToCode = /* @__PURE__ */ new Map();
|
|
225
|
+
if (Array.isArray(websitesRaw)) {
|
|
226
|
+
for (const w of websitesRaw) {
|
|
227
|
+
if (w && w.id != null && w.code != null) {
|
|
228
|
+
websiteIdToCode.set(String(w.id), String(w.code));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const storeCodeToLocale = /* @__PURE__ */ new Map();
|
|
233
|
+
if (Array.isArray(storeConfigsRaw)) {
|
|
234
|
+
for (const cfg of storeConfigsRaw) {
|
|
235
|
+
if (cfg && cfg.code != null && cfg.locale != null) {
|
|
236
|
+
storeCodeToLocale.set(String(cfg.code), String(cfg.locale));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const mappings = {};
|
|
241
|
+
if (!Array.isArray(storeViewsRaw)) return mappings;
|
|
242
|
+
for (const s of storeViewsRaw) {
|
|
243
|
+
if (!s || s.id == null || s.code == null) continue;
|
|
244
|
+
const id = String(s.id);
|
|
245
|
+
const code = String(s.code);
|
|
246
|
+
const websiteId = s.website_id != null ? String(s.website_id) : "";
|
|
247
|
+
const websiteCode = websiteIdToCode.get(websiteId) || "";
|
|
248
|
+
const languageCode = localeToLanguageCode(storeCodeToLocale.get(code)) || inferLanguageFromStoreCode(code) || "en";
|
|
249
|
+
mappings[id] = {
|
|
250
|
+
code,
|
|
251
|
+
language_code: languageCode,
|
|
252
|
+
website_code: websiteCode,
|
|
253
|
+
website_id: websiteId
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return mappings;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// web/src/hooks/useSystemConfig.js
|
|
260
|
+
var SENSITIVE_PLACEHOLDER = "__SENSITIVE_UNCHANGED__";
|
|
261
|
+
var USE_DEFAULT_SENTINEL = "__USE_DEFAULT__";
|
|
262
|
+
var DEFAULT_SCOPE = { scope: "default", scopeId: "0" };
|
|
263
|
+
var STORE_MAPPINGS_PATH = "general/settings/store_mappings";
|
|
264
|
+
function useSystemConfig(props, schema) {
|
|
265
|
+
const fields = useMemo(() => flattenFields(schema), [schema]);
|
|
266
|
+
const allPaths = useMemo(() => fields.map((f) => f.path), [fields]);
|
|
267
|
+
const sensitivePaths = useMemo(
|
|
268
|
+
() => fields.filter((f) => f.sensitive).map((f) => f.path),
|
|
269
|
+
[fields]
|
|
270
|
+
);
|
|
271
|
+
const [scopeTree, setScopeTree] = useState({ websites: [], storeGroups: [], stores: [], loading: true, error: null });
|
|
272
|
+
const [scope, setScope] = useState(DEFAULT_SCOPE);
|
|
273
|
+
const [serverItems, setServerItems] = useState({});
|
|
274
|
+
const [localValues, setLocalValues] = useState({});
|
|
275
|
+
const [loading, setLoading] = useState(false);
|
|
276
|
+
const [saving, setSaving] = useState(false);
|
|
277
|
+
const [error, setError] = useState(null);
|
|
278
|
+
const [savedAt, setSavedAt] = useState(null);
|
|
279
|
+
const parentWebsiteId = useMemo(() => {
|
|
280
|
+
if (scope.scope !== "stores") return void 0;
|
|
281
|
+
const store = scopeTree.stores.find((s) => String(s.id) === String(scope.scopeId));
|
|
282
|
+
return store == null ? void 0 : store.website_id;
|
|
283
|
+
}, [scope, scopeTree.stores]);
|
|
284
|
+
const fetchScopeTree = useCallback(async () => {
|
|
285
|
+
setScopeTree((prev) => ({ ...prev, loading: true, error: null }));
|
|
286
|
+
try {
|
|
287
|
+
const [websitesRes, groupsRes, storesRes, configsRes] = await Promise.all([
|
|
288
|
+
callAction(props, getActionKey("commerceRestGet"), "store/websites"),
|
|
289
|
+
callAction(props, getActionKey("commerceRestGet"), "store/storeGroups"),
|
|
290
|
+
callAction(props, getActionKey("commerceRestGet"), "store/storeViews"),
|
|
291
|
+
callAction(props, getActionKey("commerceRestGet"), "store/storeConfigs").catch(() => null)
|
|
292
|
+
]);
|
|
293
|
+
const websitesRaw = (websitesRes == null ? void 0 : websitesRes.body) || websitesRes;
|
|
294
|
+
const groupsRaw = (groupsRes == null ? void 0 : groupsRes.body) || groupsRes;
|
|
295
|
+
const storesRaw = (storesRes == null ? void 0 : storesRes.body) || storesRes;
|
|
296
|
+
const configsRaw = (configsRes == null ? void 0 : configsRes.body) || configsRes;
|
|
297
|
+
const websites = Array.isArray(websitesRaw) ? websitesRaw.filter((w) => w.id !== 0 && w.code !== "admin") : [];
|
|
298
|
+
const storeGroups = Array.isArray(groupsRaw) ? groupsRaw.filter((g) => g.id !== 0) : [];
|
|
299
|
+
const stores = Array.isArray(storesRaw) ? storesRaw.filter((s) => s.id !== 0 && s.code !== "admin") : [];
|
|
300
|
+
setScopeTree({ websites, storeGroups, stores, loading: false, error: null });
|
|
301
|
+
const storeMappings = buildStoreMappingsFromCommercePayload(websitesRaw, storesRaw, configsRaw);
|
|
302
|
+
if (Object.keys(storeMappings).length > 0) {
|
|
303
|
+
try {
|
|
304
|
+
await callAction(props, getActionKey("systemConfigSave"), "", {
|
|
305
|
+
values: { [STORE_MAPPINGS_PATH]: JSON.stringify(storeMappings, null, 2) },
|
|
306
|
+
sensitivePaths: [],
|
|
307
|
+
scope: "default",
|
|
308
|
+
scopeId: "0"
|
|
309
|
+
});
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error("Failed to persist store_mappings to ABDB after loading Commerce stores", err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error("Failed to load stores from Commerce", e);
|
|
316
|
+
setScopeTree({ websites: [], storeGroups: [], stores: [], loading: false, error: e.message || "Failed to fetch stores" });
|
|
317
|
+
}
|
|
318
|
+
}, [props]);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
fetchScopeTree();
|
|
321
|
+
}, [fetchScopeTree]);
|
|
322
|
+
const fetchAtScope = useCallback(async () => {
|
|
323
|
+
var _a;
|
|
324
|
+
if (allPaths.length === 0) {
|
|
325
|
+
setServerItems({});
|
|
326
|
+
setLocalValues({});
|
|
327
|
+
setLoading(false);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
setLoading(true);
|
|
331
|
+
setError(null);
|
|
332
|
+
try {
|
|
333
|
+
const response = await callAction(
|
|
334
|
+
props,
|
|
335
|
+
getActionKey("systemConfigList"),
|
|
336
|
+
"",
|
|
337
|
+
{
|
|
338
|
+
paths: allPaths,
|
|
339
|
+
sensitivePaths,
|
|
340
|
+
scope: scope.scope,
|
|
341
|
+
scopeId: scope.scopeId,
|
|
342
|
+
parentWebsiteId
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
const items = (response == null ? void 0 : response.items) || ((_a = response == null ? void 0 : response.body) == null ? void 0 : _a.items) || {};
|
|
346
|
+
setServerItems(items);
|
|
347
|
+
setLocalValues({});
|
|
348
|
+
} catch (e) {
|
|
349
|
+
console.error("Failed to load system config", e);
|
|
350
|
+
setError(e.message || "Failed to load system config");
|
|
351
|
+
} finally {
|
|
352
|
+
setLoading(false);
|
|
353
|
+
}
|
|
354
|
+
}, [props, allPaths, sensitivePaths, scope, parentWebsiteId]);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
fetchAtScope();
|
|
357
|
+
}, [fetchAtScope]);
|
|
358
|
+
const getDisplayValue = useCallback((path, fallback) => {
|
|
359
|
+
if (Object.prototype.hasOwnProperty.call(localValues, path)) {
|
|
360
|
+
return localValues[path];
|
|
361
|
+
}
|
|
362
|
+
const item = serverItems[path];
|
|
363
|
+
if (item && item.value !== void 0) return item.value;
|
|
364
|
+
return fallback;
|
|
365
|
+
}, [localValues, serverItems]);
|
|
366
|
+
const getOrigin = useCallback((path) => {
|
|
367
|
+
const item = serverItems[path];
|
|
368
|
+
return (item == null ? void 0 : item.origin) || null;
|
|
369
|
+
}, [serverItems]);
|
|
370
|
+
const isInheritedAtScope = useCallback((path) => {
|
|
371
|
+
if (scope.scope === "default") return false;
|
|
372
|
+
if (Object.prototype.hasOwnProperty.call(localValues, path)) {
|
|
373
|
+
return localValues[path] === USE_DEFAULT_SENTINEL;
|
|
374
|
+
}
|
|
375
|
+
const origin = getOrigin(path);
|
|
376
|
+
if (!origin) return true;
|
|
377
|
+
return !(origin.scope === scope.scope && String(origin.scopeId) === String(scope.scopeId));
|
|
378
|
+
}, [scope, localValues, getOrigin]);
|
|
379
|
+
const setFieldValue = useCallback((path, value) => {
|
|
380
|
+
setLocalValues((prev) => ({ ...prev, [path]: value }));
|
|
381
|
+
}, []);
|
|
382
|
+
const setUseDefault = useCallback((path, useDefault) => {
|
|
383
|
+
setLocalValues((prev) => {
|
|
384
|
+
var _a;
|
|
385
|
+
const next = { ...prev };
|
|
386
|
+
if (useDefault) {
|
|
387
|
+
next[path] = USE_DEFAULT_SENTINEL;
|
|
388
|
+
} else {
|
|
389
|
+
const current = (_a = serverItems[path]) == null ? void 0 : _a.value;
|
|
390
|
+
next[path] = current !== void 0 ? current : "";
|
|
391
|
+
}
|
|
392
|
+
return next;
|
|
393
|
+
});
|
|
394
|
+
}, [serverItems]);
|
|
395
|
+
const dirtyCount = useMemo(() => Object.keys(localValues).length, [localValues]);
|
|
396
|
+
const save = useCallback(async () => {
|
|
397
|
+
if (dirtyCount === 0) return true;
|
|
398
|
+
setSaving(true);
|
|
399
|
+
setError(null);
|
|
400
|
+
try {
|
|
401
|
+
const visibleFieldsByPath = new Map(
|
|
402
|
+
fields.filter((f) => isFieldVisibleAtScope(f.field, scope.scope)).map((f) => [f.path, f])
|
|
403
|
+
);
|
|
404
|
+
const payload = {};
|
|
405
|
+
for (const [path, value] of Object.entries(localValues)) {
|
|
406
|
+
if (!visibleFieldsByPath.has(path)) continue;
|
|
407
|
+
payload[path] = value;
|
|
408
|
+
}
|
|
409
|
+
if (Object.keys(payload).length === 0) {
|
|
410
|
+
setSaving(false);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
await callAction(
|
|
414
|
+
props,
|
|
415
|
+
getActionKey("systemConfigSave"),
|
|
416
|
+
"",
|
|
417
|
+
{
|
|
418
|
+
values: payload,
|
|
419
|
+
sensitivePaths,
|
|
420
|
+
scope: scope.scope,
|
|
421
|
+
scopeId: scope.scopeId
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
setSavedAt(Date.now());
|
|
425
|
+
await fetchAtScope();
|
|
426
|
+
return true;
|
|
427
|
+
} catch (e) {
|
|
428
|
+
console.error("Failed to save system config", e);
|
|
429
|
+
setError(e.message || "Failed to save system config");
|
|
430
|
+
return false;
|
|
431
|
+
} finally {
|
|
432
|
+
setSaving(false);
|
|
433
|
+
}
|
|
434
|
+
}, [props, dirtyCount, localValues, sensitivePaths, scope, fields, fetchAtScope]);
|
|
435
|
+
const reset = useCallback(() => {
|
|
436
|
+
setLocalValues({});
|
|
437
|
+
}, []);
|
|
438
|
+
return {
|
|
439
|
+
fields,
|
|
440
|
+
scope,
|
|
441
|
+
setScope,
|
|
442
|
+
scopeTree,
|
|
443
|
+
refreshScopeTree: fetchScopeTree,
|
|
444
|
+
getDisplayValue,
|
|
445
|
+
getOrigin,
|
|
446
|
+
isInheritedAtScope,
|
|
447
|
+
setFieldValue,
|
|
448
|
+
setUseDefault,
|
|
449
|
+
dirtyCount,
|
|
450
|
+
loading,
|
|
451
|
+
saving,
|
|
452
|
+
error,
|
|
453
|
+
savedAt,
|
|
454
|
+
save,
|
|
455
|
+
reset,
|
|
456
|
+
refresh: fetchAtScope,
|
|
457
|
+
SENSITIVE_PLACEHOLDER,
|
|
458
|
+
USE_DEFAULT_SENTINEL
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// web/src/hooks/useSystemConfigSchema.js
|
|
463
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
464
|
+
function useSystemConfigSchema(props) {
|
|
465
|
+
const [schema, setSchema] = useState2(emptySchema());
|
|
466
|
+
const [loading, setLoading] = useState2(true);
|
|
467
|
+
const [saving, setSaving] = useState2(false);
|
|
468
|
+
const [error, setError] = useState2(null);
|
|
469
|
+
const fetchSchema = useCallback2(async () => {
|
|
470
|
+
var _a;
|
|
471
|
+
setLoading(true);
|
|
472
|
+
setError(null);
|
|
473
|
+
try {
|
|
474
|
+
const response = await callAction(
|
|
475
|
+
props,
|
|
476
|
+
getActionKey("systemConfigSchema"),
|
|
477
|
+
"get"
|
|
478
|
+
);
|
|
479
|
+
const fetched = (response == null ? void 0 : response.schema) || ((_a = response == null ? void 0 : response.body) == null ? void 0 : _a.schema) || emptySchema();
|
|
480
|
+
setSchema(fetched);
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.error("Failed to load schema", e);
|
|
483
|
+
setError(e.message || "Failed to load schema");
|
|
484
|
+
setSchema(emptySchema());
|
|
485
|
+
} finally {
|
|
486
|
+
setLoading(false);
|
|
487
|
+
}
|
|
488
|
+
}, [props]);
|
|
489
|
+
useEffect2(() => {
|
|
490
|
+
fetchSchema();
|
|
491
|
+
}, [fetchSchema]);
|
|
492
|
+
const saveSchema = useCallback2(async (nextSchema, { confirmCascade = false } = {}) => {
|
|
493
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
494
|
+
setSaving(true);
|
|
495
|
+
setError(null);
|
|
496
|
+
try {
|
|
497
|
+
let response;
|
|
498
|
+
try {
|
|
499
|
+
response = await callAction(
|
|
500
|
+
props,
|
|
501
|
+
getActionKey("systemConfigSchema"),
|
|
502
|
+
"save",
|
|
503
|
+
{ schema: nextSchema, ...confirmCascade ? { confirmCascade: true } : {} }
|
|
504
|
+
);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
const removed = ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.removedPaths) || ((_c = (_b = err == null ? void 0 : err.response) == null ? void 0 : _b.body) == null ? void 0 : _c.removedPaths);
|
|
507
|
+
if ((err == null ? void 0 : err.status) === 409 && Array.isArray(removed)) {
|
|
508
|
+
return { needsConfirmation: true, removedPaths: removed };
|
|
509
|
+
}
|
|
510
|
+
throw err;
|
|
511
|
+
}
|
|
512
|
+
const saved = (response == null ? void 0 : response.schema) || ((_d = response == null ? void 0 : response.body) == null ? void 0 : _d.schema);
|
|
513
|
+
if (!saved) {
|
|
514
|
+
await fetchSchema();
|
|
515
|
+
setError("Schema save did not return the saved schema. See server logs.");
|
|
516
|
+
return { ok: false };
|
|
517
|
+
}
|
|
518
|
+
setSchema(saved);
|
|
519
|
+
return {
|
|
520
|
+
ok: true,
|
|
521
|
+
removedPaths: (response == null ? void 0 : response.removedPaths) || ((_e = response == null ? void 0 : response.body) == null ? void 0 : _e.removedPaths) || [],
|
|
522
|
+
deletedCount: (_h = (_g = response == null ? void 0 : response.deletedCount) != null ? _g : (_f = response == null ? void 0 : response.body) == null ? void 0 : _f.deletedCount) != null ? _h : 0
|
|
523
|
+
};
|
|
524
|
+
} catch (e) {
|
|
525
|
+
console.error("Failed to save schema", e);
|
|
526
|
+
setError(e.message || "Failed to save schema");
|
|
527
|
+
return { ok: false };
|
|
528
|
+
} finally {
|
|
529
|
+
setSaving(false);
|
|
530
|
+
}
|
|
531
|
+
}, [props, fetchSchema]);
|
|
532
|
+
return {
|
|
533
|
+
schema,
|
|
534
|
+
setSchema,
|
|
535
|
+
saveSchema,
|
|
536
|
+
refresh: fetchSchema,
|
|
537
|
+
loading,
|
|
538
|
+
saving,
|
|
539
|
+
error
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// web/src/hooks/useConfirm.js
|
|
544
|
+
import React, { useCallback as useCallback3, useEffect as useEffect3, useRef, useState as useState3 } from "react";
|
|
545
|
+
import ReactDOM from "react-dom";
|
|
546
|
+
|
|
547
|
+
// web/src/theme.js
|
|
548
|
+
var THEME = {
|
|
549
|
+
color: {
|
|
550
|
+
bg: "var(--sm-color-bg)",
|
|
551
|
+
surface: "var(--sm-color-surface)",
|
|
552
|
+
surfaceMuted: "var(--sm-color-surface-muted)",
|
|
553
|
+
surfaceSubtle: "var(--sm-color-surface-subtle)",
|
|
554
|
+
border: "var(--sm-color-border)",
|
|
555
|
+
borderStrong: "var(--sm-color-border-strong)",
|
|
556
|
+
text: "var(--sm-color-text)",
|
|
557
|
+
textMuted: "var(--sm-color-text-muted)",
|
|
558
|
+
textStrong: "var(--sm-color-text-strong)",
|
|
559
|
+
textSoft: "var(--sm-color-text-soft)",
|
|
560
|
+
textInverse: "var(--sm-color-text-inverse)",
|
|
561
|
+
surfacePanel: "var(--sm-color-surface-panel)",
|
|
562
|
+
accent: "var(--sm-color-accent)",
|
|
563
|
+
accentHover: "var(--sm-color-accent-hover)",
|
|
564
|
+
accentSoft: "var(--sm-color-accent-soft)",
|
|
565
|
+
accentTint: "var(--sm-color-accent-tint)",
|
|
566
|
+
success: "var(--sm-color-success)",
|
|
567
|
+
successHover: "var(--sm-color-success-hover)",
|
|
568
|
+
successSoft: "var(--sm-color-success-soft)",
|
|
569
|
+
warning: "var(--sm-color-warning)",
|
|
570
|
+
warningHover: "var(--sm-color-warning-hover)",
|
|
571
|
+
warningSoft: "var(--sm-color-warning-soft)",
|
|
572
|
+
warningBorder: "var(--sm-color-warning-border)",
|
|
573
|
+
warningText: "var(--sm-color-warning-text)",
|
|
574
|
+
warningTint: "var(--sm-color-warning-tint)",
|
|
575
|
+
danger: "var(--sm-color-danger)",
|
|
576
|
+
dangerHover: "var(--sm-color-danger-hover)",
|
|
577
|
+
dangerSoft: "var(--sm-color-danger-soft)",
|
|
578
|
+
dangerTint: "var(--sm-color-danger-tint)",
|
|
579
|
+
neutralSoft: "var(--sm-color-neutral-soft)",
|
|
580
|
+
neutralText: "var(--sm-color-neutral-text)",
|
|
581
|
+
overlay: "var(--sm-color-overlay)"
|
|
582
|
+
},
|
|
583
|
+
radius: {
|
|
584
|
+
sm: "var(--sm-radius-sm)",
|
|
585
|
+
md: "var(--sm-radius-md)",
|
|
586
|
+
lg: "var(--sm-radius-lg)",
|
|
587
|
+
xl: "var(--sm-radius-xl)",
|
|
588
|
+
xxl: "var(--sm-radius-2xl)",
|
|
589
|
+
pill: "var(--sm-radius-pill)"
|
|
590
|
+
},
|
|
591
|
+
space: {
|
|
592
|
+
1: "var(--sm-space-1)",
|
|
593
|
+
2: "var(--sm-space-2)",
|
|
594
|
+
3: "var(--sm-space-3)",
|
|
595
|
+
4: "var(--sm-space-4)",
|
|
596
|
+
5: "var(--sm-space-5)",
|
|
597
|
+
6: "var(--sm-space-6)"
|
|
598
|
+
},
|
|
599
|
+
shadow: {
|
|
600
|
+
xs: "var(--sm-shadow-xs)",
|
|
601
|
+
sm: "var(--sm-shadow-sm)",
|
|
602
|
+
md: "var(--sm-shadow-md)",
|
|
603
|
+
pill: "var(--sm-shadow-pill)",
|
|
604
|
+
floating: "var(--sm-shadow-floating)",
|
|
605
|
+
dropdown: "var(--sm-shadow-dropdown)",
|
|
606
|
+
modal: "var(--sm-shadow-modal)",
|
|
607
|
+
inset: "var(--sm-shadow-inset)"
|
|
608
|
+
},
|
|
609
|
+
font: {
|
|
610
|
+
family: "var(--sm-font-family)",
|
|
611
|
+
mono: "var(--sm-font-mono)",
|
|
612
|
+
sizeXs: "var(--sm-font-size-xs)",
|
|
613
|
+
sizeSm: "var(--sm-font-size-sm)",
|
|
614
|
+
sizeMd: "var(--sm-font-size-md)",
|
|
615
|
+
sizeLg: "var(--sm-font-size-lg)",
|
|
616
|
+
weightRegular: "var(--sm-font-weight-regular)",
|
|
617
|
+
weightMedium: "var(--sm-font-weight-medium)",
|
|
618
|
+
weightSemi: "var(--sm-font-weight-semibold)",
|
|
619
|
+
weightBold: "var(--sm-font-weight-bold)"
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
var PALETTE = { ...THEME.color };
|
|
623
|
+
var RADIUS = { ...THEME.radius };
|
|
624
|
+
var SHADOW = { ...THEME.shadow };
|
|
625
|
+
var SPACE = { ...THEME.space };
|
|
626
|
+
var FONT = { ...THEME.font };
|
|
627
|
+
|
|
628
|
+
// web/src/hooks/useConfirm.js
|
|
629
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
630
|
+
function useConfirm() {
|
|
631
|
+
const [state, setState] = useState3(null);
|
|
632
|
+
const resolverRef = useRef(null);
|
|
633
|
+
const confirm = useCallback3((opts = {}) => {
|
|
634
|
+
return new Promise((resolve) => {
|
|
635
|
+
resolverRef.current = resolve;
|
|
636
|
+
setState({ options: opts });
|
|
637
|
+
});
|
|
638
|
+
}, []);
|
|
639
|
+
const finish = useCallback3((result) => {
|
|
640
|
+
const resolve = resolverRef.current;
|
|
641
|
+
resolverRef.current = null;
|
|
642
|
+
setState(null);
|
|
643
|
+
if (resolve) resolve(result);
|
|
644
|
+
}, []);
|
|
645
|
+
useEffect3(() => {
|
|
646
|
+
if (!state) return;
|
|
647
|
+
const onKey = (e) => {
|
|
648
|
+
if (e.key === "Escape") finish(state.options && state.options.choices ? null : false);
|
|
649
|
+
};
|
|
650
|
+
window.addEventListener("keydown", onKey);
|
|
651
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
652
|
+
}, [state, finish]);
|
|
653
|
+
useEffect3(() => {
|
|
654
|
+
if (!state) return;
|
|
655
|
+
const prev = document.body.style.overflow;
|
|
656
|
+
document.body.style.overflow = "hidden";
|
|
657
|
+
return () => {
|
|
658
|
+
document.body.style.overflow = prev;
|
|
659
|
+
};
|
|
660
|
+
}, [state]);
|
|
661
|
+
const dialog = state ? ReactDOM.createPortal(
|
|
662
|
+
/* @__PURE__ */ jsx2(
|
|
663
|
+
ConfirmModal,
|
|
664
|
+
{
|
|
665
|
+
options: state.options,
|
|
666
|
+
onConfirm: () => finish(true),
|
|
667
|
+
onCancel: () => finish(state.options && state.options.choices ? null : false),
|
|
668
|
+
onChoose: (value) => finish(value)
|
|
669
|
+
}
|
|
670
|
+
),
|
|
671
|
+
document.body
|
|
672
|
+
) : null;
|
|
673
|
+
return { confirm, dialog };
|
|
674
|
+
}
|
|
675
|
+
var VARIANT_STYLES = {
|
|
676
|
+
destructive: { color: PALETTE.danger, primaryBg: PALETTE.danger, primaryBgHover: PALETTE.dangerHover, tint: PALETTE.dangerTint, icon: "\u26A0" },
|
|
677
|
+
warning: { color: PALETTE.warning, primaryBg: PALETTE.warning, primaryBgHover: PALETTE.warningHover, tint: PALETTE.warningTint, icon: "!" },
|
|
678
|
+
information: { color: PALETTE.accent, primaryBg: PALETTE.accent, primaryBgHover: PALETTE.accentHover, tint: PALETTE.accentTint, icon: "i" },
|
|
679
|
+
confirmation: { color: PALETTE.accent, primaryBg: PALETTE.accent, primaryBgHover: PALETTE.accentHover, tint: PALETTE.accentTint, icon: "?" }
|
|
680
|
+
};
|
|
681
|
+
function ConfirmModal({ options, onConfirm, onCancel, onChoose }) {
|
|
682
|
+
const variant = options.variant || "confirmation";
|
|
683
|
+
const styles = VARIANT_STYLES[variant] || VARIANT_STYLES.confirmation;
|
|
684
|
+
const confirmRef = useRef(null);
|
|
685
|
+
const hasChoices = Array.isArray(options.choices) && options.choices.length > 0;
|
|
686
|
+
useEffect3(() => {
|
|
687
|
+
if (confirmRef.current) confirmRef.current.focus();
|
|
688
|
+
}, []);
|
|
689
|
+
const renderBody = (body) => {
|
|
690
|
+
if (body == null) return null;
|
|
691
|
+
if (typeof body !== "string") return body;
|
|
692
|
+
return body.split("\n").map((line, i) => /* @__PURE__ */ jsxs2(React.Fragment, { children: [
|
|
693
|
+
line,
|
|
694
|
+
i < body.split("\n").length - 1 && /* @__PURE__ */ jsx2("br", {})
|
|
695
|
+
] }, i));
|
|
696
|
+
};
|
|
697
|
+
const SPECTRUM_FONT = "adobe-clean, 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Trebuchet MS', 'Lucida Grande', sans-serif";
|
|
698
|
+
return /* @__PURE__ */ jsx2(
|
|
699
|
+
"div",
|
|
700
|
+
{
|
|
701
|
+
role: "dialog",
|
|
702
|
+
"aria-modal": "true",
|
|
703
|
+
"aria-labelledby": "confirm-title",
|
|
704
|
+
style: {
|
|
705
|
+
position: "fixed",
|
|
706
|
+
inset: 0,
|
|
707
|
+
zIndex: 1e5,
|
|
708
|
+
background: PALETTE.overlay,
|
|
709
|
+
backdropFilter: "blur(2px)",
|
|
710
|
+
display: "flex",
|
|
711
|
+
alignItems: "center",
|
|
712
|
+
justifyContent: "center",
|
|
713
|
+
padding: 16,
|
|
714
|
+
fontFamily: SPECTRUM_FONT,
|
|
715
|
+
animation: "sm-fade-in 120ms ease-out"
|
|
716
|
+
},
|
|
717
|
+
onClick: (e) => {
|
|
718
|
+
if (e.target === e.currentTarget) onCancel();
|
|
719
|
+
},
|
|
720
|
+
children: /* @__PURE__ */ jsxs2(
|
|
721
|
+
"div",
|
|
722
|
+
{
|
|
723
|
+
style: {
|
|
724
|
+
background: PALETTE.surface,
|
|
725
|
+
borderRadius: RADIUS.xl,
|
|
726
|
+
boxShadow: SHADOW.modal,
|
|
727
|
+
width: "100%",
|
|
728
|
+
maxWidth: 520,
|
|
729
|
+
maxHeight: "calc(100vh - 32px)",
|
|
730
|
+
display: "flex",
|
|
731
|
+
flexDirection: "column",
|
|
732
|
+
overflow: "hidden",
|
|
733
|
+
animation: "sm-pop-in 160ms cubic-bezier(0.16, 1, 0.3, 1)"
|
|
734
|
+
},
|
|
735
|
+
children: [
|
|
736
|
+
/* @__PURE__ */ jsxs2(
|
|
737
|
+
"div",
|
|
738
|
+
{
|
|
739
|
+
style: {
|
|
740
|
+
padding: "20px 24px 12px",
|
|
741
|
+
display: "flex",
|
|
742
|
+
alignItems: "flex-start",
|
|
743
|
+
gap: 14
|
|
744
|
+
},
|
|
745
|
+
children: [
|
|
746
|
+
/* @__PURE__ */ jsx2(
|
|
747
|
+
"div",
|
|
748
|
+
{
|
|
749
|
+
"aria-hidden": "true",
|
|
750
|
+
style: {
|
|
751
|
+
flex: "0 0 auto",
|
|
752
|
+
width: 36,
|
|
753
|
+
height: 36,
|
|
754
|
+
borderRadius: RADIUS.pill,
|
|
755
|
+
background: styles.tint,
|
|
756
|
+
color: styles.color,
|
|
757
|
+
display: "flex",
|
|
758
|
+
alignItems: "center",
|
|
759
|
+
justifyContent: "center",
|
|
760
|
+
fontSize: 18,
|
|
761
|
+
fontWeight: 700,
|
|
762
|
+
lineHeight: 1,
|
|
763
|
+
fontFamily: SPECTRUM_FONT
|
|
764
|
+
},
|
|
765
|
+
children: styles.icon
|
|
766
|
+
}
|
|
767
|
+
),
|
|
768
|
+
/* @__PURE__ */ jsxs2("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
769
|
+
/* @__PURE__ */ jsx2(
|
|
770
|
+
"div",
|
|
771
|
+
{
|
|
772
|
+
id: "confirm-title",
|
|
773
|
+
style: {
|
|
774
|
+
fontFamily: SPECTRUM_FONT,
|
|
775
|
+
fontSize: 17,
|
|
776
|
+
fontWeight: 700,
|
|
777
|
+
lineHeight: 1.3,
|
|
778
|
+
letterSpacing: "-0.005em",
|
|
779
|
+
color: PALETTE.textStrong
|
|
780
|
+
},
|
|
781
|
+
children: options.title || "Are you sure?"
|
|
782
|
+
}
|
|
783
|
+
),
|
|
784
|
+
options.body != null && /* @__PURE__ */ jsx2(
|
|
785
|
+
"div",
|
|
786
|
+
{
|
|
787
|
+
style: {
|
|
788
|
+
marginTop: 6,
|
|
789
|
+
fontFamily: SPECTRUM_FONT,
|
|
790
|
+
color: PALETTE.textSoft,
|
|
791
|
+
fontSize: 13,
|
|
792
|
+
lineHeight: 1.55,
|
|
793
|
+
maxHeight: "40vh",
|
|
794
|
+
overflowY: "auto"
|
|
795
|
+
},
|
|
796
|
+
children: renderBody(options.body)
|
|
797
|
+
}
|
|
798
|
+
)
|
|
799
|
+
] })
|
|
800
|
+
]
|
|
801
|
+
}
|
|
802
|
+
),
|
|
803
|
+
hasChoices ? /* @__PURE__ */ jsxs2(
|
|
804
|
+
"div",
|
|
805
|
+
{
|
|
806
|
+
style: {
|
|
807
|
+
padding: "4px 16px 16px",
|
|
808
|
+
display: "flex",
|
|
809
|
+
flexDirection: "column",
|
|
810
|
+
gap: 8
|
|
811
|
+
},
|
|
812
|
+
children: [
|
|
813
|
+
options.choices.map((c, i) => {
|
|
814
|
+
var _a;
|
|
815
|
+
const cStyles = VARIANT_STYLES[c.variant] || VARIANT_STYLES.confirmation;
|
|
816
|
+
const isPrimary = i === 0;
|
|
817
|
+
return /* @__PURE__ */ jsxs2(
|
|
818
|
+
"button",
|
|
819
|
+
{
|
|
820
|
+
type: "button",
|
|
821
|
+
ref: isPrimary ? confirmRef : null,
|
|
822
|
+
onClick: () => onChoose(c.value),
|
|
823
|
+
style: {
|
|
824
|
+
textAlign: "left",
|
|
825
|
+
padding: "10px 14px",
|
|
826
|
+
borderRadius: RADIUS.lg,
|
|
827
|
+
border: isPrimary ? `1px solid ${cStyles.primaryBg}` : `1px solid ${PALETTE.borderStrong}`,
|
|
828
|
+
background: isPrimary ? cStyles.primaryBg : PALETTE.surface,
|
|
829
|
+
color: isPrimary ? PALETTE.textInverse : PALETTE.textStrong,
|
|
830
|
+
fontFamily: SPECTRUM_FONT,
|
|
831
|
+
fontSize: 14,
|
|
832
|
+
fontWeight: 600,
|
|
833
|
+
lineHeight: 1.35,
|
|
834
|
+
cursor: "pointer",
|
|
835
|
+
transition: "background 120ms ease, border-color 120ms ease",
|
|
836
|
+
display: "flex",
|
|
837
|
+
flexDirection: "column",
|
|
838
|
+
gap: 2
|
|
839
|
+
},
|
|
840
|
+
onMouseOver: (e) => {
|
|
841
|
+
e.currentTarget.style.background = isPrimary ? cStyles.primaryBgHover : PALETTE.surfaceMuted;
|
|
842
|
+
if (isPrimary) e.currentTarget.style.borderColor = cStyles.primaryBgHover;
|
|
843
|
+
},
|
|
844
|
+
onMouseOut: (e) => {
|
|
845
|
+
e.currentTarget.style.background = isPrimary ? cStyles.primaryBg : PALETTE.surface;
|
|
846
|
+
if (isPrimary) e.currentTarget.style.borderColor = cStyles.primaryBg;
|
|
847
|
+
},
|
|
848
|
+
children: [
|
|
849
|
+
/* @__PURE__ */ jsx2("span", { children: c.label }),
|
|
850
|
+
c.description && /* @__PURE__ */ jsx2(
|
|
851
|
+
"span",
|
|
852
|
+
{
|
|
853
|
+
style: {
|
|
854
|
+
fontSize: 12,
|
|
855
|
+
fontWeight: 400,
|
|
856
|
+
opacity: isPrimary ? 0.9 : 0.7
|
|
857
|
+
},
|
|
858
|
+
children: c.description
|
|
859
|
+
}
|
|
860
|
+
)
|
|
861
|
+
]
|
|
862
|
+
},
|
|
863
|
+
(_a = c.value) != null ? _a : i
|
|
864
|
+
);
|
|
865
|
+
}),
|
|
866
|
+
/* @__PURE__ */ jsx2(
|
|
867
|
+
"button",
|
|
868
|
+
{
|
|
869
|
+
type: "button",
|
|
870
|
+
onClick: onCancel,
|
|
871
|
+
style: {
|
|
872
|
+
marginTop: 4,
|
|
873
|
+
padding: "8px 14px",
|
|
874
|
+
borderRadius: RADIUS.lg,
|
|
875
|
+
border: "1px solid transparent",
|
|
876
|
+
background: "transparent",
|
|
877
|
+
color: PALETTE.textMuted,
|
|
878
|
+
fontFamily: SPECTRUM_FONT,
|
|
879
|
+
fontSize: 13,
|
|
880
|
+
fontWeight: 600,
|
|
881
|
+
lineHeight: 1.3,
|
|
882
|
+
cursor: "pointer"
|
|
883
|
+
},
|
|
884
|
+
onMouseOver: (e) => {
|
|
885
|
+
e.currentTarget.style.background = PALETTE.surfaceMuted;
|
|
886
|
+
},
|
|
887
|
+
onMouseOut: (e) => {
|
|
888
|
+
e.currentTarget.style.background = "transparent";
|
|
889
|
+
},
|
|
890
|
+
children: options.cancelLabel || "Cancel"
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
]
|
|
894
|
+
}
|
|
895
|
+
) : /* @__PURE__ */ jsxs2(
|
|
896
|
+
"div",
|
|
897
|
+
{
|
|
898
|
+
style: {
|
|
899
|
+
padding: "12px 16px",
|
|
900
|
+
background: PALETTE.surfacePanel,
|
|
901
|
+
borderTop: `1px solid ${PALETTE.border}`,
|
|
902
|
+
display: "flex",
|
|
903
|
+
justifyContent: "flex-end",
|
|
904
|
+
gap: 10
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ jsx2(
|
|
908
|
+
"button",
|
|
909
|
+
{
|
|
910
|
+
type: "button",
|
|
911
|
+
onClick: onCancel,
|
|
912
|
+
style: {
|
|
913
|
+
padding: "8px 16px",
|
|
914
|
+
minHeight: 36,
|
|
915
|
+
borderRadius: RADIUS.md,
|
|
916
|
+
border: `1px solid ${PALETTE.borderStrong}`,
|
|
917
|
+
background: PALETTE.surface,
|
|
918
|
+
color: PALETTE.textStrong,
|
|
919
|
+
fontFamily: SPECTRUM_FONT,
|
|
920
|
+
fontSize: 14,
|
|
921
|
+
fontWeight: 600,
|
|
922
|
+
lineHeight: 1.3,
|
|
923
|
+
cursor: "pointer",
|
|
924
|
+
transition: "background 120ms ease"
|
|
925
|
+
},
|
|
926
|
+
onMouseOver: (e) => {
|
|
927
|
+
e.currentTarget.style.background = PALETTE.surfaceMuted;
|
|
928
|
+
},
|
|
929
|
+
onMouseOut: (e) => {
|
|
930
|
+
e.currentTarget.style.background = PALETTE.surface;
|
|
931
|
+
},
|
|
932
|
+
children: options.cancelLabel || "Cancel"
|
|
933
|
+
}
|
|
934
|
+
),
|
|
935
|
+
/* @__PURE__ */ jsx2(
|
|
936
|
+
"button",
|
|
937
|
+
{
|
|
938
|
+
type: "button",
|
|
939
|
+
ref: confirmRef,
|
|
940
|
+
onClick: onConfirm,
|
|
941
|
+
style: {
|
|
942
|
+
padding: "8px 16px",
|
|
943
|
+
minHeight: 36,
|
|
944
|
+
borderRadius: RADIUS.md,
|
|
945
|
+
border: `1px solid ${styles.primaryBg}`,
|
|
946
|
+
background: styles.primaryBg,
|
|
947
|
+
color: PALETTE.textInverse,
|
|
948
|
+
fontFamily: SPECTRUM_FONT,
|
|
949
|
+
fontSize: 14,
|
|
950
|
+
fontWeight: 600,
|
|
951
|
+
lineHeight: 1.3,
|
|
952
|
+
cursor: "pointer",
|
|
953
|
+
transition: "background 120ms ease"
|
|
954
|
+
},
|
|
955
|
+
onMouseOver: (e) => {
|
|
956
|
+
e.currentTarget.style.background = styles.primaryBgHover;
|
|
957
|
+
e.currentTarget.style.borderColor = styles.primaryBgHover;
|
|
958
|
+
},
|
|
959
|
+
onMouseOut: (e) => {
|
|
960
|
+
e.currentTarget.style.background = styles.primaryBg;
|
|
961
|
+
e.currentTarget.style.borderColor = styles.primaryBg;
|
|
962
|
+
},
|
|
963
|
+
children: options.confirmLabel || "Confirm"
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
)
|
|
969
|
+
]
|
|
970
|
+
}
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// web/src/components/SystemConfigSchemaEditor.js
|
|
977
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useState as useState4 } from "react";
|
|
978
|
+
import {
|
|
979
|
+
View,
|
|
980
|
+
Flex,
|
|
981
|
+
Heading,
|
|
982
|
+
Text,
|
|
983
|
+
Button,
|
|
984
|
+
ActionButton,
|
|
985
|
+
TextField,
|
|
986
|
+
Picker,
|
|
987
|
+
Item,
|
|
988
|
+
Switch,
|
|
989
|
+
Checkbox,
|
|
990
|
+
Divider,
|
|
991
|
+
Well,
|
|
992
|
+
ProgressCircle
|
|
993
|
+
} from "@adobe/react-spectrum";
|
|
994
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
995
|
+
var ID_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
996
|
+
function blankField() {
|
|
997
|
+
return {
|
|
998
|
+
id: "",
|
|
999
|
+
label: "",
|
|
1000
|
+
type: "text",
|
|
1001
|
+
default: "",
|
|
1002
|
+
showIn: ["default"],
|
|
1003
|
+
sensitive: false,
|
|
1004
|
+
options: []
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function blankGroup() {
|
|
1008
|
+
return { id: "", label: "", fields: [] };
|
|
1009
|
+
}
|
|
1010
|
+
function blankSection() {
|
|
1011
|
+
return { id: "", label: "", groups: [] };
|
|
1012
|
+
}
|
|
1013
|
+
function cloneSchema(schema) {
|
|
1014
|
+
return JSON.parse(JSON.stringify(schema || emptySchema()));
|
|
1015
|
+
}
|
|
1016
|
+
function validateLocal(schema) {
|
|
1017
|
+
var _a, _b, _c;
|
|
1018
|
+
if (!Array.isArray(schema.sections)) return "sections must be an array";
|
|
1019
|
+
const seenSection = /* @__PURE__ */ new Set();
|
|
1020
|
+
for (const s of schema.sections) {
|
|
1021
|
+
if (!ID_RE.test(s.id || "")) return `Section id "${s.id}" is invalid (start with letter, [a-zA-Z0-9_])`;
|
|
1022
|
+
if (seenSection.has(s.id)) return `Duplicate section id "${s.id}"`;
|
|
1023
|
+
seenSection.add(s.id);
|
|
1024
|
+
if (!((_a = s.label) == null ? void 0 : _a.trim())) return `Section ${s.id}: label required`;
|
|
1025
|
+
const seenGroup = /* @__PURE__ */ new Set();
|
|
1026
|
+
for (const g of s.groups || []) {
|
|
1027
|
+
if (!ID_RE.test(g.id || "")) return `${s.id}: group id "${g.id}" is invalid`;
|
|
1028
|
+
if (seenGroup.has(g.id)) return `${s.id}: duplicate group id "${g.id}"`;
|
|
1029
|
+
seenGroup.add(g.id);
|
|
1030
|
+
if (!((_b = g.label) == null ? void 0 : _b.trim())) return `${s.id}.${g.id}: label required`;
|
|
1031
|
+
const seenField = /* @__PURE__ */ new Set();
|
|
1032
|
+
for (const f of g.fields || []) {
|
|
1033
|
+
if (!ID_RE.test(f.id || "")) return `${s.id}.${g.id}: field id "${f.id}" is invalid`;
|
|
1034
|
+
if (seenField.has(f.id)) return `${s.id}.${g.id}: duplicate field id "${f.id}"`;
|
|
1035
|
+
seenField.add(f.id);
|
|
1036
|
+
if (!((_c = f.label) == null ? void 0 : _c.trim())) return `${s.id}.${g.id}.${f.id}: label required`;
|
|
1037
|
+
if (!FIELD_TYPES.includes(f.type)) return `${s.id}.${g.id}.${f.id}: unknown type "${f.type}"`;
|
|
1038
|
+
if (!Array.isArray(f.showIn) || f.showIn.length === 0) {
|
|
1039
|
+
return `${s.id}.${g.id}.${f.id}: pick at least one scope in showIn`;
|
|
1040
|
+
}
|
|
1041
|
+
if (f.type === "select" && (!Array.isArray(f.options) || f.options.length === 0)) {
|
|
1042
|
+
return `${s.id}.${g.id}.${f.id}: select fields need at least one option`;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
function FieldEditor({ field, onChange, onRemove }) {
|
|
1050
|
+
const update = (patch) => onChange({ ...field, ...patch });
|
|
1051
|
+
const addOption = () => {
|
|
1052
|
+
update({ options: [...field.options || [], { value: "", label: "" }] });
|
|
1053
|
+
};
|
|
1054
|
+
const updateOption = (i, patch) => {
|
|
1055
|
+
const next = [...field.options || []];
|
|
1056
|
+
next[i] = { ...next[i], ...patch };
|
|
1057
|
+
update({ options: next });
|
|
1058
|
+
};
|
|
1059
|
+
const removeOption = (i) => {
|
|
1060
|
+
const next = [...field.options || []];
|
|
1061
|
+
next.splice(i, 1);
|
|
1062
|
+
update({ options: next });
|
|
1063
|
+
};
|
|
1064
|
+
return /* @__PURE__ */ jsxs3("div", { style: {
|
|
1065
|
+
background: PALETTE.surfaceSubtle,
|
|
1066
|
+
border: `1px solid ${PALETTE.border}`,
|
|
1067
|
+
borderRadius: RADIUS.md,
|
|
1068
|
+
padding: 14,
|
|
1069
|
+
marginBottom: 10
|
|
1070
|
+
}, children: [
|
|
1071
|
+
/* @__PURE__ */ jsxs3(Flex, { gap: "size-150", wrap: true, alignItems: "end", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Field ID", value: field.id, onChange: (v) => update({ id: v }), width: "size-2400" }),
|
|
1073
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Label", value: field.label, onChange: (v) => update({ label: v }), width: "size-3000" }),
|
|
1074
|
+
/* @__PURE__ */ jsx3(Picker, { label: "Type", selectedKey: field.type, onSelectionChange: (k) => update({ type: k }), width: "size-2000", children: FIELD_TYPES.map((t) => /* @__PURE__ */ jsx3(Item, { children: t }, t)) }),
|
|
1075
|
+
/* @__PURE__ */ jsx3(
|
|
1076
|
+
TextField,
|
|
1077
|
+
{
|
|
1078
|
+
label: "Default",
|
|
1079
|
+
value: field.default == null ? "" : String(field.default),
|
|
1080
|
+
onChange: (v) => update({ default: v }),
|
|
1081
|
+
width: "size-2400"
|
|
1082
|
+
}
|
|
1083
|
+
),
|
|
1084
|
+
/* @__PURE__ */ jsx3(ActionButton, { onPress: onRemove, children: "Remove field" })
|
|
1085
|
+
] }),
|
|
1086
|
+
/* @__PURE__ */ jsxs3(Flex, { gap: "size-200", marginTop: "size-150", wrap: true, alignItems: "center", children: [
|
|
1087
|
+
/* @__PURE__ */ jsx3(Text, { children: "Visible in:" }),
|
|
1088
|
+
SCOPES.map((scope) => /* @__PURE__ */ jsx3(
|
|
1089
|
+
Checkbox,
|
|
1090
|
+
{
|
|
1091
|
+
isSelected: (field.showIn || []).includes(scope),
|
|
1092
|
+
onChange: (checked) => {
|
|
1093
|
+
const set = new Set(field.showIn || []);
|
|
1094
|
+
if (checked) set.add(scope);
|
|
1095
|
+
else set.delete(scope);
|
|
1096
|
+
update({ showIn: Array.from(set) });
|
|
1097
|
+
},
|
|
1098
|
+
children: scope
|
|
1099
|
+
},
|
|
1100
|
+
scope
|
|
1101
|
+
)),
|
|
1102
|
+
/* @__PURE__ */ jsx3(Switch, { isSelected: !!field.sensitive, onChange: (v) => update({ sensitive: v }), children: "Sensitive (encrypt at rest)" })
|
|
1103
|
+
] }),
|
|
1104
|
+
field.type === "select" && /* @__PURE__ */ jsxs3(View, { marginTop: "size-150", children: [
|
|
1105
|
+
/* @__PURE__ */ jsx3(Text, { children: "Options" }),
|
|
1106
|
+
(field.options || []).map((opt, i) => /* @__PURE__ */ jsxs3(Flex, { gap: "size-100", marginTop: "size-75", alignItems: "end", children: [
|
|
1107
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Value", value: opt.value, onChange: (v) => updateOption(i, { value: v }), width: "size-2400" }),
|
|
1108
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Label", value: opt.label, onChange: (v) => updateOption(i, { label: v }), width: "size-3000" }),
|
|
1109
|
+
/* @__PURE__ */ jsx3(ActionButton, { onPress: () => removeOption(i), children: "Remove" })
|
|
1110
|
+
] }, i)),
|
|
1111
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", marginTop: "size-100", onPress: addOption, children: "+ Add option" })
|
|
1112
|
+
] })
|
|
1113
|
+
] });
|
|
1114
|
+
}
|
|
1115
|
+
function GroupEditor({ group, onChange, onRemove }) {
|
|
1116
|
+
const update = (patch) => onChange({ ...group, ...patch });
|
|
1117
|
+
const addField = () => update({ fields: [...group.fields || [], blankField()] });
|
|
1118
|
+
const updateField = (i, next) => {
|
|
1119
|
+
const fields = [...group.fields || []];
|
|
1120
|
+
fields[i] = next;
|
|
1121
|
+
update({ fields });
|
|
1122
|
+
};
|
|
1123
|
+
const removeField = (i) => {
|
|
1124
|
+
const fields = [...group.fields || []];
|
|
1125
|
+
fields.splice(i, 1);
|
|
1126
|
+
update({ fields });
|
|
1127
|
+
};
|
|
1128
|
+
return /* @__PURE__ */ jsxs3("div", { style: {
|
|
1129
|
+
background: PALETTE.surface,
|
|
1130
|
+
border: `1px solid ${PALETTE.border}`,
|
|
1131
|
+
borderRadius: RADIUS.lg,
|
|
1132
|
+
boxShadow: SHADOW.xs,
|
|
1133
|
+
padding: 20,
|
|
1134
|
+
marginBottom: 16
|
|
1135
|
+
}, children: [
|
|
1136
|
+
/* @__PURE__ */ jsxs3(Flex, { gap: "size-200", alignItems: "end", marginBottom: "size-150", wrap: true, children: [
|
|
1137
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Group ID", value: group.id, onChange: (v) => update({ id: v }), width: "size-2400" }),
|
|
1138
|
+
/* @__PURE__ */ jsx3(TextField, { label: "Group Label", value: group.label, onChange: (v) => update({ label: v }), width: "size-3600" }),
|
|
1139
|
+
/* @__PURE__ */ jsx3(ActionButton, { onPress: onRemove, children: "Remove group" })
|
|
1140
|
+
] }),
|
|
1141
|
+
/* @__PURE__ */ jsx3(Divider, { size: "S", marginBottom: "size-150" }),
|
|
1142
|
+
(group.fields || []).map((f, i) => /* @__PURE__ */ jsx3(
|
|
1143
|
+
FieldEditor,
|
|
1144
|
+
{
|
|
1145
|
+
field: f,
|
|
1146
|
+
onChange: (next) => updateField(i, next),
|
|
1147
|
+
onRemove: () => removeField(i)
|
|
1148
|
+
},
|
|
1149
|
+
i
|
|
1150
|
+
)),
|
|
1151
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", onPress: addField, children: "+ Add field" })
|
|
1152
|
+
] });
|
|
1153
|
+
}
|
|
1154
|
+
function SystemConfigSchemaEditor({ schema, onSave, onCancel, saving, error, palette }) {
|
|
1155
|
+
const [draft, setDraft] = useState4(() => cloneSchema(schema));
|
|
1156
|
+
const [activeSectionIdx, setActiveSectionIdx] = useState4(0);
|
|
1157
|
+
const [localError, setLocalError] = useState4(null);
|
|
1158
|
+
const { confirm, dialog: confirmDialog } = useConfirm();
|
|
1159
|
+
useEffect4(() => {
|
|
1160
|
+
setDraft(cloneSchema(schema));
|
|
1161
|
+
}, [schema]);
|
|
1162
|
+
const activeSection = draft.sections[activeSectionIdx];
|
|
1163
|
+
const updateSection = (idx, patch) => {
|
|
1164
|
+
setDraft((prev) => {
|
|
1165
|
+
const next = cloneSchema(prev);
|
|
1166
|
+
next.sections[idx] = { ...next.sections[idx], ...patch };
|
|
1167
|
+
return next;
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
const addSection = () => {
|
|
1171
|
+
setDraft((prev) => {
|
|
1172
|
+
const next = cloneSchema(prev);
|
|
1173
|
+
next.sections.push(blankSection());
|
|
1174
|
+
return next;
|
|
1175
|
+
});
|
|
1176
|
+
setActiveSectionIdx(draft.sections.length);
|
|
1177
|
+
};
|
|
1178
|
+
const removeSection = async (idx) => {
|
|
1179
|
+
var _a, _b;
|
|
1180
|
+
const label = ((_a = draft.sections[idx]) == null ? void 0 : _a.label) || ((_b = draft.sections[idx]) == null ? void 0 : _b.id) || `section ${idx + 1}`;
|
|
1181
|
+
const ok = await confirm({
|
|
1182
|
+
title: "Remove section?",
|
|
1183
|
+
body: `"${label}" and all of its groups/fields will be removed from the schema. Values already stored under those field paths will remain in the database.`,
|
|
1184
|
+
confirmLabel: "Remove",
|
|
1185
|
+
variant: "destructive"
|
|
1186
|
+
});
|
|
1187
|
+
if (!ok) return;
|
|
1188
|
+
setDraft((prev) => {
|
|
1189
|
+
const next = cloneSchema(prev);
|
|
1190
|
+
next.sections.splice(idx, 1);
|
|
1191
|
+
return next;
|
|
1192
|
+
});
|
|
1193
|
+
setActiveSectionIdx(0);
|
|
1194
|
+
};
|
|
1195
|
+
const addGroup = () => {
|
|
1196
|
+
updateSection(activeSectionIdx, { groups: [...activeSection.groups || [], blankGroup()] });
|
|
1197
|
+
};
|
|
1198
|
+
const updateGroup = (gi, next) => {
|
|
1199
|
+
const groups = [...activeSection.groups || []];
|
|
1200
|
+
groups[gi] = next;
|
|
1201
|
+
updateSection(activeSectionIdx, { groups });
|
|
1202
|
+
};
|
|
1203
|
+
const removeGroup = (gi) => {
|
|
1204
|
+
const groups = [...activeSection.groups || []];
|
|
1205
|
+
groups.splice(gi, 1);
|
|
1206
|
+
updateSection(activeSectionIdx, { groups });
|
|
1207
|
+
};
|
|
1208
|
+
const handleSave = async () => {
|
|
1209
|
+
const localMsg = validateLocal(draft);
|
|
1210
|
+
if (localMsg) {
|
|
1211
|
+
setLocalError(localMsg);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
setLocalError(null);
|
|
1215
|
+
await onSave(draft);
|
|
1216
|
+
};
|
|
1217
|
+
const combinedError = localError || error;
|
|
1218
|
+
const displayedSections = useMemo2(() => draft.sections, [draft.sections]);
|
|
1219
|
+
const P = palette || PALETTE;
|
|
1220
|
+
const card = {
|
|
1221
|
+
background: P.surface,
|
|
1222
|
+
border: `1px solid ${P.border}`,
|
|
1223
|
+
borderRadius: RADIUS.lg,
|
|
1224
|
+
boxShadow: SHADOW.xs
|
|
1225
|
+
};
|
|
1226
|
+
return /* @__PURE__ */ jsxs3(View, { children: [
|
|
1227
|
+
confirmDialog,
|
|
1228
|
+
combinedError && /* @__PURE__ */ jsx3(Well, { marginBottom: "size-200", UNSAFE_style: { borderColor: P.danger }, children: /* @__PURE__ */ jsx3(Text, { UNSAFE_style: { color: P.danger }, children: combinedError }) }),
|
|
1229
|
+
/* @__PURE__ */ jsx3(
|
|
1230
|
+
"div",
|
|
1231
|
+
{
|
|
1232
|
+
style: {
|
|
1233
|
+
position: "sticky",
|
|
1234
|
+
top: "calc(64px + var(--sc-hero-h, 160px))",
|
|
1235
|
+
marginBottom: 16,
|
|
1236
|
+
padding: "12px 20px",
|
|
1237
|
+
background: P.surface,
|
|
1238
|
+
border: `1px solid ${P.border}`,
|
|
1239
|
+
borderRadius: RADIUS.xl,
|
|
1240
|
+
boxShadow: SHADOW.floating,
|
|
1241
|
+
zIndex: 10
|
|
1242
|
+
},
|
|
1243
|
+
children: /* @__PURE__ */ jsxs3(Flex, { gap: "size-100", justifyContent: "space-between", alignItems: "center", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxs3("div", { style: { fontSize: 12, color: P.textMuted }, children: [
|
|
1245
|
+
displayedSections.length,
|
|
1246
|
+
" section",
|
|
1247
|
+
displayedSections.length === 1 ? "" : "s",
|
|
1248
|
+
" \xB7",
|
|
1249
|
+
" ",
|
|
1250
|
+
displayedSections.reduce((n, s) => n + (s.groups || []).length, 0),
|
|
1251
|
+
" groups \xB7",
|
|
1252
|
+
" ",
|
|
1253
|
+
displayedSections.reduce((n, s) => n + (s.groups || []).reduce((m, g) => m + (g.fields || []).length, 0), 0),
|
|
1254
|
+
" fields"
|
|
1255
|
+
] }),
|
|
1256
|
+
/* @__PURE__ */ jsxs3(Flex, { gap: "size-100", children: [
|
|
1257
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", onPress: onCancel, isDisabled: saving, children: "Cancel" }),
|
|
1258
|
+
/* @__PURE__ */ jsx3(Button, { variant: "cta", onPress: handleSave, isDisabled: saving, children: saving ? "Saving\u2026" : "Save schema" })
|
|
1259
|
+
] })
|
|
1260
|
+
] })
|
|
1261
|
+
}
|
|
1262
|
+
),
|
|
1263
|
+
/* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 24, alignItems: "flex-start" }, children: [
|
|
1264
|
+
/* @__PURE__ */ jsxs3(
|
|
1265
|
+
"aside",
|
|
1266
|
+
{
|
|
1267
|
+
role: "tablist",
|
|
1268
|
+
"aria-label": "Sections",
|
|
1269
|
+
style: {
|
|
1270
|
+
width: 260,
|
|
1271
|
+
flexShrink: 0,
|
|
1272
|
+
background: P.surfaceMuted,
|
|
1273
|
+
border: `1px solid ${P.border}`,
|
|
1274
|
+
borderRadius: RADIUS.xxl,
|
|
1275
|
+
boxShadow: SHADOW.inset,
|
|
1276
|
+
padding: 6,
|
|
1277
|
+
position: "sticky",
|
|
1278
|
+
// Sit below AppSectionNav (64) + hero card (measured) + save bar (64) + gap
|
|
1279
|
+
top: "calc(64px + var(--sc-hero-h, 160px) + 80px)",
|
|
1280
|
+
alignSelf: "flex-start",
|
|
1281
|
+
maxHeight: "calc(100vh - 64px - var(--sc-hero-h, 160px) - 96px)",
|
|
1282
|
+
overflowY: "auto",
|
|
1283
|
+
display: "flex",
|
|
1284
|
+
flexDirection: "column",
|
|
1285
|
+
gap: 4
|
|
1286
|
+
},
|
|
1287
|
+
children: [
|
|
1288
|
+
/* @__PURE__ */ jsx3("div", { style: {
|
|
1289
|
+
fontSize: 10,
|
|
1290
|
+
fontWeight: 700,
|
|
1291
|
+
letterSpacing: 0.8,
|
|
1292
|
+
textTransform: "uppercase",
|
|
1293
|
+
color: P.textMuted,
|
|
1294
|
+
padding: "6px 14px 4px"
|
|
1295
|
+
}, children: "Sections" }),
|
|
1296
|
+
displayedSections.map((s, idx) => {
|
|
1297
|
+
const active = idx === activeSectionIdx;
|
|
1298
|
+
return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
|
|
1299
|
+
/* @__PURE__ */ jsx3(
|
|
1300
|
+
"button",
|
|
1301
|
+
{
|
|
1302
|
+
type: "button",
|
|
1303
|
+
role: "tab",
|
|
1304
|
+
"aria-selected": active,
|
|
1305
|
+
onClick: () => setActiveSectionIdx(idx),
|
|
1306
|
+
style: {
|
|
1307
|
+
flex: 1,
|
|
1308
|
+
display: "flex",
|
|
1309
|
+
alignItems: "center",
|
|
1310
|
+
padding: "10px 14px",
|
|
1311
|
+
border: 0,
|
|
1312
|
+
borderRadius: RADIUS.pill,
|
|
1313
|
+
background: active ? P.surface : "transparent",
|
|
1314
|
+
cursor: active ? "default" : "pointer",
|
|
1315
|
+
font: "inherit",
|
|
1316
|
+
color: active ? P.accent : PALETTE.neutralText,
|
|
1317
|
+
fontWeight: active ? 700 : 600,
|
|
1318
|
+
textAlign: "left",
|
|
1319
|
+
fontSize: 13,
|
|
1320
|
+
overflow: "hidden",
|
|
1321
|
+
textOverflow: "ellipsis",
|
|
1322
|
+
whiteSpace: "nowrap",
|
|
1323
|
+
boxShadow: active ? SHADOW.pill : "none",
|
|
1324
|
+
transition: "background 140ms ease, color 140ms ease, box-shadow 140ms ease"
|
|
1325
|
+
},
|
|
1326
|
+
onMouseOver: (e) => {
|
|
1327
|
+
if (!active) {
|
|
1328
|
+
e.currentTarget.style.background = PALETTE.surface;
|
|
1329
|
+
e.currentTarget.style.color = PALETTE.text;
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
onMouseOut: (e) => {
|
|
1333
|
+
if (!active) {
|
|
1334
|
+
e.currentTarget.style.background = "transparent";
|
|
1335
|
+
e.currentTarget.style.color = PALETTE.neutralText;
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
children: s.label || s.id || `(section ${idx + 1})`
|
|
1339
|
+
}
|
|
1340
|
+
),
|
|
1341
|
+
/* @__PURE__ */ jsx3(ActionButton, { isQuiet: true, onPress: () => removeSection(idx), "aria-label": "Remove", children: "\u2715" })
|
|
1342
|
+
] }, idx);
|
|
1343
|
+
}),
|
|
1344
|
+
/* @__PURE__ */ jsx3("div", { style: { padding: "6px 6px 4px" }, children: /* @__PURE__ */ jsx3(Button, { variant: "secondary", onPress: addSection, UNSAFE_style: { width: "100%", borderRadius: RADIUS.pill }, children: "+ Add section" }) })
|
|
1345
|
+
]
|
|
1346
|
+
}
|
|
1347
|
+
),
|
|
1348
|
+
/* @__PURE__ */ jsx3("div", { style: { flex: 1, minWidth: 0 }, children: !activeSection ? /* @__PURE__ */ jsxs3("div", { style: { ...card, padding: 40, textAlign: "center" }, children: [
|
|
1349
|
+
/* @__PURE__ */ jsx3(Heading, { level: 3, marginTop: 0, children: "No section selected" }),
|
|
1350
|
+
/* @__PURE__ */ jsx3(Text, { UNSAFE_style: { color: P.textMuted }, children: "Add a section on the left to begin building your configuration schema." })
|
|
1351
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1352
|
+
/* @__PURE__ */ jsxs3("div", { style: { ...card, padding: 20, marginBottom: 16 }, children: [
|
|
1353
|
+
/* @__PURE__ */ jsx3("div", { style: {
|
|
1354
|
+
fontSize: 11,
|
|
1355
|
+
fontWeight: 700,
|
|
1356
|
+
letterSpacing: 0.8,
|
|
1357
|
+
textTransform: "uppercase",
|
|
1358
|
+
color: P.textMuted,
|
|
1359
|
+
marginBottom: 12
|
|
1360
|
+
}, children: "Section properties" }),
|
|
1361
|
+
/* @__PURE__ */ jsxs3(Flex, { gap: "size-200", alignItems: "end", wrap: true, children: [
|
|
1362
|
+
/* @__PURE__ */ jsx3(
|
|
1363
|
+
TextField,
|
|
1364
|
+
{
|
|
1365
|
+
label: "Section ID",
|
|
1366
|
+
value: activeSection.id,
|
|
1367
|
+
onChange: (v) => updateSection(activeSectionIdx, { id: v }),
|
|
1368
|
+
width: "size-2400"
|
|
1369
|
+
}
|
|
1370
|
+
),
|
|
1371
|
+
/* @__PURE__ */ jsx3(
|
|
1372
|
+
TextField,
|
|
1373
|
+
{
|
|
1374
|
+
label: "Section Label",
|
|
1375
|
+
value: activeSection.label,
|
|
1376
|
+
onChange: (v) => updateSection(activeSectionIdx, { label: v }),
|
|
1377
|
+
width: "size-3600"
|
|
1378
|
+
}
|
|
1379
|
+
)
|
|
1380
|
+
] })
|
|
1381
|
+
] }),
|
|
1382
|
+
(activeSection.groups || []).map((g, gi) => /* @__PURE__ */ jsx3(
|
|
1383
|
+
GroupEditor,
|
|
1384
|
+
{
|
|
1385
|
+
group: g,
|
|
1386
|
+
onChange: (next) => updateGroup(gi, next),
|
|
1387
|
+
onRemove: () => removeGroup(gi)
|
|
1388
|
+
},
|
|
1389
|
+
gi
|
|
1390
|
+
)),
|
|
1391
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", onPress: addGroup, children: "+ Add group" })
|
|
1392
|
+
] }) })
|
|
1393
|
+
] })
|
|
1394
|
+
] });
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// web/src/components/SystemConfig.js
|
|
1398
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1399
|
+
var APP_NAV_OFFSET = 64;
|
|
1400
|
+
var HERO_HEIGHT = 160;
|
|
1401
|
+
var SAVE_BAR_HEIGHT = 64;
|
|
1402
|
+
var HERO_VAR = `var(--sc-hero-h, ${HERO_HEIGHT}px)`;
|
|
1403
|
+
function buildScopeTreeForPicker(scopeTree) {
|
|
1404
|
+
const def = { key: "default::0", label: "Default Config", scope: "default", scopeId: "0" };
|
|
1405
|
+
const websites = [];
|
|
1406
|
+
const all = [def];
|
|
1407
|
+
const groupsById = new Map((scopeTree.storeGroups || []).map((g) => [String(g.id), g]));
|
|
1408
|
+
for (const w of scopeTree.websites) {
|
|
1409
|
+
const websiteOption = {
|
|
1410
|
+
key: `websites::${w.id}`,
|
|
1411
|
+
label: w.name || w.code || `Website ${w.id}`,
|
|
1412
|
+
scope: "websites",
|
|
1413
|
+
scopeId: String(w.id)
|
|
1414
|
+
};
|
|
1415
|
+
all.push(websiteOption);
|
|
1416
|
+
const storesForWebsite = (scopeTree.stores || []).filter(
|
|
1417
|
+
(s) => String(s.website_id) === String(w.id)
|
|
1418
|
+
);
|
|
1419
|
+
storesForWebsite.sort((a, b) => {
|
|
1420
|
+
var _a, _b;
|
|
1421
|
+
const ga = ((_a = groupsById.get(String(a.store_group_id))) == null ? void 0 : _a.name) || "";
|
|
1422
|
+
const gb = ((_b = groupsById.get(String(b.store_group_id))) == null ? void 0 : _b.name) || "";
|
|
1423
|
+
if (ga !== gb) return ga.localeCompare(gb);
|
|
1424
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
1425
|
+
});
|
|
1426
|
+
const items = storesForWebsite.map((s) => {
|
|
1427
|
+
var _a;
|
|
1428
|
+
const groupName = ((_a = groupsById.get(String(s.store_group_id))) == null ? void 0 : _a.name) || "";
|
|
1429
|
+
const label = groupName ? `${groupName} / ${s.name}` : s.name;
|
|
1430
|
+
const option = { key: `stores::${s.id}`, label, scope: "stores", scopeId: String(s.id) };
|
|
1431
|
+
all.push(option);
|
|
1432
|
+
return option;
|
|
1433
|
+
});
|
|
1434
|
+
websites.push({
|
|
1435
|
+
websiteId: String(w.id),
|
|
1436
|
+
websiteName: websiteOption.label,
|
|
1437
|
+
websiteOption,
|
|
1438
|
+
items
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
return { all, default: def, websites };
|
|
1442
|
+
}
|
|
1443
|
+
function Pill({ children, tone = "neutral" }) {
|
|
1444
|
+
const tones = {
|
|
1445
|
+
neutral: { bg: PALETTE.neutralSoft, fg: PALETTE.neutralText },
|
|
1446
|
+
accent: { bg: PALETTE.accentSoft, fg: PALETTE.accent },
|
|
1447
|
+
warning: { bg: PALETTE.warningSoft, fg: PALETTE.warning },
|
|
1448
|
+
success: { bg: PALETTE.successSoft, fg: PALETTE.success },
|
|
1449
|
+
danger: { bg: PALETTE.dangerSoft, fg: PALETTE.danger }
|
|
1450
|
+
};
|
|
1451
|
+
const t = tones[tone] || tones.neutral;
|
|
1452
|
+
return /* @__PURE__ */ jsx4(
|
|
1453
|
+
"span",
|
|
1454
|
+
{
|
|
1455
|
+
style: {
|
|
1456
|
+
display: "inline-flex",
|
|
1457
|
+
alignItems: "center",
|
|
1458
|
+
gap: 4,
|
|
1459
|
+
padding: "2px 8px",
|
|
1460
|
+
borderRadius: RADIUS.pill,
|
|
1461
|
+
background: t.bg,
|
|
1462
|
+
color: t.fg,
|
|
1463
|
+
fontSize: 11,
|
|
1464
|
+
fontWeight: 600,
|
|
1465
|
+
lineHeight: "16px",
|
|
1466
|
+
letterSpacing: 0.2,
|
|
1467
|
+
whiteSpace: "nowrap"
|
|
1468
|
+
},
|
|
1469
|
+
children
|
|
1470
|
+
}
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
function Card({ children, padded = true, style = {} }) {
|
|
1474
|
+
return /* @__PURE__ */ jsx4(
|
|
1475
|
+
"div",
|
|
1476
|
+
{
|
|
1477
|
+
style: {
|
|
1478
|
+
background: PALETTE.surface,
|
|
1479
|
+
border: `1px solid ${PALETTE.border}`,
|
|
1480
|
+
borderRadius: RADIUS.lg,
|
|
1481
|
+
boxShadow: SHADOW.xs,
|
|
1482
|
+
...padded ? { padding: 20 } : {},
|
|
1483
|
+
...style
|
|
1484
|
+
},
|
|
1485
|
+
children
|
|
1486
|
+
}
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
function FieldControl({ field, value, disabled, sensitivePlaceholder, onChange }) {
|
|
1490
|
+
const isMasked = value === sensitivePlaceholder;
|
|
1491
|
+
switch (field.type) {
|
|
1492
|
+
case "textarea":
|
|
1493
|
+
return /* @__PURE__ */ jsx4(View2, { width: "size-4600", children: /* @__PURE__ */ jsx4(
|
|
1494
|
+
TextArea,
|
|
1495
|
+
{
|
|
1496
|
+
"aria-label": field.label,
|
|
1497
|
+
value: value != null ? value : "",
|
|
1498
|
+
isDisabled: disabled,
|
|
1499
|
+
onChange,
|
|
1500
|
+
width: "100%",
|
|
1501
|
+
UNSAFE_className: "sm-textarea"
|
|
1502
|
+
}
|
|
1503
|
+
) });
|
|
1504
|
+
case "password":
|
|
1505
|
+
return /* @__PURE__ */ jsx4(
|
|
1506
|
+
TextField2,
|
|
1507
|
+
{
|
|
1508
|
+
"aria-label": field.label,
|
|
1509
|
+
type: "password",
|
|
1510
|
+
value: isMasked ? "" : value != null ? value : "",
|
|
1511
|
+
isDisabled: disabled,
|
|
1512
|
+
onChange,
|
|
1513
|
+
placeholder: isMasked ? "\u2022\u2022\u2022\u2022\u2022 (encrypted, leave blank to keep)" : "",
|
|
1514
|
+
width: "size-4600"
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
case "number":
|
|
1518
|
+
return /* @__PURE__ */ jsx4(
|
|
1519
|
+
NumberField,
|
|
1520
|
+
{
|
|
1521
|
+
"aria-label": field.label,
|
|
1522
|
+
value: typeof value === "number" ? value : Number(value) || 0,
|
|
1523
|
+
isDisabled: disabled,
|
|
1524
|
+
onChange,
|
|
1525
|
+
width: "size-3000"
|
|
1526
|
+
}
|
|
1527
|
+
);
|
|
1528
|
+
case "boolean":
|
|
1529
|
+
return /* @__PURE__ */ jsx4(Switch2, { isSelected: !!value, isDisabled: disabled, onChange, children: value ? "Yes" : "No" });
|
|
1530
|
+
case "select":
|
|
1531
|
+
return /* @__PURE__ */ jsx4(
|
|
1532
|
+
Picker2,
|
|
1533
|
+
{
|
|
1534
|
+
"aria-label": field.label,
|
|
1535
|
+
selectedKey: value != null ? value : field.default,
|
|
1536
|
+
isDisabled: disabled,
|
|
1537
|
+
onSelectionChange: onChange,
|
|
1538
|
+
width: "size-3600",
|
|
1539
|
+
children: (field.options || []).map((opt) => /* @__PURE__ */ jsx4(Item2, { children: opt.label }, opt.value))
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
case "text":
|
|
1543
|
+
default:
|
|
1544
|
+
return /* @__PURE__ */ jsx4(
|
|
1545
|
+
TextField2,
|
|
1546
|
+
{
|
|
1547
|
+
"aria-label": field.label,
|
|
1548
|
+
value: value != null ? value : "",
|
|
1549
|
+
isDisabled: disabled,
|
|
1550
|
+
onChange,
|
|
1551
|
+
width: "size-4600"
|
|
1552
|
+
}
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function FieldRow({
|
|
1557
|
+
field,
|
|
1558
|
+
path,
|
|
1559
|
+
scope,
|
|
1560
|
+
displayValue,
|
|
1561
|
+
origin,
|
|
1562
|
+
inherited,
|
|
1563
|
+
onFieldChange,
|
|
1564
|
+
onUseDefaultChange,
|
|
1565
|
+
sensitivePlaceholder
|
|
1566
|
+
}) {
|
|
1567
|
+
const allowed = isFieldVisibleAtScope(field, scope.scope);
|
|
1568
|
+
const showUseDefault = scope.scope !== "default" && allowed;
|
|
1569
|
+
const editorDisabled = !allowed || showUseDefault && inherited;
|
|
1570
|
+
const isTextarea = field.type === "textarea";
|
|
1571
|
+
const originLabel = origin ? origin.scope === "default" ? "inherited from Default Config" : `set at ${origin.scope}:${origin.scopeId}` : "unset";
|
|
1572
|
+
return /* @__PURE__ */ jsxs4(
|
|
1573
|
+
"div",
|
|
1574
|
+
{
|
|
1575
|
+
style: {
|
|
1576
|
+
display: "grid",
|
|
1577
|
+
gridTemplateColumns: "220px 1fr auto",
|
|
1578
|
+
gap: 16,
|
|
1579
|
+
alignItems: isTextarea ? "start" : "center",
|
|
1580
|
+
padding: "14px 0",
|
|
1581
|
+
borderBottom: `1px solid ${PALETTE.border}`
|
|
1582
|
+
},
|
|
1583
|
+
children: [
|
|
1584
|
+
/* @__PURE__ */ jsxs4("div", { style: { paddingTop: isTextarea ? 6 : 0 }, children: [
|
|
1585
|
+
/* @__PURE__ */ jsxs4("div", { style: {
|
|
1586
|
+
fontSize: 13,
|
|
1587
|
+
fontWeight: 600,
|
|
1588
|
+
color: PALETTE.text,
|
|
1589
|
+
display: "flex",
|
|
1590
|
+
alignItems: "center",
|
|
1591
|
+
gap: 6
|
|
1592
|
+
}, children: [
|
|
1593
|
+
field.label,
|
|
1594
|
+
field.sensitive && /* @__PURE__ */ jsxs4(TooltipTrigger, { children: [
|
|
1595
|
+
/* @__PURE__ */ jsx4("span", { style: { display: "inline-flex", color: PALETTE.textMuted }, children: /* @__PURE__ */ jsx4(LockClosed, { size: "XS" }) }),
|
|
1596
|
+
/* @__PURE__ */ jsx4(Tooltip, { children: "Encrypted at rest" })
|
|
1597
|
+
] })
|
|
1598
|
+
] }),
|
|
1599
|
+
/* @__PURE__ */ jsxs4("div", { style: { marginTop: 4, display: "flex", gap: 6, flexWrap: "wrap" }, children: [
|
|
1600
|
+
!allowed && /* @__PURE__ */ jsx4(Pill, { tone: "warning", children: "Not configurable here" }),
|
|
1601
|
+
allowed && scope.scope !== "default" && /* @__PURE__ */ jsx4(Pill, { tone: inherited ? "neutral" : "accent", children: inherited ? originLabel : "overridden" })
|
|
1602
|
+
] })
|
|
1603
|
+
] }),
|
|
1604
|
+
/* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
|
|
1605
|
+
FieldControl,
|
|
1606
|
+
{
|
|
1607
|
+
field,
|
|
1608
|
+
value: displayValue,
|
|
1609
|
+
disabled: editorDisabled,
|
|
1610
|
+
sensitivePlaceholder,
|
|
1611
|
+
onChange: (v) => onFieldChange(path, v)
|
|
1612
|
+
}
|
|
1613
|
+
) }),
|
|
1614
|
+
/* @__PURE__ */ jsx4("div", { children: showUseDefault && /* @__PURE__ */ jsx4(
|
|
1615
|
+
Checkbox2,
|
|
1616
|
+
{
|
|
1617
|
+
isSelected: inherited,
|
|
1618
|
+
onChange: (checked) => onUseDefaultChange(path, checked),
|
|
1619
|
+
children: "Use Default"
|
|
1620
|
+
}
|
|
1621
|
+
) })
|
|
1622
|
+
]
|
|
1623
|
+
}
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
function GroupCard({
|
|
1627
|
+
group,
|
|
1628
|
+
sectionId,
|
|
1629
|
+
scope,
|
|
1630
|
+
collapsed,
|
|
1631
|
+
onToggle,
|
|
1632
|
+
getDisplayValue,
|
|
1633
|
+
getOrigin,
|
|
1634
|
+
isInheritedAtScope,
|
|
1635
|
+
setFieldValue,
|
|
1636
|
+
setUseDefault,
|
|
1637
|
+
sensitivePlaceholder
|
|
1638
|
+
}) {
|
|
1639
|
+
return /* @__PURE__ */ jsxs4(Card, { padded: false, style: { marginBottom: 16 }, children: [
|
|
1640
|
+
/* @__PURE__ */ jsx4(
|
|
1641
|
+
"button",
|
|
1642
|
+
{
|
|
1643
|
+
type: "button",
|
|
1644
|
+
onClick: onToggle,
|
|
1645
|
+
"aria-expanded": !collapsed,
|
|
1646
|
+
style: {
|
|
1647
|
+
display: "flex",
|
|
1648
|
+
alignItems: "center",
|
|
1649
|
+
justifyContent: "space-between",
|
|
1650
|
+
width: "100%",
|
|
1651
|
+
padding: "14px 20px",
|
|
1652
|
+
background: "transparent",
|
|
1653
|
+
border: 0,
|
|
1654
|
+
borderBottom: collapsed ? 0 : `1px solid ${PALETTE.border}`,
|
|
1655
|
+
cursor: "pointer",
|
|
1656
|
+
userSelect: "none",
|
|
1657
|
+
font: "inherit",
|
|
1658
|
+
color: "inherit",
|
|
1659
|
+
textAlign: "left"
|
|
1660
|
+
},
|
|
1661
|
+
children: /* @__PURE__ */ jsxs4("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
|
|
1662
|
+
/* @__PURE__ */ jsx4("span", { style: { color: PALETTE.textMuted, display: "inline-flex" }, children: collapsed ? /* @__PURE__ */ jsx4(ChevronRight, { size: "S" }) : /* @__PURE__ */ jsx4(ChevronDown, { size: "S" }) }),
|
|
1663
|
+
/* @__PURE__ */ jsx4("span", { style: { fontWeight: 700, fontSize: 15, color: PALETTE.text }, children: group.label }),
|
|
1664
|
+
/* @__PURE__ */ jsxs4(Pill, { tone: "neutral", children: [
|
|
1665
|
+
(group.fields || []).length,
|
|
1666
|
+
" fields"
|
|
1667
|
+
] })
|
|
1668
|
+
] })
|
|
1669
|
+
}
|
|
1670
|
+
),
|
|
1671
|
+
!collapsed && /* @__PURE__ */ jsx4("div", { style: { padding: "4px 20px 16px" }, children: (group.fields || []).map((field) => {
|
|
1672
|
+
const path = `${sectionId}/${group.id}/${field.id}`;
|
|
1673
|
+
const inherited = isInheritedAtScope(path);
|
|
1674
|
+
const displayValue = getDisplayValue(path, coerceDefault(field));
|
|
1675
|
+
return /* @__PURE__ */ jsx4(
|
|
1676
|
+
FieldRow,
|
|
1677
|
+
{
|
|
1678
|
+
field,
|
|
1679
|
+
path,
|
|
1680
|
+
scope,
|
|
1681
|
+
displayValue,
|
|
1682
|
+
origin: getOrigin(path),
|
|
1683
|
+
inherited,
|
|
1684
|
+
onFieldChange: setFieldValue,
|
|
1685
|
+
onUseDefaultChange: setUseDefault,
|
|
1686
|
+
sensitivePlaceholder
|
|
1687
|
+
},
|
|
1688
|
+
path
|
|
1689
|
+
);
|
|
1690
|
+
}) })
|
|
1691
|
+
] });
|
|
1692
|
+
}
|
|
1693
|
+
function Sidebar({ sections, activeSectionId, onSelect }) {
|
|
1694
|
+
return /* @__PURE__ */ jsxs4(
|
|
1695
|
+
"aside",
|
|
1696
|
+
{
|
|
1697
|
+
role: "tablist",
|
|
1698
|
+
"aria-label": "Sections",
|
|
1699
|
+
style: {
|
|
1700
|
+
width: 260,
|
|
1701
|
+
flexShrink: 0,
|
|
1702
|
+
// Pill-track styling that matches the top AppSectionNav: muted grey
|
|
1703
|
+
// track with inset shadow, full-rounded radius, holding individual
|
|
1704
|
+
// rounded pill buttons.
|
|
1705
|
+
background: PALETTE.surfaceMuted,
|
|
1706
|
+
border: `1px solid ${PALETTE.border}`,
|
|
1707
|
+
borderRadius: RADIUS.xxl,
|
|
1708
|
+
boxShadow: SHADOW.inset,
|
|
1709
|
+
padding: 6,
|
|
1710
|
+
position: "sticky",
|
|
1711
|
+
// Sit below the hero + save bar (which are also sticky) so the
|
|
1712
|
+
// sidebar never overlaps either of them. Uses the runtime-measured
|
|
1713
|
+
// hero height so the offset stays correct on viewport resize.
|
|
1714
|
+
top: `calc(${APP_NAV_OFFSET}px + ${HERO_VAR} + ${SAVE_BAR_HEIGHT + 16}px)`,
|
|
1715
|
+
alignSelf: "flex-start",
|
|
1716
|
+
maxHeight: `calc(100vh - ${APP_NAV_OFFSET}px - ${HERO_VAR} - ${SAVE_BAR_HEIGHT + 32}px)`,
|
|
1717
|
+
overflowY: "auto",
|
|
1718
|
+
zIndex: 5,
|
|
1719
|
+
display: "flex",
|
|
1720
|
+
flexDirection: "column",
|
|
1721
|
+
gap: 4
|
|
1722
|
+
},
|
|
1723
|
+
children: [
|
|
1724
|
+
/* @__PURE__ */ jsx4("div", { style: {
|
|
1725
|
+
padding: "6px 14px 4px",
|
|
1726
|
+
fontSize: 10,
|
|
1727
|
+
fontWeight: 700,
|
|
1728
|
+
letterSpacing: 0.8,
|
|
1729
|
+
textTransform: "uppercase",
|
|
1730
|
+
color: PALETTE.textMuted
|
|
1731
|
+
}, children: "Sections" }),
|
|
1732
|
+
sections.map((section) => {
|
|
1733
|
+
const active = section.id === activeSectionId;
|
|
1734
|
+
const fieldCount = (section.groups || []).reduce((n, g) => n + (g.fields || []).length, 0);
|
|
1735
|
+
return /* @__PURE__ */ jsxs4(
|
|
1736
|
+
"button",
|
|
1737
|
+
{
|
|
1738
|
+
type: "button",
|
|
1739
|
+
role: "tab",
|
|
1740
|
+
"aria-selected": active,
|
|
1741
|
+
onClick: () => onSelect(section.id),
|
|
1742
|
+
style: {
|
|
1743
|
+
display: "flex",
|
|
1744
|
+
alignItems: "center",
|
|
1745
|
+
gap: 10,
|
|
1746
|
+
width: "100%",
|
|
1747
|
+
padding: "10px 14px",
|
|
1748
|
+
border: 0,
|
|
1749
|
+
borderRadius: RADIUS.pill,
|
|
1750
|
+
background: active ? PALETTE.surface : "transparent",
|
|
1751
|
+
cursor: active ? "default" : "pointer",
|
|
1752
|
+
font: "inherit",
|
|
1753
|
+
color: active ? PALETTE.accent : PALETTE.neutralText,
|
|
1754
|
+
fontWeight: active ? 700 : 600,
|
|
1755
|
+
fontSize: 13,
|
|
1756
|
+
textAlign: "left",
|
|
1757
|
+
boxShadow: active ? SHADOW.pill : "none",
|
|
1758
|
+
transition: "background 140ms ease, color 140ms ease, box-shadow 140ms ease"
|
|
1759
|
+
},
|
|
1760
|
+
onMouseOver: (e) => {
|
|
1761
|
+
if (!active) {
|
|
1762
|
+
e.currentTarget.style.background = PALETTE.surface;
|
|
1763
|
+
e.currentTarget.style.color = PALETTE.text;
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
onMouseOut: (e) => {
|
|
1767
|
+
if (!active) {
|
|
1768
|
+
e.currentTarget.style.background = "transparent";
|
|
1769
|
+
e.currentTarget.style.color = PALETTE.neutralText;
|
|
1770
|
+
}
|
|
1771
|
+
},
|
|
1772
|
+
children: [
|
|
1773
|
+
/* @__PURE__ */ jsx4("span", { style: { display: "inline-flex", opacity: active ? 1 : 0.7 }, children: /* @__PURE__ */ jsx4(Settings2, { size: "XS" }) }),
|
|
1774
|
+
/* @__PURE__ */ jsx4("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: section.label }),
|
|
1775
|
+
/* @__PURE__ */ jsx4(Pill, { tone: active ? "accent" : "neutral", children: fieldCount })
|
|
1776
|
+
]
|
|
1777
|
+
},
|
|
1778
|
+
section.id
|
|
1779
|
+
);
|
|
1780
|
+
})
|
|
1781
|
+
]
|
|
1782
|
+
}
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
function ValuesView({ schema, onEditSchema, toolsOpen, setToolsOpen, configCtx }) {
|
|
1786
|
+
var _a, _b;
|
|
1787
|
+
const {
|
|
1788
|
+
scope,
|
|
1789
|
+
scopeTree,
|
|
1790
|
+
getDisplayValue,
|
|
1791
|
+
getOrigin,
|
|
1792
|
+
isInheritedAtScope,
|
|
1793
|
+
setFieldValue,
|
|
1794
|
+
setUseDefault,
|
|
1795
|
+
dirtyCount,
|
|
1796
|
+
loading,
|
|
1797
|
+
saving,
|
|
1798
|
+
error,
|
|
1799
|
+
savedAt,
|
|
1800
|
+
save,
|
|
1801
|
+
reset,
|
|
1802
|
+
refresh,
|
|
1803
|
+
SENSITIVE_PLACEHOLDER: SENSITIVE_PLACEHOLDER2
|
|
1804
|
+
} = configCtx;
|
|
1805
|
+
const sections = (schema == null ? void 0 : schema.sections) || [];
|
|
1806
|
+
const [activeSectionId, setActiveSectionId] = useState5((_a = sections[0]) == null ? void 0 : _a.id);
|
|
1807
|
+
const activeSection = useMemo3(() => {
|
|
1808
|
+
if (sections.length === 0) return null;
|
|
1809
|
+
return sections.find((s) => s.id === activeSectionId) || sections[0];
|
|
1810
|
+
}, [sections, activeSectionId]);
|
|
1811
|
+
const scopeTreeForPicker = useMemo3(() => buildScopeTreeForPicker(scopeTree), [scopeTree]);
|
|
1812
|
+
const scopeKey = `${scope.scope}::${scope.scopeId}`;
|
|
1813
|
+
const activeScopeLabel = ((_b = scopeTreeForPicker.all.find((o) => o.key === scopeKey)) == null ? void 0 : _b.label) || "Default Config";
|
|
1814
|
+
const [collapsedGroups, setCollapsedGroups] = useState5({});
|
|
1815
|
+
useEffect5(() => {
|
|
1816
|
+
setCollapsedGroups({});
|
|
1817
|
+
}, [activeSection == null ? void 0 : activeSection.id]);
|
|
1818
|
+
const toggleGroup = (gid) => setCollapsedGroups((prev) => ({ ...prev, [gid]: !prev[gid] }));
|
|
1819
|
+
const setAllGroups = (collapsed) => {
|
|
1820
|
+
const next = {};
|
|
1821
|
+
for (const g of (activeSection == null ? void 0 : activeSection.groups) || []) next[g.id] = collapsed;
|
|
1822
|
+
setCollapsedGroups(next);
|
|
1823
|
+
};
|
|
1824
|
+
if (sections.length === 0) {
|
|
1825
|
+
return /* @__PURE__ */ jsx4(Card, { children: /* @__PURE__ */ jsxs4("div", { style: { textAlign: "center", padding: "40px 20px" }, children: [
|
|
1826
|
+
/* @__PURE__ */ jsx4("div", { style: {
|
|
1827
|
+
display: "inline-flex",
|
|
1828
|
+
padding: 16,
|
|
1829
|
+
background: PALETTE.accentSoft,
|
|
1830
|
+
borderRadius: "50%",
|
|
1831
|
+
marginBottom: 12,
|
|
1832
|
+
color: PALETTE.accent
|
|
1833
|
+
}, children: /* @__PURE__ */ jsx4(Settings2, { size: "L" }) }),
|
|
1834
|
+
/* @__PURE__ */ jsx4(Heading2, { level: 3, marginTop: 0, children: "No configuration schema yet" }),
|
|
1835
|
+
/* @__PURE__ */ jsx4(Text2, { UNSAFE_style: { color: PALETTE.textMuted, maxWidth: 460, display: "inline-block" }, children: "Open the Schema Designer to define sections, groups, and fields for your sync integrations." }),
|
|
1836
|
+
/* @__PURE__ */ jsx4(Flex2, { justifyContent: "center", gap: "size-150", marginTop: "size-200", children: /* @__PURE__ */ jsx4(Button2, { variant: "cta", onPress: onEditSchema, children: "Open Schema Designer" }) })
|
|
1837
|
+
] }) });
|
|
1838
|
+
}
|
|
1839
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1840
|
+
error && /* @__PURE__ */ jsx4(Well2, { marginBottom: "size-200", UNSAFE_style: { borderColor: PALETTE.danger }, children: /* @__PURE__ */ jsx4(Text2, { UNSAFE_style: { color: PALETTE.danger }, children: error }) }),
|
|
1841
|
+
/* @__PURE__ */ jsx4(
|
|
1842
|
+
"div",
|
|
1843
|
+
{
|
|
1844
|
+
style: {
|
|
1845
|
+
position: "sticky",
|
|
1846
|
+
// Hero card sticks at APP_NAV_OFFSET; this save bar sits flush
|
|
1847
|
+
// against the hero's bottom edge (measured at runtime via
|
|
1848
|
+
// --sc-hero-h so the gap is always zero regardless of subtitle
|
|
1849
|
+
// wrap).
|
|
1850
|
+
top: `calc(${APP_NAV_OFFSET}px + ${HERO_VAR})`,
|
|
1851
|
+
marginBottom: 16,
|
|
1852
|
+
padding: "12px 20px",
|
|
1853
|
+
background: PALETTE.surface,
|
|
1854
|
+
border: `1px solid ${PALETTE.border}`,
|
|
1855
|
+
borderRadius: RADIUS.xl,
|
|
1856
|
+
boxShadow: SHADOW.floating,
|
|
1857
|
+
zIndex: 10
|
|
1858
|
+
},
|
|
1859
|
+
children: /* @__PURE__ */ jsxs4(Flex2, { gap: "size-150", alignItems: "center", justifyContent: "space-between", children: [
|
|
1860
|
+
/* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: PALETTE.textMuted }, children: dirtyCount > 0 ? /* @__PURE__ */ jsxs4("span", { style: { color: PALETTE.warning, fontWeight: 600 }, children: [
|
|
1861
|
+
dirtyCount,
|
|
1862
|
+
" unsaved change",
|
|
1863
|
+
dirtyCount === 1 ? "" : "s"
|
|
1864
|
+
] }) : savedAt && !saving ? /* @__PURE__ */ jsxs4("span", { style: { color: PALETTE.success, fontWeight: 600 }, children: [
|
|
1865
|
+
"\u2713 Saved ",
|
|
1866
|
+
new Date(savedAt).toLocaleTimeString()
|
|
1867
|
+
] }) : "All changes saved" }),
|
|
1868
|
+
/* @__PURE__ */ jsxs4(Flex2, { gap: "size-100", alignItems: "center", children: [
|
|
1869
|
+
/* @__PURE__ */ jsx4(Button2, { variant: "secondary", onPress: refresh, isDisabled: saving || loading, children: "Reload" }),
|
|
1870
|
+
/* @__PURE__ */ jsx4(Button2, { variant: "secondary", onPress: reset, isDisabled: saving || dirtyCount === 0, children: "Reset" }),
|
|
1871
|
+
/* @__PURE__ */ jsx4(Button2, { variant: "cta", onPress: save, isDisabled: saving || loading || dirtyCount === 0, children: saving ? "Saving\u2026" : `Save Config${dirtyCount ? ` (${dirtyCount})` : ""}` })
|
|
1872
|
+
] })
|
|
1873
|
+
] })
|
|
1874
|
+
}
|
|
1875
|
+
),
|
|
1876
|
+
/* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 24, alignItems: "flex-start" }, children: [
|
|
1877
|
+
/* @__PURE__ */ jsx4(
|
|
1878
|
+
Sidebar,
|
|
1879
|
+
{
|
|
1880
|
+
sections,
|
|
1881
|
+
activeSectionId: activeSection == null ? void 0 : activeSection.id,
|
|
1882
|
+
onSelect: setActiveSectionId
|
|
1883
|
+
}
|
|
1884
|
+
),
|
|
1885
|
+
/* @__PURE__ */ jsxs4("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
1886
|
+
/* @__PURE__ */ jsxs4("div", { style: {
|
|
1887
|
+
display: "flex",
|
|
1888
|
+
justifyContent: "space-between",
|
|
1889
|
+
alignItems: "center",
|
|
1890
|
+
marginBottom: 16
|
|
1891
|
+
}, children: [
|
|
1892
|
+
/* @__PURE__ */ jsxs4("div", { children: [
|
|
1893
|
+
/* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: PALETTE.textMuted, fontWeight: 600, marginBottom: 4 }, children: activeScopeLabel }),
|
|
1894
|
+
/* @__PURE__ */ jsx4(Heading2, { level: 2, marginTop: 0, marginBottom: 0, children: activeSection == null ? void 0 : activeSection.label })
|
|
1895
|
+
] }),
|
|
1896
|
+
((activeSection == null ? void 0 : activeSection.groups) || []).length > 1 && /* @__PURE__ */ jsxs4(Flex2, { gap: "size-50", children: [
|
|
1897
|
+
/* @__PURE__ */ jsx4(ActionButton2, { onPress: () => setAllGroups(false), isQuiet: true, children: "Expand all" }),
|
|
1898
|
+
/* @__PURE__ */ jsx4(ActionButton2, { onPress: () => setAllGroups(true), isQuiet: true, children: "Collapse all" })
|
|
1899
|
+
] })
|
|
1900
|
+
] }),
|
|
1901
|
+
loading ? /* @__PURE__ */ jsx4(Card, { children: /* @__PURE__ */ jsx4(Flex2, { justifyContent: "center", marginY: "size-400", children: /* @__PURE__ */ jsx4(ProgressCircle2, { "aria-label": "Loading values", isIndeterminate: true }) }) }) : ((activeSection == null ? void 0 : activeSection.groups) || []).map((group) => /* @__PURE__ */ jsx4(
|
|
1902
|
+
GroupCard,
|
|
1903
|
+
{
|
|
1904
|
+
group,
|
|
1905
|
+
sectionId: activeSection.id,
|
|
1906
|
+
scope,
|
|
1907
|
+
collapsed: !!collapsedGroups[group.id],
|
|
1908
|
+
onToggle: () => toggleGroup(group.id),
|
|
1909
|
+
getDisplayValue,
|
|
1910
|
+
getOrigin,
|
|
1911
|
+
isInheritedAtScope,
|
|
1912
|
+
setFieldValue,
|
|
1913
|
+
setUseDefault,
|
|
1914
|
+
sensitivePlaceholder: SENSITIVE_PLACEHOLDER2
|
|
1915
|
+
},
|
|
1916
|
+
group.id
|
|
1917
|
+
)),
|
|
1918
|
+
/* @__PURE__ */ jsx4("div", { style: { height: 80 } })
|
|
1919
|
+
] })
|
|
1920
|
+
] })
|
|
1921
|
+
] });
|
|
1922
|
+
}
|
|
1923
|
+
function ScopePicker({ scopeTreeForPicker, selectedKey, onChange, disabled }) {
|
|
1924
|
+
const [open, setOpen] = useState5(false);
|
|
1925
|
+
const wrapperRef = useRef2(null);
|
|
1926
|
+
useEffect5(() => {
|
|
1927
|
+
if (!open) return;
|
|
1928
|
+
const onDoc = (e) => {
|
|
1929
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
|
|
1930
|
+
};
|
|
1931
|
+
const onKey = (e) => {
|
|
1932
|
+
if (e.key === "Escape") setOpen(false);
|
|
1933
|
+
};
|
|
1934
|
+
document.addEventListener("mousedown", onDoc);
|
|
1935
|
+
document.addEventListener("keydown", onKey);
|
|
1936
|
+
return () => {
|
|
1937
|
+
document.removeEventListener("mousedown", onDoc);
|
|
1938
|
+
document.removeEventListener("keydown", onKey);
|
|
1939
|
+
};
|
|
1940
|
+
}, [open]);
|
|
1941
|
+
const selected = scopeTreeForPicker.all.find((o) => o.key === selectedKey);
|
|
1942
|
+
const selectedLabel = (selected == null ? void 0 : selected.label) || "Default Config";
|
|
1943
|
+
const select = (key) => {
|
|
1944
|
+
onChange(key);
|
|
1945
|
+
setOpen(false);
|
|
1946
|
+
};
|
|
1947
|
+
const renderItem = ({ key, label, indent = 0, isWebsite = false }) => {
|
|
1948
|
+
const active = key === selectedKey;
|
|
1949
|
+
return /* @__PURE__ */ jsxs4(
|
|
1950
|
+
"button",
|
|
1951
|
+
{
|
|
1952
|
+
type: "button",
|
|
1953
|
+
onClick: () => select(key),
|
|
1954
|
+
style: {
|
|
1955
|
+
display: "flex",
|
|
1956
|
+
alignItems: "center",
|
|
1957
|
+
justifyContent: "space-between",
|
|
1958
|
+
width: "100%",
|
|
1959
|
+
padding: `8px 12px 8px ${12 + indent * 18}px`,
|
|
1960
|
+
background: active ? PALETTE.accentSoft : "transparent",
|
|
1961
|
+
color: active ? PALETTE.accent : PALETTE.text,
|
|
1962
|
+
fontSize: 13,
|
|
1963
|
+
fontWeight: active ? 700 : isWebsite ? 600 : 500,
|
|
1964
|
+
border: 0,
|
|
1965
|
+
textAlign: "left",
|
|
1966
|
+
cursor: "pointer",
|
|
1967
|
+
font: "inherit"
|
|
1968
|
+
},
|
|
1969
|
+
onMouseOver: (e) => {
|
|
1970
|
+
if (!active) e.currentTarget.style.background = PALETTE.surfaceMuted;
|
|
1971
|
+
},
|
|
1972
|
+
onMouseOut: (e) => {
|
|
1973
|
+
if (!active) e.currentTarget.style.background = "transparent";
|
|
1974
|
+
},
|
|
1975
|
+
children: [
|
|
1976
|
+
/* @__PURE__ */ jsxs4("span", { style: { display: "flex", alignItems: "center", gap: 6, fontFamily: "inherit" }, children: [
|
|
1977
|
+
indent > 0 && /* @__PURE__ */ jsx4("span", { style: { color: PALETTE.textMuted }, children: "\u21B3" }),
|
|
1978
|
+
/* @__PURE__ */ jsx4("span", { children: label })
|
|
1979
|
+
] }),
|
|
1980
|
+
active && /* @__PURE__ */ jsx4("span", { style: { color: PALETTE.accent, fontSize: 14 }, children: "\u2713" })
|
|
1981
|
+
]
|
|
1982
|
+
},
|
|
1983
|
+
key
|
|
1984
|
+
);
|
|
1985
|
+
};
|
|
1986
|
+
return /* @__PURE__ */ jsxs4("div", { ref: wrapperRef, style: { position: "relative" }, children: [
|
|
1987
|
+
/* @__PURE__ */ jsxs4(
|
|
1988
|
+
"button",
|
|
1989
|
+
{
|
|
1990
|
+
type: "button",
|
|
1991
|
+
onClick: () => !disabled && setOpen((o) => !o),
|
|
1992
|
+
disabled,
|
|
1993
|
+
"aria-haspopup": "listbox",
|
|
1994
|
+
"aria-expanded": open,
|
|
1995
|
+
style: {
|
|
1996
|
+
display: "inline-flex",
|
|
1997
|
+
alignItems: "center",
|
|
1998
|
+
gap: 8,
|
|
1999
|
+
background: PALETTE.surface,
|
|
2000
|
+
border: `1px solid ${PALETTE.border}`,
|
|
2001
|
+
borderRadius: RADIUS.md,
|
|
2002
|
+
padding: "6px 10px",
|
|
2003
|
+
minWidth: 220,
|
|
2004
|
+
fontFamily: "inherit",
|
|
2005
|
+
fontSize: 13,
|
|
2006
|
+
fontWeight: 600,
|
|
2007
|
+
color: PALETTE.text,
|
|
2008
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
2009
|
+
opacity: disabled ? 0.6 : 1
|
|
2010
|
+
},
|
|
2011
|
+
children: [
|
|
2012
|
+
/* @__PURE__ */ jsx4(Globe, { size: "XS" }),
|
|
2013
|
+
/* @__PURE__ */ jsx4("span", { style: { flex: 1, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: selectedLabel }),
|
|
2014
|
+
/* @__PURE__ */ jsx4("span", { style: { color: PALETTE.textMuted, fontSize: 11 }, children: "\u25BE" })
|
|
2015
|
+
]
|
|
2016
|
+
}
|
|
2017
|
+
),
|
|
2018
|
+
open && /* @__PURE__ */ jsxs4(
|
|
2019
|
+
"div",
|
|
2020
|
+
{
|
|
2021
|
+
role: "listbox",
|
|
2022
|
+
style: {
|
|
2023
|
+
position: "absolute",
|
|
2024
|
+
top: "100%",
|
|
2025
|
+
right: 0,
|
|
2026
|
+
marginTop: 4,
|
|
2027
|
+
minWidth: 280,
|
|
2028
|
+
maxHeight: 420,
|
|
2029
|
+
overflowY: "auto",
|
|
2030
|
+
background: PALETTE.surface,
|
|
2031
|
+
border: `1px solid ${PALETTE.border}`,
|
|
2032
|
+
borderRadius: RADIUS.lg,
|
|
2033
|
+
boxShadow: SHADOW.dropdown,
|
|
2034
|
+
zIndex: 100,
|
|
2035
|
+
padding: 4
|
|
2036
|
+
},
|
|
2037
|
+
children: [
|
|
2038
|
+
renderItem({ key: scopeTreeForPicker.default.key, label: scopeTreeForPicker.default.label, indent: 0 }),
|
|
2039
|
+
scopeTreeForPicker.websites.map((w) => /* @__PURE__ */ jsxs4("div", { style: { marginTop: 6, paddingTop: 6, borderTop: `1px solid ${PALETTE.border}` }, children: [
|
|
2040
|
+
/* @__PURE__ */ jsx4("div", { style: {
|
|
2041
|
+
padding: "6px 12px 4px",
|
|
2042
|
+
fontSize: 10,
|
|
2043
|
+
fontWeight: 700,
|
|
2044
|
+
letterSpacing: 0.8,
|
|
2045
|
+
textTransform: "uppercase",
|
|
2046
|
+
color: PALETTE.textMuted
|
|
2047
|
+
}, children: "Website" }),
|
|
2048
|
+
renderItem({ key: w.websiteOption.key, label: w.websiteOption.label, indent: 0, isWebsite: true }),
|
|
2049
|
+
w.items.map((s) => renderItem({ key: s.key, label: s.label, indent: 1 }))
|
|
2050
|
+
] }, w.websiteId))
|
|
2051
|
+
]
|
|
2052
|
+
}
|
|
2053
|
+
)
|
|
2054
|
+
] });
|
|
2055
|
+
}
|
|
2056
|
+
function PageHeader({
|
|
2057
|
+
heroRef,
|
|
2058
|
+
mode,
|
|
2059
|
+
setMode,
|
|
2060
|
+
scopeTree,
|
|
2061
|
+
scopeTreeForPicker,
|
|
2062
|
+
scopeKey,
|
|
2063
|
+
onScopeChange,
|
|
2064
|
+
onReloadStores,
|
|
2065
|
+
onOpenTools,
|
|
2066
|
+
toolsOpen
|
|
2067
|
+
}) {
|
|
2068
|
+
const isSchemaMode = mode === "schema";
|
|
2069
|
+
return /* @__PURE__ */ jsxs4(
|
|
2070
|
+
"div",
|
|
2071
|
+
{
|
|
2072
|
+
ref: heroRef,
|
|
2073
|
+
style: {
|
|
2074
|
+
// Hero card. Identical chrome to DataIngestion's hero — same border,
|
|
2075
|
+
// radius, padding, shadow, font. Sticky so the title + scope picker
|
|
2076
|
+
// stay reachable while scrolling long pages of fields.
|
|
2077
|
+
position: "sticky",
|
|
2078
|
+
top: APP_NAV_OFFSET,
|
|
2079
|
+
zIndex: 20,
|
|
2080
|
+
background: PALETTE.surface,
|
|
2081
|
+
border: `1px solid ${PALETTE.border}`,
|
|
2082
|
+
borderRadius: RADIUS.xl,
|
|
2083
|
+
padding: "20px 24px",
|
|
2084
|
+
boxShadow: SHADOW.xs,
|
|
2085
|
+
display: "flex",
|
|
2086
|
+
gap: 24,
|
|
2087
|
+
alignItems: "flex-start",
|
|
2088
|
+
justifyContent: "space-between",
|
|
2089
|
+
flexWrap: "wrap",
|
|
2090
|
+
fontFamily: "adobe-clean, 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
|
|
2091
|
+
},
|
|
2092
|
+
children: [
|
|
2093
|
+
/* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 16, alignItems: "flex-start", minWidth: 0 }, children: [
|
|
2094
|
+
/* @__PURE__ */ jsx4("div", { style: {
|
|
2095
|
+
display: "inline-flex",
|
|
2096
|
+
padding: 10,
|
|
2097
|
+
background: PALETTE.accentSoft,
|
|
2098
|
+
color: PALETTE.accent,
|
|
2099
|
+
borderRadius: RADIUS.lg,
|
|
2100
|
+
flexShrink: 0
|
|
2101
|
+
}, children: /* @__PURE__ */ jsx4(Settings2, { size: "S" }) }),
|
|
2102
|
+
/* @__PURE__ */ jsxs4("div", { style: { minWidth: 0 }, children: [
|
|
2103
|
+
/* @__PURE__ */ jsx4("div", { style: {
|
|
2104
|
+
fontSize: 11,
|
|
2105
|
+
fontWeight: 700,
|
|
2106
|
+
letterSpacing: 0.6,
|
|
2107
|
+
textTransform: "uppercase",
|
|
2108
|
+
color: PALETTE.textMuted,
|
|
2109
|
+
marginBottom: 6
|
|
2110
|
+
}, children: "Configurations / App Builder" }),
|
|
2111
|
+
/* @__PURE__ */ jsx4("div", { style: { fontSize: 24, fontWeight: 700, color: PALETTE.text, lineHeight: 1.2 }, children: isSchemaMode ? "Schema Designer" : "System Configuration" }),
|
|
2112
|
+
/* @__PURE__ */ jsx4("div", { style: { fontSize: 13, color: PALETTE.textMuted, marginTop: 6, maxWidth: 540 }, children: isSchemaMode ? "Define sections, groups, and fields. Renaming an id strands existing values; removing one prompts to delete its stored values." : "Manage configuration values across Default Config, websites, and store views \u2014 stored in App Builder DB." })
|
|
2113
|
+
] })
|
|
2114
|
+
] }),
|
|
2115
|
+
/* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: mode === "values" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
2116
|
+
/* @__PURE__ */ jsx4(
|
|
2117
|
+
ScopePicker,
|
|
2118
|
+
{
|
|
2119
|
+
scopeTreeForPicker,
|
|
2120
|
+
selectedKey: scopeKey,
|
|
2121
|
+
onChange: onScopeChange,
|
|
2122
|
+
disabled: scopeTree.loading
|
|
2123
|
+
}
|
|
2124
|
+
),
|
|
2125
|
+
/* @__PURE__ */ jsxs4(TooltipTrigger, { children: [
|
|
2126
|
+
/* @__PURE__ */ jsx4(ActionButton2, { onPress: onReloadStores, isDisabled: scopeTree.loading, "aria-label": "Reload stores", children: /* @__PURE__ */ jsx4(Refresh, {}) }),
|
|
2127
|
+
/* @__PURE__ */ jsx4(Tooltip, { children: "Reload websites & stores from Commerce" })
|
|
2128
|
+
] }),
|
|
2129
|
+
/* @__PURE__ */ jsxs4(TooltipTrigger, { children: [
|
|
2130
|
+
/* @__PURE__ */ jsx4(ActionButton2, { onPress: onOpenTools, "aria-label": "Open tools", isQuiet: !toolsOpen, children: /* @__PURE__ */ jsx4(CloudUpload, {}) }),
|
|
2131
|
+
/* @__PURE__ */ jsx4(Tooltip, { children: "Legacy migration tools" })
|
|
2132
|
+
] }),
|
|
2133
|
+
/* @__PURE__ */ jsxs4(TooltipTrigger, { children: [
|
|
2134
|
+
/* @__PURE__ */ jsx4(ActionButton2, { onPress: () => setMode("schema"), "aria-label": "Edit schema", children: /* @__PURE__ */ jsx4(Edit, {}) }),
|
|
2135
|
+
/* @__PURE__ */ jsx4(Tooltip, { children: "Edit schema" })
|
|
2136
|
+
] })
|
|
2137
|
+
] }) })
|
|
2138
|
+
]
|
|
2139
|
+
}
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
function ToolsPanel({
|
|
2143
|
+
onClose,
|
|
2144
|
+
// Export / Import
|
|
2145
|
+
onExport,
|
|
2146
|
+
exporting,
|
|
2147
|
+
onImport,
|
|
2148
|
+
importing,
|
|
2149
|
+
ioMsg,
|
|
2150
|
+
ioProgress,
|
|
2151
|
+
// { phase, done, total, label }
|
|
2152
|
+
importSourceKey,
|
|
2153
|
+
setImportSourceKey,
|
|
2154
|
+
// Commerce sync
|
|
2155
|
+
onSyncStoreMappings,
|
|
2156
|
+
syncingStoreMappings,
|
|
2157
|
+
syncMsg
|
|
2158
|
+
}) {
|
|
2159
|
+
return /* @__PURE__ */ jsxs4(Card, { style: { marginBottom: 16 }, children: [
|
|
2160
|
+
/* @__PURE__ */ jsxs4(Flex2, { justifyContent: "space-between", alignItems: "center", marginBottom: "size-150", children: [
|
|
2161
|
+
/* @__PURE__ */ jsxs4(Flex2, { gap: "size-100", alignItems: "center", children: [
|
|
2162
|
+
/* @__PURE__ */ jsx4(CloudUpload, { size: "S" }),
|
|
2163
|
+
/* @__PURE__ */ jsx4(Heading2, { level: 4, margin: 0, children: "Export / Import" })
|
|
2164
|
+
] }),
|
|
2165
|
+
/* @__PURE__ */ jsx4(ActionButton2, { isQuiet: true, onPress: onClose, "aria-label": "Close tools", children: "\u2715" })
|
|
2166
|
+
] }),
|
|
2167
|
+
/* @__PURE__ */ jsx4(Text2, { UNSAFE_style: { color: PALETTE.textMuted, fontSize: 13, display: "block", marginBottom: 12 }, children: "Download the entire configuration bundle as JSON for backup or to copy between workspaces." }),
|
|
2168
|
+
/* @__PURE__ */ jsxs4(Flex2, { gap: "size-150", alignItems: "center", wrap: true, children: [
|
|
2169
|
+
/* @__PURE__ */ jsx4(Button2, { variant: "secondary", onPress: onExport, isDisabled: exporting || importing, children: exporting ? "Exporting\u2026" : "Export Configuration" }),
|
|
2170
|
+
/* @__PURE__ */ jsx4(Button2, { variant: "secondary", onPress: onImport, isDisabled: importing || exporting, children: importing ? "Importing\u2026" : "Import Configuration" })
|
|
2171
|
+
] }),
|
|
2172
|
+
/* @__PURE__ */ jsx4(View2, { marginTop: "size-150", UNSAFE_style: { maxWidth: 520 }, children: /* @__PURE__ */ jsx4(
|
|
2173
|
+
TextField2,
|
|
2174
|
+
{
|
|
2175
|
+
label: "Source encryption key (only for legacy v1 dumps)",
|
|
2176
|
+
type: "password",
|
|
2177
|
+
value: importSourceKey,
|
|
2178
|
+
onChange: setImportSourceKey,
|
|
2179
|
+
isDisabled: importing,
|
|
2180
|
+
width: "100%"
|
|
2181
|
+
}
|
|
2182
|
+
) }),
|
|
2183
|
+
ioProgress && ioProgress.phase === "running" && /* @__PURE__ */ jsx4(View2, { marginTop: "size-200", children: ioProgress.total > 0 ? /* @__PURE__ */ jsx4(
|
|
2184
|
+
ProgressBar,
|
|
2185
|
+
{
|
|
2186
|
+
label: ioProgress.label || "Working\u2026",
|
|
2187
|
+
value: ioProgress.done,
|
|
2188
|
+
maxValue: ioProgress.total,
|
|
2189
|
+
valueLabel: `${ioProgress.done} / ${ioProgress.total}`,
|
|
2190
|
+
width: "100%"
|
|
2191
|
+
}
|
|
2192
|
+
) : /* @__PURE__ */ jsx4(
|
|
2193
|
+
ProgressBar,
|
|
2194
|
+
{
|
|
2195
|
+
label: ioProgress.label || "Working\u2026",
|
|
2196
|
+
isIndeterminate: true,
|
|
2197
|
+
width: "100%"
|
|
2198
|
+
}
|
|
2199
|
+
) }),
|
|
2200
|
+
ioMsg && /* @__PURE__ */ jsx4(
|
|
2201
|
+
View2,
|
|
2202
|
+
{
|
|
2203
|
+
marginTop: "size-150",
|
|
2204
|
+
padding: "size-150",
|
|
2205
|
+
UNSAFE_style: {
|
|
2206
|
+
background: PALETTE.surface,
|
|
2207
|
+
border: `1px solid ${PALETTE.border}`,
|
|
2208
|
+
borderRadius: RADIUS.md
|
|
2209
|
+
},
|
|
2210
|
+
children: /* @__PURE__ */ jsx4(Text2, { UNSAFE_style: { whiteSpace: "pre-line", fontSize: 13, fontFamily: "ui-monospace, Menlo, monospace" }, children: ioMsg })
|
|
2211
|
+
}
|
|
2212
|
+
),
|
|
2213
|
+
/* @__PURE__ */ jsx4(Divider2, { size: "S", marginY: "size-250" }),
|
|
2214
|
+
/* @__PURE__ */ jsx4(Flex2, { justifyContent: "space-between", alignItems: "center", marginBottom: "size-100", children: /* @__PURE__ */ jsx4(Heading2, { level: 4, margin: 0, children: "Sync Store Mappings" }) }),
|
|
2215
|
+
/* @__PURE__ */ jsxs4(Text2, { UNSAFE_style: { color: PALETTE.textMuted, fontSize: 13, display: "block", marginBottom: 12 }, children: [
|
|
2216
|
+
"Rebuild ",
|
|
2217
|
+
/* @__PURE__ */ jsx4("code", { children: "general/settings/store_mappings" }),
|
|
2218
|
+
" from Commerce."
|
|
2219
|
+
] }),
|
|
2220
|
+
/* @__PURE__ */ jsx4(Flex2, { gap: "size-150", alignItems: "center", wrap: true, children: /* @__PURE__ */ jsx4(
|
|
2221
|
+
Button2,
|
|
2222
|
+
{
|
|
2223
|
+
variant: "secondary",
|
|
2224
|
+
onPress: onSyncStoreMappings,
|
|
2225
|
+
isDisabled: syncingStoreMappings || exporting || importing,
|
|
2226
|
+
children: syncingStoreMappings ? "Syncing\u2026" : "Sync Store Mappings"
|
|
2227
|
+
}
|
|
2228
|
+
) }),
|
|
2229
|
+
syncMsg && /* @__PURE__ */ jsx4(
|
|
2230
|
+
View2,
|
|
2231
|
+
{
|
|
2232
|
+
marginTop: "size-150",
|
|
2233
|
+
padding: "size-150",
|
|
2234
|
+
UNSAFE_style: {
|
|
2235
|
+
background: PALETTE.surface,
|
|
2236
|
+
border: `1px solid ${PALETTE.border}`,
|
|
2237
|
+
borderRadius: RADIUS.md
|
|
2238
|
+
},
|
|
2239
|
+
children: /* @__PURE__ */ jsx4(Text2, { UNSAFE_style: { whiteSpace: "pre-line", fontSize: 13, fontFamily: "ui-monospace, Menlo, monospace" }, children: syncMsg })
|
|
2240
|
+
}
|
|
2241
|
+
)
|
|
2242
|
+
] });
|
|
2243
|
+
}
|
|
2244
|
+
function SystemConfig(props) {
|
|
2245
|
+
const {
|
|
2246
|
+
schema,
|
|
2247
|
+
saveSchema,
|
|
2248
|
+
refresh: refreshSchema,
|
|
2249
|
+
loading: schemaLoading,
|
|
2250
|
+
saving: schemaSaving,
|
|
2251
|
+
error: schemaError
|
|
2252
|
+
} = useSystemConfigSchema(props);
|
|
2253
|
+
const [mode, setMode] = useState5("values");
|
|
2254
|
+
const [toolsOpen, setToolsOpen] = useState5(false);
|
|
2255
|
+
const [exporting, setExporting] = useState5(false);
|
|
2256
|
+
const [importing, setImporting] = useState5(false);
|
|
2257
|
+
const [ioMsg, setIoMsg] = useState5(null);
|
|
2258
|
+
const [ioProgress, setIoProgress] = useState5({ phase: "idle", done: 0, total: 0, label: "" });
|
|
2259
|
+
const [importSourceKey, setImportSourceKey] = useState5("");
|
|
2260
|
+
const [syncingStoreMappings, setSyncingStoreMappings] = useState5(false);
|
|
2261
|
+
const [syncMsg, setSyncMsg] = useState5(null);
|
|
2262
|
+
const { confirm, dialog: confirmDialog } = useConfirm();
|
|
2263
|
+
const heroRef = useRef2(null);
|
|
2264
|
+
useEffect5(() => {
|
|
2265
|
+
if (!heroRef.current) return void 0;
|
|
2266
|
+
const update = () => {
|
|
2267
|
+
const h = heroRef.current ? heroRef.current.offsetHeight : HERO_HEIGHT;
|
|
2268
|
+
document.documentElement.style.setProperty("--sc-hero-h", `${h}px`);
|
|
2269
|
+
};
|
|
2270
|
+
update();
|
|
2271
|
+
const ro = new ResizeObserver(update);
|
|
2272
|
+
ro.observe(heroRef.current);
|
|
2273
|
+
return () => {
|
|
2274
|
+
ro.disconnect();
|
|
2275
|
+
};
|
|
2276
|
+
}, [mode]);
|
|
2277
|
+
const configCtx = useSystemConfig(
|
|
2278
|
+
props,
|
|
2279
|
+
mode === "values" ? schema : { sections: [] }
|
|
2280
|
+
);
|
|
2281
|
+
const { scope, setScope, scopeTree, refreshScopeTree } = configCtx;
|
|
2282
|
+
const scopeTreeForPicker = useMemo3(() => buildScopeTreeForPicker(scopeTree), [scopeTree]);
|
|
2283
|
+
const scopeKey = `${scope.scope}::${scope.scopeId}`;
|
|
2284
|
+
const onScopeChange = (key) => {
|
|
2285
|
+
const opt = scopeTreeForPicker.all.find((o) => o.key === key);
|
|
2286
|
+
if (!opt) return;
|
|
2287
|
+
setScope({ scope: opt.scope, scopeId: opt.scopeId });
|
|
2288
|
+
};
|
|
2289
|
+
const onSchemaSave = async (next) => {
|
|
2290
|
+
let result = await saveSchema(next);
|
|
2291
|
+
if (result == null ? void 0 : result.needsConfirmation) {
|
|
2292
|
+
const removed = result.removedPaths || [];
|
|
2293
|
+
const ok = await confirm({
|
|
2294
|
+
title: "Removing schema entries will delete stored values",
|
|
2295
|
+
body: "The following field path(s) are being removed from the schema. Their values will be permanently deleted from system_config_data across every scope:\n\n \u2022 " + removed.join("\n \u2022 ") + "\n\nContinue?",
|
|
2296
|
+
confirmLabel: "Delete & save",
|
|
2297
|
+
cancelLabel: "Cancel",
|
|
2298
|
+
variant: "destructive"
|
|
2299
|
+
});
|
|
2300
|
+
if (!ok) return;
|
|
2301
|
+
result = await saveSchema(next, { confirmCascade: true });
|
|
2302
|
+
}
|
|
2303
|
+
if (!(result == null ? void 0 : result.ok)) return;
|
|
2304
|
+
if ((result.deletedCount || 0) > 0) {
|
|
2305
|
+
try {
|
|
2306
|
+
await configCtx.refresh();
|
|
2307
|
+
} catch (_) {
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
setMode("values");
|
|
2311
|
+
};
|
|
2312
|
+
const onExport = async () => {
|
|
2313
|
+
var _a, _b, _c;
|
|
2314
|
+
setExporting(true);
|
|
2315
|
+
setIoMsg(null);
|
|
2316
|
+
setIoProgress({ phase: "running", done: 0, total: 0, label: "Collecting schema + values from ABDB\u2026" });
|
|
2317
|
+
try {
|
|
2318
|
+
const response = await callAction(
|
|
2319
|
+
props,
|
|
2320
|
+
getActionKey("exportConfig"),
|
|
2321
|
+
"",
|
|
2322
|
+
{}
|
|
2323
|
+
);
|
|
2324
|
+
const dump = (response == null ? void 0 : response.dump) || ((_a = response == null ? void 0 : response.body) == null ? void 0 : _a.dump);
|
|
2325
|
+
if (!dump) throw new Error("Export response missing `dump`");
|
|
2326
|
+
setIoProgress((p) => ({ ...p, label: "Building file\u2026" }));
|
|
2327
|
+
const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" });
|
|
2328
|
+
const filename = `system-config-export-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
|
|
2329
|
+
const url = URL.createObjectURL(blob);
|
|
2330
|
+
const a = document.createElement("a");
|
|
2331
|
+
a.href = url;
|
|
2332
|
+
a.download = filename;
|
|
2333
|
+
document.body.appendChild(a);
|
|
2334
|
+
a.click();
|
|
2335
|
+
document.body.removeChild(a);
|
|
2336
|
+
URL.revokeObjectURL(url);
|
|
2337
|
+
const c = dump.counts || {};
|
|
2338
|
+
setIoProgress({ phase: "done", done: c.values || 0, total: c.values || 0, label: "Export complete" });
|
|
2339
|
+
setIoMsg(`\u2713 Exported ${(_b = c.sections) != null ? _b : "?"} section(s) and ${(_c = c.values) != null ? _c : "?"} value(s) \u2192 ${filename}`);
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
console.error("Export failed", e);
|
|
2342
|
+
setIoProgress({ phase: "error", done: 0, total: 0, label: "Export failed" });
|
|
2343
|
+
setIoMsg(`Export failed: ${e.message || e}`);
|
|
2344
|
+
} finally {
|
|
2345
|
+
setExporting(false);
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
const IMPORT_CHUNK_SIZE = 25;
|
|
2349
|
+
const onImport = async () => {
|
|
2350
|
+
var _a, _b;
|
|
2351
|
+
const input = document.createElement("input");
|
|
2352
|
+
input.type = "file";
|
|
2353
|
+
input.accept = ".json,application/json";
|
|
2354
|
+
input.style.display = "none";
|
|
2355
|
+
document.body.appendChild(input);
|
|
2356
|
+
const file = await new Promise((resolve) => {
|
|
2357
|
+
input.onchange = () => {
|
|
2358
|
+
resolve(input.files && input.files[0]);
|
|
2359
|
+
};
|
|
2360
|
+
input.click();
|
|
2361
|
+
});
|
|
2362
|
+
document.body.removeChild(input);
|
|
2363
|
+
if (!file) return;
|
|
2364
|
+
let dump;
|
|
2365
|
+
try {
|
|
2366
|
+
const text = await file.text();
|
|
2367
|
+
dump = JSON.parse(text);
|
|
2368
|
+
} catch (e) {
|
|
2369
|
+
setIoMsg(`Could not parse "${file.name}": ${e.message}`);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const choice = await confirm({
|
|
2373
|
+
title: `Import "${file.name}"?`,
|
|
2374
|
+
variant: "information",
|
|
2375
|
+
body: /* @__PURE__ */ jsxs4("span", { children: [
|
|
2376
|
+
"Schema + values from this dump will be applied to the current workspace. website_id / store_id are remapped on the fly by matching",
|
|
2377
|
+
/* @__PURE__ */ jsx4("code", { children: " website_code " }),
|
|
2378
|
+
" and store ",
|
|
2379
|
+
/* @__PURE__ */ jsx4("code", { children: "code" }),
|
|
2380
|
+
" against the target environment's Commerce instance."
|
|
2381
|
+
] }),
|
|
2382
|
+
choices: [
|
|
2383
|
+
{
|
|
2384
|
+
label: "Overwrite existing values",
|
|
2385
|
+
value: "overwrite",
|
|
2386
|
+
variant: "destructive",
|
|
2387
|
+
description: "Recommended for restoring a backup. Existing rows are replaced."
|
|
2388
|
+
},
|
|
2389
|
+
{
|
|
2390
|
+
label: "Insert-only",
|
|
2391
|
+
value: "insert",
|
|
2392
|
+
variant: "information",
|
|
2393
|
+
description: "Skip rows that already exist; only add new ones."
|
|
2394
|
+
}
|
|
2395
|
+
],
|
|
2396
|
+
cancelLabel: "Cancel"
|
|
2397
|
+
});
|
|
2398
|
+
if (!choice) return;
|
|
2399
|
+
const overwrite = choice === "overwrite";
|
|
2400
|
+
const allValues = Array.isArray(dump.values) ? dump.values : [];
|
|
2401
|
+
const schemaPayload = dump.schema;
|
|
2402
|
+
const total = allValues.length;
|
|
2403
|
+
setImporting(true);
|
|
2404
|
+
setIoMsg(null);
|
|
2405
|
+
setIoProgress({
|
|
2406
|
+
phase: "running",
|
|
2407
|
+
done: 0,
|
|
2408
|
+
total,
|
|
2409
|
+
label: schemaPayload ? "Importing schema\u2026" : "Importing values\u2026"
|
|
2410
|
+
});
|
|
2411
|
+
const aggregate = {
|
|
2412
|
+
schemaImported: false,
|
|
2413
|
+
schemaSkipped: false,
|
|
2414
|
+
valuesInserted: 0,
|
|
2415
|
+
valuesUpserted: 0,
|
|
2416
|
+
valuesSkipped: 0,
|
|
2417
|
+
unmappedSkipped: 0,
|
|
2418
|
+
unmapped: [],
|
|
2419
|
+
invalid: [],
|
|
2420
|
+
idMap: null,
|
|
2421
|
+
sensitiveReencrypted: 0,
|
|
2422
|
+
sensitiveDecryptFailed: 0
|
|
2423
|
+
};
|
|
2424
|
+
const sensitiveCount = allValues.filter(
|
|
2425
|
+
(v) => typeof (v == null ? void 0 : v.value) === "string" && v.value.startsWith("enc:v1:")
|
|
2426
|
+
).length;
|
|
2427
|
+
try {
|
|
2428
|
+
if (schemaPayload) {
|
|
2429
|
+
const r = await callAction(
|
|
2430
|
+
props,
|
|
2431
|
+
getActionKey("importConfig"),
|
|
2432
|
+
"",
|
|
2433
|
+
{ schema: schemaPayload, overwrite, valuesOnly: false, schemaOnly: true }
|
|
2434
|
+
);
|
|
2435
|
+
const s = (r == null ? void 0 : r.summary) || ((_a = r == null ? void 0 : r.body) == null ? void 0 : _a.summary);
|
|
2436
|
+
if (s) {
|
|
2437
|
+
aggregate.schemaImported = !!s.schemaImported;
|
|
2438
|
+
aggregate.schemaSkipped = !!s.schemaSkipped;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const sensitivePaths = Array.isArray(dump.sensitivePaths) ? dump.sensitivePaths : void 0;
|
|
2442
|
+
setIoProgress((p) => ({ ...p, label: "Importing values\u2026" }));
|
|
2443
|
+
for (let i = 0; i < total; i += IMPORT_CHUNK_SIZE) {
|
|
2444
|
+
const chunk = allValues.slice(i, i + IMPORT_CHUNK_SIZE);
|
|
2445
|
+
const r = await callAction(
|
|
2446
|
+
props,
|
|
2447
|
+
getActionKey("importConfig"),
|
|
2448
|
+
"",
|
|
2449
|
+
{
|
|
2450
|
+
values: chunk,
|
|
2451
|
+
overwrite,
|
|
2452
|
+
valuesOnly: true,
|
|
2453
|
+
// Re-encrypt sensitive ciphertext against the target env's key.
|
|
2454
|
+
sourceCryptKey: importSourceKey ? importSourceKey.trim() : void 0,
|
|
2455
|
+
// sensitivePaths on every chunk so the backend knows what to
|
|
2456
|
+
// encrypt even before the schema row lands.
|
|
2457
|
+
dump: sensitivePaths ? { sensitivePaths } : void 0
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
const s = (r == null ? void 0 : r.summary) || ((_b = r == null ? void 0 : r.body) == null ? void 0 : _b.summary);
|
|
2461
|
+
if (s) {
|
|
2462
|
+
aggregate.valuesInserted += s.valuesInserted || 0;
|
|
2463
|
+
aggregate.valuesUpserted += s.valuesUpserted || 0;
|
|
2464
|
+
aggregate.valuesSkipped += s.valuesSkipped || 0;
|
|
2465
|
+
aggregate.unmappedSkipped += s.unmappedSkipped || 0;
|
|
2466
|
+
aggregate.sensitiveReencrypted += s.sensitiveReencrypted || 0;
|
|
2467
|
+
aggregate.sensitiveDecryptFailed += s.sensitiveDecryptFailed || 0;
|
|
2468
|
+
if (Array.isArray(s.unmapped)) aggregate.unmapped.push(...s.unmapped);
|
|
2469
|
+
if (Array.isArray(s.invalid)) aggregate.invalid.push(...s.invalid);
|
|
2470
|
+
if (s.idMap) {
|
|
2471
|
+
if (!aggregate.idMap) {
|
|
2472
|
+
aggregate.idMap = { ...s.idMap };
|
|
2473
|
+
} else {
|
|
2474
|
+
aggregate.idMap.matchedByCode = (aggregate.idMap.matchedByCode || 0) + (s.idMap.matchedByCode || 0);
|
|
2475
|
+
aggregate.idMap.matchedById = (aggregate.idMap.matchedById || 0) + (s.idMap.matchedById || 0);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
setIoProgress({
|
|
2480
|
+
phase: "running",
|
|
2481
|
+
done: Math.min(i + chunk.length, total),
|
|
2482
|
+
total,
|
|
2483
|
+
label: `Importing values\u2026 (${Math.min(i + chunk.length, total)}/${total})`
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
const lines = [
|
|
2487
|
+
`\u2713 Import complete (${overwrite ? "overwrite" : "insert-only"})`,
|
|
2488
|
+
` Schema: ${aggregate.schemaImported ? "imported" : aggregate.schemaSkipped ? "skipped (exists)" : "no schema in dump"}`,
|
|
2489
|
+
` Values: inserted=${aggregate.valuesInserted} upserted=${aggregate.valuesUpserted} skipped=${aggregate.valuesSkipped}`,
|
|
2490
|
+
aggregate.unmappedSkipped ? ` \u26A0 Unmapped rows skipped (no matching website_code/store_code in target): ${aggregate.unmappedSkipped}` : "",
|
|
2491
|
+
sensitiveCount ? ` Sensitive: ${sensitiveCount} ciphertext row(s) in dump \u2192 re-encrypted=${aggregate.sensitiveReencrypted}, decrypt-failed=${aggregate.sensitiveDecryptFailed}${importSourceKey ? "" : " (no source key provided \u2014 values may show blank if this env's key differs)"}` : "",
|
|
2492
|
+
aggregate.invalid.length ? ` \u26A0 Invalid rows: ${aggregate.invalid.length}` : "",
|
|
2493
|
+
aggregate.idMap ? [
|
|
2494
|
+
` id remap \u2192 target(${aggregate.idMap.targetSource || "none"}, websites=${aggregate.idMap.targetWebsiteCount || 0}, stores=${aggregate.idMap.targetStoreCount || 0}) matched(by-code=${aggregate.idMap.matchedByCode || 0}, by-id=${aggregate.idMap.matchedById || 0})`,
|
|
2495
|
+
!aggregate.idMap.hasTarget ? " \u26A0 Target env Commerce returned no stores \u2014 check COMMERCE_BASE_URL / OAuth1 secrets in this workspace." : ""
|
|
2496
|
+
].filter(Boolean).join("\n") : ""
|
|
2497
|
+
].filter(Boolean);
|
|
2498
|
+
setIoMsg(lines.join("\n"));
|
|
2499
|
+
setIoProgress({ phase: "done", done: total, total, label: "Import complete" });
|
|
2500
|
+
await refreshSchema();
|
|
2501
|
+
try {
|
|
2502
|
+
await configCtx.refresh();
|
|
2503
|
+
} catch (_) {
|
|
2504
|
+
}
|
|
2505
|
+
} catch (e) {
|
|
2506
|
+
console.error("Import failed", e);
|
|
2507
|
+
setIoProgress((p) => ({ ...p, phase: "error", label: "Import failed" }));
|
|
2508
|
+
setIoMsg(`Import failed: ${e.message || e}`);
|
|
2509
|
+
} finally {
|
|
2510
|
+
setImporting(false);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
const onSyncStoreMappings = async () => {
|
|
2514
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2515
|
+
setSyncingStoreMappings(true);
|
|
2516
|
+
setSyncMsg("Fetching websites + store views from Commerce\u2026");
|
|
2517
|
+
try {
|
|
2518
|
+
const response = await callAction(
|
|
2519
|
+
props,
|
|
2520
|
+
getActionKey("syncStoreMappings"),
|
|
2521
|
+
"",
|
|
2522
|
+
{}
|
|
2523
|
+
);
|
|
2524
|
+
const ok = (_b = response == null ? void 0 : response.ok) != null ? _b : (_a = response == null ? void 0 : response.body) == null ? void 0 : _a.ok;
|
|
2525
|
+
const count = (_d = response == null ? void 0 : response.count) != null ? _d : (_c = response == null ? void 0 : response.body) == null ? void 0 : _c.count;
|
|
2526
|
+
const mapping = (_f = response == null ? void 0 : response.mapping) != null ? _f : (_e = response == null ? void 0 : response.body) == null ? void 0 : _e.mapping;
|
|
2527
|
+
if (!ok) throw new Error("Sync response missing `ok`");
|
|
2528
|
+
const sample = mapping ? Object.entries(mapping).slice(0, 5).map(
|
|
2529
|
+
([id, m]) => ` ${id}: ${m.code} \u2192 website ${m.website_code}(${m.website_id}), lang=${m.language_code}`
|
|
2530
|
+
).join("\n") : "";
|
|
2531
|
+
setSyncMsg(
|
|
2532
|
+
`\u2713 Synced ${count} store(s) \u2192 general/settings/store_mappings
|
|
2533
|
+
` + (sample ? sample + (count > 5 ? `
|
|
2534
|
+
\u2026 (${count - 5} more)` : "") : "")
|
|
2535
|
+
);
|
|
2536
|
+
try {
|
|
2537
|
+
await configCtx.refresh();
|
|
2538
|
+
} catch (_) {
|
|
2539
|
+
}
|
|
2540
|
+
} catch (e) {
|
|
2541
|
+
console.error("Store-mapping sync failed", e);
|
|
2542
|
+
setSyncMsg(`Sync failed: ${e.message || e}`);
|
|
2543
|
+
} finally {
|
|
2544
|
+
setSyncingStoreMappings(false);
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
return /* @__PURE__ */ jsxs4(
|
|
2548
|
+
View2,
|
|
2549
|
+
{
|
|
2550
|
+
UNSAFE_style: {
|
|
2551
|
+
background: PALETTE.bg,
|
|
2552
|
+
minHeight: "100vh",
|
|
2553
|
+
color: PALETTE.text
|
|
2554
|
+
},
|
|
2555
|
+
children: [
|
|
2556
|
+
confirmDialog,
|
|
2557
|
+
/* @__PURE__ */ jsxs4(View2, { padding: "size-400", maxWidth: "1400px", marginX: "auto", children: [
|
|
2558
|
+
/* @__PURE__ */ jsx4(
|
|
2559
|
+
PageHeader,
|
|
2560
|
+
{
|
|
2561
|
+
heroRef,
|
|
2562
|
+
mode,
|
|
2563
|
+
setMode,
|
|
2564
|
+
scopeTree,
|
|
2565
|
+
scopeTreeForPicker,
|
|
2566
|
+
scopeKey,
|
|
2567
|
+
onScopeChange,
|
|
2568
|
+
onReloadStores: refreshScopeTree,
|
|
2569
|
+
onOpenTools: () => setToolsOpen((o) => !o),
|
|
2570
|
+
toolsOpen
|
|
2571
|
+
}
|
|
2572
|
+
),
|
|
2573
|
+
/* @__PURE__ */ jsxs4("div", { style: { paddingTop: 24 }, children: [
|
|
2574
|
+
toolsOpen && mode === "values" && /* @__PURE__ */ jsx4(
|
|
2575
|
+
ToolsPanel,
|
|
2576
|
+
{
|
|
2577
|
+
onClose: () => setToolsOpen(false),
|
|
2578
|
+
onExport,
|
|
2579
|
+
exporting,
|
|
2580
|
+
onImport,
|
|
2581
|
+
importing,
|
|
2582
|
+
ioMsg,
|
|
2583
|
+
ioProgress,
|
|
2584
|
+
importSourceKey,
|
|
2585
|
+
setImportSourceKey,
|
|
2586
|
+
onSyncStoreMappings,
|
|
2587
|
+
syncingStoreMappings,
|
|
2588
|
+
syncMsg
|
|
2589
|
+
}
|
|
2590
|
+
),
|
|
2591
|
+
schemaLoading ? /* @__PURE__ */ jsx4(Card, { children: /* @__PURE__ */ jsx4(Flex2, { justifyContent: "center", marginY: "size-400", children: /* @__PURE__ */ jsx4(ProgressCircle2, { "aria-label": "Loading schema", isIndeterminate: true }) }) }) : mode === "schema" ? /* @__PURE__ */ jsx4(
|
|
2592
|
+
SystemConfigSchemaEditor,
|
|
2593
|
+
{
|
|
2594
|
+
schema,
|
|
2595
|
+
onSave: onSchemaSave,
|
|
2596
|
+
onCancel: () => setMode("values"),
|
|
2597
|
+
saving: schemaSaving,
|
|
2598
|
+
error: schemaError,
|
|
2599
|
+
palette: PALETTE
|
|
2600
|
+
}
|
|
2601
|
+
) : /* @__PURE__ */ jsx4(
|
|
2602
|
+
ValuesView,
|
|
2603
|
+
{
|
|
2604
|
+
schema,
|
|
2605
|
+
onEditSchema: () => setMode("schema"),
|
|
2606
|
+
toolsOpen,
|
|
2607
|
+
setToolsOpen,
|
|
2608
|
+
configCtx
|
|
2609
|
+
}
|
|
2610
|
+
)
|
|
2611
|
+
] })
|
|
2612
|
+
] })
|
|
2613
|
+
]
|
|
2614
|
+
}
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// web/src/components/MainPage.js
|
|
2619
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2620
|
+
var MainPage = (props) => {
|
|
2621
|
+
const location = useLocation2();
|
|
2622
|
+
useEffect6(() => {
|
|
2623
|
+
const fetchCredentials = async () => {
|
|
2624
|
+
var _a, _b;
|
|
2625
|
+
if (!props.ims.token) {
|
|
2626
|
+
const guestConnection = await attach({ id: getExtensionId() });
|
|
2627
|
+
props.ims.token = (_a = guestConnection == null ? void 0 : guestConnection.sharedContext) == null ? void 0 : _a.get("imsToken");
|
|
2628
|
+
props.ims.org = (_b = guestConnection == null ? void 0 : guestConnection.sharedContext) == null ? void 0 : _b.get("imsOrgId");
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
fetchCredentials();
|
|
2632
|
+
}, []);
|
|
2633
|
+
const renderContent = () => {
|
|
2634
|
+
switch (location.pathname) {
|
|
2635
|
+
default:
|
|
2636
|
+
return /* @__PURE__ */ jsx5(SystemConfig, { runtime: props.runtime, ims: props.ims });
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
return /* @__PURE__ */ jsxs5(View3, { UNSAFE_style: { overflowX: "clip" }, children: [
|
|
2640
|
+
/* @__PURE__ */ jsx5(AppSectionNav, {}),
|
|
2641
|
+
/* @__PURE__ */ jsx5(View3, { children: renderContent() })
|
|
2642
|
+
] });
|
|
2643
|
+
};
|
|
2644
|
+
|
|
2645
|
+
// web/src/components/ExtensionRegistration.js
|
|
2646
|
+
import { useEffect as useEffect7 } from "react";
|
|
2647
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
2648
|
+
function ExtensionRegistration(props) {
|
|
2649
|
+
useEffect7(() => {
|
|
2650
|
+
(async () => {
|
|
2651
|
+
await register({
|
|
2652
|
+
id: getExtensionId(),
|
|
2653
|
+
methods: {}
|
|
2654
|
+
});
|
|
2655
|
+
})();
|
|
2656
|
+
}, []);
|
|
2657
|
+
return /* @__PURE__ */ jsx6(MainPage, { ims: props.ims, runtime: props.runtime });
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// web/src/components/App.js
|
|
2661
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2662
|
+
function App(props) {
|
|
2663
|
+
props.runtime.on("configuration", ({ imsOrg, imsToken }) => {
|
|
2664
|
+
console.log("configuration change", { imsOrg, imsToken });
|
|
2665
|
+
});
|
|
2666
|
+
return /* @__PURE__ */ jsx7(ErrorBoundary, { onError, FallbackComponent: fallbackComponent, children: /* @__PURE__ */ jsx7(HashRouter, { children: /* @__PURE__ */ jsx7(
|
|
2667
|
+
Provider,
|
|
2668
|
+
{
|
|
2669
|
+
theme: lightTheme,
|
|
2670
|
+
colorScheme: "light",
|
|
2671
|
+
UNSAFE_className: "sm-provider",
|
|
2672
|
+
children: /* @__PURE__ */ jsx7(Routes, { children: /* @__PURE__ */ jsx7(Route, { index: true, element: /* @__PURE__ */ jsx7(ExtensionRegistration, { runtime: props.runtime, ims: props.ims }) }) })
|
|
2673
|
+
}
|
|
2674
|
+
) }) });
|
|
2675
|
+
function onError(e, componentStack) {
|
|
2676
|
+
}
|
|
2677
|
+
function fallbackComponent({ componentStack, error }) {
|
|
2678
|
+
return /* @__PURE__ */ jsxs6(React2.Fragment, { children: [
|
|
2679
|
+
/* @__PURE__ */ jsx7("h1", { style: { textAlign: "center", marginTop: "20px" }, children: "Something went wrong :(" }),
|
|
2680
|
+
/* @__PURE__ */ jsx7("pre", { children: componentStack + "\n" + error.message })
|
|
2681
|
+
] });
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
var App_default = App;
|
|
2685
|
+
export {
|
|
2686
|
+
App_default as App,
|
|
2687
|
+
AppSectionNav,
|
|
2688
|
+
App_default as ConfigurationManagementApp,
|
|
2689
|
+
DEFAULT_ACTION_KEYS,
|
|
2690
|
+
ExtensionRegistration,
|
|
2691
|
+
FIELD_TYPES,
|
|
2692
|
+
FONT,
|
|
2693
|
+
MainPage,
|
|
2694
|
+
NAV_ITEMS,
|
|
2695
|
+
PALETTE,
|
|
2696
|
+
RADIUS,
|
|
2697
|
+
SCOPES,
|
|
2698
|
+
SHADOW,
|
|
2699
|
+
SPACE,
|
|
2700
|
+
SystemConfig,
|
|
2701
|
+
SystemConfigSchemaEditor,
|
|
2702
|
+
THEME,
|
|
2703
|
+
buildStoreMappingsFromCommercePayload,
|
|
2704
|
+
callAction,
|
|
2705
|
+
coerceDefault,
|
|
2706
|
+
configureWeb,
|
|
2707
|
+
emptySchema,
|
|
2708
|
+
flattenFields,
|
|
2709
|
+
getActionKey,
|
|
2710
|
+
getExtensionId,
|
|
2711
|
+
getFieldPath,
|
|
2712
|
+
isFieldSensitive,
|
|
2713
|
+
isFieldVisibleAtScope,
|
|
2714
|
+
useConfirm,
|
|
2715
|
+
useSystemConfig,
|
|
2716
|
+
useSystemConfigSchema
|
|
2717
|
+
};
|