@saasicat/ui-vue 0.24.2 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
- package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
- package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
- package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
- package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
- package/dist/client/index.cjs +758 -83
- package/dist/client/index.d.cts +414 -9
- package/dist/client/index.d.ts +414 -9
- package/dist/client/index.js +84 -12
- package/dist/index.cjs +1325 -294
- package/dist/index.d.cts +205 -15
- package/dist/index.d.ts +205 -15
- package/dist/index.js +566 -240
- package/dist/quasar/index.cjs +574 -62
- package/dist/quasar/index.d.cts +61 -6
- package/dist/quasar/index.d.ts +61 -6
- package/dist/quasar/index.js +83 -19
- package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
- package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
- package/package.json +4 -3
- package/src/client/admin-error.ts +552 -0
- package/src/client/admin-resource-client.ts +9 -12
- package/src/client/batch-column-fetcher.ts +2 -2
- package/src/client/boot-loader.ts +12 -1
- package/src/client/http/axios-http-client.ts +383 -0
- package/src/client/http/fetch-http-client.ts +89 -0
- package/src/client/http/index.ts +6 -0
- package/src/client/http-json.ts +82 -16
- package/src/client/i18n/messages/discovery.ts +0 -6
- package/src/client/i18n/messages/errors.ts +44 -0
- package/src/client/i18n/messages/marketing.ts +0 -6
- package/src/client/i18n/messages/promos.ts +0 -2
- package/src/client/i18n/messages/shell.ts +12 -0
- package/src/client/i18n/messages.ts +3 -0
- package/src/client/index.ts +6 -0
- package/src/client/manifest-loader.ts +65 -15
- package/src/client/resources/audit.resource.ts +25 -0
- package/src/client/resources/define-resource.ts +166 -0
- package/src/client/resources/index.ts +42 -0
- package/src/client/resources/list-resource.ts +176 -0
- package/src/client/resources/plans.resource.ts +169 -0
- package/src/client/resources/resource-request.ts +117 -0
- package/src/client/resources/tenants.resource.ts +27 -0
- package/src/client/types.ts +10 -3
- package/src/components/BundleVersionPublishDialog.vue +1 -1
- package/src/components/MarketingPromotionsTab.vue +263 -241
- package/src/components/ThemeSwitcher.vue +85 -0
- package/src/components/admin-page/AdminAccordion.vue +125 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
- package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
- package/src/components/plan/PlanCycleToggle.vue +1 -1
- package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
- package/src/index.ts +5 -0
- package/src/pages-standard/AdminLayout.vue +57 -0
- package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
- package/src/pages-standard/BundlesPage.vue +5 -35
- package/src/pages-standard/DiscoveryPage.vue +8 -1
- package/src/pages-standard/MarketingCatalogPage.vue +14 -3
- package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
- package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
- package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
- package/src/quasar/confirm.ts +83 -0
- package/src/quasar/create-super-admin-app.ts +84 -10
- package/src/quasar/dark-bridge.ts +47 -10
- package/src/quasar/index.ts +1 -0
- package/src/ui/theme/components/accordion.css +118 -0
- package/src/ui/theme/index.css +1 -0
- package/src/vue/create-admin-routes.ts +9 -1
- package/src/vue/platform-loaders.ts +12 -2
- package/src/vue/resource-registry.ts +211 -0
- package/src/vue/super-admin-context.ts +20 -5
- package/src/vue/ui-confirm.ts +76 -0
- package/src/vue/use-api-list.ts +50 -45
- package/src/vue/use-async-action.ts +170 -0
- package/src/vue/use-async-data.ts +83 -0
- package/src/vue/use-bundles.ts +34 -7
- package/src/vue/use-catalog-entries.ts +37 -12
- package/src/vue/use-discovery.ts +27 -6
- package/src/vue/use-marketing-projections.ts +29 -7
- package/src/vue/use-plans.ts +36 -8
- package/src/vue/use-promotions.ts +27 -7
- package/src/vue/use-resource-list.ts +272 -0
- package/src/vue/use-sa-theme.ts +34 -5
- package/src/vue/use-tenant-subscription-bundles.ts +22 -3
|
@@ -1,3 +1,284 @@
|
|
|
1
|
+
// src/client/i18n/format.ts
|
|
2
|
+
import { formatErrorMessage } from "@saasicat/types";
|
|
3
|
+
function formatMessage(template, params) {
|
|
4
|
+
return formatErrorMessage(template, params);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/client/admin-error.ts
|
|
8
|
+
var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
|
|
9
|
+
var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
|
|
10
|
+
function markPlatformError(error) {
|
|
11
|
+
Object.defineProperty(error, PLATFORM_ERROR, { value: true });
|
|
12
|
+
}
|
|
13
|
+
function isPlatformError(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && PLATFORM_ERROR in value;
|
|
15
|
+
}
|
|
16
|
+
var EMPTY_RESPONSE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/EmptyResponse");
|
|
17
|
+
function markEmptyResponse(error) {
|
|
18
|
+
Object.defineProperty(error, EMPTY_RESPONSE, { value: true });
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
function isEmptyResponse(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && EMPTY_RESPONSE in value;
|
|
23
|
+
}
|
|
24
|
+
var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
|
|
25
|
+
function markTransportFailure(error) {
|
|
26
|
+
if (typeof error === "object" && error !== null) {
|
|
27
|
+
try {
|
|
28
|
+
Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
function isTransportFailure(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && TRANSPORT_FAILURE in value;
|
|
36
|
+
}
|
|
37
|
+
function describe(init) {
|
|
38
|
+
const status = init.status ?? 0;
|
|
39
|
+
const where = [init.method, init.url].filter(Boolean).join(" ");
|
|
40
|
+
const what = status > 0 ? `HTTP ${status}` : "request failed";
|
|
41
|
+
const head = where ? `${where} \u2192 ${what}` : what;
|
|
42
|
+
const code = init.code ? ` (${init.code})` : "";
|
|
43
|
+
return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
|
|
44
|
+
}
|
|
45
|
+
var AdminError = class extends Error {
|
|
46
|
+
/** HTTP status. `0` means the request never produced one. */
|
|
47
|
+
status;
|
|
48
|
+
/** Machine-readable code from the response body, when it carried one. */
|
|
49
|
+
code;
|
|
50
|
+
/** Parsed response body, when there was one. */
|
|
51
|
+
body;
|
|
52
|
+
/** Requested URL, when known. */
|
|
53
|
+
url;
|
|
54
|
+
/** Request method, when known. */
|
|
55
|
+
method;
|
|
56
|
+
/**
|
|
57
|
+
* Text the failing side supplied — the only candidate for user-facing
|
|
58
|
+
* output. `undefined` when nothing was supplied.
|
|
59
|
+
*/
|
|
60
|
+
detail;
|
|
61
|
+
/** The request completed but the answer was unusable. See `AdminErrorInit`. */
|
|
62
|
+
emptyResponse;
|
|
63
|
+
/** The request never reached the server. See `AdminErrorInit`. */
|
|
64
|
+
transportFailure;
|
|
65
|
+
constructor(init = {}) {
|
|
66
|
+
super(init.message ?? describe(init), { cause: init.cause });
|
|
67
|
+
this.name = "AdminError";
|
|
68
|
+
this.status = init.status ?? 0;
|
|
69
|
+
this.code = init.code;
|
|
70
|
+
this.body = init.body;
|
|
71
|
+
this.url = init.url;
|
|
72
|
+
this.method = init.method;
|
|
73
|
+
this.detail = init.detail;
|
|
74
|
+
this.emptyResponse = init.emptyResponse ?? false;
|
|
75
|
+
this.transportFailure = init.transportFailure ?? false;
|
|
76
|
+
Object.defineProperty(this, ADMIN_ERROR, { value: true });
|
|
77
|
+
markPlatformError(this);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function isAdminError(err) {
|
|
81
|
+
return typeof err === "object" && err !== null && ADMIN_ERROR in err;
|
|
82
|
+
}
|
|
83
|
+
function asRecord(value) {
|
|
84
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
85
|
+
}
|
|
86
|
+
function asString(value) {
|
|
87
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
88
|
+
}
|
|
89
|
+
function isAxiosNoResponseError(value) {
|
|
90
|
+
const record = asRecord(value);
|
|
91
|
+
if (!record) return false;
|
|
92
|
+
return record.isAxiosError === true && record.request != null && record.response == null;
|
|
93
|
+
}
|
|
94
|
+
function readErrorDetail(body) {
|
|
95
|
+
const record = asRecord(body);
|
|
96
|
+
if (!record) return asString(body);
|
|
97
|
+
const message = record.message;
|
|
98
|
+
if (Array.isArray(message)) {
|
|
99
|
+
const parts = message.filter((part) => typeof part === "string");
|
|
100
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
101
|
+
}
|
|
102
|
+
return asString(message) ?? asString(record.error) ?? asString(record.reason);
|
|
103
|
+
}
|
|
104
|
+
function readErrorCode(body) {
|
|
105
|
+
return asString(asRecord(body)?.code);
|
|
106
|
+
}
|
|
107
|
+
function toAdminError(err) {
|
|
108
|
+
if (isAdminError(err)) return err;
|
|
109
|
+
const record = asRecord(err);
|
|
110
|
+
const response = asRecord(record?.response);
|
|
111
|
+
if (response && typeof response.status === "number") {
|
|
112
|
+
const config = asRecord(record?.config);
|
|
113
|
+
const body = response.data;
|
|
114
|
+
return new AdminError({
|
|
115
|
+
status: response.status,
|
|
116
|
+
code: readErrorCode(body),
|
|
117
|
+
body,
|
|
118
|
+
url: asString(config?.url),
|
|
119
|
+
method: asString(config?.method)?.toUpperCase(),
|
|
120
|
+
// Only what the response actually said. `err.message` here is
|
|
121
|
+
// axios's own generated line ("Request failed with status code
|
|
122
|
+
// 401") — English, untranslated, and about the transport rather
|
|
123
|
+
// than about what went wrong. Letting it through as `detail` would
|
|
124
|
+
// make `adminErrorMessage` return it in preference to the
|
|
125
|
+
// localized wording, which is the whole point of that wording.
|
|
126
|
+
detail: readErrorDetail(body),
|
|
127
|
+
cause: err
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (record && typeof record.status === "number") {
|
|
131
|
+
const ours = isPlatformError(err);
|
|
132
|
+
return new AdminError({
|
|
133
|
+
status: record.status,
|
|
134
|
+
code: readErrorCode(record.body) ?? asString(record.code),
|
|
135
|
+
body: record.body,
|
|
136
|
+
// Same reasoning as above: `BundlesApiError`'s message is
|
|
137
|
+
// "Bundles API responded with HTTP 403", a diagnostic. It stays on
|
|
138
|
+
// `message`, where logs read it.
|
|
139
|
+
detail: readErrorDetail(record.body) ?? (ours ? void 0 : asString(record.message)),
|
|
140
|
+
// Asked of the throw site, not of the class: only the site that
|
|
141
|
+
// read the response knows whether one arrived. `status === 0 &&
|
|
142
|
+
// ours` was the same question put to the wrong witness — the
|
|
143
|
+
// loaders raise branded errors from a status the client chose, so
|
|
144
|
+
// a client that resolves a transport failure as `0` turned a
|
|
145
|
+
// failed GET into "the change may have been applied".
|
|
146
|
+
emptyResponse: isEmptyResponse(err),
|
|
147
|
+
transportFailure: isTransportFailure(err),
|
|
148
|
+
message: asString(record.message),
|
|
149
|
+
cause: err
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (err instanceof Error) {
|
|
153
|
+
const transport = isTransportFailure(err) || isAxiosNoResponseError(err);
|
|
154
|
+
const saidSomethingReadable = !transport && !(err instanceof TypeError);
|
|
155
|
+
return new AdminError({
|
|
156
|
+
detail: saidSomethingReadable ? asString(err.message) : void 0,
|
|
157
|
+
transportFailure: transport,
|
|
158
|
+
message: err.message,
|
|
159
|
+
cause: err
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const text = asString(err) ?? asString(record?.message);
|
|
163
|
+
return new AdminError({
|
|
164
|
+
code: asString(record?.code),
|
|
165
|
+
detail: text,
|
|
166
|
+
message: text,
|
|
167
|
+
cause: err
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function statusKey(status) {
|
|
171
|
+
if (status === 400 || status === 422) return "validation";
|
|
172
|
+
if (status === 401) return "unauthorized";
|
|
173
|
+
if (status === 403) return "forbidden";
|
|
174
|
+
if (status === 404) return "notFound";
|
|
175
|
+
if (status === 409) return "conflict";
|
|
176
|
+
if (status === 429) return "rateLimited";
|
|
177
|
+
if (status >= 500) return "server";
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
function adminErrorMessage(err, msgs) {
|
|
181
|
+
const error = toAdminError(err);
|
|
182
|
+
if (error.detail) return error.detail;
|
|
183
|
+
if (error.emptyResponse) return msgs.emptyResponse;
|
|
184
|
+
if (error.transportFailure) return msgs.network;
|
|
185
|
+
if (error.status > 0) {
|
|
186
|
+
const key = statusKey(error.status);
|
|
187
|
+
return key ? msgs[key] : formatMessage(msgs.httpStatus, { status: error.status });
|
|
188
|
+
}
|
|
189
|
+
return msgs.unexpected;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/client/http-json.ts
|
|
193
|
+
function serverAnswered(status) {
|
|
194
|
+
return status > 0;
|
|
195
|
+
}
|
|
196
|
+
function requireServerAnswer(status, method, url, raise) {
|
|
197
|
+
if (serverAnswered(status)) return;
|
|
198
|
+
throw markTransportFailure(
|
|
199
|
+
raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
var HttpJsonError = AdminError;
|
|
203
|
+
async function readErrorBody(res) {
|
|
204
|
+
try {
|
|
205
|
+
return await res.json();
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function failed(res, method, url) {
|
|
211
|
+
const body = await readErrorBody(res);
|
|
212
|
+
return new AdminError({
|
|
213
|
+
status: res.status,
|
|
214
|
+
// Read here rather than left to the status: `status: 0` reaches
|
|
215
|
+
// `adminErrorMessage` as "nothing is known about this failure", and
|
|
216
|
+
// this seam knows better — it held the response.
|
|
217
|
+
transportFailure: !serverAnswered(res.status),
|
|
218
|
+
code: readErrorCode(body),
|
|
219
|
+
body,
|
|
220
|
+
url,
|
|
221
|
+
method,
|
|
222
|
+
// Shared with `toAdminError` on purpose: a NestJS `ValidationPipe`
|
|
223
|
+
// rejection carries `message` as an array, and reading it here in a
|
|
224
|
+
// second, narrower way is how those constraints went missing.
|
|
225
|
+
detail: readErrorDetail(body)
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function trimTrailingSlashes(url) {
|
|
229
|
+
let end = url.length;
|
|
230
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
231
|
+
return url.slice(0, end);
|
|
232
|
+
}
|
|
233
|
+
async function getJson(http, url) {
|
|
234
|
+
const res = await http(url);
|
|
235
|
+
if (res.status < 200 || res.status >= 300) {
|
|
236
|
+
throw await failed(res, "GET", url);
|
|
237
|
+
}
|
|
238
|
+
return await res.json();
|
|
239
|
+
}
|
|
240
|
+
async function postJson(http, url, body) {
|
|
241
|
+
const res = await http(url, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
body: JSON.stringify(body)
|
|
245
|
+
});
|
|
246
|
+
if (res.status < 200 || res.status >= 300) {
|
|
247
|
+
throw await failed(res, "POST", url);
|
|
248
|
+
}
|
|
249
|
+
return await res.json();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/client/http/fetch-http-client.ts
|
|
253
|
+
var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
|
|
254
|
+
function resolveUrl(url, baseUrl) {
|
|
255
|
+
if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
|
|
256
|
+
return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
257
|
+
}
|
|
258
|
+
function createFetchHttpClient(options = {}) {
|
|
259
|
+
return async (url, init) => {
|
|
260
|
+
const headers = new Headers(await options.headers?.());
|
|
261
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
262
|
+
for (const [name, value] of Object.entries(init?.headers ?? {})) {
|
|
263
|
+
headers.set(name, value);
|
|
264
|
+
}
|
|
265
|
+
if (init?.body !== void 0 && !headers.has("Content-Type")) {
|
|
266
|
+
headers.set("Content-Type", "application/json");
|
|
267
|
+
}
|
|
268
|
+
let response;
|
|
269
|
+
try {
|
|
270
|
+
response = await fetch(resolveUrl(url, options.baseUrl), {
|
|
271
|
+
method: init?.method ?? "GET",
|
|
272
|
+
headers,
|
|
273
|
+
body: init?.body
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw markTransportFailure(err);
|
|
277
|
+
}
|
|
278
|
+
return response;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
1
282
|
// src/client/types.ts
|
|
2
283
|
var NOOP_KV_STORE = {
|
|
3
284
|
get: () => null,
|
|
@@ -39,8 +320,385 @@ function resolveLocalStorage() {
|
|
|
39
320
|
}
|
|
40
321
|
}
|
|
41
322
|
function defaultHttpClient() {
|
|
42
|
-
return (
|
|
323
|
+
return createFetchHttpClient();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/client/resources/define-resource.ts
|
|
327
|
+
function defineResource(key, ops, options = {}) {
|
|
328
|
+
return { key, ops, projectScoped: options.projectScoped ?? false };
|
|
329
|
+
}
|
|
330
|
+
function requireProjectKey(def, ctx) {
|
|
331
|
+
if (!def.projectScoped) return;
|
|
332
|
+
if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Resource "${def.key}" is project-scoped, but the context it is bound to names no project. Its requests would go out as \`?projectKey=\`, which the admin API answers for no project at all: the catalogue reads as empty and the first create fails validation instead. Name the project this admin administers \u2014 \`endpoints.projectKey\` for createSuperAdminApp(), \`context.projectKey\` for createResourceRegistry().`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
function bindResource(def, http, ctx) {
|
|
338
|
+
const readContext = typeof ctx === "function" ? ctx : () => ctx;
|
|
339
|
+
if (def.projectScoped) requireProjectKey(def, readContext());
|
|
340
|
+
const bound = {};
|
|
341
|
+
for (const [name, op] of Object.entries(def.ops)) {
|
|
342
|
+
bound[name] = async (...args) => {
|
|
343
|
+
const context = readContext();
|
|
344
|
+
requireProjectKey(def, context);
|
|
345
|
+
return op(http, context, ...args);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return bound;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/client/resources/resource-request.ts
|
|
352
|
+
async function requestJson(http, url, init = {}) {
|
|
353
|
+
const method = init.method ?? "GET";
|
|
354
|
+
const response = await http(url, {
|
|
355
|
+
method,
|
|
356
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
357
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
358
|
+
});
|
|
359
|
+
requireServerAnswer(
|
|
360
|
+
response.status,
|
|
361
|
+
method,
|
|
362
|
+
url,
|
|
363
|
+
(diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
|
|
364
|
+
);
|
|
365
|
+
if (response.status === 204) return null;
|
|
366
|
+
const body = await response.json().catch(() => null);
|
|
367
|
+
if (response.status >= 400) {
|
|
368
|
+
throw new AdminError({
|
|
369
|
+
status: response.status,
|
|
370
|
+
code: readErrorCode(body),
|
|
371
|
+
body,
|
|
372
|
+
// The third place this had to be read, and the second time it was
|
|
373
|
+
// read a narrower way: without `detail` a page migrating onto these
|
|
374
|
+
// resources loses "Plan already exists" and shows the generic
|
|
375
|
+
// status sentence instead. One decision, one function.
|
|
376
|
+
detail: readErrorDetail(body),
|
|
377
|
+
url,
|
|
378
|
+
method
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return body;
|
|
382
|
+
}
|
|
383
|
+
async function requestJsonBody(http, url, what, init = {}) {
|
|
384
|
+
const body = await requestJson(http, url, init);
|
|
385
|
+
if (body === null || body === void 0) {
|
|
386
|
+
throw new AdminError({
|
|
387
|
+
status: 0,
|
|
388
|
+
url,
|
|
389
|
+
method: init.method ?? "GET",
|
|
390
|
+
// `what` is a diagnostic — "Create returned no body" — so it goes
|
|
391
|
+
// on `message`, not `detail`. As `detail` it out-ranked the
|
|
392
|
+
// localized wording and the operator read an English internal
|
|
393
|
+
// string instead of being told the change may already have landed.
|
|
394
|
+
message: what,
|
|
395
|
+
emptyResponse: true
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return body;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/client/resources/list-resource.ts
|
|
402
|
+
var LIST_FIRST_PAGE = 1;
|
|
403
|
+
var LIST_PAGE_SIZE_DEFAULT = 50;
|
|
404
|
+
var LIST_PAGE_SIZE_MAX = 200;
|
|
405
|
+
var LIST_PAGINATION_PARAMS = ["page", "pageSize"];
|
|
406
|
+
function isSentInQuery(value) {
|
|
407
|
+
return value !== void 0 && value !== null && value !== "";
|
|
408
|
+
}
|
|
409
|
+
function appendFilter(params, filter) {
|
|
410
|
+
for (const [key, value] of Object.entries(filter ?? {})) {
|
|
411
|
+
if (!isSentInQuery(value)) continue;
|
|
412
|
+
params.set(key, String(value));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function filterQueryString(filter) {
|
|
416
|
+
const params = new URLSearchParams();
|
|
417
|
+
appendFilter(params, filter);
|
|
418
|
+
const query = params.toString();
|
|
419
|
+
return query ? `?${query}` : "";
|
|
420
|
+
}
|
|
421
|
+
function listUrl(endpoint, query = {}) {
|
|
422
|
+
const params = new URLSearchParams();
|
|
423
|
+
params.set("page", String(query.page ?? LIST_FIRST_PAGE));
|
|
424
|
+
params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
|
|
425
|
+
appendFilter(params, query.filter);
|
|
426
|
+
return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
427
|
+
}
|
|
428
|
+
function readListPage(raw) {
|
|
429
|
+
if (Array.isArray(raw)) return { items: raw, total: raw.length };
|
|
430
|
+
if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
|
|
431
|
+
const body = raw;
|
|
432
|
+
const page = {
|
|
433
|
+
items: Array.isArray(body.items) ? body.items : []
|
|
434
|
+
};
|
|
435
|
+
if (typeof body.total === "number") page.total = body.total;
|
|
436
|
+
if (typeof body.page === "number") page.page = body.page;
|
|
437
|
+
if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
|
|
438
|
+
return page;
|
|
439
|
+
}
|
|
440
|
+
function clampListPage(page) {
|
|
441
|
+
return Math.max(LIST_FIRST_PAGE, Math.floor(page));
|
|
442
|
+
}
|
|
443
|
+
function clampListPageSize(size) {
|
|
444
|
+
return Math.min(LIST_PAGE_SIZE_MAX, Math.max(1, Math.floor(size)));
|
|
43
445
|
}
|
|
446
|
+
function defineListOp(endpoint) {
|
|
447
|
+
return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/client/resources/plans.resource.ts
|
|
451
|
+
function plansUrl(ctx) {
|
|
452
|
+
return `${ctx.apiBase}/catalog/plans`;
|
|
453
|
+
}
|
|
454
|
+
function versionsUrl(ctx) {
|
|
455
|
+
return `${ctx.apiBase}/catalog/plan-versions`;
|
|
456
|
+
}
|
|
457
|
+
function scoped(url, ctx) {
|
|
458
|
+
return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
|
|
459
|
+
}
|
|
460
|
+
var plansResource = defineResource(
|
|
461
|
+
"plans",
|
|
462
|
+
{
|
|
463
|
+
list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
|
|
464
|
+
tenantCounts: async (http, ctx) => await requestJson(
|
|
465
|
+
http,
|
|
466
|
+
scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
|
|
467
|
+
) ?? {},
|
|
468
|
+
/**
|
|
469
|
+
* The project comes from the context, not from the caller.
|
|
470
|
+
*
|
|
471
|
+
* The endpoint takes it in the body, and the composable this replaces
|
|
472
|
+
* made that the caller's job — so a page had to keep hold of the
|
|
473
|
+
* project it was administering just to create in it, and `list()` and
|
|
474
|
+
* `create()` read two different sources for one value. A stale one
|
|
475
|
+
* creates a plan outside the project the list is showing.
|
|
476
|
+
*/
|
|
477
|
+
create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
|
|
478
|
+
method: "POST",
|
|
479
|
+
body: { ...data, projectKey: ctx.projectKey }
|
|
480
|
+
}),
|
|
481
|
+
update: (http, ctx, planId, data) => requestJsonBody(
|
|
482
|
+
http,
|
|
483
|
+
`${plansUrl(ctx)}/${planId}`,
|
|
484
|
+
"Update returned no body",
|
|
485
|
+
{
|
|
486
|
+
method: "PATCH",
|
|
487
|
+
body: data
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
/** Marks the plan deleted. Fails while it still has versions. */
|
|
491
|
+
softDelete: async (http, ctx, planId) => {
|
|
492
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
|
|
493
|
+
},
|
|
494
|
+
/** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
|
|
495
|
+
hardDelete: async (http, ctx, planId) => {
|
|
496
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
{ projectScoped: true }
|
|
500
|
+
);
|
|
501
|
+
var planVersionsResource = defineResource("planVersions", {
|
|
502
|
+
listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
|
|
503
|
+
createDraft: (http, ctx, planId, data) => requestJsonBody(
|
|
504
|
+
http,
|
|
505
|
+
`${plansUrl(ctx)}/${planId}/versions`,
|
|
506
|
+
"CreateDraft returned no body",
|
|
507
|
+
{ method: "POST", body: data }
|
|
508
|
+
),
|
|
509
|
+
updateDraft: (http, ctx, versionId, data) => requestJsonBody(
|
|
510
|
+
http,
|
|
511
|
+
`${versionsUrl(ctx)}/${versionId}`,
|
|
512
|
+
"UpdateDraft returned no body",
|
|
513
|
+
{ method: "PATCH", body: data }
|
|
514
|
+
),
|
|
515
|
+
publish: (http, ctx, versionId, options = {}) => requestJsonBody(
|
|
516
|
+
http,
|
|
517
|
+
`${versionsUrl(ctx)}/${versionId}/publish`,
|
|
518
|
+
"Publish returned no body",
|
|
519
|
+
{ method: "POST", body: options }
|
|
520
|
+
),
|
|
521
|
+
discardDraft: async (http, ctx, versionId) => {
|
|
522
|
+
await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
|
|
523
|
+
},
|
|
524
|
+
/**
|
|
525
|
+
* Ends a live version on `endsAt`, with no successor. Idempotent — a
|
|
526
|
+
* second call with another date overwrites the first.
|
|
527
|
+
*
|
|
528
|
+
* `endsAt` is the date itself, not a payload: the endpoint's body is
|
|
529
|
+
* `{ endsAt }` and wrapping it is this operation's job, not the caller's.
|
|
530
|
+
*/
|
|
531
|
+
terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
|
|
532
|
+
http,
|
|
533
|
+
`${versionsUrl(ctx)}/${versionId}/terminate`,
|
|
534
|
+
"Terminate returned no body",
|
|
535
|
+
{ method: "POST", body: { endsAt } }
|
|
536
|
+
)
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// src/client/resources/tenants.resource.ts
|
|
540
|
+
var tenantsResource = defineResource("tenants", {
|
|
541
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// src/client/resources/audit.resource.ts
|
|
545
|
+
var auditResource = defineResource("audit", {
|
|
546
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// src/client/resources/index.ts
|
|
550
|
+
var platformResources = {
|
|
551
|
+
plans: plansResource,
|
|
552
|
+
planVersions: planVersionsResource,
|
|
553
|
+
tenants: tenantsResource,
|
|
554
|
+
audit: auditResource
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/client/boot-loader.ts
|
|
558
|
+
var BootLoadError = class extends Error {
|
|
559
|
+
constructor(status, message) {
|
|
560
|
+
super(message);
|
|
561
|
+
this.status = status;
|
|
562
|
+
this.name = "BootLoadError";
|
|
563
|
+
markPlatformError(this);
|
|
564
|
+
}
|
|
565
|
+
status;
|
|
566
|
+
};
|
|
567
|
+
var BootLoader = class {
|
|
568
|
+
endpoint;
|
|
569
|
+
http;
|
|
570
|
+
constructor(options) {
|
|
571
|
+
if (!options?.endpoint) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
'BootLoader: `endpoint` is required (e.g. "/api/admin/boot" or "/api/v1/admin/boot"). The platform has no default because apps use different globalPrefix conventions.'
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
this.endpoint = options.endpoint;
|
|
577
|
+
this.http = options.http ?? defaultHttpClient();
|
|
578
|
+
}
|
|
579
|
+
async load() {
|
|
580
|
+
const res = await this.http(this.endpoint);
|
|
581
|
+
requireServerAnswer(
|
|
582
|
+
res.status,
|
|
583
|
+
"GET",
|
|
584
|
+
this.endpoint,
|
|
585
|
+
(diagnostic) => new BootLoadError(res.status, diagnostic)
|
|
586
|
+
);
|
|
587
|
+
if (res.status !== 200) {
|
|
588
|
+
throw new BootLoadError(res.status, `Boot endpoint responded with HTTP ${res.status}`);
|
|
589
|
+
}
|
|
590
|
+
return await res.json();
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/client/manifest-loader.ts
|
|
595
|
+
var NOT_MODIFIED = 304;
|
|
596
|
+
var ManifestLoadError = class extends Error {
|
|
597
|
+
constructor(status, message) {
|
|
598
|
+
super(message);
|
|
599
|
+
this.status = status;
|
|
600
|
+
this.name = "ManifestLoadError";
|
|
601
|
+
markPlatformError(this);
|
|
602
|
+
}
|
|
603
|
+
status;
|
|
604
|
+
};
|
|
605
|
+
var ManifestLoader = class {
|
|
606
|
+
endpoint;
|
|
607
|
+
http;
|
|
608
|
+
storage;
|
|
609
|
+
bodyKey;
|
|
610
|
+
etagKey;
|
|
611
|
+
getAuthToken;
|
|
612
|
+
constructor(options) {
|
|
613
|
+
if (!options?.endpoint) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
'ManifestLoader: `endpoint` is required (e.g. "/api/admin/manifest" or "/api/v1/admin/manifest"). The platform has no default because apps use different globalPrefix conventions.'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
this.endpoint = options.endpoint;
|
|
619
|
+
this.http = options.http ?? defaultHttpClient();
|
|
620
|
+
this.storage = options.storage ?? defaultKvStore();
|
|
621
|
+
const prefix = options.storageKeyPrefix ?? "";
|
|
622
|
+
this.bodyKey = `${prefix}manifest:body`;
|
|
623
|
+
this.etagKey = `${prefix}manifest:etag`;
|
|
624
|
+
this.getAuthToken = options.getAuthToken;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Loads the current manifest. On a cache hit (304) the cached
|
|
628
|
+
* body is returned — otherwise the fresh server body.
|
|
629
|
+
*
|
|
630
|
+
* A 304 with no usable cached body is not a failure to report but one to
|
|
631
|
+
* undo: the request only carried `If-None-Match` because this loader put
|
|
632
|
+
* the ETag there, and the cache it validated against is this loader's own.
|
|
633
|
+
* So it drops the pair and asks once more without the header, which is the
|
|
634
|
+
* same recovery a person would have had to trigger by hand — except that
|
|
635
|
+
* the person reading an admin screen has neither this loader nor a
|
|
636
|
+
* console. Exactly one retry: a 304 to a request that asked nothing
|
|
637
|
+
* conditional is a server fault, and no repetition fixes it.
|
|
638
|
+
*/
|
|
639
|
+
async load() {
|
|
640
|
+
const cachedEtag = this.storage.get(this.etagKey);
|
|
641
|
+
const res = await this.request(cachedEtag);
|
|
642
|
+
if (res.status !== NOT_MODIFIED) return this.acceptFresh(res);
|
|
643
|
+
const cached = this.readCachedBody();
|
|
644
|
+
if (cached) return cached.body;
|
|
645
|
+
if (cachedEtag) {
|
|
646
|
+
this.clearCache();
|
|
647
|
+
const fresh = await this.request(null);
|
|
648
|
+
if (fresh.status !== NOT_MODIFIED) return this.acceptFresh(fresh);
|
|
649
|
+
}
|
|
650
|
+
throw new ManifestLoadError(
|
|
651
|
+
NOT_MODIFIED,
|
|
652
|
+
"Manifest endpoint answered HTTP 304 to a request without If-None-Match"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
/** Sends the manifest GET, conditional when an ETag is supplied. */
|
|
656
|
+
request(etag) {
|
|
657
|
+
const headers = {};
|
|
658
|
+
const token = this.getAuthToken?.();
|
|
659
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
660
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
661
|
+
return this.http(this.endpoint, { method: "GET", headers });
|
|
662
|
+
}
|
|
663
|
+
/** Takes a non-304 response as the current manifest and caches it. */
|
|
664
|
+
async acceptFresh(res) {
|
|
665
|
+
requireServerAnswer(
|
|
666
|
+
res.status,
|
|
667
|
+
"GET",
|
|
668
|
+
this.endpoint,
|
|
669
|
+
(diagnostic) => new ManifestLoadError(res.status, diagnostic)
|
|
670
|
+
);
|
|
671
|
+
if (res.status !== 200) {
|
|
672
|
+
throw new ManifestLoadError(
|
|
673
|
+
res.status,
|
|
674
|
+
`Manifest endpoint responded with HTTP ${res.status}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const body = await res.json();
|
|
678
|
+
const etag = res.headers.get("ETag") ?? res.headers.get("etag");
|
|
679
|
+
if (etag) {
|
|
680
|
+
this.storage.set(this.etagKey, etag);
|
|
681
|
+
this.storage.set(this.bodyKey, JSON.stringify(body));
|
|
682
|
+
}
|
|
683
|
+
return body;
|
|
684
|
+
}
|
|
685
|
+
/** Reads the cached manifest body from storage; null if absent. */
|
|
686
|
+
readCachedBody() {
|
|
687
|
+
const etag = this.storage.get(this.etagKey);
|
|
688
|
+
const bodyStr = this.storage.get(this.bodyKey);
|
|
689
|
+
if (!etag || !bodyStr) return null;
|
|
690
|
+
try {
|
|
691
|
+
return { etag, body: JSON.parse(bodyStr) };
|
|
692
|
+
} catch {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** Clears the cache — e.g. on logout or after `manifest reload`. */
|
|
697
|
+
clearCache() {
|
|
698
|
+
this.storage.remove(this.etagKey);
|
|
699
|
+
this.storage.remove(this.bodyKey);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
44
702
|
|
|
45
703
|
// src/client/i18n/locale.ts
|
|
46
704
|
var SA_LOCALES = ["de", "en"];
|
|
@@ -78,12 +736,6 @@ function mergeMessages(base, overrides) {
|
|
|
78
736
|
return result;
|
|
79
737
|
}
|
|
80
738
|
|
|
81
|
-
// src/client/i18n/format.ts
|
|
82
|
-
import { formatErrorMessage } from "@saasicat/types";
|
|
83
|
-
function formatMessage(template, params) {
|
|
84
|
-
return formatErrorMessage(template, params);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
739
|
// src/client/i18n/currency.ts
|
|
88
740
|
var DEFAULT_CURRENCY = "EUR";
|
|
89
741
|
var formatters = /* @__PURE__ */ new Map();
|
|
@@ -781,9 +1433,6 @@ var discoveryMessages = defineMessages(
|
|
|
781
1433
|
replacedBy: "ersetzt durch {key}",
|
|
782
1434
|
replaces: "ersetzt: {keys}",
|
|
783
1435
|
reapproveEmphasis: "erneut freigeben",
|
|
784
|
-
errorDiscoveryHttp: "Discovery-Endpoint antwortete mit HTTP {status}",
|
|
785
|
-
errorRescanHttp: "Discovery-Rescan antwortete mit HTTP {status}",
|
|
786
|
-
errorCatalogHttp: "Catalog-Entries-API antwortete mit HTTP {status}",
|
|
787
1436
|
capList: {
|
|
788
1437
|
orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
|
|
789
1438
|
removedFromCode: "aus Code entfernt"
|
|
@@ -885,9 +1534,6 @@ var discoveryMessages = defineMessages(
|
|
|
885
1534
|
replacedBy: "replaced by {key}",
|
|
886
1535
|
replaces: "replaces: {keys}",
|
|
887
1536
|
reapproveEmphasis: "approve again",
|
|
888
|
-
errorDiscoveryHttp: "Discovery endpoint responded with HTTP {status}",
|
|
889
|
-
errorRescanHttp: "Discovery rescan responded with HTTP {status}",
|
|
890
|
-
errorCatalogHttp: "Catalog entries API responded with HTTP {status}",
|
|
891
1537
|
capList: {
|
|
892
1538
|
orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
|
|
893
1539
|
removedFromCode: "removed from code"
|
|
@@ -1092,6 +1738,36 @@ var emailMessages = defineMessages(
|
|
|
1092
1738
|
}
|
|
1093
1739
|
);
|
|
1094
1740
|
|
|
1741
|
+
// src/client/i18n/messages/errors.ts
|
|
1742
|
+
var errorsMessages = defineMessages(
|
|
1743
|
+
{
|
|
1744
|
+
network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
|
|
1745
|
+
emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
|
|
1746
|
+
unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
|
|
1747
|
+
forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
|
|
1748
|
+
notFound: "Der angeforderte Eintrag existiert nicht mehr.",
|
|
1749
|
+
conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
|
|
1750
|
+
validation: "Die Eingaben wurden nicht akzeptiert.",
|
|
1751
|
+
rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
|
|
1752
|
+
server: "Der Server konnte die Anfrage nicht verarbeiten.",
|
|
1753
|
+
httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
|
|
1754
|
+
unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
network: "The request could not be completed. Please check your connection.",
|
|
1758
|
+
emptyResponse: "The server returned nothing. Please check whether the change was applied.",
|
|
1759
|
+
unauthorized: "Your session has expired. Please sign in again.",
|
|
1760
|
+
forbidden: "You do not have permission for this action.",
|
|
1761
|
+
notFound: "The requested entry no longer exists.",
|
|
1762
|
+
conflict: "The entry changed in the meantime. Please reload.",
|
|
1763
|
+
validation: "The input was not accepted.",
|
|
1764
|
+
rateLimited: "Too many requests. Please wait a moment and try again.",
|
|
1765
|
+
server: "The server could not process the request.",
|
|
1766
|
+
httpStatus: "The request failed (HTTP {status}).",
|
|
1767
|
+
unexpected: "Something went wrong. Please try again."
|
|
1768
|
+
}
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1095
1771
|
// src/client/i18n/messages/marketing.ts
|
|
1096
1772
|
var marketingMessages = defineMessages(
|
|
1097
1773
|
{
|
|
@@ -1216,9 +1892,6 @@ var marketingMessages = defineMessages(
|
|
|
1216
1892
|
colorLabel: "Farbe",
|
|
1217
1893
|
delete: "Aktion l\xF6schen",
|
|
1218
1894
|
deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
|
|
1219
|
-
},
|
|
1220
|
-
errors: {
|
|
1221
|
-
projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
|
|
1222
1895
|
}
|
|
1223
1896
|
},
|
|
1224
1897
|
{
|
|
@@ -1343,9 +2016,6 @@ var marketingMessages = defineMessages(
|
|
|
1343
2016
|
colorLabel: "Color",
|
|
1344
2017
|
delete: "Delete promotion",
|
|
1345
2018
|
deleteConfirm: 'Really delete promotion "{label}"?'
|
|
1346
|
-
},
|
|
1347
|
-
errors: {
|
|
1348
|
-
projectionsApi: "MarketingProjections API responded with HTTP {status}"
|
|
1349
2019
|
}
|
|
1350
2020
|
}
|
|
1351
2021
|
);
|
|
@@ -2589,7 +3259,6 @@ var promosMessages = defineMessages(
|
|
|
2589
3259
|
{
|
|
2590
3260
|
title: "Promo-Codes",
|
|
2591
3261
|
createAction: "Promo-Code anlegen",
|
|
2592
|
-
apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
|
|
2593
3262
|
list: {
|
|
2594
3263
|
searchLabel: "Suche (Code)",
|
|
2595
3264
|
tileScheduled: "Geplant",
|
|
@@ -2687,7 +3356,6 @@ var promosMessages = defineMessages(
|
|
|
2687
3356
|
{
|
|
2688
3357
|
title: "Promo codes",
|
|
2689
3358
|
createAction: "Create promo code",
|
|
2690
|
-
apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
|
|
2691
3359
|
list: {
|
|
2692
3360
|
searchLabel: "Search (code)",
|
|
2693
3361
|
tileScheduled: "Scheduled",
|
|
@@ -2795,7 +3463,13 @@ var shellMessages = defineMessages(
|
|
|
2795
3463
|
subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
|
|
2796
3464
|
roleBadge: "SUPER ADMIN",
|
|
2797
3465
|
logout: "Abmelden",
|
|
2798
|
-
language: "Sprache"
|
|
3466
|
+
language: "Sprache",
|
|
3467
|
+
theme: {
|
|
3468
|
+
label: "Darstellung",
|
|
3469
|
+
light: "Hell",
|
|
3470
|
+
dark: "Dunkel",
|
|
3471
|
+
system: "System"
|
|
3472
|
+
}
|
|
2799
3473
|
},
|
|
2800
3474
|
drawer: {
|
|
2801
3475
|
docs: "Doku \xF6ffnen"
|
|
@@ -2864,7 +3538,13 @@ var shellMessages = defineMessages(
|
|
|
2864
3538
|
subtitle: "SuperAdmin \xB7 Platform administration",
|
|
2865
3539
|
roleBadge: "SUPER ADMIN",
|
|
2866
3540
|
logout: "Sign out",
|
|
2867
|
-
language: "Language"
|
|
3541
|
+
language: "Language",
|
|
3542
|
+
theme: {
|
|
3543
|
+
label: "Appearance",
|
|
3544
|
+
light: "Light",
|
|
3545
|
+
dark: "Dark",
|
|
3546
|
+
system: "System"
|
|
3547
|
+
}
|
|
2868
3548
|
},
|
|
2869
3549
|
drawer: {
|
|
2870
3550
|
docs: "Open documentation"
|
|
@@ -3108,6 +3788,7 @@ var usersMessages = defineMessages(
|
|
|
3108
3788
|
function catalogFor(locale) {
|
|
3109
3789
|
return {
|
|
3110
3790
|
common: commonMessages[locale],
|
|
3791
|
+
errors: errorsMessages[locale],
|
|
3111
3792
|
nav: navMessages[locale],
|
|
3112
3793
|
shell: shellMessages[locale],
|
|
3113
3794
|
dashboard: dashboardMessages[locale],
|
|
@@ -3167,8 +3848,52 @@ function createSaCatalog(options = {}) {
|
|
|
3167
3848
|
}
|
|
3168
3849
|
|
|
3169
3850
|
export {
|
|
3851
|
+
formatMessage,
|
|
3852
|
+
markPlatformError,
|
|
3853
|
+
isPlatformError,
|
|
3854
|
+
markEmptyResponse,
|
|
3855
|
+
isEmptyResponse,
|
|
3856
|
+
markTransportFailure,
|
|
3857
|
+
isTransportFailure,
|
|
3858
|
+
AdminError,
|
|
3859
|
+
isAdminError,
|
|
3860
|
+
isAxiosNoResponseError,
|
|
3861
|
+
readErrorDetail,
|
|
3862
|
+
readErrorCode,
|
|
3863
|
+
toAdminError,
|
|
3864
|
+
adminErrorMessage,
|
|
3865
|
+
requireServerAnswer,
|
|
3866
|
+
HttpJsonError,
|
|
3867
|
+
trimTrailingSlashes,
|
|
3868
|
+
getJson,
|
|
3869
|
+
postJson,
|
|
3870
|
+
createFetchHttpClient,
|
|
3170
3871
|
defaultKvStore,
|
|
3171
3872
|
defaultHttpClient,
|
|
3873
|
+
defineResource,
|
|
3874
|
+
bindResource,
|
|
3875
|
+
requestJson,
|
|
3876
|
+
requestJsonBody,
|
|
3877
|
+
LIST_FIRST_PAGE,
|
|
3878
|
+
LIST_PAGE_SIZE_DEFAULT,
|
|
3879
|
+
LIST_PAGE_SIZE_MAX,
|
|
3880
|
+
LIST_PAGINATION_PARAMS,
|
|
3881
|
+
isSentInQuery,
|
|
3882
|
+
filterQueryString,
|
|
3883
|
+
listUrl,
|
|
3884
|
+
readListPage,
|
|
3885
|
+
clampListPage,
|
|
3886
|
+
clampListPageSize,
|
|
3887
|
+
defineListOp,
|
|
3888
|
+
plansResource,
|
|
3889
|
+
planVersionsResource,
|
|
3890
|
+
tenantsResource,
|
|
3891
|
+
auditResource,
|
|
3892
|
+
platformResources,
|
|
3893
|
+
BootLoadError,
|
|
3894
|
+
BootLoader,
|
|
3895
|
+
ManifestLoadError,
|
|
3896
|
+
ManifestLoader,
|
|
3172
3897
|
SA_LOCALES,
|
|
3173
3898
|
DEFAULT_SA_LOCALE,
|
|
3174
3899
|
SA_INTL_LOCALES,
|
|
@@ -3177,7 +3902,6 @@ export {
|
|
|
3177
3902
|
defineMessages,
|
|
3178
3903
|
mergeMessages,
|
|
3179
3904
|
navMessages,
|
|
3180
|
-
formatMessage,
|
|
3181
3905
|
formatCurrency,
|
|
3182
3906
|
SA_MESSAGES,
|
|
3183
3907
|
resolveMessages,
|