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