@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,169 +1,103 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_SA_LOCALE,
|
|
3
3
|
defaultHttpClient,
|
|
4
|
-
|
|
4
|
+
filterQueryString,
|
|
5
|
+
isAxiosNoResponseError,
|
|
5
6
|
isSaBuiltinLocale,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
markTransportFailure,
|
|
8
|
+
navMessages,
|
|
9
|
+
trimTrailingSlashes
|
|
10
|
+
} from "./chunk-BPF2BMCQ.js";
|
|
8
11
|
|
|
9
12
|
// src/client/version.ts
|
|
10
13
|
var ADMIN_UI_VERSION = "1.2.0";
|
|
11
14
|
|
|
12
|
-
// src/client/http-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async function extractCode(res) {
|
|
24
|
-
try {
|
|
25
|
-
const body = await res.json();
|
|
26
|
-
return typeof body?.code === "string" ? body.code : void 0;
|
|
27
|
-
} catch {
|
|
28
|
-
return void 0;
|
|
29
|
-
}
|
|
15
|
+
// src/client/http/axios-http-client.ts
|
|
16
|
+
function stripped(url, prefixes) {
|
|
17
|
+
for (const prefix of prefixes) {
|
|
18
|
+
const trimmed = trimTrailingSlashes(prefix);
|
|
19
|
+
if (!trimmed || !url.startsWith(trimmed)) continue;
|
|
20
|
+
const rest = url.slice(trimmed.length);
|
|
21
|
+
if (rest === "") return "/";
|
|
22
|
+
if (rest.startsWith("/")) return rest;
|
|
23
|
+
if (rest.startsWith("?") || rest.startsWith("#")) return `/${rest}`;
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
30
26
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
function headerReader(headers) {
|
|
28
|
+
const record = typeof headers === "object" && headers !== null ? headers : {};
|
|
29
|
+
const lowered = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const [key, value] of Object.entries(record)) lowered.set(key.toLowerCase(), value);
|
|
31
|
+
return (name) => {
|
|
32
|
+
const value = lowered.get(name.toLowerCase());
|
|
33
|
+
return value == null ? null : String(value);
|
|
34
|
+
};
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
function bodyIsRaw(response, declared) {
|
|
37
|
+
if (declared !== "auto") return declared === "raw";
|
|
38
|
+
if (response.data === "") return true;
|
|
39
|
+
const config = response.config;
|
|
40
|
+
if (config?.transformResponse === null) return true;
|
|
41
|
+
if (Array.isArray(config?.transformResponse) && config.transformResponse.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const responseType = config?.responseType;
|
|
45
|
+
if (responseType === "json") return false;
|
|
46
|
+
if (typeof responseType === "string" && responseType !== "") return true;
|
|
47
|
+
const transitional = config?.transitional;
|
|
48
|
+
return typeof transitional === "object" && transitional !== null && transitional.forcedJSONParsing === false;
|
|
42
49
|
}
|
|
43
|
-
async function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
async function readBody(data) {
|
|
51
|
+
if (typeof data === "string" || data === null || data === void 0) return data;
|
|
52
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
53
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
|
|
54
|
+
const maybe = data;
|
|
55
|
+
if (typeof maybe.pipe === "function" || typeof maybe.getReader === "function") {
|
|
56
|
+
throw new TypeError(
|
|
57
|
+
"createAxiosHttpClient cannot read a streamed body: responseType 'stream' can be consumed only once, and never synchronously. Use 'arraybuffer' or the default for the instance the platform receives."
|
|
58
|
+
);
|
|
51
59
|
}
|
|
52
|
-
|
|
60
|
+
if (typeof maybe.text === "function") return maybe.text.call(data);
|
|
61
|
+
return data;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
http;
|
|
67
|
-
constructor(options) {
|
|
68
|
-
if (!options?.endpoint) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
'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.'
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
this.endpoint = options.endpoint;
|
|
74
|
-
this.http = options.http ?? defaultHttpClient();
|
|
75
|
-
}
|
|
76
|
-
async load() {
|
|
77
|
-
const res = await this.http(this.endpoint);
|
|
78
|
-
if (res.status !== 200) {
|
|
79
|
-
throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
|
|
63
|
+
function adapt(response, declared) {
|
|
64
|
+
return {
|
|
65
|
+
status: response.status,
|
|
66
|
+
headers: { get: headerReader(response.headers) },
|
|
67
|
+
json: async () => {
|
|
68
|
+
const data = await readBody(response.data);
|
|
69
|
+
if (typeof data !== "string") return data;
|
|
70
|
+
return bodyIsRaw({ ...response, data }, declared) ? JSON.parse(data) : data;
|
|
71
|
+
},
|
|
72
|
+
text: async () => {
|
|
73
|
+
const data = await readBody(response.data);
|
|
74
|
+
return typeof data === "string" ? data : JSON.stringify(data);
|
|
80
75
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
http;
|
|
97
|
-
storage;
|
|
98
|
-
bodyKey;
|
|
99
|
-
etagKey;
|
|
100
|
-
getAuthToken;
|
|
101
|
-
constructor(options) {
|
|
102
|
-
if (!options?.endpoint) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
'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.'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createAxiosHttpClient(instance, options = {}) {
|
|
79
|
+
const prefixes = typeof options.stripPrefix === "string" ? [options.stripPrefix] : options.stripPrefix ?? [];
|
|
80
|
+
const responseBody = options.responseBody ?? "auto";
|
|
81
|
+
return async (url, init) => {
|
|
82
|
+
try {
|
|
83
|
+
return adapt(
|
|
84
|
+
await instance.request({
|
|
85
|
+
url: stripped(url, prefixes),
|
|
86
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
87
|
+
headers: init?.headers,
|
|
88
|
+
data: init?.body
|
|
89
|
+
}),
|
|
90
|
+
responseBody
|
|
105
91
|
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const prefix = options.storageKeyPrefix ?? "";
|
|
111
|
-
this.bodyKey = `${prefix}manifest:body`;
|
|
112
|
-
this.etagKey = `${prefix}manifest:etag`;
|
|
113
|
-
this.getAuthToken = options.getAuthToken;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Loads the current manifest. On a cache hit (304) the cached
|
|
117
|
-
* body is returned — otherwise the fresh server body.
|
|
118
|
-
*/
|
|
119
|
-
async load() {
|
|
120
|
-
const cachedEtag = this.storage.get(this.etagKey);
|
|
121
|
-
const headers = {};
|
|
122
|
-
const token = this.getAuthToken?.();
|
|
123
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
124
|
-
if (cachedEtag) headers["If-None-Match"] = cachedEtag;
|
|
125
|
-
const res = await this.http(this.endpoint, { method: "GET", headers });
|
|
126
|
-
if (res.status === 304) {
|
|
127
|
-
const cachedBody = this.readCachedBody();
|
|
128
|
-
if (!cachedBody) {
|
|
129
|
-
throw new ManifestLoadError(
|
|
130
|
-
304,
|
|
131
|
-
"Server returned 304 but the cache body is missing \u2014 call clearCache() and reload"
|
|
132
|
-
);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const response = err?.response;
|
|
94
|
+
if (response && typeof response.status === "number") {
|
|
95
|
+
return adapt(response, responseBody);
|
|
133
96
|
}
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (res.status !== 200) {
|
|
137
|
-
throw new ManifestLoadError(
|
|
138
|
-
res.status,
|
|
139
|
-
`Manifest endpoint responded with HTTP ${res.status}`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const body = await res.json();
|
|
143
|
-
const etag = res.headers.get("ETag") ?? res.headers.get("etag");
|
|
144
|
-
if (etag) {
|
|
145
|
-
this.storage.set(this.etagKey, etag);
|
|
146
|
-
this.storage.set(this.bodyKey, JSON.stringify(body));
|
|
147
|
-
}
|
|
148
|
-
return body;
|
|
149
|
-
}
|
|
150
|
-
/** Reads the cached manifest body from storage; null if absent. */
|
|
151
|
-
readCachedBody() {
|
|
152
|
-
const etag = this.storage.get(this.etagKey);
|
|
153
|
-
const bodyStr = this.storage.get(this.bodyKey);
|
|
154
|
-
if (!etag || !bodyStr) return null;
|
|
155
|
-
try {
|
|
156
|
-
return { etag, body: JSON.parse(bodyStr) };
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
97
|
+
throw isAxiosNoResponseError(err) ? markTransportFailure(err) : err;
|
|
159
98
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
clearCache() {
|
|
163
|
-
this.storage.remove(this.etagKey);
|
|
164
|
-
this.storage.remove(this.bodyKey);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
167
101
|
|
|
168
102
|
// src/client/nav-builder.ts
|
|
169
103
|
var DEFAULT_STANDARD_PAGE_ROUTES = {
|
|
@@ -381,7 +315,7 @@ var ActionRegistry = class {
|
|
|
381
315
|
// src/client/batch-column-fetcher.ts
|
|
382
316
|
var BatchColumnDriftError = class extends Error {
|
|
383
317
|
constructor(column, reason) {
|
|
384
|
-
super(`
|
|
318
|
+
super(`Column "${column.key}": ${reason}`);
|
|
385
319
|
this.column = column;
|
|
386
320
|
this.name = "BatchColumnDriftError";
|
|
387
321
|
}
|
|
@@ -424,7 +358,7 @@ var BatchColumnFetcher = class {
|
|
|
424
358
|
const res = await this.http(url, { method: "GET", headers });
|
|
425
359
|
if (res.status !== 200) {
|
|
426
360
|
throw new Error(
|
|
427
|
-
`
|
|
361
|
+
`Column "${column.key}" \u2014 endpoint ${column.endpoint} responded with HTTP ${res.status}`
|
|
428
362
|
);
|
|
429
363
|
}
|
|
430
364
|
const body = await res.json();
|
|
@@ -482,7 +416,7 @@ var BatchColumnFetcher = class {
|
|
|
482
416
|
function createAdminResourceClient(options) {
|
|
483
417
|
const base = options.adminBase ?? "/api/v1/admin";
|
|
484
418
|
const tenantsEndpoint = `${base}/tenants`;
|
|
485
|
-
const
|
|
419
|
+
const getJson = async (url) => {
|
|
486
420
|
const response = await options.http(url, { method: "GET" });
|
|
487
421
|
assertSuccess(response.status, "GET", url);
|
|
488
422
|
return await response.json();
|
|
@@ -498,12 +432,12 @@ function createAdminResourceClient(options) {
|
|
|
498
432
|
};
|
|
499
433
|
return {
|
|
500
434
|
tenantsEndpoint,
|
|
501
|
-
loadTenantDetail: (slug) =>
|
|
502
|
-
loadUsers: (filter) =>
|
|
503
|
-
`${base}/users${
|
|
435
|
+
loadTenantDetail: (slug) => getJson(`${tenantsEndpoint}/${encodeURIComponent(slug)}`),
|
|
436
|
+
loadUsers: (filter) => getJson(
|
|
437
|
+
`${base}/users${filterQueryString({ q: filter.q, tenant: filter.tenant })}`
|
|
504
438
|
),
|
|
505
|
-
loadAudit: (filter) =>
|
|
506
|
-
`${base}/audit${
|
|
439
|
+
loadAudit: (filter) => getJson(
|
|
440
|
+
`${base}/audit${filterQueryString({
|
|
507
441
|
actor: filter.actor,
|
|
508
442
|
action: filter.action,
|
|
509
443
|
entity: filter.entity,
|
|
@@ -511,9 +445,9 @@ function createAdminResourceClient(options) {
|
|
|
511
445
|
limit: filter.limit
|
|
512
446
|
})}`
|
|
513
447
|
),
|
|
514
|
-
loadSubscriptions: () =>
|
|
515
|
-
loadPromos: (filter) =>
|
|
516
|
-
`${base}/promo-codes${
|
|
448
|
+
loadSubscriptions: () => getJson(`${base}/subscriptions`),
|
|
449
|
+
loadPromos: (filter) => getJson(
|
|
450
|
+
`${base}/promo-codes${filterQueryString({
|
|
517
451
|
search: filter.search,
|
|
518
452
|
status: filter.status
|
|
519
453
|
})}`
|
|
@@ -538,15 +472,6 @@ function assertSuccess(status, method, url) {
|
|
|
538
472
|
throw new Error(`${method} ${url} \u2192 HTTP ${status}`);
|
|
539
473
|
}
|
|
540
474
|
}
|
|
541
|
-
function queryString(params) {
|
|
542
|
-
const search = new URLSearchParams();
|
|
543
|
-
for (const [key, value2] of Object.entries(params)) {
|
|
544
|
-
if (value2 === void 0 || value2 === null || value2 === "") continue;
|
|
545
|
-
search.set(key, String(value2));
|
|
546
|
-
}
|
|
547
|
-
const value = search.toString();
|
|
548
|
-
return value ? `?${value}` : "";
|
|
549
|
-
}
|
|
550
475
|
|
|
551
476
|
// src/client/login-branding.ts
|
|
552
477
|
var DEFAULT_TAG = "SuperAdmin";
|
|
@@ -676,14 +601,7 @@ function identityChipStyle(accent) {
|
|
|
676
601
|
|
|
677
602
|
export {
|
|
678
603
|
ADMIN_UI_VERSION,
|
|
679
|
-
|
|
680
|
-
trimTrailingSlashes,
|
|
681
|
-
getJson,
|
|
682
|
-
postJson,
|
|
683
|
-
BootLoadError,
|
|
684
|
-
BootLoader,
|
|
685
|
-
ManifestLoadError,
|
|
686
|
-
ManifestLoader,
|
|
604
|
+
createAxiosHttpClient,
|
|
687
605
|
DEFAULT_STANDARD_PAGE_ROUTES,
|
|
688
606
|
defaultSectionOrder,
|
|
689
607
|
buildRoutes,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BootLoader,
|
|
3
|
+
ManifestLoader,
|
|
4
|
+
bindResource,
|
|
2
5
|
createSaCatalog,
|
|
3
6
|
defaultKvStore
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-BPF2BMCQ.js";
|
|
5
8
|
|
|
6
9
|
// src/vue/super-admin-context.ts
|
|
7
10
|
var SUPER_ADMIN_BRAND_KEY = /* @__PURE__ */ Symbol.for(
|
|
@@ -73,6 +76,11 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
|
|
|
73
76
|
"@saasicat/ui-vue/SUPER_ADMIN_NOTIFY"
|
|
74
77
|
);
|
|
75
78
|
|
|
79
|
+
// src/vue/ui-confirm.ts
|
|
80
|
+
var SUPER_ADMIN_CONFIRM_KEY = /* @__PURE__ */ Symbol.for(
|
|
81
|
+
"@saasicat/ui-vue/SUPER_ADMIN_CONFIRM"
|
|
82
|
+
);
|
|
83
|
+
|
|
76
84
|
// src/vue/use-super-admin-i18n.ts
|
|
77
85
|
import {
|
|
78
86
|
computed,
|
|
@@ -129,14 +137,15 @@ function useSaMessages(namespace) {
|
|
|
129
137
|
import {
|
|
130
138
|
computed as computed2,
|
|
131
139
|
inject as inject2,
|
|
140
|
+
isReadonly as isReadonly2,
|
|
132
141
|
isRef as isRef2,
|
|
133
142
|
ref as ref2,
|
|
134
143
|
watch as watch2
|
|
135
144
|
} from "vue";
|
|
136
|
-
var
|
|
137
|
-
var
|
|
145
|
+
var SA_COLOR_SCHEMES = ["light", "dark", "system"];
|
|
146
|
+
var SA_THEME_STORAGE_KEY = "saasicat.theme.scheme";
|
|
138
147
|
function isScheme(value) {
|
|
139
|
-
return typeof value === "string" &&
|
|
148
|
+
return typeof value === "string" && SA_COLOR_SCHEMES.includes(value);
|
|
140
149
|
}
|
|
141
150
|
function systemScheme() {
|
|
142
151
|
const value = ref2("light");
|
|
@@ -157,13 +166,19 @@ function createSaTheme(options = {}) {
|
|
|
157
166
|
const persist = options.persist !== false;
|
|
158
167
|
const provided = options.scheme;
|
|
159
168
|
const appOwnsIt = isRef2(provided);
|
|
160
|
-
const
|
|
169
|
+
const key = `${options.storageKeyPrefix ?? ""}${SA_THEME_STORAGE_KEY}`;
|
|
170
|
+
const stored = persist && !appOwnsIt ? storage.get(key) : null;
|
|
161
171
|
const scheme = isRef2(provided) ? provided : ref2(isScheme(stored) ? stored : provided ?? "system");
|
|
162
172
|
const { value: system, unsubscribe } = systemScheme();
|
|
163
|
-
const stopPersisting = persist && !appOwnsIt ? watch2(scheme, (next) => storage.set(
|
|
173
|
+
const stopPersisting = persist && !appOwnsIt ? watch2(scheme, (next) => storage.set(key, next)) : null;
|
|
164
174
|
return {
|
|
165
175
|
scheme,
|
|
166
176
|
resolved: computed2(() => scheme.value === "system" ? system.value : scheme.value),
|
|
177
|
+
// A readonly ref (typically a `computed` derived from a profile
|
|
178
|
+
// setting) silently swallows writes, so the switcher would be a dead
|
|
179
|
+
// control. TypeScript ignores `readonly` when checking assignability,
|
|
180
|
+
// so the guard has to be a runtime one — same as the locale next door.
|
|
181
|
+
switcherEnabled: (options.switcher ?? true) && !isReadonly2(scheme),
|
|
167
182
|
dispose: () => {
|
|
168
183
|
unsubscribe();
|
|
169
184
|
stopPersisting?.();
|
|
@@ -179,6 +194,95 @@ function useSaTheme() {
|
|
|
179
194
|
return fallbackTheme;
|
|
180
195
|
}
|
|
181
196
|
|
|
197
|
+
// src/vue/resource-registry.ts
|
|
198
|
+
import { inject as inject3 } from "vue";
|
|
199
|
+
function boundWithOverride(def, http, readContext, override) {
|
|
200
|
+
const context = override?.context ? () => ({ ...readContext(), ...override.context }) : readContext;
|
|
201
|
+
const platform = bindResource(def, override?.http ?? http, context);
|
|
202
|
+
if (!override?.ops) return platform;
|
|
203
|
+
const result = { ...platform };
|
|
204
|
+
for (const [name, wrap] of Object.entries(override.ops)) {
|
|
205
|
+
if (typeof wrap !== "function") continue;
|
|
206
|
+
if (!Object.hasOwn(platform, name)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`createResourceRegistry: resource "${def.key}" has no operation "${name}" to override. It offers: ${Object.keys(platform).join(", ")}.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const next = platform[name];
|
|
212
|
+
result[name] = (...args) => wrap(next, ...args);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function createResourceRegistry(options) {
|
|
217
|
+
if (!options?.http) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"createResourceRegistry: `http` is required. Pass the HttpClient your app already uses \u2014 createAxiosHttpClient(instance) or createFetchHttpClient({ headers }) \u2014 so every platform request carries its auth."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const readContext = typeof options.context === "function" ? options.context : () => options.context;
|
|
223
|
+
const unknown = Object.keys(options.overrides ?? {}).filter(
|
|
224
|
+
(key) => !Object.hasOwn(options.resources, key)
|
|
225
|
+
);
|
|
226
|
+
if (unknown.length > 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`createResourceRegistry: no resource named ${unknown.map((k) => `"${k}"`).join(", ")} to override. The registry holds: ${Object.keys(options.resources).join(", ")}.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const bound = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const [key, def] of Object.entries(options.resources)) {
|
|
233
|
+
bound.set(key, boundWithOverride(def, options.http, readContext, options.overrides?.[key]));
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
get(key) {
|
|
237
|
+
const ops = bound.get(key);
|
|
238
|
+
if (!ops) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`useResource("${String(key)}"): no such resource. The registry offers: ${[...bound.keys()].join(", ")}.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return ops;
|
|
244
|
+
},
|
|
245
|
+
keys: () => [...bound.keys()]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
var SUPER_ADMIN_RESOURCES_KEY = /* @__PURE__ */ Symbol.for(
|
|
249
|
+
"@saasicat/ui-vue/SUPER_ADMIN_RESOURCES"
|
|
250
|
+
);
|
|
251
|
+
function useResource(key) {
|
|
252
|
+
const registry = inject3(SUPER_ADMIN_RESOURCES_KEY, null);
|
|
253
|
+
if (!registry) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`useResource("${key}"): no resource registry in scope. Either the component was not mounted inside createSuperAdminApp(), or the app did not pass an \`http\` client \u2014 the registry is not installed without one, because a bare fetch would send every request without your Authorization header and fail silently.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return registry.get(key);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/vue/platform-loaders.ts
|
|
262
|
+
function resolveSuperAdminEndpoints(endpoints) {
|
|
263
|
+
return {
|
|
264
|
+
apiBase: endpoints.apiBase,
|
|
265
|
+
publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
|
|
266
|
+
manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`,
|
|
267
|
+
projectKey: endpoints.projectKey ?? ""
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function createPlatformLoaders(options) {
|
|
271
|
+
const resolved = resolveSuperAdminEndpoints(options.endpoints);
|
|
272
|
+
const bootLoader = new BootLoader({
|
|
273
|
+
endpoint: resolved.publicBootEndpoint,
|
|
274
|
+
http: options.http
|
|
275
|
+
});
|
|
276
|
+
const manifestLoader = new ManifestLoader({
|
|
277
|
+
endpoint: resolved.manifestEndpoint,
|
|
278
|
+
http: options.http,
|
|
279
|
+
storage: options.storage,
|
|
280
|
+
storageKeyPrefix: options.storageKeyPrefix,
|
|
281
|
+
getAuthToken: options.getAuthToken
|
|
282
|
+
});
|
|
283
|
+
return { bootLoader, manifestLoader };
|
|
284
|
+
}
|
|
285
|
+
|
|
182
286
|
export {
|
|
183
287
|
SUPER_ADMIN_BRAND_KEY,
|
|
184
288
|
SUPER_ADMIN_ENDPOINTS_KEY,
|
|
@@ -190,12 +294,20 @@ export {
|
|
|
190
294
|
SUPER_ADMIN_HTTP_KEY,
|
|
191
295
|
buildNavigationGuard,
|
|
192
296
|
SUPER_ADMIN_NOTIFY_KEY,
|
|
297
|
+
SUPER_ADMIN_CONFIRM_KEY,
|
|
193
298
|
SA_LOCALE_STORAGE_KEY,
|
|
194
299
|
SUPER_ADMIN_I18N_KEY,
|
|
195
300
|
createSuperAdminI18n,
|
|
196
301
|
useSuperAdminI18n,
|
|
197
302
|
useSaMessages,
|
|
303
|
+
SA_COLOR_SCHEMES,
|
|
304
|
+
SA_THEME_STORAGE_KEY,
|
|
198
305
|
createSaTheme,
|
|
199
306
|
SA_THEME_KEY,
|
|
200
|
-
useSaTheme
|
|
307
|
+
useSaTheme,
|
|
308
|
+
createResourceRegistry,
|
|
309
|
+
SUPER_ADMIN_RESOURCES_KEY,
|
|
310
|
+
useResource,
|
|
311
|
+
resolveSuperAdminEndpoints,
|
|
312
|
+
createPlatformLoaders
|
|
201
313
|
};
|