@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-btn
|
|
3
|
+
v-if="visible"
|
|
4
|
+
class="sa-theme-switcher"
|
|
5
|
+
flat
|
|
6
|
+
dense
|
|
7
|
+
no-caps
|
|
8
|
+
icon="contrast"
|
|
9
|
+
:label="activeLabel"
|
|
10
|
+
:aria-label="msg.header.theme?.label"
|
|
11
|
+
>
|
|
12
|
+
<q-menu auto-close anchor="bottom right" self="top right">
|
|
13
|
+
<q-list dense class="sa-theme-switcher__list">
|
|
14
|
+
<q-item
|
|
15
|
+
v-for="option in options"
|
|
16
|
+
:key="option.scheme"
|
|
17
|
+
clickable
|
|
18
|
+
:active="option.scheme === scheme"
|
|
19
|
+
@click="select(option.scheme)"
|
|
20
|
+
>
|
|
21
|
+
<q-item-section>{{ option.label }}</q-item-section>
|
|
22
|
+
<q-item-section v-if="option.scheme === scheme" side>
|
|
23
|
+
<q-icon name="check" size="16px" />
|
|
24
|
+
</q-item-section>
|
|
25
|
+
</q-item>
|
|
26
|
+
</q-list>
|
|
27
|
+
</q-menu>
|
|
28
|
+
</q-btn>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { computed } from 'vue';
|
|
33
|
+
|
|
34
|
+
import { useSaMessages } from '../vue/use-super-admin-i18n.js';
|
|
35
|
+
import { SA_COLOR_SCHEMES, useSaTheme, type SaColorScheme } from '../vue/use-sa-theme.js';
|
|
36
|
+
|
|
37
|
+
// Colour-scheme switcher for the shell chrome, the sibling of
|
|
38
|
+
// `LocaleSwitcher` — same button, same menu, same fail-open rule, and the two
|
|
39
|
+
// sit next to each other in every header that carries one.
|
|
40
|
+
//
|
|
41
|
+
// It writes the PICKED scheme, never the resolved one. That distinction is the
|
|
42
|
+
// whole reason the control has three entries instead of a toggle: 'system' is
|
|
43
|
+
// a standing instruction to follow the operating system, and collapsing it to
|
|
44
|
+
// whatever the machine says right now silently turns it into a fourth state
|
|
45
|
+
// nobody chose.
|
|
46
|
+
//
|
|
47
|
+
// The button inherits its color from the surrounding chrome (`currentColor`),
|
|
48
|
+
// so it needs no variant prop — each host sets the color it wants. The header
|
|
49
|
+
// is permanently dark in both themes and its foreground is an
|
|
50
|
+
// `--sa-color-inverse-*` role; inheriting is what keeps this button out of that
|
|
51
|
+
// decision.
|
|
52
|
+
|
|
53
|
+
const { scheme, switcherEnabled } = useSaTheme();
|
|
54
|
+
const msg = useSaMessages('shell');
|
|
55
|
+
|
|
56
|
+
// Only an explicit `false` hides the switcher. The injection key is a
|
|
57
|
+
// `Symbol.for`, so a context created by an older copy of this package resolves
|
|
58
|
+
// here just as well — and that one has no `switcherEnabled` at all. Treating
|
|
59
|
+
// the missing field as "off" would make every switcher vanish after such an
|
|
60
|
+
// upgrade, without a warning anywhere. Chrome fails open.
|
|
61
|
+
const visible = switcherEnabled !== false;
|
|
62
|
+
|
|
63
|
+
// Same reason as above, on the i18n side: a catalog from before these keys
|
|
64
|
+
// existed has no `header.theme` at all, and reading through it unguarded turns
|
|
65
|
+
// an upgrade skew into a render-time exception instead of a missing word.
|
|
66
|
+
const options = computed(() =>
|
|
67
|
+
SA_COLOR_SCHEMES.map((value) => ({
|
|
68
|
+
scheme: value,
|
|
69
|
+
label: msg.value.header.theme?.[value] ?? value,
|
|
70
|
+
})),
|
|
71
|
+
);
|
|
72
|
+
const activeLabel = computed(
|
|
73
|
+
() => options.value.find((option) => option.scheme === scheme.value)?.label ?? scheme.value,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
function select(next: SaColorScheme): void {
|
|
77
|
+
scheme.value = next;
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style scoped>
|
|
82
|
+
.sa-theme-switcher {
|
|
83
|
+
color: inherit;
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sa-accordion" :class="{ 'sa-accordion--open': open }">
|
|
3
|
+
<div class="sa-accordion__head">
|
|
4
|
+
<button
|
|
5
|
+
:id="triggerId"
|
|
6
|
+
type="button"
|
|
7
|
+
class="sa-accordion__trigger"
|
|
8
|
+
:aria-expanded="open"
|
|
9
|
+
:aria-controls="bodyId"
|
|
10
|
+
:disabled="disabled"
|
|
11
|
+
@click="emit('update:open', !open)"
|
|
12
|
+
>
|
|
13
|
+
<span
|
|
14
|
+
v-if="$slots.mark"
|
|
15
|
+
class="sa-accordion__mark"
|
|
16
|
+
:class="`sa-accordion__mark--${markTone}`"
|
|
17
|
+
>
|
|
18
|
+
<slot name="mark" />
|
|
19
|
+
</span>
|
|
20
|
+
<span class="sa-accordion__label"><slot name="header" /></span>
|
|
21
|
+
<q-icon name="chevron_right" size="18px" class="sa-accordion__chev" />
|
|
22
|
+
</button>
|
|
23
|
+
<div v-if="$slots['header-actions']" class="sa-accordion__actions">
|
|
24
|
+
<slot name="header-actions" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div
|
|
28
|
+
v-if="open"
|
|
29
|
+
:id="bodyId"
|
|
30
|
+
role="region"
|
|
31
|
+
:aria-labelledby="triggerId"
|
|
32
|
+
class="sa-accordion__body"
|
|
33
|
+
>
|
|
34
|
+
<slot />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { useId } from 'vue';
|
|
41
|
+
|
|
42
|
+
// One disclosure: a header that opens a body. Eight surfaces in this package
|
|
43
|
+
// built their own, in three incompatible idioms, and agreed on nothing —
|
|
44
|
+
// three header paddings, three body paddings, two radii, two colours for the
|
|
45
|
+
// open border, two transition durations, and hover feedback on exactly one of
|
|
46
|
+
// six despite five setting `cursor: pointer`.
|
|
47
|
+
//
|
|
48
|
+
// The reason is not that eight decisions were taken. It is that none was: there
|
|
49
|
+
// is an `AdminTable` and an `AdminSection`, and for "open this" there was
|
|
50
|
+
// nothing, so every page wrote it again. `admin-page-shell.test.ts` rule 14
|
|
51
|
+
// records the same story for tables — "nine pages used QTable directly and
|
|
52
|
+
// agreed on nothing".
|
|
53
|
+
//
|
|
54
|
+
// **The trigger is a `<button>`, and that is the point of the component.**
|
|
55
|
+
// Four of the eight were a `<div>` with a click handler: no `tabindex`, no
|
|
56
|
+
// keyboard, and a screen reader announcing text rather than a control. A grep
|
|
57
|
+
// for `aria-expanded` across the whole package returned nothing at all. A
|
|
58
|
+
// shared component has that argument once; eight surfaces have to win it eight
|
|
59
|
+
// times, and the score was 4:4.
|
|
60
|
+
//
|
|
61
|
+
// `header-actions` is a SIBLING of the button rather than a slot inside it, and
|
|
62
|
+
// both halves of that matter. A control inside the trigger would fire the
|
|
63
|
+
// toggle on its way past — the discovery cards put a live status control in
|
|
64
|
+
// their clickable header — and interactive content nested in a `<button>` is
|
|
65
|
+
// not valid HTML, so the browser's own behaviour there is not something to
|
|
66
|
+
// rely on.
|
|
67
|
+
//
|
|
68
|
+
// The state is NOT owned here. Five of the eight take it from the page
|
|
69
|
+
// (`openKey === row.id`), and in the bundle list opening one card while closing
|
|
70
|
+
// another is tied to loading that bundle's versions — a component that owned a
|
|
71
|
+
// boolean would break that. The other four simply hold the id, and lose nothing
|
|
72
|
+
// by the arrangement. `v-model:open` keeps the page in charge either way.
|
|
73
|
+
//
|
|
74
|
+
// The body is `v-if`, which is what all seven JS implementations already did.
|
|
75
|
+
// Uniform by accident until now; uniform on purpose from here.
|
|
76
|
+
const {
|
|
77
|
+
open,
|
|
78
|
+
disabled = false,
|
|
79
|
+
markTone = 'accent',
|
|
80
|
+
} = defineProps<{
|
|
81
|
+
/** Whether the body is shown. The page owns this — see above. */
|
|
82
|
+
open: boolean;
|
|
83
|
+
/** A locked row: the trigger is inert and announces itself as such. */
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The `mark` badge's tone. `accent` unless the row is in a state the badge
|
|
87
|
+
* itself should report — the discovery quota with no usage provider is the
|
|
88
|
+
* one case, and it swaps its glyph as well.
|
|
89
|
+
*
|
|
90
|
+
* Two values because two exist. The frame, size, radius and position do not
|
|
91
|
+
* change with it: that is what makes the badge the same badge everywhere.
|
|
92
|
+
*/
|
|
93
|
+
markTone?: 'accent' | 'negative';
|
|
94
|
+
}>();
|
|
95
|
+
|
|
96
|
+
const emit = defineEmits<{ 'update:open': [value: boolean] }>();
|
|
97
|
+
|
|
98
|
+
defineSlots<{
|
|
99
|
+
/**
|
|
100
|
+
* The icon that identifies the row, in a badge this component draws.
|
|
101
|
+
*
|
|
102
|
+
* A slot rather than a prop, because the glyph is sometimes conditional and
|
|
103
|
+
* sometimes carries its own attributes. The BADGE is not the page's
|
|
104
|
+
* business: three surfaces had three answers — a 34px accent-tinted square,
|
|
105
|
+
* a bare 22px glyph, and a bare 20px glyph coloured by a Quasar palette
|
|
106
|
+
* prop — so the same row on two pages did not look like the same row.
|
|
107
|
+
*/
|
|
108
|
+
mark?(): unknown;
|
|
109
|
+
/** The header's content — a title, keys, chips, whatever the row needs. */
|
|
110
|
+
header(): unknown;
|
|
111
|
+
/**
|
|
112
|
+
* Controls on the right of the header that must NOT toggle it.
|
|
113
|
+
* Rendered outside the trigger, so a click here is only a click here.
|
|
114
|
+
*/
|
|
115
|
+
'header-actions'?(): unknown;
|
|
116
|
+
/** The body. Only rendered while open. */
|
|
117
|
+
default(): unknown;
|
|
118
|
+
}>();
|
|
119
|
+
|
|
120
|
+
// `useId`, as in `AdminSection`: `aria-controls` and `aria-labelledby` have to
|
|
121
|
+
// point at THIS instance, and an accordion is by nature rendered many times on
|
|
122
|
+
// one page.
|
|
123
|
+
const triggerId = useId();
|
|
124
|
+
const bodyId = useId();
|
|
125
|
+
</script>
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
<div>
|
|
158
158
|
<div class="bcp-section-title">
|
|
159
159
|
{{ msg.fields.quotas }}
|
|
160
|
-
<span class="bcp-field-hint" style="margin-left:
|
|
160
|
+
<span class="bcp-field-hint" style="margin-left: var(--sa-gap-inline)">
|
|
161
161
|
{{ msg.create.quotasOptional }}
|
|
162
162
|
</span>
|
|
163
163
|
</div>
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
class="pc-input"
|
|
123
123
|
type="number"
|
|
124
124
|
min="1"
|
|
125
|
-
style="margin-top:
|
|
125
|
+
style="margin-top: var(--sa-space-3); max-width: 120px"
|
|
126
126
|
:placeholder="
|
|
127
127
|
form.durationType === 'MONTHS'
|
|
128
128
|
? msg.form.durationMonthsPlaceholder
|
|
@@ -205,13 +205,20 @@
|
|
|
205
205
|
</div>
|
|
206
206
|
</div>
|
|
207
207
|
|
|
208
|
-
<!-- Section: Advanced (backend-only fields, collapsed)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
<!-- Section: Advanced (backend-only fields, collapsed).
|
|
209
|
+
|
|
210
|
+
An `AdminAccordion` rather than a fourth `.pc-section`: this is the one
|
|
211
|
+
section of the four that opens, and the package has one way of saying
|
|
212
|
+
that. It reads as `bg-surface` where its three siblings are
|
|
213
|
+
`bg-surface-raised` — the section that behaves differently is the one
|
|
214
|
+
that looks different, and the radius is the same rung either way. -->
|
|
215
|
+
<AdminAccordion v-model:open="advancedOpen">
|
|
216
|
+
<template #header>
|
|
217
|
+
<span class="pc-section__title pc-section__title--inline">
|
|
218
|
+
{{ msg.form.advancedToggle }}
|
|
219
|
+
</span>
|
|
220
|
+
</template>
|
|
221
|
+
<div class="pc-advanced">
|
|
215
222
|
<div class="pc-grid pc-grid--2">
|
|
216
223
|
<div class="pc-field">
|
|
217
224
|
<div class="pc-field__label">{{ msg.form.billingCycleLabel }}</div>
|
|
@@ -253,7 +260,7 @@
|
|
|
253
260
|
/>
|
|
254
261
|
</div>
|
|
255
262
|
</div>
|
|
256
|
-
</
|
|
263
|
+
</AdminAccordion>
|
|
257
264
|
|
|
258
265
|
<!-- Live preview -->
|
|
259
266
|
<div class="pc-preview">
|
|
@@ -268,6 +275,7 @@
|
|
|
268
275
|
|
|
269
276
|
<script setup lang="ts">
|
|
270
277
|
import { computed } from 'vue';
|
|
278
|
+
import AdminAccordion from '../admin-page/AdminAccordion.vue';
|
|
271
279
|
import { IDENTITY_NEUTRAL, identityChipStyle } from '../../client/identity-accents.js';
|
|
272
280
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
273
281
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
@@ -427,23 +435,15 @@ const advancedOpen = defineModel<boolean>('advancedOpen', { default: false });
|
|
|
427
435
|
letter-spacing: -0.005em;
|
|
428
436
|
}
|
|
429
437
|
|
|
430
|
-
.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
cursor: pointer;
|
|
434
|
-
font: 600 var(--sa-text-md) var(--sa-font-body, system-ui, sans-serif);
|
|
435
|
-
color: var(--sa-color-fg-secondary);
|
|
436
|
-
display: inline-flex;
|
|
437
|
-
align-items: center;
|
|
438
|
-
gap: 4px;
|
|
439
|
-
padding: 0;
|
|
438
|
+
/* The same title, in a header that already spaces itself. */
|
|
439
|
+
.pc-section__title--inline {
|
|
440
|
+
margin-bottom: 0;
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
.pc-advanced {
|
|
443
444
|
display: flex;
|
|
444
445
|
flex-direction: column;
|
|
445
446
|
gap: 12px;
|
|
446
|
-
margin-top: 12px;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
.pc-grid {
|
|
@@ -90,7 +90,10 @@
|
|
|
90
90
|
<div>
|
|
91
91
|
<div class="pd-vcol">
|
|
92
92
|
<span class="pd-v-num">v{{ v.version }}</span>
|
|
93
|
-
<span
|
|
93
|
+
<span
|
|
94
|
+
:class="['chip dot', statusChip(v)]"
|
|
95
|
+
style="font-size: var(--sa-text-2xs)"
|
|
96
|
+
>
|
|
94
97
|
{{ statusOf(v) }}
|
|
95
98
|
</span>
|
|
96
99
|
</div>
|
|
@@ -105,7 +108,7 @@
|
|
|
105
108
|
<span v-if="v.validUntil" class="pd-validity-date">
|
|
106
109
|
{{ v.validUntil.slice(0, 10) }}
|
|
107
110
|
</span>
|
|
108
|
-
<span v-else class="pd-arrow-inf"
|
|
111
|
+
<span v-else class="pd-arrow-inf">∞</span>
|
|
109
112
|
</div>
|
|
110
113
|
<span class="pd-validity-sub">{{ validityLabel(v) }}</span>
|
|
111
114
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from './client/index.js';
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
export * from './vue/super-admin-context.js';
|
|
33
33
|
export * from './vue/ui-notify.js';
|
|
34
|
+
export * from './vue/ui-confirm.js';
|
|
34
35
|
export * from './vue/use-super-admin-context.js';
|
|
35
36
|
export * from './vue/use-super-admin-i18n.js';
|
|
36
37
|
export * from './vue/use-sa-theme.js';
|
|
@@ -41,6 +42,10 @@ export * from './vue/use-sa-theme.js';
|
|
|
41
42
|
export * from './vue/create-admin-routes.js';
|
|
42
43
|
export * from './vue/use-api-list.js';
|
|
43
44
|
export * from './vue/use-pagination.js';
|
|
45
|
+
export * from './vue/use-async-action.js';
|
|
46
|
+
export * from './vue/use-async-data.js';
|
|
47
|
+
export * from './vue/resource-registry.js';
|
|
48
|
+
export * from './vue/use-resource-list.js';
|
|
44
49
|
export * from './vue/use-tenants.js';
|
|
45
50
|
export * from './vue/use-audit-entries.js';
|
|
46
51
|
export * from './vue/use-entitlement.js';
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<q-space />
|
|
16
16
|
<slot name="header-actions" />
|
|
17
17
|
<LocaleSwitcher class="q-mr-sm" />
|
|
18
|
+
<ThemeSwitcher class="q-mr-sm" />
|
|
18
19
|
<q-badge class="sa-admin-badge q-mr-sm">{{ msg.header.roleBadge }}</q-badge>
|
|
19
20
|
<div class="sa-admin-user">
|
|
20
21
|
<q-avatar size="32px" class="sa-admin-user__avatar">
|
|
@@ -101,6 +102,7 @@ import { useRoute } from 'vue-router';
|
|
|
101
102
|
import type { AdminManifest, StandardPageKey } from '@saasicat/types';
|
|
102
103
|
import { buildRoutes, buildSidebar, defaultSectionOrder } from '../client/nav-builder.js';
|
|
103
104
|
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
|
105
|
+
import ThemeSwitcher from '../components/ThemeSwitcher.vue';
|
|
104
106
|
import { SUPER_ADMIN_BRAND_KEY, SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
|
|
105
107
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
106
108
|
import { useSignOut } from '../vue/use-sign-out.js';
|
|
@@ -352,18 +354,73 @@ const currentPageTitle = computed(() => {
|
|
|
352
354
|
display: flex;
|
|
353
355
|
align-items: center;
|
|
354
356
|
gap: 8px;
|
|
357
|
+
/* The row's flexible member. Without this the identity block below sits at
|
|
358
|
+
* its content width, and a long name pushes the sign-out button off the
|
|
359
|
+
* end of a `nowrap` toolbar instead of being shortened. */
|
|
360
|
+
min-width: 0;
|
|
355
361
|
}
|
|
356
362
|
.sa-admin-user__avatar {
|
|
357
363
|
background: var(--sa-admin-user-avatar-bg, var(--sa-color-inverse-accent));
|
|
358
364
|
color: var(--sa-admin-user-avatar-fg, var(--sa-color-inverse-accent-fg));
|
|
359
365
|
}
|
|
366
|
+
.sa-admin-user__avatar,
|
|
367
|
+
.sa-admin-user .q-btn {
|
|
368
|
+
/* The two things in this group that are controls or fixed squares. A flex
|
|
369
|
+
* item shrinks from its own basis, so without this the avatar and the
|
|
370
|
+
* sign-out button donate width to a name they should be donating none to —
|
|
371
|
+
* the same shrink that squeezed the login mark to 0px at 320. */
|
|
372
|
+
flex: none;
|
|
373
|
+
}
|
|
360
374
|
.sa-admin-user__name {
|
|
361
375
|
line-height: 1.05;
|
|
376
|
+
min-width: 0;
|
|
377
|
+
}
|
|
378
|
+
/* Text is what shrinking is for; a control is not. Both lines therefore
|
|
379
|
+
* shorten rather than widen the group. */
|
|
380
|
+
.sa-admin-user__name > div {
|
|
381
|
+
max-width: 100%;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
text-overflow: ellipsis;
|
|
384
|
+
white-space: nowrap;
|
|
362
385
|
}
|
|
363
386
|
.sa-admin-user__email {
|
|
364
387
|
color: var(--sa-admin-user-email, var(--sa-color-inverse-accent));
|
|
365
388
|
}
|
|
366
389
|
|
|
390
|
+
/* Below `sm` the header row cannot hold everything it holds on a desktop, and
|
|
391
|
+
* the switcher LABELS are the cheapest thing in it: the icon still names the
|
|
392
|
+
* control, the menu still spells every option out, and — unlike hiding the
|
|
393
|
+
* button — the operator can still reach both. The role badge goes with them
|
|
394
|
+
* because the subtitle two lines up already carries the same word: it is the
|
|
395
|
+
* platform's own `header.subtitle`, "SuperAdmin · …", and it stays on screen at
|
|
396
|
+
* every width. The drawer's brand tag repeats it a third time by default.
|
|
397
|
+
*
|
|
398
|
+
* `599.98px` is Quasar's `xs` upper bound from `theme/_breakpoints.scss`; a
|
|
399
|
+
* `<style>` block cannot read the SCSS, and a custom property is not
|
|
400
|
+
* substituted inside a media condition. */
|
|
401
|
+
@media (max-width: 599.98px) {
|
|
402
|
+
/* `!important` because the thing being hidden is Quasar's own `.block`
|
|
403
|
+
* utility, which declares `display: block !important` — specificity alone
|
|
404
|
+
* loses to it however long the selector gets. Same reason the active drawer
|
|
405
|
+
* item above carries one. */
|
|
406
|
+
.sa-admin-header :deep(.sa-locale-switcher .q-btn__content > .block),
|
|
407
|
+
.sa-admin-header :deep(.sa-theme-switcher .q-btn__content > .block) {
|
|
408
|
+
display: none !important;
|
|
409
|
+
}
|
|
410
|
+
.sa-admin-badge {
|
|
411
|
+
display: none;
|
|
412
|
+
}
|
|
413
|
+
/* And the operator's own name and address, which at this width would be
|
|
414
|
+
* bought from the page title: measured at 360 with both filled in, the
|
|
415
|
+
* title got 24px — one letter — while the row spelled out an email nobody
|
|
416
|
+
* came to the page to read. The avatar keeps the identity present and the
|
|
417
|
+
* full text is one breakpoint away. The truncation above is what covers
|
|
418
|
+
* the band where the block is shown and space is still tight. */
|
|
419
|
+
.sa-admin-user__name {
|
|
420
|
+
display: none;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
367
424
|
.sa-admin-drawer :deep(.q-drawer),
|
|
368
425
|
.sa-admin-drawer :deep(.q-drawer__content),
|
|
369
426
|
.sa-admin-drawer__stack {
|
|
@@ -82,9 +82,10 @@ function retry(): void | Promise<void> {
|
|
|
82
82
|
* backend has recovered.
|
|
83
83
|
*
|
|
84
84
|
* And the cache must go first. The loader keeps an ETag in storage, which a
|
|
85
|
-
* full document load does not touch
|
|
86
|
-
*
|
|
87
|
-
*
|
|
85
|
+
* full document load does not touch, so a plain retry revalidates: a 304 hands
|
|
86
|
+
* back the manifest the app already had, and an operator retrying after a
|
|
87
|
+
* deployment would keep being shown the old one. Clearing first is what makes
|
|
88
|
+
* the retry ask an open question.
|
|
88
89
|
*
|
|
89
90
|
* `router.resolve` rather than a bare path so an app served under a base href
|
|
90
91
|
* lands inside its own app and not at the domain root.
|
|
@@ -687,32 +687,14 @@ const classifyDiff = computed(() => props.classifyDiff);
|
|
|
687
687
|
flex-direction: column;
|
|
688
688
|
gap: 8px;
|
|
689
689
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
overflow: hidden;
|
|
695
|
-
}
|
|
696
|
-
.sa-bd-card.open {
|
|
697
|
-
border-color: var(--sa-color-accent);
|
|
698
|
-
}
|
|
690
|
+
/* The card's surface, open border, head padding, chevron and body now come
|
|
691
|
+
* from `AdminAccordion` — twelve declarations that were this page's fourth
|
|
692
|
+
* opinion on what an accordion looks like. What is left below is what the
|
|
693
|
+
* BUNDLE row puts inside that header. */
|
|
699
694
|
.sa-bd-card__head {
|
|
700
695
|
display: flex;
|
|
701
|
-
gap:
|
|
696
|
+
gap: var(--sa-space-4);
|
|
702
697
|
align-items: center;
|
|
703
|
-
padding: 12px 14px;
|
|
704
|
-
cursor: pointer;
|
|
705
|
-
}
|
|
706
|
-
.sa-bd-card__mark {
|
|
707
|
-
width: 34px;
|
|
708
|
-
height: 34px;
|
|
709
|
-
border-radius: 8px;
|
|
710
|
-
background: var(--sa-color-accent-surface-strong);
|
|
711
|
-
color: var(--sa-color-accent-strong);
|
|
712
|
-
display: flex;
|
|
713
|
-
align-items: center;
|
|
714
|
-
justify-content: center;
|
|
715
|
-
flex-shrink: 0;
|
|
716
698
|
}
|
|
717
699
|
.sa-bd-card__titlewrap {
|
|
718
700
|
flex: 1;
|
|
@@ -737,18 +719,6 @@ const classifyDiff = computed(() => props.classifyDiff);
|
|
|
737
719
|
font-size: var(--sa-text-sm);
|
|
738
720
|
color: var(--sa-color-fg-muted);
|
|
739
721
|
}
|
|
740
|
-
.sa-bd-card__chev {
|
|
741
|
-
transition: transform 0.15s;
|
|
742
|
-
color: var(--sa-color-fg-subtle);
|
|
743
|
-
}
|
|
744
|
-
.sa-bd-card__chev.open {
|
|
745
|
-
transform: rotate(90deg);
|
|
746
|
-
}
|
|
747
|
-
.sa-bd-card__body {
|
|
748
|
-
border-top: 1px solid var(--sa-color-border);
|
|
749
|
-
padding: 14px;
|
|
750
|
-
background: var(--sa-color-bg-sunken);
|
|
751
|
-
}
|
|
752
722
|
.sa-bd-grid {
|
|
753
723
|
display: grid;
|
|
754
724
|
grid-template-columns: minmax(280px, 360px) 1fr;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
/>
|
|
19
19
|
<q-banner v-if="error" class="sa-discovery__error" rounded>
|
|
20
20
|
<template #avatar><q-icon name="warning" color="negative" /></template>
|
|
21
|
-
{{ common.error }}: {{
|
|
21
|
+
{{ common.error }}: {{ errorText }}
|
|
22
22
|
</q-banner>
|
|
23
23
|
|
|
24
24
|
<AdminSection class="q-mt-lg">
|
|
@@ -165,6 +165,7 @@ import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
|
165
165
|
import DiscoveryKpis from './discovery-page/DiscoveryKpis.vue';
|
|
166
166
|
import DiscoveryMetaBanner from './discovery-page/DiscoveryMetaBanner.vue';
|
|
167
167
|
import DiscoveryQuotaCard from './discovery-page/DiscoveryQuotaCard.vue';
|
|
168
|
+
import { adminErrorMessage } from '../client/admin-error.js';
|
|
168
169
|
import { formatMessage } from '../client/i18n/format.js';
|
|
169
170
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
170
171
|
import { statusLabel } from './discovery-page/discovery-ui.js';
|
|
@@ -216,8 +217,14 @@ const expandedQuota = ref<string | null>(null);
|
|
|
216
217
|
|
|
217
218
|
const msg = useSaMessages('discovery');
|
|
218
219
|
const common = useSaMessages('common');
|
|
220
|
+
const errors = useSaMessages('errors');
|
|
219
221
|
const { intlLocale } = useSuperAdminI18n();
|
|
220
222
|
|
|
223
|
+
// The banner shows what the failing side said, or the catalog's sentence for
|
|
224
|
+
// what happened — never the error's own `message`, which is an English
|
|
225
|
+
// diagnostic for the log and was reaching this screen verbatim.
|
|
226
|
+
const errorText = computed(() => (props.error ? adminErrorMessage(props.error, errors.value) : ''));
|
|
227
|
+
|
|
221
228
|
const statusFilterOptions = computed<Array<{ label: string; value: DiscoveryStatus | 'all' }>>(
|
|
222
229
|
() => [
|
|
223
230
|
{ label: msg.value.statusFilterAll, value: 'all' },
|
|
@@ -148,6 +148,7 @@ import { usePlans } from '../vue/use-plans.js';
|
|
|
148
148
|
import { useMarketingProjections } from '../vue/use-marketing-projections.js';
|
|
149
149
|
import { usePromotions } from '../vue/use-promotions.js';
|
|
150
150
|
import { useCatalogEntries } from '../vue/use-catalog-entries.js';
|
|
151
|
+
import { adminErrorMessage } from '../client/admin-error.js';
|
|
151
152
|
import { formatCurrency } from '../client/i18n/currency.js';
|
|
152
153
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
153
154
|
import { formatMessage } from '../client/i18n/format.js';
|
|
@@ -185,8 +186,18 @@ const props = defineProps<{
|
|
|
185
186
|
|
|
186
187
|
const msg = useSaMessages('marketing');
|
|
187
188
|
const common = useSaMessages('common');
|
|
189
|
+
const errors = useSaMessages('errors');
|
|
188
190
|
const { locale, intlLocale } = useSuperAdminI18n();
|
|
189
191
|
|
|
192
|
+
/**
|
|
193
|
+
* What to put in the page banner for a caught failure: what the failing side
|
|
194
|
+
* said, or the catalog's sentence for what happened. Never `err.message` — for
|
|
195
|
+
* this page's own composables that is an English diagnostic for the log.
|
|
196
|
+
*/
|
|
197
|
+
function bannerText(err: unknown): string {
|
|
198
|
+
return adminErrorMessage(err, errors.value);
|
|
199
|
+
}
|
|
200
|
+
|
|
190
201
|
const availableLocales = computed(() =>
|
|
191
202
|
props.availableLocales && props.availableLocales.length > 0 ? props.availableLocales : ['de'],
|
|
192
203
|
);
|
|
@@ -398,7 +409,7 @@ async function reloadAll(): Promise<void> {
|
|
|
398
409
|
]);
|
|
399
410
|
await reloadVersions();
|
|
400
411
|
} catch (err) {
|
|
401
|
-
pageError.value =
|
|
412
|
+
pageError.value = bannerText(err);
|
|
402
413
|
} finally {
|
|
403
414
|
busy.value = false;
|
|
404
415
|
}
|
|
@@ -744,7 +755,7 @@ async function patch(row: MarketingRow, partial: Partial<ResolvedMarketing>): Pr
|
|
|
744
755
|
});
|
|
745
756
|
}
|
|
746
757
|
} catch (err) {
|
|
747
|
-
pageError.value =
|
|
758
|
+
pageError.value = bannerText(err);
|
|
748
759
|
await projectionsApi.load();
|
|
749
760
|
} finally {
|
|
750
761
|
busy.value = false;
|
|
@@ -831,7 +842,7 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
831
842
|
locale: loc,
|
|
832
843
|
});
|
|
833
844
|
} catch (err) {
|
|
834
|
-
pageError.value =
|
|
845
|
+
pageError.value = bannerText(err);
|
|
835
846
|
} finally {
|
|
836
847
|
busy.value = false;
|
|
837
848
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<div class="sa-login-brand__tag">{{ tagText }}</div>
|
|
20
20
|
</div>
|
|
21
21
|
<LocaleSwitcher class="sa-login-locale" />
|
|
22
|
+
<ThemeSwitcher class="sa-login-theme" />
|
|
22
23
|
</div>
|
|
23
24
|
|
|
24
25
|
<h1 class="sa-login-title">{{ msg.login.signIn }}</h1>
|
|
@@ -101,6 +102,7 @@ import { getJson } from '../client/http-json.js';
|
|
|
101
102
|
import { isProductionBoot, resolveLoginBranding } from '../client/login-branding.js';
|
|
102
103
|
import { useSaMessages } from '../vue/use-super-admin-i18n.js';
|
|
103
104
|
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
|
105
|
+
import ThemeSwitcher from '../components/ThemeSwitcher.vue';
|
|
104
106
|
import SuperAdminSetupWizard from './SuperAdminSetupWizard.vue';
|
|
105
107
|
|
|
106
108
|
interface Props {
|
|
@@ -194,13 +196,18 @@ async function handleSubmit(): Promise<void> {
|
|
|
194
196
|
</script>
|
|
195
197
|
|
|
196
198
|
<style scoped>
|
|
197
|
-
.sa-login-locale
|
|
199
|
+
.sa-login-locale,
|
|
200
|
+
.sa-login-theme {
|
|
198
201
|
/* In the card's flow rather than floating over it: a fixed overlay lands
|
|
199
202
|
on top of the centered card on short viewports. */
|
|
200
|
-
margin-left: auto;
|
|
201
203
|
align-self: flex-start;
|
|
202
204
|
color: var(--sa-login-tag-color, var(--sa-color-fg-secondary));
|
|
203
205
|
}
|
|
206
|
+
/* One `auto` on the first of the pair moves both — a second would put the gap
|
|
207
|
+
between them at the mercy of the card width instead of the row's own. */
|
|
208
|
+
.sa-login-locale {
|
|
209
|
+
margin-left: auto;
|
|
210
|
+
}
|
|
204
211
|
.sa-login-wrap {
|
|
205
212
|
min-height: 100vh;
|
|
206
213
|
display: flex;
|
|
@@ -231,6 +238,13 @@ async function handleSubmit(): Promise<void> {
|
|
|
231
238
|
.sa-login-logo {
|
|
232
239
|
width: 44px;
|
|
233
240
|
height: 44px;
|
|
241
|
+
/* The row has to give width up somewhere once the switchers joined it, and
|
|
242
|
+
the mark is the wrong donor: a flex item shrinks from its own basis, so
|
|
243
|
+
the logo went to 32.73px at 390 and to nothing at all at 320. Pinning it
|
|
244
|
+
moves the deficit onto the brand text beside it, which has somewhere to
|
|
245
|
+
put it — it wraps. `min-width: 0` on that text is the other half; without
|
|
246
|
+
it the text refuses the deficit too and the row leaves the viewport. */
|
|
247
|
+
flex: none;
|
|
234
248
|
border-radius: 10px;
|
|
235
249
|
display: flex;
|
|
236
250
|
align-items: center;
|
|
@@ -259,6 +273,13 @@ async function handleSubmit(): Promise<void> {
|
|
|
259
273
|
height: 100%;
|
|
260
274
|
object-fit: contain;
|
|
261
275
|
}
|
|
276
|
+
/* `min-width: auto` is a flex item's default and it refuses to shrink below the
|
|
277
|
+
longest word — which is why the row's deficit landed on the mark instead, and
|
|
278
|
+
why pinning the mark on its own is not the whole fix. Zero lets this block
|
|
279
|
+
take the deficit and wrap. */
|
|
280
|
+
.sa-login-brand__text {
|
|
281
|
+
min-width: 0;
|
|
282
|
+
}
|
|
262
283
|
.sa-login-brand__name {
|
|
263
284
|
font-weight: 800;
|
|
264
285
|
font-size: var(--sa-text-lg);
|