@saasicat/ui-vue 0.2.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.
Files changed (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing-e2e/admin-pages-suite.ts
21
+ var admin_pages_suite_exports = {};
22
+ __export(admin_pages_suite_exports, {
23
+ runAdminPagesSuite: () => runAdminPagesSuite
24
+ });
25
+ module.exports = __toCommonJS(admin_pages_suite_exports);
26
+ var RESOURCE_FAILURE_MARKER = /Failed to load resource:/;
27
+ function attachListeners(page, consoleAllowlist = [], networkAllowlist = []) {
28
+ const out = { pageErrors: [], consoleErrors: [], networkErrors: [] };
29
+ page.on("pageerror", (err) => out.pageErrors.push(err.message));
30
+ page.on("console", (msg) => {
31
+ if (msg.type() !== "error") return;
32
+ const text = msg.text();
33
+ if (RESOURCE_FAILURE_MARKER.test(text)) return;
34
+ if (consoleAllowlist.some((re) => re.test(text))) return;
35
+ out.consoleErrors.push(text);
36
+ });
37
+ page.on("response", (resp) => {
38
+ if (resp.status() < 400) return;
39
+ const url = resp.url();
40
+ if (!url.includes("/api/")) return;
41
+ if (networkAllowlist.some((re) => re.test(url))) return;
42
+ out.networkErrors.push(`HTTP ${resp.status()} ${url}`);
43
+ });
44
+ return out;
45
+ }
46
+ async function loginIfNeeded(page, config) {
47
+ if (!page.url().includes("/login")) {
48
+ await page.goto(config.loginUrl, { waitUntil: "networkidle" });
49
+ }
50
+ if (!page.url().includes("/login")) return;
51
+ await page.locator('input[type="email"]').first().fill(config.email);
52
+ const pw = page.locator('input[type="password"]').first();
53
+ await pw.fill(config.password);
54
+ await pw.press("Enter");
55
+ await page.waitForURL((u) => !u.toString().includes("/login"), {
56
+ timeout: 15e3
57
+ });
58
+ }
59
+ function runAdminPagesSuite(test, expect, config) {
60
+ const dashboardPath = config.dashboardPath ?? "/admin/";
61
+ test.describe(`${config.appName}: SuperAdmin-Pages Smoke`, () => {
62
+ for (const pageDef of config.pages) {
63
+ test(`${pageDef.name} rendert ohne Fehler`, async ({ page }) => {
64
+ const errors = attachListeners(
65
+ page,
66
+ config.consoleErrorAllowlist,
67
+ pageDef.allowedFailures
68
+ );
69
+ await loginIfNeeded(page, config);
70
+ const origin = new URL(config.loginUrl).origin;
71
+ await page.goto(`${origin}${pageDef.path}`, {
72
+ waitUntil: "networkidle",
73
+ timeout: 15e3
74
+ });
75
+ await page.waitForTimeout(800);
76
+ if (pageDef.expectVisible) {
77
+ await expect(page.locator(pageDef.expectVisible).first()).toBeVisible({
78
+ timeout: 5e3
79
+ });
80
+ }
81
+ expect(errors.pageErrors, `pageerror-Events auf ${pageDef.path}`).toHaveLength(0);
82
+ expect(
83
+ errors.consoleErrors,
84
+ `console.error-Events auf ${pageDef.path}`
85
+ ).toHaveLength(0);
86
+ expect(
87
+ errors.networkErrors,
88
+ `HTTP-Errors auf /api/ w\xE4hrend ${pageDef.path}`
89
+ ).toHaveLength(0);
90
+ });
91
+ }
92
+ test("Dashboard zeigt erwartete KPIs, Distributions und Shortcuts", async ({ page }) => {
93
+ attachListeners(page, config.consoleErrorAllowlist);
94
+ await loginIfNeeded(page, config);
95
+ const origin = new URL(config.loginUrl).origin;
96
+ await page.goto(`${origin}${dashboardPath}`, {
97
+ waitUntil: "networkidle",
98
+ timeout: 15e3
99
+ });
100
+ const kpiLabels = await page.locator(".sa-kpi__label").allTextContents();
101
+ const normalizedKpis = kpiLabels.map((s) => s.trim());
102
+ for (const expected of config.expectedKpiLabels) {
103
+ expect(normalizedKpis, `KPI '${expected}' fehlt auf Dashboard`).toContain(expected);
104
+ }
105
+ if (config.expectedDistributionTitles?.length) {
106
+ const distTitles = await page.locator(".sa-dashboard__row-head h2").allTextContents();
107
+ const normalizedDist = distTitles.map((s) => s.trim());
108
+ for (const expected of config.expectedDistributionTitles) {
109
+ expect(
110
+ normalizedDist,
111
+ `Distribution '${expected}' fehlt auf Dashboard`
112
+ ).toContain(expected);
113
+ }
114
+ }
115
+ const shortcutTitles = await page.locator(".sa-dashboard__shortcut-title").allTextContents();
116
+ const normalizedShortcuts = shortcutTitles.map((s) => s.trim());
117
+ for (const expected of config.expectedShortcutTitles) {
118
+ expect(normalizedShortcuts, `Shortcut '${expected}' fehlt auf Dashboard`).toContain(
119
+ expected
120
+ );
121
+ }
122
+ for (const forbidden of config.forbiddenShortcutTitles ?? []) {
123
+ expect(
124
+ normalizedShortcuts,
125
+ `Shortcut '${forbidden}' darf nicht auf Dashboard sein`
126
+ ).not.toContain(forbidden);
127
+ }
128
+ });
129
+ });
130
+ }
131
+ // Annotate the CommonJS export names for ESM import in node:
132
+ 0 && (module.exports = {
133
+ runAdminPagesSuite
134
+ });
@@ -0,0 +1,37 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ interface AdminPagesSuiteConfig {
4
+ /** Display name, e.g. 'demoapp' / 'clubapp'. */
5
+ appName: string;
6
+ /** URL of the login page (full). */
7
+ loginUrl: string;
8
+ /** Test account. */
9
+ email: string;
10
+ password: string;
11
+ /** Paths the smoke test should click through. */
12
+ pages: ReadonlyArray<{
13
+ /** Display name in the test description. */
14
+ name: string;
15
+ /** Path relative to the origin (e.g. '/admin/tenants'). */
16
+ path: string;
17
+ /** Optional: if set, check that this selector exists. */
18
+ expectVisible?: string;
19
+ /** Optional: HTTP patterns that are OK as 4xx/5xx (e.g. missing backend endpoints). */
20
+ allowedFailures?: readonly RegExp[];
21
+ }>;
22
+ /** Expected KPI card labels on the dashboard page. */
23
+ expectedKpiLabels: readonly string[];
24
+ /** Expected distribution section titles. */
25
+ expectedDistributionTitles?: readonly string[];
26
+ /** Expected shortcut titles on the dashboard. */
27
+ expectedShortcutTitles: readonly string[];
28
+ /** Shortcuts that must NOT appear (e.g. "Dashboard"). */
29
+ forbiddenShortcutTitles?: readonly string[];
30
+ /** Console error patterns that should be ignored. */
31
+ consoleErrorAllowlist?: readonly RegExp[];
32
+ /** Path to the dashboard page (default `/admin/`). */
33
+ dashboardPath?: string;
34
+ }
35
+ declare function runAdminPagesSuite(test: typeof test, expect: typeof expect, config: AdminPagesSuiteConfig): void;
36
+
37
+ export { type AdminPagesSuiteConfig, runAdminPagesSuite };
@@ -0,0 +1,37 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ interface AdminPagesSuiteConfig {
4
+ /** Display name, e.g. 'demoapp' / 'clubapp'. */
5
+ appName: string;
6
+ /** URL of the login page (full). */
7
+ loginUrl: string;
8
+ /** Test account. */
9
+ email: string;
10
+ password: string;
11
+ /** Paths the smoke test should click through. */
12
+ pages: ReadonlyArray<{
13
+ /** Display name in the test description. */
14
+ name: string;
15
+ /** Path relative to the origin (e.g. '/admin/tenants'). */
16
+ path: string;
17
+ /** Optional: if set, check that this selector exists. */
18
+ expectVisible?: string;
19
+ /** Optional: HTTP patterns that are OK as 4xx/5xx (e.g. missing backend endpoints). */
20
+ allowedFailures?: readonly RegExp[];
21
+ }>;
22
+ /** Expected KPI card labels on the dashboard page. */
23
+ expectedKpiLabels: readonly string[];
24
+ /** Expected distribution section titles. */
25
+ expectedDistributionTitles?: readonly string[];
26
+ /** Expected shortcut titles on the dashboard. */
27
+ expectedShortcutTitles: readonly string[];
28
+ /** Shortcuts that must NOT appear (e.g. "Dashboard"). */
29
+ forbiddenShortcutTitles?: readonly string[];
30
+ /** Console error patterns that should be ignored. */
31
+ consoleErrorAllowlist?: readonly RegExp[];
32
+ /** Path to the dashboard page (default `/admin/`). */
33
+ dashboardPath?: string;
34
+ }
35
+ declare function runAdminPagesSuite(test: typeof test, expect: typeof expect, config: AdminPagesSuiteConfig): void;
36
+
37
+ export { type AdminPagesSuiteConfig, runAdminPagesSuite };
@@ -0,0 +1,109 @@
1
+ // src/testing-e2e/admin-pages-suite.ts
2
+ var RESOURCE_FAILURE_MARKER = /Failed to load resource:/;
3
+ function attachListeners(page, consoleAllowlist = [], networkAllowlist = []) {
4
+ const out = { pageErrors: [], consoleErrors: [], networkErrors: [] };
5
+ page.on("pageerror", (err) => out.pageErrors.push(err.message));
6
+ page.on("console", (msg) => {
7
+ if (msg.type() !== "error") return;
8
+ const text = msg.text();
9
+ if (RESOURCE_FAILURE_MARKER.test(text)) return;
10
+ if (consoleAllowlist.some((re) => re.test(text))) return;
11
+ out.consoleErrors.push(text);
12
+ });
13
+ page.on("response", (resp) => {
14
+ if (resp.status() < 400) return;
15
+ const url = resp.url();
16
+ if (!url.includes("/api/")) return;
17
+ if (networkAllowlist.some((re) => re.test(url))) return;
18
+ out.networkErrors.push(`HTTP ${resp.status()} ${url}`);
19
+ });
20
+ return out;
21
+ }
22
+ async function loginIfNeeded(page, config) {
23
+ if (!page.url().includes("/login")) {
24
+ await page.goto(config.loginUrl, { waitUntil: "networkidle" });
25
+ }
26
+ if (!page.url().includes("/login")) return;
27
+ await page.locator('input[type="email"]').first().fill(config.email);
28
+ const pw = page.locator('input[type="password"]').first();
29
+ await pw.fill(config.password);
30
+ await pw.press("Enter");
31
+ await page.waitForURL((u) => !u.toString().includes("/login"), {
32
+ timeout: 15e3
33
+ });
34
+ }
35
+ function runAdminPagesSuite(test, expect, config) {
36
+ const dashboardPath = config.dashboardPath ?? "/admin/";
37
+ test.describe(`${config.appName}: SuperAdmin-Pages Smoke`, () => {
38
+ for (const pageDef of config.pages) {
39
+ test(`${pageDef.name} rendert ohne Fehler`, async ({ page }) => {
40
+ const errors = attachListeners(
41
+ page,
42
+ config.consoleErrorAllowlist,
43
+ pageDef.allowedFailures
44
+ );
45
+ await loginIfNeeded(page, config);
46
+ const origin = new URL(config.loginUrl).origin;
47
+ await page.goto(`${origin}${pageDef.path}`, {
48
+ waitUntil: "networkidle",
49
+ timeout: 15e3
50
+ });
51
+ await page.waitForTimeout(800);
52
+ if (pageDef.expectVisible) {
53
+ await expect(page.locator(pageDef.expectVisible).first()).toBeVisible({
54
+ timeout: 5e3
55
+ });
56
+ }
57
+ expect(errors.pageErrors, `pageerror-Events auf ${pageDef.path}`).toHaveLength(0);
58
+ expect(
59
+ errors.consoleErrors,
60
+ `console.error-Events auf ${pageDef.path}`
61
+ ).toHaveLength(0);
62
+ expect(
63
+ errors.networkErrors,
64
+ `HTTP-Errors auf /api/ w\xE4hrend ${pageDef.path}`
65
+ ).toHaveLength(0);
66
+ });
67
+ }
68
+ test("Dashboard zeigt erwartete KPIs, Distributions und Shortcuts", async ({ page }) => {
69
+ attachListeners(page, config.consoleErrorAllowlist);
70
+ await loginIfNeeded(page, config);
71
+ const origin = new URL(config.loginUrl).origin;
72
+ await page.goto(`${origin}${dashboardPath}`, {
73
+ waitUntil: "networkidle",
74
+ timeout: 15e3
75
+ });
76
+ const kpiLabels = await page.locator(".sa-kpi__label").allTextContents();
77
+ const normalizedKpis = kpiLabels.map((s) => s.trim());
78
+ for (const expected of config.expectedKpiLabels) {
79
+ expect(normalizedKpis, `KPI '${expected}' fehlt auf Dashboard`).toContain(expected);
80
+ }
81
+ if (config.expectedDistributionTitles?.length) {
82
+ const distTitles = await page.locator(".sa-dashboard__row-head h2").allTextContents();
83
+ const normalizedDist = distTitles.map((s) => s.trim());
84
+ for (const expected of config.expectedDistributionTitles) {
85
+ expect(
86
+ normalizedDist,
87
+ `Distribution '${expected}' fehlt auf Dashboard`
88
+ ).toContain(expected);
89
+ }
90
+ }
91
+ const shortcutTitles = await page.locator(".sa-dashboard__shortcut-title").allTextContents();
92
+ const normalizedShortcuts = shortcutTitles.map((s) => s.trim());
93
+ for (const expected of config.expectedShortcutTitles) {
94
+ expect(normalizedShortcuts, `Shortcut '${expected}' fehlt auf Dashboard`).toContain(
95
+ expected
96
+ );
97
+ }
98
+ for (const forbidden of config.forbiddenShortcutTitles ?? []) {
99
+ expect(
100
+ normalizedShortcuts,
101
+ `Shortcut '${forbidden}' darf nicht auf Dashboard sein`
102
+ ).not.toContain(forbidden);
103
+ }
104
+ });
105
+ });
106
+ }
107
+ export {
108
+ runAdminPagesSuite
109
+ };
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@saasicat/ui-vue",
3
+ "version": "0.2.0",
4
+ "description": "Vue 3 components + composables for the SuperAdmin UI shell. Provides boot loader, manifest loader (ETag cache), nav builder, action registry and standard pages.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./pages/*": "./src/pages-standard/*",
21
+ "./pages-standard/*": "./src/pages-standard/*",
22
+ "./pages-tenant/*": "./src/pages-tenant/*",
23
+ "./components/*": "./src/components/*",
24
+ "./sa-theme.css": "./src/pages-standard/sa-theme.css",
25
+ "./testing-e2e/*": {
26
+ "types": "./dist/testing-e2e/*.d.ts",
27
+ "import": "./dist/testing-e2e/*.js",
28
+ "require": "./dist/testing-e2e/*.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src"
34
+ ],
35
+ "dependencies": {
36
+ "@saasicat/types": "^0.2.0"
37
+ },
38
+ "peerDependencies": {
39
+ "pinia": "^2.0.0 || ^3.0.0",
40
+ "quasar": "^2.18.0",
41
+ "vue": "^3.5.0",
42
+ "vue-router": "^4.5.0 || ^5.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "pinia": {
46
+ "optional": true
47
+ },
48
+ "quasar": {
49
+ "optional": true
50
+ },
51
+ "vue-router": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@playwright/test": "^1.59.1",
57
+ "@types/node": "^25.6.0",
58
+ "pinia": "^3.0.0",
59
+ "quasar": "^2.18.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^6.0.0",
62
+ "vue": "^3.5.0",
63
+ "vue-router": "^5.0.0",
64
+ "vue-tsc": "^3.3.3"
65
+ },
66
+ "license": "Apache-2.0",
67
+ "author": "Taci Uelker",
68
+ "homepage": "https://github.com/uelker70/saasicat",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "https://github.com/uelker70/saasicat.git",
72
+ "directory": "packages/saas-platform-ui-vue"
73
+ },
74
+ "bugs": {
75
+ "url": "https://github.com/uelker70/saasicat/issues"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "scripts": {
81
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @saasicat/types --external vue --external vue-router --external pinia --external quasar && tsup src/testing-e2e/admin-pages-suite.ts --format esm,cjs --dts --out-dir dist/testing-e2e --external @playwright/test",
82
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
83
+ "pretest": "pnpm run build",
84
+ "test": "node --test tests/**/*.test.js",
85
+ "test:e2e": "playwright test"
86
+ }
87
+ }
@@ -0,0 +1,110 @@
1
+ // ActionRegistry — central dispatch for `TenantActionDef`-declared
2
+ // operations.
3
+ //
4
+ // The consumer shell registers an `actions:` map with
5
+ // `actionKey → ActionHandler` bindings. When the manifest action declares
6
+ // e.g. `actionKey: 'TENANT_SUSPEND'`, the shell must provide a handler under
7
+ // the same key — otherwise `dispatch` throws a `MissingHandlerError` with a
8
+ // clear diagnostic.
9
+ //
10
+ // MFA prompt + confirm modal stay consumer-specific (Quasar dialog,
11
+ // headless UI etc.); the registry only supplies the action-definition pair
12
+ // `{def, handler}` so the shell UI layer can trigger the appropriate
13
+ // pre-flow.
14
+
15
+ import type { AdminManifest, TenantActionDef } from '@saasicat/types';
16
+
17
+ /** Consumer implementation; receives the action inputs as a generic object. */
18
+ export type ActionHandler<TInput = unknown, TResult = unknown> = (
19
+ input: TInput,
20
+ ) => Promise<TResult>;
21
+
22
+ export interface ResolvedAction<TInput = unknown, TResult = unknown> {
23
+ def: TenantActionDef;
24
+ handler: ActionHandler<TInput, TResult>;
25
+ }
26
+
27
+ export class MissingHandlerError extends Error {
28
+ constructor(actionKey: string) {
29
+ super(
30
+ `Kein Handler für actionKey "${actionKey}" registriert. ` +
31
+ `Konsument-Shell muss \`actions["${actionKey}"]\` bereitstellen.`,
32
+ );
33
+ this.name = 'MissingHandlerError';
34
+ }
35
+ }
36
+
37
+ export class ActionDefNotInManifestError extends Error {
38
+ constructor(actionKey: string) {
39
+ super(
40
+ `actionKey "${actionKey}" ist im Manifest nicht deklariert. ` +
41
+ `Bitte als TenantAction in einer ManifestContribution registrieren.`,
42
+ );
43
+ this.name = 'ActionDefNotInManifestError';
44
+ }
45
+ }
46
+
47
+ export class ActionRegistry {
48
+ private readonly defs: Map<string, TenantActionDef> = new Map();
49
+ private readonly handlers: Map<string, ActionHandler> = new Map();
50
+
51
+ constructor(manifest: AdminManifest, handlers: Record<string, ActionHandler> = {}) {
52
+ for (const def of manifest.tenants?.actions ?? []) {
53
+ this.defs.set(def.actionKey, def);
54
+ }
55
+ for (const [key, handler] of Object.entries(handlers)) {
56
+ this.handlers.set(key, handler);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Registers a handler after the fact (e.g. when consumer code loads
62
+ * lazily). Throws `ActionDefNotInManifestError` if the `actionKey` is not
63
+ * declared in the manifest — prevents dead registrations.
64
+ */
65
+ register<TInput, TResult>(actionKey: string, handler: ActionHandler<TInput, TResult>): void {
66
+ if (!this.defs.has(actionKey)) {
67
+ throw new ActionDefNotInManifestError(actionKey);
68
+ }
69
+ this.handlers.set(actionKey, handler as ActionHandler);
70
+ }
71
+
72
+ /**
73
+ * Returns the `{def, handler}` pair. Throws if the key is missing from
74
+ * the manifest or no handler is registered. The shell's UI layer calls
75
+ * the method, checks `def.requiresMfa` / `def.confirmType` for the
76
+ * pre-flow, and then calls `handler(input)`.
77
+ */
78
+ get<TInput = unknown, TResult = unknown>(actionKey: string): ResolvedAction<TInput, TResult> {
79
+ const def = this.defs.get(actionKey);
80
+ if (!def) throw new ActionDefNotInManifestError(actionKey);
81
+ const handler = this.handlers.get(actionKey);
82
+ if (!handler) throw new MissingHandlerError(actionKey);
83
+ return { def, handler: handler as ActionHandler<TInput, TResult> };
84
+ }
85
+
86
+ /**
87
+ * Convenience: `get(key).handler(input)`. UI convenience for actions
88
+ * that need neither MFA nor confirm.
89
+ */
90
+ async dispatch<TInput, TResult>(actionKey: string, input: TInput): Promise<TResult> {
91
+ return this.get<TInput, TResult>(actionKey).handler(input);
92
+ }
93
+
94
+ /**
95
+ * List of actionKeys that are declared in the manifest but have no
96
+ * handler. Consumers use this in a doctor check to detect drift between
97
+ * the manifest and the shell build.
98
+ */
99
+ listOrphanedDefs(): string[] {
100
+ return [...this.defs.keys()].filter((k) => !this.handlers.has(k));
101
+ }
102
+
103
+ /**
104
+ * List of registered handlers that are missing from the manifest. Drift
105
+ * in the other direction.
106
+ */
107
+ listOrphanedHandlers(): string[] {
108
+ return [...this.handlers.keys()].filter((k) => !this.defs.has(k));
109
+ }
110
+ }
@@ -0,0 +1,152 @@
1
+ // BatchColumnFetcher — loads custom TenantColumn data in ONE request per
2
+ // column (required by SPEC §4.6: no N+1 for the tenant page table).
3
+ //
4
+ // Manifest contract: every `TenantColumnDef.endpoint` MUST be batch-capable —
5
+ // no `{slug}` or `{tenantId}` placeholder in the path. The fetcher appends
6
+ // `?tenantIds=t1,t2,...,tN` (or `tenantIds=t1&tenantIds=t2&...` — the
7
+ // `paramStyle` option picks the format) and expects a response of the form
8
+ // `Record<tenantId, value>`.
9
+ //
10
+ // Capability filter: columns without a satisfied `requiredCapability` are not
11
+ // fetched at all. Manifest drift (a column `endpoint` with a `{slug}` pattern)
12
+ // is reported in a structured way via `BatchColumnDriftError` — the consumer
13
+ // shell then renders a warning.
14
+
15
+ import type { AdminManifest, TenantColumnDef } from '@saasicat/types';
16
+ import { defaultHttpClient, type HttpClient } from './types.js';
17
+
18
+ export type BatchColumnValue = unknown;
19
+
20
+ /** One value per `tenantId` (freely structured). */
21
+ export type BatchColumnRow = Record<string, BatchColumnValue>;
22
+
23
+ /** One `tenantId → value` map per column key. */
24
+ export type BatchColumnData = Record<string, BatchColumnRow>;
25
+
26
+ export type ParamStyle = 'comma' | 'repeat';
27
+
28
+ export interface BatchColumnFetcherOptions {
29
+ http?: HttpClient;
30
+ /**
31
+ * How the `tenantIds` are passed to the endpoint. Default `'comma'` →
32
+ * `?tenantIds=t1,t2,t3`. `'repeat'` → `?tenantIds=t1&tenantIds=t2`.
33
+ * Consumer backends decide based on their framework (NestJS accepts
34
+ * comma-separated strings by default, or via `@Query() ids: string[]`).
35
+ */
36
+ paramStyle?: ParamStyle;
37
+ /**
38
+ * Auth-token provider for `Authorization: Bearer <token>`. The consumer
39
+ * supplies a function that pulls the current token from the auth store.
40
+ */
41
+ getAuthToken?: () => string | null;
42
+ }
43
+
44
+ export class BatchColumnDriftError extends Error {
45
+ constructor(
46
+ public readonly column: TenantColumnDef,
47
+ reason: string,
48
+ ) {
49
+ super(`Spalte "${column.key}": ${reason}`);
50
+ this.name = 'BatchColumnDriftError';
51
+ }
52
+ }
53
+
54
+ export class BatchColumnFetcher {
55
+ private readonly http: HttpClient;
56
+ private readonly paramStyle: ParamStyle;
57
+ private readonly getAuthToken?: () => string | null;
58
+
59
+ constructor(options: BatchColumnFetcherOptions = {}) {
60
+ this.http = options.http ?? defaultHttpClient();
61
+ this.paramStyle = options.paramStyle ?? 'comma';
62
+ this.getAuthToken = options.getAuthToken;
63
+ }
64
+
65
+ /**
66
+ * Loads the data for all columns declared in the manifest in parallel
67
+ * (one request per column, all `tenantIds` in the batch). Columns without
68
+ * a satisfied capability are ignored.
69
+ */
70
+ async fetchAll(manifest: AdminManifest, tenantIds: string[]): Promise<BatchColumnData> {
71
+ if (tenantIds.length === 0) return {};
72
+ const cols = this.eligibleColumns(manifest);
73
+ const results = await Promise.all(
74
+ cols.map((col) =>
75
+ this.fetchOne(col, tenantIds).then((data) => [col.key, data] as const),
76
+ ),
77
+ );
78
+ const out: BatchColumnData = {};
79
+ for (const [key, data] of results) out[key] = data;
80
+ return out;
81
+ }
82
+
83
+ /** Fetch a single column (consumers may also use this directly). */
84
+ async fetchOne(column: TenantColumnDef, tenantIds: string[]): Promise<BatchColumnRow> {
85
+ this.validateBatchEndpoint(column);
86
+ if (tenantIds.length === 0) return {};
87
+ const url = this.buildUrl(column.endpoint, tenantIds);
88
+ const headers: Record<string, string> = {};
89
+ const token = this.getAuthToken?.();
90
+ if (token) headers.Authorization = `Bearer ${token}`;
91
+ const res = await this.http(url, { method: 'GET', headers });
92
+ if (res.status !== 200) {
93
+ throw new Error(
94
+ `Spalte "${column.key}" — Endpoint ${column.endpoint} antwortete HTTP ${res.status}`,
95
+ );
96
+ }
97
+ const body = (await res.json()) as BatchColumnRow;
98
+ return body;
99
+ }
100
+
101
+ /**
102
+ * Column drift against the manifest. Consumers use this for CI smoke
103
+ * tests of the manifest-vs-shell build.
104
+ */
105
+ listDriftIssues(manifest: AdminManifest): BatchColumnDriftError[] {
106
+ const issues: BatchColumnDriftError[] = [];
107
+ for (const col of manifest.tenants?.columns ?? []) {
108
+ try {
109
+ this.validateBatchEndpoint(col);
110
+ } catch (err) {
111
+ if (err instanceof BatchColumnDriftError) issues.push(err);
112
+ }
113
+ }
114
+ return issues;
115
+ }
116
+
117
+ /**
118
+ * Which columns have a satisfied `requiredCapability`? Manifest =
119
+ * discovery, so a local check is enough.
120
+ */
121
+ eligibleColumns(manifest: AdminManifest): TenantColumnDef[] {
122
+ const caps = manifest.capabilities ?? {};
123
+ return (manifest.tenants?.columns ?? []).filter((col) => {
124
+ if (!col.requiredCapability) return true;
125
+ return caps[col.requiredCapability] === true;
126
+ });
127
+ }
128
+
129
+ private validateBatchEndpoint(col: TenantColumnDef): void {
130
+ if (!col.endpoint || col.endpoint.trim().length === 0) {
131
+ throw new BatchColumnDriftError(col, 'endpoint ist leer');
132
+ }
133
+ if (col.endpoint.includes('{slug}') || col.endpoint.includes('{tenantId}')) {
134
+ throw new BatchColumnDriftError(
135
+ col,
136
+ 'endpoint enthält per-Tenant-Placeholder ({slug}/{tenantId}). ' +
137
+ 'Spalten-Endpoints müssen batchfähig sein — bitte Backend ' +
138
+ 'auf `?tenantIds=...`-Parameter umstellen.',
139
+ );
140
+ }
141
+ }
142
+
143
+ private buildUrl(endpoint: string, tenantIds: string[]): string {
144
+ const sep = endpoint.includes('?') ? '&' : '?';
145
+ if (this.paramStyle === 'comma') {
146
+ const enc = tenantIds.map(encodeURIComponent).join(',');
147
+ return `${endpoint}${sep}tenantIds=${enc}`;
148
+ }
149
+ const params = tenantIds.map((id) => `tenantIds=${encodeURIComponent(id)}`).join('&');
150
+ return `${endpoint}${sep}${params}`;
151
+ }
152
+ }