@saasicat/ui-vue 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{catalog-C4Gv2_KM.d.cts → catalog-JXdKdIqG.d.cts} +1 -12
- package/dist/{catalog-C4Gv2_KM.d.ts → catalog-JXdKdIqG.d.ts} +1 -12
- package/dist/{chunk-IZGFS5RS.js → chunk-CC4WK3DC.js} +0 -4
- package/dist/{chunk-XPSD5HKG.js → chunk-VRCVJTY4.js} +1 -1
- package/dist/{chunk-247RJPU4.js → chunk-Y5MWMWMD.js} +1 -1
- package/dist/client/index.cjs +0 -5
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -4
- package/dist/index.cjs +0 -5
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -5
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +2 -2
- package/dist/{use-super-admin-i18n-BdPqeTHl.d.cts → use-super-admin-i18n-1nCOkvgi.d.cts} +1 -1
- package/dist/{use-super-admin-i18n-uteRCL9q.d.ts → use-super-admin-i18n-BlqcD0N7.d.ts} +1 -1
- package/package.json +2 -2
- package/src/client/i18n/locale.ts +0 -17
- package/src/components/bundle-editor/BundleVersionStrip.vue +1 -1
- package/src/components/bundle-editor/bundle-version-status.ts +9 -10
- package/src/components/dialogs/PilotCreateDialog.vue +15 -188
- package/src/components/dialogs/PilotEditDialog.vue +51 -233
- package/src/components/dialogs/PromoCodeCreateDialog.vue +19 -560
- package/src/components/dialogs/PromoCodeDialogFields.vue +666 -0
- package/src/components/dialogs/PromoCodeEditDialog.vue +22 -570
- package/src/components/dialogs/pilot-dialog.css +222 -0
- package/src/pages-standard/DiscoveryPage.vue +1 -1
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +2 -3
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +4 -4
- package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +7 -7
- package/src/pages-standard/discovery-page/discovery-ui.ts +24 -15
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +2 -3
- package/src/pages-standard/plan-versions/format.ts +7 -8
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Building blocks of the pilot dialogs — chrome, sections, inputs, plan
|
|
3
|
+
* picker, end-date presets.
|
|
4
|
+
*
|
|
5
|
+
* Create and edit draw the same elements, so these rules existed twice,
|
|
6
|
+
* identically. Both dialogs pull this file in through
|
|
7
|
+
* `<style src="..." scoped>`, which keeps scoping intact: each component still
|
|
8
|
+
* gets its own id.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.pl-dlg__head {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: flex-start;
|
|
14
|
+
gap: 16px;
|
|
15
|
+
padding: 18px 22px 14px;
|
|
16
|
+
border-bottom: 1px solid var(--sa-border, #e2e8f0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.pl-dlg__title {
|
|
20
|
+
font-family: var(--sa-font-head, system-ui, sans-serif);
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
font-size: 17px;
|
|
23
|
+
color: var(--sa-heading, #0f172a);
|
|
24
|
+
letter-spacing: -0.015em;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.pl-dlg__close {
|
|
28
|
+
margin-left: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.pl-dlg__foot {
|
|
32
|
+
border-top: 1px solid var(--sa-border, #e2e8f0);
|
|
33
|
+
background: #fbfbfd;
|
|
34
|
+
padding: 12px 18px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.pl-section:last-of-type {
|
|
38
|
+
margin-bottom: 4px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.pl-section__head {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: flex-start;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
margin-bottom: 12px;
|
|
46
|
+
padding-bottom: 10px;
|
|
47
|
+
border-bottom: 1px solid #f1f5f9;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.pl-section__num {
|
|
51
|
+
width: 24px;
|
|
52
|
+
height: 24px;
|
|
53
|
+
background: #eff6ff;
|
|
54
|
+
color: #1d4ed8;
|
|
55
|
+
border-radius: 50%;
|
|
56
|
+
font: 700 12px var(--sa-font-body, system-ui, sans-serif);
|
|
57
|
+
display: grid;
|
|
58
|
+
place-items: center;
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.pl-section__title {
|
|
63
|
+
font-size: 13.5px;
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
color: var(--sa-heading, #0f172a);
|
|
66
|
+
letter-spacing: -0.005em;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.pl-section__sub {
|
|
70
|
+
font-size: 11.5px;
|
|
71
|
+
color: #94a3b8;
|
|
72
|
+
margin-top: 2px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.pl-input {
|
|
76
|
+
width: 100%;
|
|
77
|
+
padding: 9px 12px;
|
|
78
|
+
background: #fff;
|
|
79
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
font: 13px var(--sa-font-body, system-ui, sans-serif);
|
|
82
|
+
color: var(--sa-body, #1e293b);
|
|
83
|
+
outline: 0;
|
|
84
|
+
transition:
|
|
85
|
+
border-color 0.12s,
|
|
86
|
+
box-shadow 0.12s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.pl-input:focus {
|
|
90
|
+
border-color: var(--sa-primary, #3f6bff);
|
|
91
|
+
box-shadow: 0 0 0 3px rgba(63, 107, 255, 0.12);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.pl-plan-select {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: 5px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.pl-plan-opt {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 10px;
|
|
104
|
+
padding: 8px 11px;
|
|
105
|
+
background: #fff;
|
|
106
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
text-align: left;
|
|
110
|
+
font-family: var(--sa-font-body, system-ui, sans-serif);
|
|
111
|
+
transition:
|
|
112
|
+
border-color 0.12s,
|
|
113
|
+
background 0.12s;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.pl-plan-opt:hover {
|
|
117
|
+
border-color: #93c5fd;
|
|
118
|
+
background: #fafbff;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.pl-plan-opt--active {
|
|
122
|
+
border-color: var(--sa-primary, #2563eb);
|
|
123
|
+
background: #eff6ff;
|
|
124
|
+
box-shadow: 0 0 0 1px var(--sa-primary, #2563eb) inset;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.pl-plan-opt__dot {
|
|
128
|
+
width: 10px;
|
|
129
|
+
height: 10px;
|
|
130
|
+
border-radius: 50%;
|
|
131
|
+
flex: 0 0 auto;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.pl-plan-opt__text {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
flex: 1;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.pl-plan-opt__key {
|
|
142
|
+
font: 700 11px var(--sa-font-mono, ui-monospace, monospace);
|
|
143
|
+
letter-spacing: 0.05em;
|
|
144
|
+
color: var(--sa-heading, #0f172a);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.pl-plan-opt__label {
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
color: var(--sa-muted, #64748b);
|
|
150
|
+
margin-top: 1px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.pl-plan-opt__check {
|
|
154
|
+
color: #1d4ed8;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.pl-end-row {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 6px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.pl-end-row .pl-input {
|
|
164
|
+
flex: 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pl-btn-mini {
|
|
168
|
+
display: inline-flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 4px;
|
|
171
|
+
padding: 7px 10px;
|
|
172
|
+
background: #fff;
|
|
173
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
174
|
+
border-radius: 7px;
|
|
175
|
+
color: var(--sa-muted-dark, #475569);
|
|
176
|
+
font: 500 11.5px var(--sa-font-body, system-ui, sans-serif);
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.pl-btn-mini:hover {
|
|
182
|
+
background: #f8fafc;
|
|
183
|
+
border-color: #cbd5e1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.pl-end-presets {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex-wrap: wrap;
|
|
189
|
+
gap: 5px;
|
|
190
|
+
margin-top: 5px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.pl-preset-btn {
|
|
194
|
+
padding: 4px 9px;
|
|
195
|
+
background: #fff;
|
|
196
|
+
border: 1px dashed #cbd5e1;
|
|
197
|
+
border-radius: 999px;
|
|
198
|
+
font: 600 11px var(--sa-font-body, system-ui, sans-serif);
|
|
199
|
+
color: var(--sa-muted-dark, #475569);
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
transition:
|
|
202
|
+
background 0.12s,
|
|
203
|
+
border-color 0.12s,
|
|
204
|
+
color 0.12s;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.pl-preset-btn:hover {
|
|
208
|
+
background: #eff6ff;
|
|
209
|
+
border-color: #93c5fd;
|
|
210
|
+
border-style: solid;
|
|
211
|
+
color: #1d4ed8;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.pl-error {
|
|
215
|
+
background: #fef2f2;
|
|
216
|
+
border: 1px solid #fecaca;
|
|
217
|
+
color: #b91c1c;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
margin: 12px 0 0;
|
|
220
|
+
padding: 8px 12px;
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
}
|
|
@@ -195,7 +195,7 @@ const statusFilterOptions = computed<Array<{ label: string; value: DiscoveryStat
|
|
|
195
195
|
() => [
|
|
196
196
|
{ label: msg.value.statusFilterAll, value: 'all' },
|
|
197
197
|
...(['pending', 'approved', 'outdated', 'obsolete'] as const).map((status) => ({
|
|
198
|
-
label: statusLabel(status,
|
|
198
|
+
label: statusLabel(status, msg.value),
|
|
199
199
|
value: status,
|
|
200
200
|
})),
|
|
201
201
|
],
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
type BundleAggregateStatus,
|
|
51
51
|
} from '../../components/bundle-editor/bundle-version-status.js';
|
|
52
52
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
53
|
-
import { useSaMessages
|
|
53
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
54
54
|
|
|
55
55
|
const props = defineProps<{
|
|
56
56
|
filteredBundles: BundleRow[];
|
|
@@ -69,10 +69,9 @@ const emit = defineEmits<{
|
|
|
69
69
|
}>();
|
|
70
70
|
|
|
71
71
|
const msg = useSaMessages('bundles');
|
|
72
|
-
const { locale } = useSuperAdminI18n();
|
|
73
72
|
|
|
74
73
|
function statusMetaOf(bundle: BundleRow) {
|
|
75
|
-
return bundleStatusMeta(props.aggregateStatusOf(bundle),
|
|
74
|
+
return bundleStatusMeta(props.aggregateStatusOf(bundle), msg.value);
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
function statusClass(bundle: BundleRow): string {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
<div class="sa-trans-fields">
|
|
11
11
|
<div v-for="f in fields" :key="f" class="sa-trans-field">
|
|
12
|
-
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f,
|
|
12
|
+
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f, msg, common) }}</label>
|
|
13
13
|
<q-input
|
|
14
14
|
v-if="f !== 'unit'"
|
|
15
15
|
dense
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
<div class="sa-trans-fields">
|
|
49
49
|
<div v-for="f in fields" :key="f" class="sa-trans-field">
|
|
50
|
-
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f,
|
|
50
|
+
<label class="sa-trans-field__cap">{{ i18nFieldLabel(f, msg, common) }}</label>
|
|
51
51
|
<q-input
|
|
52
52
|
dense
|
|
53
53
|
outlined
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
import { computed, reactive } from 'vue';
|
|
72
72
|
import type { CatalogEntryI18nFields } from '@saasicat/types';
|
|
73
73
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
74
|
-
import { useSaMessages
|
|
74
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
75
75
|
import {
|
|
76
76
|
DISCOVERY_DEFAULT_LOCALE,
|
|
77
77
|
i18nFieldLabel,
|
|
@@ -107,7 +107,7 @@ const emit = defineEmits<{
|
|
|
107
107
|
|
|
108
108
|
const msg = useSaMessages('discovery');
|
|
109
109
|
// Renamed: `locale` is the catalog-entry data locale everywhere else in this file.
|
|
110
|
-
const
|
|
110
|
+
const common = useSaMessages('common');
|
|
111
111
|
|
|
112
112
|
const targetLocales = computed(() => props.activeLocales.filter((l) => l !== props.defaultLocale));
|
|
113
113
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="sa-stc" @click.stop>
|
|
3
3
|
<span class="sa-review" :class="`sa-review--${status}`">
|
|
4
|
-
{{ statusLabel(status,
|
|
5
|
-
<q-tooltip>{{ statusHint(status,
|
|
4
|
+
{{ statusLabel(status, msg) }}
|
|
5
|
+
<q-tooltip>{{ statusHint(status, msg) }}</q-tooltip>
|
|
6
6
|
</span>
|
|
7
7
|
<q-btn
|
|
8
8
|
dense
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
:loading="busy"
|
|
16
16
|
@click="emit('set-status', primary.to)"
|
|
17
17
|
>
|
|
18
|
-
<q-tooltip>{{ statusHint(primary.to,
|
|
18
|
+
<q-tooltip>{{ statusHint(primary.to, msg) }}</q-tooltip>
|
|
19
19
|
</q-btn>
|
|
20
20
|
<q-btn v-if="menu.length" dense flat round size="sm" icon="more_horiz">
|
|
21
21
|
<q-menu auto-close anchor="bottom right" self="top right">
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
40
|
import { computed } from 'vue';
|
|
41
41
|
import type { DiscoveryStatus } from '@saasicat/types';
|
|
42
|
-
import {
|
|
42
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
43
43
|
import { primaryReviewAction, reviewMenuActions, statusHint, statusLabel } from './discovery-ui.js';
|
|
44
44
|
|
|
45
45
|
// StatusControl (#20, design sim): status chip + contextual primary action
|
|
@@ -57,10 +57,10 @@ const emit = defineEmits<{
|
|
|
57
57
|
'set-status': [target: DiscoveryStatus];
|
|
58
58
|
}>();
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const msg = useSaMessages('discovery');
|
|
61
61
|
|
|
62
|
-
const primary = computed(() => primaryReviewAction(props.status,
|
|
63
|
-
const menu = computed(() => reviewMenuActions(props.status,
|
|
62
|
+
const primary = computed(() => primaryReviewAction(props.status, msg.value));
|
|
63
|
+
const menu = computed(() => reviewMenuActions(props.status, msg.value));
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
66
|
<style scoped>
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// labels/colors/coverage calculations.
|
|
5
5
|
|
|
6
6
|
import type { CatalogEntryI18n, DiscoveryStatus } from '@saasicat/types';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import type { SaMessages } from '../../client/i18n/messages.js';
|
|
8
|
+
|
|
9
|
+
// The resolved slices these helpers render from. Taking the catalog rather
|
|
10
|
+
// than a locale code is what lets an app-supplied language and `i18n.overrides`
|
|
11
|
+
// reach them — a raw `discoveryMessages[locale]` lookup only ever sees the two
|
|
12
|
+
// catalogs the platform ships.
|
|
13
|
+
type SaDiscoveryMessages = SaMessages['discovery'];
|
|
14
|
+
type SaCommonMessages = SaMessages['common'];
|
|
10
15
|
|
|
11
16
|
/** Translatable fields of a catalog entry. */
|
|
12
17
|
export type I18nField = 'label' | 'description' | 'unit';
|
|
@@ -42,20 +47,24 @@ export function localeFull(locale: string): string {
|
|
|
42
47
|
return LOCALE_NAMES[locale]?.full ?? locale;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
export function i18nFieldLabel(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
export function i18nFieldLabel(
|
|
51
|
+
f: I18nField,
|
|
52
|
+
discovery: SaDiscoveryMessages,
|
|
53
|
+
common: SaCommonMessages,
|
|
54
|
+
): string {
|
|
55
|
+
if (f === 'label') return discovery.trans.fieldLabel;
|
|
56
|
+
if (f === 'description') return common.description;
|
|
57
|
+
return discovery.unit;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
// ─── Approval lifecycle (#20): status display + state machine ────────────────
|
|
52
61
|
|
|
53
|
-
export function statusLabel(status: DiscoveryStatus,
|
|
54
|
-
return
|
|
62
|
+
export function statusLabel(status: DiscoveryStatus, discovery: SaDiscoveryMessages): string {
|
|
63
|
+
return discovery.statusLabels[status];
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
export function statusHint(status: DiscoveryStatus,
|
|
58
|
-
return
|
|
66
|
+
export function statusHint(status: DiscoveryStatus, discovery: SaDiscoveryMessages): string {
|
|
67
|
+
return discovery.statusHints[status];
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export interface ReviewAction {
|
|
@@ -69,9 +78,9 @@ export interface ReviewAction {
|
|
|
69
78
|
/** Primary action per status (design sim `StatusControl`, #20). */
|
|
70
79
|
export function primaryReviewAction(
|
|
71
80
|
status: DiscoveryStatus,
|
|
72
|
-
|
|
81
|
+
discovery: SaDiscoveryMessages,
|
|
73
82
|
): ReviewAction {
|
|
74
|
-
const actions =
|
|
83
|
+
const actions = discovery.reviewActions;
|
|
75
84
|
switch (status) {
|
|
76
85
|
case 'pending':
|
|
77
86
|
return { label: actions.approve, to: 'approved', emphasized: true };
|
|
@@ -87,9 +96,9 @@ export function primaryReviewAction(
|
|
|
87
96
|
/** Kebab menu actions per status (design sim `StatusControl`, #20). */
|
|
88
97
|
export function reviewMenuActions(
|
|
89
98
|
status: DiscoveryStatus,
|
|
90
|
-
|
|
99
|
+
discovery: SaDiscoveryMessages,
|
|
91
100
|
): ReviewAction[] {
|
|
92
|
-
const actions =
|
|
101
|
+
const actions = discovery.reviewActions;
|
|
93
102
|
const markObsolete: ReviewAction = {
|
|
94
103
|
label: actions.markObsolete,
|
|
95
104
|
to: 'obsolete',
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<script setup lang="ts">
|
|
74
74
|
import { computed } from 'vue';
|
|
75
75
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
76
|
-
import { useSaMessages
|
|
76
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
77
77
|
import type { CatalogSnapshot } from './catalog-history.types.js';
|
|
78
78
|
import { formatRelative } from './format.js';
|
|
79
79
|
|
|
@@ -91,7 +91,6 @@ const emit = defineEmits<{
|
|
|
91
91
|
}>();
|
|
92
92
|
|
|
93
93
|
const msg = useSaMessages('planVersions');
|
|
94
|
-
const { locale } = useSuperAdminI18n();
|
|
95
94
|
|
|
96
95
|
const snapshotCountLabel = computed(() =>
|
|
97
96
|
formatMessage(msg.value.timeline.snapshotCount, { count: props.snapshots.length }),
|
|
@@ -109,7 +108,7 @@ function metaText(s: CatalogSnapshot): string {
|
|
|
109
108
|
? msg.value.timeline.noOpenDrafts
|
|
110
109
|
: formatMessage(msg.value.timeline.openCount, { count: s.draftCount });
|
|
111
110
|
}
|
|
112
|
-
if (s.publishedAt) return formatRelative(s.publishedAt,
|
|
111
|
+
if (s.publishedAt) return formatRelative(s.publishedAt, msg.value);
|
|
113
112
|
return msg.value.timeline.unknown;
|
|
114
113
|
}
|
|
115
114
|
</script>
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
// Formatting helper retained for the reusable catalog-history timeline.
|
|
2
2
|
|
|
3
3
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
4
|
-
import {
|
|
5
|
-
import { planVersionsMessages } from '../../client/i18n/messages/plan-versions.js';
|
|
4
|
+
import type { SaMessages } from '../../client/i18n/messages.js';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
// The resolved slice, not a locale code: only this way do an app-supplied
|
|
7
|
+
// language and `i18n.overrides` reach the relative-date wording.
|
|
8
|
+
type SaPlanVersionsMessages = SaMessages['planVersions'];
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
return planVersionsMessages[builtinLocaleOf(locale)].format;
|
|
11
|
-
}
|
|
10
|
+
const EMPTY_VALUE = '—';
|
|
12
11
|
|
|
13
12
|
export function formatRelative(
|
|
14
13
|
iso: string | undefined,
|
|
15
|
-
|
|
14
|
+
planVersions: SaPlanVersionsMessages,
|
|
16
15
|
now: Date = new Date(),
|
|
17
16
|
): string {
|
|
18
17
|
if (!iso) return EMPTY_VALUE;
|
|
19
|
-
const msg =
|
|
18
|
+
const msg = planVersions.format;
|
|
20
19
|
const d = new Date(iso);
|
|
21
20
|
const diffMs = now.getTime() - d.getTime();
|
|
22
21
|
const days = Math.floor(diffMs / 86_400_000);
|