@saasicat/ui-vue 0.4.0 → 0.6.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 +64 -4
- package/dist/chunk-A3W2N26K.js +714 -0
- package/dist/chunk-QZOSDCNP.js +98 -0
- package/dist/chunk-X5SQ5HMB.js +3231 -0
- package/dist/client/index.cjs +3988 -0
- package/dist/client/index.d.cts +321 -0
- package/dist/client/index.d.ts +321 -0
- package/dist/client/index.js +70 -0
- package/dist/index.cjs +4446 -1249
- package/dist/index.d.cts +297 -888
- package/dist/index.d.ts +297 -888
- package/dist/index.js +609 -1348
- package/dist/messages-7b0P8W75.d.cts +1652 -0
- package/dist/messages-7b0P8W75.d.ts +1652 -0
- package/dist/quasar/index.cjs +3340 -0
- package/dist/quasar/index.d.cts +95 -0
- package/dist/quasar/index.d.ts +95 -0
- package/dist/quasar/index.js +96 -0
- package/dist/testing-e2e/admin-pages-suite.cjs +14 -11
- package/dist/testing-e2e/admin-pages-suite.js +14 -11
- package/dist/use-super-admin-i18n-B3v3-SWZ.d.ts +210 -0
- package/dist/use-super-admin-i18n-DMktRVEt.d.cts +210 -0
- package/package.json +23 -3
- package/src/{action-registry.ts → client/action-registry.ts} +4 -4
- package/src/{batch-column-fetcher.ts → client/batch-column-fetcher.ts} +4 -4
- package/src/{boot-loader.ts → client/boot-loader.ts} +3 -3
- package/src/client/i18n/currency.ts +42 -0
- package/src/client/i18n/define.ts +59 -0
- package/src/client/i18n/format.ts +12 -0
- package/src/client/i18n/index.ts +14 -0
- package/src/client/i18n/locale.ts +15 -0
- package/src/client/i18n/messages/audit.ts +42 -0
- package/src/client/i18n/messages/bundles.ts +459 -0
- package/src/client/i18n/messages/common.ts +147 -0
- package/src/client/i18n/messages/dashboard.ts +16 -0
- package/src/client/i18n/messages/discovery.ts +229 -0
- package/src/client/i18n/messages/email.ts +125 -0
- package/src/client/i18n/messages/marketing.ts +270 -0
- package/src/client/i18n/messages/nav.ts +72 -0
- package/src/client/i18n/messages/pilots.ts +192 -0
- package/src/client/i18n/messages/plan-detail.ts +266 -0
- package/src/client/i18n/messages/plan-editor.ts +177 -0
- package/src/client/i18n/messages/plan-versions.ts +251 -0
- package/src/client/i18n/messages/plans.ts +503 -0
- package/src/client/i18n/messages/promos.ts +202 -0
- package/src/client/i18n/messages/shell.ts +145 -0
- package/src/client/i18n/messages/tenants.ts +104 -0
- package/src/client/i18n/messages/users.ts +78 -0
- package/src/client/i18n/messages.ts +84 -0
- package/src/client/index.ts +27 -0
- package/src/{manifest-loader.ts → client/manifest-loader.ts} +4 -4
- package/src/{nav-builder.ts → client/nav-builder.ts} +44 -51
- package/src/{plan-versions-catalog.ts → client/plan-versions-catalog.ts} +42 -30
- package/src/components/BundleVersionPublishDialog.vue +35 -24
- package/src/components/FeatureGate.vue +1 -1
- package/src/components/MarketingPromotionsTab.vue +112 -59
- package/src/components/MfaPromptDialog.vue +11 -11
- package/src/components/TenantActionConfirmDialog.vue +19 -17
- package/src/components/VersionDiffPreview.vue +72 -43
- package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
- package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
- package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
- package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
- package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
- package/src/components/bundle-editor/bundle-version-status.ts +29 -29
- package/src/components/dialogs/PilotCreateDialog.vue +58 -63
- package/src/components/dialogs/PilotEditDialog.vue +40 -34
- package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
- package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
- package/src/components/dialogs/types.ts +3 -13
- package/src/components/plan/PlanCycleToggle.vue +21 -10
- package/src/components/plan/PlanGrid.vue +2 -2
- package/src/components/plan/PriceSummary.vue +1 -1
- package/src/components/plan/PromoCodeInput.vue +1 -1
- package/src/components/plan/PublicBundleGrid.vue +1 -1
- package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
- package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
- package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
- package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
- package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
- package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
- package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
- package/src/components/plan-detail/PlanAuditLog.vue +11 -6
- package/src/components/plan-detail/PlanDetail.vue +21 -23
- package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
- package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
- package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
- package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
- package/src/components/plan-list/PlanList.vue +99 -57
- package/src/components/plan-matrix/PlanMatrix.vue +85 -38
- package/src/components/plan-review/PlanReview.vue +105 -80
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
- package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
- package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
- package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
- package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
- package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
- package/src/index.ts +82 -79
- package/src/pages-standard/AdminLayout.vue +43 -18
- package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
- package/src/pages-standard/AuditPage.vue +58 -19
- package/src/pages-standard/BundlesPage.vue +29 -18
- package/src/pages-standard/DashboardPage.vue +31 -17
- package/src/pages-standard/DiscoveryPage.vue +40 -28
- package/src/pages-standard/EmailHistoryPage.vue +98 -69
- package/src/pages-standard/MarketingCatalogPage.vue +40 -21
- package/src/pages-standard/PilotsPage.vue +64 -52
- package/src/pages-standard/PlanVersionsPage.vue +9 -5
- package/src/pages-standard/PlansPage.vue +91 -59
- package/src/pages-standard/PlatformEmailPage.vue +120 -53
- package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
- package/src/pages-standard/PromoCodesPage.vue +51 -54
- package/src/pages-standard/SubscriptionsPage.vue +14 -9
- package/src/pages-standard/SuperAdminLoginPage.vue +19 -16
- package/src/pages-standard/SuperAdminSetupWizard.vue +53 -49
- package/src/pages-standard/TenantDetailPage.vue +54 -40
- package/src/pages-standard/TenantsPage.vue +57 -44
- package/src/pages-standard/UsersPage.vue +70 -52
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
- package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
- package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
- package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
- package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
- package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
- package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
- package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
- package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
- package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
- package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
- package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
- package/src/pages-standard/plan-versions/PlanVersionHeader.vue +42 -26
- package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +46 -27
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +8 -6
- package/src/pages-standard/plan-versions/PlanVersionsList.vue +40 -17
- package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +1 -1
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +25 -13
- package/src/pages-standard/plan-versions/format.ts +33 -17
- package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
- package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
- package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
- package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
- package/src/pages-standard/tenants/format.ts +3 -2
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +73 -38
- package/src/pages-tenant/OnboardingConfigurator.vue +3 -3
- package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
- package/src/pages-tenant/PlanChangeWizard.vue +10 -6
- package/src/pages-tenant/TenantPlanSection.vue +20 -16
- package/src/pages-tenant/default-i18n.ts +217 -1
- package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +9 -7
- package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +11 -12
- package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
- package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +1 -1
- package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +1 -1
- package/src/quasar/create-super-admin-app.ts +192 -0
- package/src/quasar/index.ts +11 -0
- package/src/quasar/notify.ts +30 -0
- package/src/testing-e2e/admin-pages-suite.ts +14 -11
- package/src/{manifest-store-factory.ts → vue/manifest-store-factory.ts} +1 -1
- package/src/{platform-loaders.ts → vue/platform-loaders.ts} +4 -4
- package/src/{project-page-host.ts → vue/project-page-host.ts} +6 -4
- package/src/{create-super-admin-app.ts → vue/super-admin-context.ts} +20 -139
- package/src/vue/ui-notify.ts +30 -0
- package/src/{use-actions.ts → vue/use-actions.ts} +1 -1
- package/src/{use-api-list.ts → vue/use-api-list.ts} +1 -1
- package/src/{use-audit-entries.ts → vue/use-audit-entries.ts} +3 -3
- package/src/{use-batch-columns.ts → vue/use-batch-columns.ts} +1 -1
- package/src/{use-bulk-publish.ts → vue/use-bulk-publish.ts} +7 -5
- package/src/{use-bundle-versions-map.ts → vue/use-bundle-versions-map.ts} +7 -5
- package/src/{use-bundles.ts → vue/use-bundles.ts} +14 -14
- package/src/{use-catalog-entries.ts → vue/use-catalog-entries.ts} +14 -11
- package/src/{use-discovery.ts → vue/use-discovery.ts} +9 -6
- package/src/{use-entitlement.ts → vue/use-entitlement.ts} +4 -4
- package/src/{use-live-plan-versions.ts → vue/use-live-plan-versions.ts} +5 -5
- package/src/{use-manifest.ts → vue/use-manifest.ts} +1 -1
- package/src/{use-marketing-projections.ts → vue/use-marketing-projections.ts} +10 -7
- package/src/{use-nav.ts → vue/use-nav.ts} +9 -3
- package/src/{use-plan-editor.ts → vue/use-plan-editor.ts} +2 -2
- package/src/{use-plan-versions.ts → vue/use-plan-versions.ts} +3 -3
- package/src/{use-plans.ts → vue/use-plans.ts} +13 -13
- package/src/{use-platform-tenant-actions.ts → vue/use-platform-tenant-actions.ts} +9 -6
- package/src/{use-promotions.ts → vue/use-promotions.ts} +10 -11
- package/src/{use-public-boot.ts → vue/use-public-boot.ts} +1 -1
- package/src/{use-subscription-draft.ts → vue/use-subscription-draft.ts} +2 -3
- package/src/{use-super-admin-context.ts → vue/use-super-admin-context.ts} +5 -5
- package/src/vue/use-super-admin-i18n.ts +82 -0
- package/src/{use-tenant-action-flow.ts → vue/use-tenant-action-flow.ts} +12 -7
- package/src/{use-tenant-billing-catalog.ts → vue/use-tenant-billing-catalog.ts} +2 -2
- package/src/{use-tenant-billing.ts → vue/use-tenant-billing.ts} +2 -2
- package/src/{use-tenant-manifest.ts → vue/use-tenant-manifest.ts} +2 -4
- package/src/{use-tenant-subscription-bundles.ts → vue/use-tenant-subscription-bundles.ts} +5 -5
- package/src/{use-tenants.ts → vue/use-tenants.ts} +3 -3
- package/dist/testing-e2e/admin-pages-suite.d.cts +0 -37
- package/dist/testing-e2e/admin-pages-suite.d.ts +0 -37
- package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
- package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
- package/src/pages-standard/BusinessTypesPage.vue +0 -662
- package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
- package/src/use-business-types.ts +0 -269
- /package/src/{http-json.ts → client/http-json.ts} +0 -0
- /package/src/{types.ts → client/types.ts} +0 -0
- /package/src/{version.ts → client/version.ts} +0 -0
- /package/src/{create-admin-routes.ts → vue/create-admin-routes.ts} +0 -0
- /package/src/{entitlement-provider.ts → vue/entitlement-provider.ts} +0 -0
- /package/src/{feature-router-guard.ts → vue/feature-router-guard.ts} +0 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SA_LOCALE,
|
|
3
|
+
SA_INTL_LOCALES,
|
|
4
|
+
defaultHttpClient,
|
|
5
|
+
defaultKvStore,
|
|
6
|
+
formatMessage,
|
|
7
|
+
navMessages,
|
|
8
|
+
planVersionsMessages
|
|
9
|
+
} from "./chunk-X5SQ5HMB.js";
|
|
10
|
+
|
|
11
|
+
// src/client/version.ts
|
|
12
|
+
var ADMIN_UI_VERSION = "1.2.0";
|
|
13
|
+
|
|
14
|
+
// src/client/http-json.ts
|
|
15
|
+
var HttpJsonError = class extends Error {
|
|
16
|
+
constructor(status, code) {
|
|
17
|
+
super(code ?? `HTTP ${status}`);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "HttpJsonError";
|
|
21
|
+
}
|
|
22
|
+
status;
|
|
23
|
+
code;
|
|
24
|
+
};
|
|
25
|
+
async function extractCode(res) {
|
|
26
|
+
try {
|
|
27
|
+
const body = await res.json();
|
|
28
|
+
return typeof body?.code === "string" ? body.code : void 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function trimTrailingSlashes(url) {
|
|
34
|
+
let end = url.length;
|
|
35
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
36
|
+
return url.slice(0, end);
|
|
37
|
+
}
|
|
38
|
+
async function getJson(http, url) {
|
|
39
|
+
const res = await http(url);
|
|
40
|
+
if (res.status < 200 || res.status >= 300) {
|
|
41
|
+
throw new HttpJsonError(res.status, await extractCode(res));
|
|
42
|
+
}
|
|
43
|
+
return await res.json();
|
|
44
|
+
}
|
|
45
|
+
async function postJson(http, url, body) {
|
|
46
|
+
const res = await http(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(body)
|
|
50
|
+
});
|
|
51
|
+
if (res.status < 200 || res.status >= 300) {
|
|
52
|
+
throw new HttpJsonError(res.status, await extractCode(res));
|
|
53
|
+
}
|
|
54
|
+
return await res.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/client/boot-loader.ts
|
|
58
|
+
var BootLoadError = class extends Error {
|
|
59
|
+
constructor(status, message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.name = "BootLoadError";
|
|
63
|
+
}
|
|
64
|
+
status;
|
|
65
|
+
};
|
|
66
|
+
var BootLoader = class {
|
|
67
|
+
endpoint;
|
|
68
|
+
http;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
if (!options?.endpoint) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'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.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
this.endpoint = options.endpoint;
|
|
76
|
+
this.http = options.http ?? defaultHttpClient();
|
|
77
|
+
}
|
|
78
|
+
async load() {
|
|
79
|
+
const res = await this.http(this.endpoint);
|
|
80
|
+
if (res.status !== 200) {
|
|
81
|
+
throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
|
|
82
|
+
}
|
|
83
|
+
return await res.json();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/client/manifest-loader.ts
|
|
88
|
+
var ManifestLoadError = class extends Error {
|
|
89
|
+
constructor(status, message) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.status = status;
|
|
92
|
+
this.name = "ManifestLoadError";
|
|
93
|
+
}
|
|
94
|
+
status;
|
|
95
|
+
};
|
|
96
|
+
var ManifestLoader = class {
|
|
97
|
+
endpoint;
|
|
98
|
+
http;
|
|
99
|
+
storage;
|
|
100
|
+
bodyKey;
|
|
101
|
+
etagKey;
|
|
102
|
+
getAuthToken;
|
|
103
|
+
constructor(options) {
|
|
104
|
+
if (!options?.endpoint) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'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.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
this.endpoint = options.endpoint;
|
|
110
|
+
this.http = options.http ?? defaultHttpClient();
|
|
111
|
+
this.storage = options.storage ?? defaultKvStore();
|
|
112
|
+
const prefix = options.storageKeyPrefix ?? "";
|
|
113
|
+
this.bodyKey = `${prefix}manifest:body`;
|
|
114
|
+
this.etagKey = `${prefix}manifest:etag`;
|
|
115
|
+
this.getAuthToken = options.getAuthToken;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Loads the current manifest. On a cache hit (304) the cached
|
|
119
|
+
* body is returned — otherwise the fresh server body.
|
|
120
|
+
*/
|
|
121
|
+
async load() {
|
|
122
|
+
const cachedEtag = this.storage.get(this.etagKey);
|
|
123
|
+
const headers = {};
|
|
124
|
+
const token = this.getAuthToken?.();
|
|
125
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
126
|
+
if (cachedEtag) headers["If-None-Match"] = cachedEtag;
|
|
127
|
+
const res = await this.http(this.endpoint, { method: "GET", headers });
|
|
128
|
+
if (res.status === 304) {
|
|
129
|
+
const cachedBody = this.readCachedBody();
|
|
130
|
+
if (!cachedBody) {
|
|
131
|
+
throw new ManifestLoadError(
|
|
132
|
+
304,
|
|
133
|
+
"Server returned 304 but the cache body is missing \u2014 call clearCache() and reload"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return cachedBody.body;
|
|
137
|
+
}
|
|
138
|
+
if (res.status !== 200) {
|
|
139
|
+
throw new ManifestLoadError(
|
|
140
|
+
res.status,
|
|
141
|
+
`Manifest-Endpunkt antwortete HTTP ${res.status}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
const etag = res.headers.get("ETag") ?? res.headers.get("etag");
|
|
146
|
+
if (etag) {
|
|
147
|
+
this.storage.set(this.etagKey, etag);
|
|
148
|
+
this.storage.set(this.bodyKey, JSON.stringify(body));
|
|
149
|
+
}
|
|
150
|
+
return body;
|
|
151
|
+
}
|
|
152
|
+
/** Reads the cached manifest body from storage; null if absent. */
|
|
153
|
+
readCachedBody() {
|
|
154
|
+
const etag = this.storage.get(this.etagKey);
|
|
155
|
+
const bodyStr = this.storage.get(this.bodyKey);
|
|
156
|
+
if (!etag || !bodyStr) return null;
|
|
157
|
+
try {
|
|
158
|
+
return { etag, body: JSON.parse(bodyStr) };
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Clears the cache — e.g. on logout or after `manifest reload`. */
|
|
164
|
+
clearCache() {
|
|
165
|
+
this.storage.remove(this.etagKey);
|
|
166
|
+
this.storage.remove(this.bodyKey);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/client/nav-builder.ts
|
|
171
|
+
var DEFAULT_STANDARD_PAGE_ROUTES = {
|
|
172
|
+
dashboard: "/admin/dashboard",
|
|
173
|
+
tenants: "/admin/tenants",
|
|
174
|
+
subscriptions: "/admin/subscriptions",
|
|
175
|
+
promoCodes: "/admin/promo-codes",
|
|
176
|
+
plans: "/admin/plans",
|
|
177
|
+
planVersions: "/admin/plan-versions",
|
|
178
|
+
audit: "/admin/audit",
|
|
179
|
+
users: "/admin/users",
|
|
180
|
+
pilots: "/admin/pilots",
|
|
181
|
+
discovery: "/admin/discovery",
|
|
182
|
+
bundles: "/admin/bundles",
|
|
183
|
+
marketingCatalog: "/admin/marketing-catalog",
|
|
184
|
+
platformEmail: "/admin/platform-email",
|
|
185
|
+
platformEmailHistory: "/admin/platform-email-history"
|
|
186
|
+
};
|
|
187
|
+
var DEFAULT_ICONS = {
|
|
188
|
+
dashboard: "dashboard",
|
|
189
|
+
tenants: "business",
|
|
190
|
+
subscriptions: "card_membership",
|
|
191
|
+
promoCodes: "local_activity",
|
|
192
|
+
plans: "workspace_premium",
|
|
193
|
+
planVersions: "history_edu",
|
|
194
|
+
audit: "history",
|
|
195
|
+
users: "people",
|
|
196
|
+
pilots: "flight_takeoff",
|
|
197
|
+
discovery: "travel_explore",
|
|
198
|
+
bundles: "inventory_2",
|
|
199
|
+
marketingCatalog: "campaign",
|
|
200
|
+
platformEmail: "mail",
|
|
201
|
+
platformEmailHistory: "mark_email_read"
|
|
202
|
+
};
|
|
203
|
+
var PAGE_SECTIONS = {
|
|
204
|
+
dashboard: "overview",
|
|
205
|
+
discovery: "catalog",
|
|
206
|
+
bundles: "catalog",
|
|
207
|
+
plans: "catalog",
|
|
208
|
+
planVersions: "catalog",
|
|
209
|
+
marketingCatalog: "catalog",
|
|
210
|
+
promoCodes: "catalog",
|
|
211
|
+
tenants: "customers",
|
|
212
|
+
subscriptions: "customers",
|
|
213
|
+
users: "customers",
|
|
214
|
+
pilots: "customers",
|
|
215
|
+
audit: "system",
|
|
216
|
+
platformEmail: "system",
|
|
217
|
+
platformEmailHistory: "system"
|
|
218
|
+
};
|
|
219
|
+
function defaultSectionOrder(locale = DEFAULT_SA_LOCALE) {
|
|
220
|
+
const sections = navMessages[locale].sections;
|
|
221
|
+
return [sections.overview, sections.catalog, sections.customers, sections.system];
|
|
222
|
+
}
|
|
223
|
+
function buildRoutes(manifest, options = {}) {
|
|
224
|
+
const routes = [];
|
|
225
|
+
const capabilities = manifest.capabilities ?? {};
|
|
226
|
+
const standard = manifest.navigation?.standardPages ?? {};
|
|
227
|
+
const nav = navMessages[options.locale ?? DEFAULT_SA_LOCALE];
|
|
228
|
+
const defaultNavSections = Object.fromEntries(
|
|
229
|
+
Object.entries(PAGE_SECTIONS).map(([page, section]) => [page, nav.sections[section]])
|
|
230
|
+
);
|
|
231
|
+
const overrideRoutes = { ...DEFAULT_STANDARD_PAGE_ROUTES, ...options.standardPageRoutes };
|
|
232
|
+
const overrideLabels = { ...nav.pages, ...options.standardPageLabels };
|
|
233
|
+
const overrideIcons = { ...DEFAULT_ICONS, ...options.standardPageIcons };
|
|
234
|
+
const overrideNavSection = { ...defaultNavSections, ...options.standardPageNavSection };
|
|
235
|
+
for (const key of Object.keys(standard)) {
|
|
236
|
+
const def = standard[key];
|
|
237
|
+
if (!def?.enabled) continue;
|
|
238
|
+
const cap = def.requiredCapability ?? null;
|
|
239
|
+
if (cap && capabilities[cap] !== true) continue;
|
|
240
|
+
routes.push({
|
|
241
|
+
id: key,
|
|
242
|
+
path: overrideRoutes[key],
|
|
243
|
+
label: overrideLabels[key],
|
|
244
|
+
icon: overrideIcons[key],
|
|
245
|
+
navSection: overrideNavSection[key],
|
|
246
|
+
componentKey: `platform-${key}`,
|
|
247
|
+
requiredCapability: cap,
|
|
248
|
+
isStandard: true
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const projectPages = manifest.navigation?.projectPages ?? [];
|
|
252
|
+
const exts = options.availableExtensions;
|
|
253
|
+
for (const p of projectPages) {
|
|
254
|
+
const cap = p.requiredCapability ?? null;
|
|
255
|
+
if (cap && capabilities[cap] !== true) continue;
|
|
256
|
+
if (exts && !exts.has(p.componentKey)) continue;
|
|
257
|
+
routes.push({
|
|
258
|
+
id: p.id,
|
|
259
|
+
path: p.route,
|
|
260
|
+
label: p.label,
|
|
261
|
+
icon: p.icon,
|
|
262
|
+
navSection: p.navSection,
|
|
263
|
+
componentKey: p.componentKey,
|
|
264
|
+
requiredCapability: cap,
|
|
265
|
+
isStandard: false,
|
|
266
|
+
prefetchOnIdle: p.prefetchOnIdle
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return routes;
|
|
270
|
+
}
|
|
271
|
+
function buildSidebar(routes, sectionOrder = defaultSectionOrder()) {
|
|
272
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const r of routes) {
|
|
274
|
+
const section = r.navSection ?? null;
|
|
275
|
+
const list = bySection.get(section) ?? [];
|
|
276
|
+
list.push({ id: r.id, path: r.path, label: r.label, icon: r.icon });
|
|
277
|
+
bySection.set(section, list);
|
|
278
|
+
}
|
|
279
|
+
const sections = [];
|
|
280
|
+
if (bySection.has(null)) {
|
|
281
|
+
sections.push({ section: null, items: bySection.get(null) });
|
|
282
|
+
}
|
|
283
|
+
const ordered = /* @__PURE__ */ new Set();
|
|
284
|
+
for (const name of sectionOrder) {
|
|
285
|
+
if (bySection.has(name)) {
|
|
286
|
+
sections.push({ section: name, items: bySection.get(name) });
|
|
287
|
+
ordered.add(name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const remaining = [...bySection.entries()].filter(([s]) => s !== null && !ordered.has(s)).sort((a, b) => a[0].localeCompare(b[0]));
|
|
291
|
+
for (const [section, items] of remaining) {
|
|
292
|
+
sections.push({ section, items });
|
|
293
|
+
}
|
|
294
|
+
return sections;
|
|
295
|
+
}
|
|
296
|
+
function resolveExtension(componentKey, extensions) {
|
|
297
|
+
return extensions[componentKey] ?? null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/client/action-registry.ts
|
|
301
|
+
var MissingHandlerError = class extends Error {
|
|
302
|
+
constructor(actionKey) {
|
|
303
|
+
super(
|
|
304
|
+
`No handler registered for actionKey "${actionKey}". The consumer shell must provide \`actions["${actionKey}"]\`.`
|
|
305
|
+
);
|
|
306
|
+
this.name = "MissingHandlerError";
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
var ActionDefNotInManifestError = class extends Error {
|
|
310
|
+
constructor(actionKey) {
|
|
311
|
+
super(
|
|
312
|
+
`actionKey "${actionKey}" is not declared in the manifest. Register it as a TenantAction in a ManifestContribution.`
|
|
313
|
+
);
|
|
314
|
+
this.name = "ActionDefNotInManifestError";
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var ActionRegistry = class {
|
|
318
|
+
defs = /* @__PURE__ */ new Map();
|
|
319
|
+
handlers = /* @__PURE__ */ new Map();
|
|
320
|
+
constructor(manifest, handlers = {}) {
|
|
321
|
+
for (const def of manifest.tenants?.actions ?? []) {
|
|
322
|
+
this.defs.set(def.actionKey, def);
|
|
323
|
+
}
|
|
324
|
+
for (const [key, handler] of Object.entries(handlers)) {
|
|
325
|
+
this.handlers.set(key, handler);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Registers a handler after the fact (e.g. when consumer code loads
|
|
330
|
+
* lazily). Throws `ActionDefNotInManifestError` if the `actionKey` is not
|
|
331
|
+
* declared in the manifest — prevents dead registrations.
|
|
332
|
+
*/
|
|
333
|
+
register(actionKey, handler) {
|
|
334
|
+
if (!this.defs.has(actionKey)) {
|
|
335
|
+
throw new ActionDefNotInManifestError(actionKey);
|
|
336
|
+
}
|
|
337
|
+
this.handlers.set(actionKey, handler);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Returns the `{def, handler}` pair. Throws if the key is missing from
|
|
341
|
+
* the manifest or no handler is registered. The shell's UI layer calls
|
|
342
|
+
* the method, checks `def.requiresMfa` / `def.confirmType` for the
|
|
343
|
+
* pre-flow, and then calls `handler(input)`.
|
|
344
|
+
*/
|
|
345
|
+
get(actionKey) {
|
|
346
|
+
const def = this.defs.get(actionKey);
|
|
347
|
+
if (!def) throw new ActionDefNotInManifestError(actionKey);
|
|
348
|
+
const handler = this.handlers.get(actionKey);
|
|
349
|
+
if (!handler) throw new MissingHandlerError(actionKey);
|
|
350
|
+
return { def, handler };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Convenience: `get(key).handler(input)`. UI convenience for actions
|
|
354
|
+
* that need neither MFA nor confirm.
|
|
355
|
+
*/
|
|
356
|
+
async dispatch(actionKey, input) {
|
|
357
|
+
return this.get(actionKey).handler(input);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* List of actionKeys that are declared in the manifest but have no
|
|
361
|
+
* handler. Consumers use this in a doctor check to detect drift between
|
|
362
|
+
* the manifest and the shell build.
|
|
363
|
+
*/
|
|
364
|
+
listOrphanedDefs() {
|
|
365
|
+
return [...this.defs.keys()].filter((k) => !this.handlers.has(k));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* List of registered handlers that are missing from the manifest. Drift
|
|
369
|
+
* in the other direction.
|
|
370
|
+
*/
|
|
371
|
+
listOrphanedHandlers() {
|
|
372
|
+
return [...this.handlers.keys()].filter((k) => !this.defs.has(k));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/client/batch-column-fetcher.ts
|
|
377
|
+
var BatchColumnDriftError = class extends Error {
|
|
378
|
+
constructor(column, reason) {
|
|
379
|
+
super(`Spalte "${column.key}": ${reason}`);
|
|
380
|
+
this.column = column;
|
|
381
|
+
this.name = "BatchColumnDriftError";
|
|
382
|
+
}
|
|
383
|
+
column;
|
|
384
|
+
};
|
|
385
|
+
var BatchColumnFetcher = class {
|
|
386
|
+
http;
|
|
387
|
+
paramStyle;
|
|
388
|
+
getAuthToken;
|
|
389
|
+
constructor(options = {}) {
|
|
390
|
+
this.http = options.http ?? defaultHttpClient();
|
|
391
|
+
this.paramStyle = options.paramStyle ?? "comma";
|
|
392
|
+
this.getAuthToken = options.getAuthToken;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Loads the data for all columns declared in the manifest in parallel
|
|
396
|
+
* (one request per column, all `tenantIds` in the batch). Columns without
|
|
397
|
+
* a satisfied capability are ignored.
|
|
398
|
+
*/
|
|
399
|
+
async fetchAll(manifest, tenantIds) {
|
|
400
|
+
if (tenantIds.length === 0) return {};
|
|
401
|
+
const cols = this.eligibleColumns(manifest);
|
|
402
|
+
const results = await Promise.all(
|
|
403
|
+
cols.map(
|
|
404
|
+
(col) => this.fetchOne(col, tenantIds).then((data) => [col.key, data])
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
const out = {};
|
|
408
|
+
for (const [key, data] of results) out[key] = data;
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
/** Fetch a single column (consumers may also use this directly). */
|
|
412
|
+
async fetchOne(column, tenantIds) {
|
|
413
|
+
this.validateBatchEndpoint(column);
|
|
414
|
+
if (tenantIds.length === 0) return {};
|
|
415
|
+
const url = this.buildUrl(column.endpoint, tenantIds);
|
|
416
|
+
const headers = {};
|
|
417
|
+
const token = this.getAuthToken?.();
|
|
418
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
419
|
+
const res = await this.http(url, { method: "GET", headers });
|
|
420
|
+
if (res.status !== 200) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Spalte "${column.key}" \u2014 Endpoint ${column.endpoint} antwortete HTTP ${res.status}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const body = await res.json();
|
|
426
|
+
return body;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Column drift against the manifest. Consumers use this for CI smoke
|
|
430
|
+
* tests of the manifest-vs-shell build.
|
|
431
|
+
*/
|
|
432
|
+
listDriftIssues(manifest) {
|
|
433
|
+
const issues = [];
|
|
434
|
+
for (const col of manifest.tenants?.columns ?? []) {
|
|
435
|
+
try {
|
|
436
|
+
this.validateBatchEndpoint(col);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err instanceof BatchColumnDriftError) issues.push(err);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return issues;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Which columns have a satisfied `requiredCapability`? Manifest =
|
|
445
|
+
* discovery, so a local check is enough.
|
|
446
|
+
*/
|
|
447
|
+
eligibleColumns(manifest) {
|
|
448
|
+
const caps = manifest.capabilities ?? {};
|
|
449
|
+
return (manifest.tenants?.columns ?? []).filter((col) => {
|
|
450
|
+
if (!col.requiredCapability) return true;
|
|
451
|
+
return caps[col.requiredCapability] === true;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
validateBatchEndpoint(col) {
|
|
455
|
+
if (!col.endpoint || col.endpoint.trim().length === 0) {
|
|
456
|
+
throw new BatchColumnDriftError(col, "endpoint is empty");
|
|
457
|
+
}
|
|
458
|
+
if (col.endpoint.includes("{slug}") || col.endpoint.includes("{tenantId}")) {
|
|
459
|
+
throw new BatchColumnDriftError(
|
|
460
|
+
col,
|
|
461
|
+
"endpoint contains per-tenant placeholders ({slug}/{tenantId}). Column endpoints must be batchable \u2014 switch the backend to a `?tenantIds=...` parameter."
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
buildUrl(endpoint, tenantIds) {
|
|
466
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
467
|
+
if (this.paramStyle === "comma") {
|
|
468
|
+
const enc = tenantIds.map(encodeURIComponent).join(",");
|
|
469
|
+
return `${endpoint}${sep}tenantIds=${enc}`;
|
|
470
|
+
}
|
|
471
|
+
const params = tenantIds.map((id) => `tenantIds=${encodeURIComponent(id)}`).join("&");
|
|
472
|
+
return `${endpoint}${sep}${params}`;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/client/plan-versions-catalog.ts
|
|
477
|
+
var dec = (s) => Number(s);
|
|
478
|
+
function extractQuotas(p) {
|
|
479
|
+
if (p.quotas && Object.keys(p.quotas).length > 0) {
|
|
480
|
+
return { ...p.quotas };
|
|
481
|
+
}
|
|
482
|
+
const out = {};
|
|
483
|
+
if (typeof p.maxUsers === "number") out.users = p.maxUsers;
|
|
484
|
+
if (typeof p.maxVehicles === "number") out.vehicles = p.maxVehicles;
|
|
485
|
+
if (typeof p.maxStorageGb === "number") out.storageGb = p.maxStorageGb;
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
var sortByPublishedDesc = (rows) => [...rows].sort((a, b) => {
|
|
489
|
+
const at = a.publishedAt ? Date.parse(a.publishedAt) : Number.POSITIVE_INFINITY;
|
|
490
|
+
const bt = b.publishedAt ? Date.parse(b.publishedAt) : Number.POSITIVE_INFINITY;
|
|
491
|
+
if (at !== bt) return bt - at;
|
|
492
|
+
return b.version - a.version;
|
|
493
|
+
});
|
|
494
|
+
function findLiveAt(rows, asOfMs) {
|
|
495
|
+
const candidates = rows.filter((r) => {
|
|
496
|
+
if (r.publishedAt === null) return false;
|
|
497
|
+
if (Date.parse(r.publishedAt) > asOfMs) return false;
|
|
498
|
+
if (r.supersededAt !== null && Date.parse(r.supersededAt) <= asOfMs) return false;
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
if (candidates.length === 0) return null;
|
|
502
|
+
return [...candidates].sort((a, b) => b.version - a.version)[0] ?? null;
|
|
503
|
+
}
|
|
504
|
+
function findCurrentLive(rows) {
|
|
505
|
+
return rows.find((r) => r.publishedAt !== null && r.supersededAt === null) ?? null;
|
|
506
|
+
}
|
|
507
|
+
function findDraft(rows) {
|
|
508
|
+
return rows.find((r) => r.publishedAt === null) ?? null;
|
|
509
|
+
}
|
|
510
|
+
function groupBy(rows, keyOf) {
|
|
511
|
+
const out = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const r of rows) {
|
|
513
|
+
const k = keyOf(r);
|
|
514
|
+
const list = out.get(k) ?? [];
|
|
515
|
+
list.push(r);
|
|
516
|
+
out.set(k, list);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
function resolvePlan(source, liveBase) {
|
|
521
|
+
const quotas = extractQuotas(source);
|
|
522
|
+
return {
|
|
523
|
+
source,
|
|
524
|
+
liveBase,
|
|
525
|
+
isDraft: source.publishedAt === null,
|
|
526
|
+
planId: source.planId,
|
|
527
|
+
features: source.features,
|
|
528
|
+
quotas,
|
|
529
|
+
monthlyNet: dec(source.monthlyNet),
|
|
530
|
+
yearlyNet: dec(source.yearlyNet),
|
|
531
|
+
marketed: source.marketed,
|
|
532
|
+
version: source.version,
|
|
533
|
+
maxUsers: quotas.users,
|
|
534
|
+
maxVehicles: quotas.vehicles,
|
|
535
|
+
maxStorageGb: quotas.storageGb
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function draftsTitle(draftCount, msg) {
|
|
539
|
+
if (draftCount === 0) return msg.draftsTitleEmpty;
|
|
540
|
+
const template = draftCount === 1 ? msg.draftsTitleOne : msg.draftsTitleMany;
|
|
541
|
+
return formatMessage(template, { count: draftCount });
|
|
542
|
+
}
|
|
543
|
+
function dateLabel(iso, locale) {
|
|
544
|
+
return new Date(iso).toLocaleDateString(SA_INTL_LOCALES[locale], {
|
|
545
|
+
day: "2-digit",
|
|
546
|
+
month: "short",
|
|
547
|
+
year: "numeric"
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function buildDraftsSnapshot(data, sortPlansFn, locale) {
|
|
551
|
+
const msg = planVersionsMessages[locale].catalog;
|
|
552
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
553
|
+
const plans = [];
|
|
554
|
+
for (const [, rows] of planByKey) {
|
|
555
|
+
const draft = findDraft(rows);
|
|
556
|
+
const live = findCurrentLive(rows);
|
|
557
|
+
if (draft) plans.push(resolvePlan(draft, live));
|
|
558
|
+
else if (live) plans.push(resolvePlan(live, null));
|
|
559
|
+
}
|
|
560
|
+
const draftCount = plans.filter((p) => p.isDraft).length;
|
|
561
|
+
return {
|
|
562
|
+
id: "drafts",
|
|
563
|
+
kind: "drafts",
|
|
564
|
+
status: "DRAFT",
|
|
565
|
+
label: msg.draftsLabel,
|
|
566
|
+
title: draftsTitle(draftCount, msg),
|
|
567
|
+
description: draftCount === 0 ? msg.draftsDescriptionEmpty : msg.draftsDescription,
|
|
568
|
+
asOf: null,
|
|
569
|
+
createdAt: null,
|
|
570
|
+
publishedAt: null,
|
|
571
|
+
authorEmail: null,
|
|
572
|
+
plans: sortPlansFn(plans),
|
|
573
|
+
draftCount,
|
|
574
|
+
regressionCount: 0
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function buildActiveSnapshot(data, sortPlansFn, locale) {
|
|
578
|
+
const msg = planVersionsMessages[locale].catalog;
|
|
579
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
580
|
+
const plans = [];
|
|
581
|
+
for (const [, rows] of planByKey) {
|
|
582
|
+
const live = findCurrentLive(rows);
|
|
583
|
+
if (live) plans.push(resolvePlan(live, null));
|
|
584
|
+
}
|
|
585
|
+
if (plans.length === 0) return null;
|
|
586
|
+
const publishedDates = plans.map((p) => p.source.publishedAt).filter((t) => t !== null).sort();
|
|
587
|
+
const latestPublished = publishedDates[publishedDates.length - 1];
|
|
588
|
+
return {
|
|
589
|
+
id: "active",
|
|
590
|
+
kind: "active",
|
|
591
|
+
status: "ACTIVE",
|
|
592
|
+
label: msg.activeLabel,
|
|
593
|
+
title: msg.activeTitle,
|
|
594
|
+
description: msg.activeDescription,
|
|
595
|
+
asOf: latestPublished ?? null,
|
|
596
|
+
createdAt: null,
|
|
597
|
+
publishedAt: latestPublished ?? null,
|
|
598
|
+
authorEmail: null,
|
|
599
|
+
plans: sortPlansFn(plans),
|
|
600
|
+
draftCount: 0,
|
|
601
|
+
regressionCount: 0
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function buildHistoricalSnapshots(data, sortPlansFn, locale) {
|
|
605
|
+
const msg = planVersionsMessages[locale].catalog;
|
|
606
|
+
const allPublishedAt = /* @__PURE__ */ new Set();
|
|
607
|
+
for (const r of data.planVersions) if (r.publishedAt) allPublishedAt.add(r.publishedAt);
|
|
608
|
+
if (allPublishedAt.size === 0) return [];
|
|
609
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
610
|
+
const liveTimestamps = /* @__PURE__ */ new Set();
|
|
611
|
+
for (const rows of planByKey.values()) {
|
|
612
|
+
const live = findCurrentLive(rows);
|
|
613
|
+
if (live?.publishedAt) liveTimestamps.add(live.publishedAt);
|
|
614
|
+
}
|
|
615
|
+
const liveSorted = [...liveTimestamps].sort();
|
|
616
|
+
const latestLive = liveSorted[liveSorted.length - 1];
|
|
617
|
+
const sortedDesc = [...allPublishedAt].sort((a, b) => Date.parse(b) - Date.parse(a));
|
|
618
|
+
const out = [];
|
|
619
|
+
for (const ts of sortedDesc) {
|
|
620
|
+
if (latestLive && Date.parse(ts) >= Date.parse(latestLive)) continue;
|
|
621
|
+
const tMs = Date.parse(ts) + 1;
|
|
622
|
+
const plans = [];
|
|
623
|
+
for (const [, rows] of planByKey) {
|
|
624
|
+
const live = findLiveAt(rows, tMs);
|
|
625
|
+
if (live) plans.push(resolvePlan(live, null));
|
|
626
|
+
}
|
|
627
|
+
if (plans.length === 0) continue;
|
|
628
|
+
const publishedAtThisEvent = data.planVersions.filter((r) => r.publishedAt === ts);
|
|
629
|
+
const regressionCount = publishedAtThisEvent.filter((r) => !r.nonRegressive).length;
|
|
630
|
+
const author = publishedAtThisEvent[0]?.publishedByUserId ?? null;
|
|
631
|
+
out.push({
|
|
632
|
+
id: `hist-${ts}`,
|
|
633
|
+
kind: "historical",
|
|
634
|
+
status: "ARCHIVED",
|
|
635
|
+
label: dateLabel(ts, locale),
|
|
636
|
+
title: summarizeEvent(publishedAtThisEvent, msg),
|
|
637
|
+
description: detailEvent(publishedAtThisEvent, msg),
|
|
638
|
+
asOf: ts,
|
|
639
|
+
createdAt: ts,
|
|
640
|
+
publishedAt: ts,
|
|
641
|
+
authorEmail: author,
|
|
642
|
+
plans: sortPlansFn(plans),
|
|
643
|
+
draftCount: 0,
|
|
644
|
+
regressionCount
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
function summarizeEvent(rows, msg) {
|
|
650
|
+
if (rows.length === 0) return msg.publishEvent;
|
|
651
|
+
if (rows.length === 1) {
|
|
652
|
+
const r = rows[0];
|
|
653
|
+
return formatMessage(msg.publishEventSingle, { planId: r.planId, version: r.version });
|
|
654
|
+
}
|
|
655
|
+
return formatMessage(msg.publishEventMultiple, { count: rows.length });
|
|
656
|
+
}
|
|
657
|
+
function detailEvent(rows, msg) {
|
|
658
|
+
if (rows.length === 0) return "";
|
|
659
|
+
const note = rows[0]?.changeNote ?? "";
|
|
660
|
+
return note.length > 0 ? note : msg.reconstructedSnapshot;
|
|
661
|
+
}
|
|
662
|
+
function sortPlansBy(order) {
|
|
663
|
+
if (!order || order.length === 0) {
|
|
664
|
+
return (rows) => [...rows].sort((a, b) => a.planId.localeCompare(b.planId));
|
|
665
|
+
}
|
|
666
|
+
const idx = {};
|
|
667
|
+
for (let i = 0; i < order.length; i++) idx[order[i]] = i;
|
|
668
|
+
return (rows) => [...rows].sort((a, b) => {
|
|
669
|
+
const ai = idx[a.planId] ?? Number.POSITIVE_INFINITY;
|
|
670
|
+
const bi = idx[b.planId] ?? Number.POSITIVE_INFINITY;
|
|
671
|
+
if (ai !== bi) return ai - bi;
|
|
672
|
+
return a.planId.localeCompare(b.planId);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function buildSnapshots(data, options = {}) {
|
|
676
|
+
const locale = options.locale ?? DEFAULT_SA_LOCALE;
|
|
677
|
+
const sortPlansFn = sortPlansBy(options.planSortOrder);
|
|
678
|
+
const drafts = buildDraftsSnapshot(data, sortPlansFn, locale);
|
|
679
|
+
const active = buildActiveSnapshot(data, sortPlansFn, locale);
|
|
680
|
+
const historical = buildHistoricalSnapshots(data, sortPlansFn, locale);
|
|
681
|
+
const out = [drafts];
|
|
682
|
+
if (active) out.push(active);
|
|
683
|
+
out.push(...historical);
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function listOpenDrafts(data) {
|
|
687
|
+
return {
|
|
688
|
+
plans: sortByPublishedDesc(data.planVersions).filter((r) => r.publishedAt === null)
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export {
|
|
693
|
+
ADMIN_UI_VERSION,
|
|
694
|
+
HttpJsonError,
|
|
695
|
+
trimTrailingSlashes,
|
|
696
|
+
getJson,
|
|
697
|
+
postJson,
|
|
698
|
+
BootLoadError,
|
|
699
|
+
BootLoader,
|
|
700
|
+
ManifestLoadError,
|
|
701
|
+
ManifestLoader,
|
|
702
|
+
DEFAULT_STANDARD_PAGE_ROUTES,
|
|
703
|
+
defaultSectionOrder,
|
|
704
|
+
buildRoutes,
|
|
705
|
+
buildSidebar,
|
|
706
|
+
resolveExtension,
|
|
707
|
+
MissingHandlerError,
|
|
708
|
+
ActionDefNotInManifestError,
|
|
709
|
+
ActionRegistry,
|
|
710
|
+
BatchColumnDriftError,
|
|
711
|
+
BatchColumnFetcher,
|
|
712
|
+
buildSnapshots,
|
|
713
|
+
listOpenDrafts
|
|
714
|
+
};
|