@cat-factory/app 0.53.0 → 0.54.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/app/components/bootstrap/BootstrapModal.vue +81 -69
- package/app/components/environments/EnvironmentStatusPanel.vue +37 -14
- package/app/components/fragments/FragmentLibraryManager.vue +104 -69
- package/app/components/fragments/FragmentLibraryPanel.vue +2 -1
- package/app/components/kaizen/KaizenPanel.vue +36 -25
- package/app/components/kaizen/KaizenStepStatus.vue +13 -10
- package/app/components/media/ArtifactLightbox.vue +20 -14
- package/app/components/media/ImageCompare.vue +35 -22
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +27 -14
- package/app/components/recurring/RecurrenceEditor.vue +22 -15
- package/app/components/sandbox/SandboxPanel.vue +126 -59
- package/app/components/settings/InfrastructureBackendPicker.vue +304 -0
- package/app/components/settings/InfrastructureWindow.vue +6 -10
- package/app/components/settings/KubernetesRunnerForm.vue +30 -0
- package/app/components/settings/ProviderConnectionTab.vue +46 -85
- package/app/components/settings/ProviderManifestEditor.vue +19 -3
- package/i18n/locales/en.json +425 -13
- package/i18n/locales/es.json +403 -11
- package/i18n/locales/fr.json +403 -11
- package/i18n/locales/pl.json +403 -11
- package/i18n/locales/uk.json +403 -11
- package/package.json +2 -2
- package/app/components/settings/ExecutionBackendSelector.vue +0 -171
|
@@ -28,6 +28,8 @@ const emit = defineEmits<{
|
|
|
28
28
|
(e: 'update:index', value: number): void
|
|
29
29
|
}>()
|
|
30
30
|
|
|
31
|
+
const { t, n } = useI18n()
|
|
32
|
+
|
|
31
33
|
const MIN_SCALE = 1
|
|
32
34
|
const MAX_SCALE = 8
|
|
33
35
|
const scale = ref(1)
|
|
@@ -161,32 +163,36 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
161
163
|
class="fixed inset-0 z-[60] flex flex-col bg-slate-950/95 backdrop-blur-sm focus:outline-none"
|
|
162
164
|
role="dialog"
|
|
163
165
|
aria-modal="true"
|
|
164
|
-
:aria-label="
|
|
166
|
+
:aria-label="
|
|
167
|
+
current
|
|
168
|
+
? t('media.lightbox.ariaLabel', { label: current.label })
|
|
169
|
+
: t('media.lightbox.ariaLabelFallback')
|
|
170
|
+
"
|
|
165
171
|
@click.self="close"
|
|
166
172
|
>
|
|
167
173
|
<!-- Toolbar -->
|
|
168
174
|
<div class="flex items-center gap-3 border-b border-slate-800/60 px-4 py-2.5">
|
|
169
175
|
<span class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-200">
|
|
170
|
-
{{ current?.label ?? '
|
|
176
|
+
{{ current?.label ?? t('media.lightbox.fallbackTitle') }}
|
|
171
177
|
</span>
|
|
172
178
|
<span v-if="total > 1" class="shrink-0 text-[12px] tabular-nums text-slate-400">
|
|
173
|
-
{{ index + 1
|
|
179
|
+
{{ t('media.lightbox.counter', { current: index + 1, total }) }}
|
|
174
180
|
</span>
|
|
175
181
|
<div class="flex shrink-0 items-center gap-1">
|
|
176
182
|
<button
|
|
177
183
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200 disabled:opacity-40"
|
|
178
|
-
title="
|
|
184
|
+
:title="t('media.lightbox.zoomOut')"
|
|
179
185
|
:disabled="scale <= MIN_SCALE"
|
|
180
186
|
@click="zoomBy(1 / 1.25)"
|
|
181
187
|
>
|
|
182
188
|
<UIcon name="i-lucide-zoom-out" class="h-4 w-4" />
|
|
183
189
|
</button>
|
|
184
|
-
<span class="w-10 text-center text-[11px] tabular-nums text-slate-500"
|
|
185
|
-
|
|
186
|
-
>
|
|
190
|
+
<span class="w-10 text-center text-[11px] tabular-nums text-slate-500">{{
|
|
191
|
+
n(scale, 'percent')
|
|
192
|
+
}}</span>
|
|
187
193
|
<button
|
|
188
194
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200 disabled:opacity-40"
|
|
189
|
-
title="
|
|
195
|
+
:title="t('media.lightbox.zoomIn')"
|
|
190
196
|
:disabled="scale >= MAX_SCALE"
|
|
191
197
|
@click="zoomBy(1.25)"
|
|
192
198
|
>
|
|
@@ -194,14 +200,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
194
200
|
</button>
|
|
195
201
|
<button
|
|
196
202
|
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
197
|
-
title="
|
|
203
|
+
:title="t('media.lightbox.reset')"
|
|
198
204
|
@click="resetView"
|
|
199
205
|
>
|
|
200
206
|
<UIcon name="i-lucide-maximize" class="h-4 w-4" />
|
|
201
207
|
</button>
|
|
202
208
|
<button
|
|
203
209
|
class="ml-1 rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
204
|
-
title="
|
|
210
|
+
:title="t('media.lightbox.close')"
|
|
205
211
|
@click="close"
|
|
206
212
|
>
|
|
207
213
|
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
@@ -218,7 +224,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
218
224
|
<button
|
|
219
225
|
v-if="total > 1"
|
|
220
226
|
class="absolute left-3 top-1/2 z-10 -translate-y-1/2 rounded-full bg-slate-900/80 p-2 text-slate-300 hover:bg-slate-800 hover:text-white"
|
|
221
|
-
title="
|
|
227
|
+
:title="t('media.lightbox.prev')"
|
|
222
228
|
@click="go(-1)"
|
|
223
229
|
>
|
|
224
230
|
<UIcon name="i-lucide-chevron-left" class="h-5 w-5" />
|
|
@@ -248,21 +254,21 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
248
254
|
:class="state === 'error' ? '' : 'animate-spin'"
|
|
249
255
|
/>
|
|
250
256
|
<p class="text-[12px]">
|
|
251
|
-
{{ state === 'error' ? '
|
|
257
|
+
{{ state === 'error' ? t('media.lightbox.failed') : t('media.lightbox.loading') }}
|
|
252
258
|
</p>
|
|
253
259
|
<button
|
|
254
260
|
v-if="state === 'error' && current"
|
|
255
261
|
class="text-[12px] text-amber-300 hover:underline"
|
|
256
262
|
@click="props.blobs.retry(current.artifactId)"
|
|
257
263
|
>
|
|
258
|
-
|
|
264
|
+
{{ t('common.retry') }}
|
|
259
265
|
</button>
|
|
260
266
|
</div>
|
|
261
267
|
|
|
262
268
|
<button
|
|
263
269
|
v-if="total > 1"
|
|
264
270
|
class="absolute right-3 top-1/2 z-10 -translate-y-1/2 rounded-full bg-slate-900/80 p-2 text-slate-300 hover:bg-slate-800 hover:text-white"
|
|
265
|
-
title="
|
|
271
|
+
:title="t('media.lightbox.next')"
|
|
266
272
|
@click="go(1)"
|
|
267
273
|
>
|
|
268
274
|
<UIcon name="i-lucide-chevron-right" class="h-5 w-5" />
|
|
@@ -23,6 +23,8 @@ const emit = defineEmits<{
|
|
|
23
23
|
(e: 'uploadReference', file: File): void
|
|
24
24
|
}>()
|
|
25
25
|
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
|
|
26
28
|
type Mode = 'side-by-side' | 'overlay' | 'swipe' | 'diff'
|
|
27
29
|
|
|
28
30
|
const actualUrl = computed(() => props.blobs.urlFor(props.actualId))
|
|
@@ -32,14 +34,15 @@ const hasBoth = computed(() => !!actualUrl.value && !!refUrl.value)
|
|
|
32
34
|
const diffFailed = ref(false)
|
|
33
35
|
const MODES = computed<{ id: Mode; icon: string; label: string }[]>(() => {
|
|
34
36
|
const base: { id: Mode; icon: string; label: string }[] = [
|
|
35
|
-
{ id: 'side-by-side', icon: 'i-lucide-columns-2', label: '
|
|
37
|
+
{ id: 'side-by-side', icon: 'i-lucide-columns-2', label: t('media.compare.mode.sideBySide') },
|
|
36
38
|
]
|
|
37
39
|
if (hasBoth.value) {
|
|
38
40
|
base.push(
|
|
39
|
-
{ id: 'overlay', icon: 'i-lucide-layers', label: '
|
|
40
|
-
{ id: 'swipe', icon: 'i-lucide-flip-horizontal-2', label: '
|
|
41
|
+
{ id: 'overlay', icon: 'i-lucide-layers', label: t('media.compare.mode.overlay') },
|
|
42
|
+
{ id: 'swipe', icon: 'i-lucide-flip-horizontal-2', label: t('media.compare.mode.swipe') },
|
|
41
43
|
)
|
|
42
|
-
if (!diffFailed.value)
|
|
44
|
+
if (!diffFailed.value)
|
|
45
|
+
base.push({ id: 'diff', icon: 'i-lucide-contrast', label: t('media.compare.mode.diff') })
|
|
43
46
|
}
|
|
44
47
|
return base
|
|
45
48
|
})
|
|
@@ -162,13 +165,19 @@ function onRefInput(e: Event) {
|
|
|
162
165
|
<!-- SIDE BY SIDE -->
|
|
163
166
|
<div v-if="mode === 'side-by-side'" class="grid grid-cols-2 gap-3">
|
|
164
167
|
<figure class="space-y-1">
|
|
165
|
-
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
168
|
+
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
169
|
+
{{ t('media.compare.actual') }}
|
|
170
|
+
</figcaption>
|
|
166
171
|
<button
|
|
167
172
|
v-if="actualUrl"
|
|
168
173
|
class="block w-full overflow-hidden rounded border border-slate-800 hover:border-slate-600"
|
|
169
174
|
@click="actualId && emit('expand', actualId)"
|
|
170
175
|
>
|
|
171
|
-
<img
|
|
176
|
+
<img
|
|
177
|
+
:src="actualUrl"
|
|
178
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
179
|
+
class="w-full cursor-zoom-in"
|
|
180
|
+
/>
|
|
172
181
|
</button>
|
|
173
182
|
<div
|
|
174
183
|
v-else
|
|
@@ -176,29 +185,33 @@ function onRefInput(e: Event) {
|
|
|
176
185
|
>
|
|
177
186
|
{{
|
|
178
187
|
props.blobs.statusFor(actualId) === 'error'
|
|
179
|
-
? '
|
|
188
|
+
? t('media.compare.failedToLoad')
|
|
180
189
|
: actualId
|
|
181
|
-
? '
|
|
182
|
-
: '
|
|
190
|
+
? t('media.compare.loading')
|
|
191
|
+
: t('media.compare.notCaptured')
|
|
183
192
|
}}
|
|
184
193
|
</div>
|
|
185
194
|
</figure>
|
|
186
195
|
|
|
187
196
|
<figure class="space-y-1">
|
|
188
197
|
<figcaption class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
189
|
-
|
|
198
|
+
{{ t('media.compare.reference') }}
|
|
190
199
|
</figcaption>
|
|
191
200
|
<button
|
|
192
201
|
v-if="refUrl"
|
|
193
202
|
class="group relative block w-full overflow-hidden rounded border border-slate-800 hover:border-slate-600"
|
|
194
203
|
@click="referenceId && emit('expand', referenceId)"
|
|
195
204
|
>
|
|
196
|
-
<img
|
|
205
|
+
<img
|
|
206
|
+
:src="refUrl"
|
|
207
|
+
:alt="t('media.compare.referenceAlt', { view })"
|
|
208
|
+
class="w-full cursor-zoom-in"
|
|
209
|
+
/>
|
|
197
210
|
<span
|
|
198
211
|
class="absolute bottom-1 right-1 rounded bg-slate-950/80 px-1.5 py-0.5 text-[10px] text-slate-300 opacity-0 group-hover:opacity-100"
|
|
199
212
|
@click.stop="refInput?.click()"
|
|
200
213
|
>
|
|
201
|
-
|
|
214
|
+
{{ t('media.compare.replace') }}
|
|
202
215
|
</span>
|
|
203
216
|
</button>
|
|
204
217
|
<!-- Drop zone when no reference yet -->
|
|
@@ -216,7 +229,7 @@ function onRefInput(e: Event) {
|
|
|
216
229
|
@drop.prevent="onDrop"
|
|
217
230
|
>
|
|
218
231
|
<UIcon name="i-lucide-image-up" class="h-5 w-5" />
|
|
219
|
-
<span>
|
|
232
|
+
<span>{{ t('media.compare.dropHint') }}</span>
|
|
220
233
|
</div>
|
|
221
234
|
</figure>
|
|
222
235
|
</div>
|
|
@@ -224,17 +237,17 @@ function onRefInput(e: Event) {
|
|
|
224
237
|
<!-- OVERLAY (onion-skin) -->
|
|
225
238
|
<div v-else-if="mode === 'overlay'" class="space-y-2">
|
|
226
239
|
<div class="relative w-full overflow-hidden rounded border border-slate-800">
|
|
227
|
-
<img :src="refUrl" :alt="
|
|
240
|
+
<img :src="refUrl" :alt="t('media.compare.referenceAlt', { view })" class="w-full" />
|
|
228
241
|
<!-- object-contain so a differing aspect ratio onion-skins undistorted over the reference. -->
|
|
229
242
|
<img
|
|
230
243
|
:src="actualUrl"
|
|
231
|
-
:alt="
|
|
244
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
232
245
|
class="absolute inset-0 h-full w-full object-contain"
|
|
233
246
|
:style="{ opacity: overlayOpacity / 100 }"
|
|
234
247
|
/>
|
|
235
248
|
</div>
|
|
236
249
|
<div class="flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-500">
|
|
237
|
-
<span>
|
|
250
|
+
<span>{{ t('media.compare.reference') }}</span>
|
|
238
251
|
<input
|
|
239
252
|
v-model.number="overlayOpacity"
|
|
240
253
|
type="range"
|
|
@@ -242,7 +255,7 @@ function onRefInput(e: Event) {
|
|
|
242
255
|
max="100"
|
|
243
256
|
class="flex-1 accent-amber-500"
|
|
244
257
|
/>
|
|
245
|
-
<span>
|
|
258
|
+
<span>{{ t('media.compare.actual') }}</span>
|
|
246
259
|
</div>
|
|
247
260
|
</div>
|
|
248
261
|
|
|
@@ -256,7 +269,7 @@ function onRefInput(e: Event) {
|
|
|
256
269
|
@pointerup="onSwipeUp"
|
|
257
270
|
@pointercancel="onSwipeUp"
|
|
258
271
|
>
|
|
259
|
-
<img :src="refUrl" :alt="
|
|
272
|
+
<img :src="refUrl" :alt="t('media.compare.referenceAlt', { view })" class="block w-full" />
|
|
260
273
|
<div
|
|
261
274
|
class="absolute inset-0 overflow-hidden"
|
|
262
275
|
:style="{ clipPath: `inset(0 ${100 - splitPct}% 0 0)` }"
|
|
@@ -265,7 +278,7 @@ function onRefInput(e: Event) {
|
|
|
265
278
|
ratio doesn't stretch it; the split then compares like-for-like. -->
|
|
266
279
|
<img
|
|
267
280
|
:src="actualUrl"
|
|
268
|
-
:alt="
|
|
281
|
+
:alt="t('media.compare.actualAlt', { view })"
|
|
269
282
|
class="absolute inset-0 block h-full w-full object-contain"
|
|
270
283
|
/>
|
|
271
284
|
</div>
|
|
@@ -278,18 +291,18 @@ function onRefInput(e: Event) {
|
|
|
278
291
|
</div>
|
|
279
292
|
<span
|
|
280
293
|
class="absolute left-1 top-1 rounded bg-slate-950/70 px-1 text-[9px] uppercase text-slate-300"
|
|
281
|
-
>
|
|
294
|
+
>{{ t('media.compare.actual') }}</span
|
|
282
295
|
>
|
|
283
296
|
<span
|
|
284
297
|
class="absolute right-1 top-1 rounded bg-slate-950/70 px-1 text-[9px] uppercase text-slate-300"
|
|
285
|
-
>
|
|
298
|
+
>{{ t('media.compare.reference') }}</span
|
|
286
299
|
>
|
|
287
300
|
</div>
|
|
288
301
|
|
|
289
302
|
<!-- DIFF (canvas) -->
|
|
290
303
|
<div v-else-if="mode === 'diff'" class="space-y-1">
|
|
291
304
|
<canvas ref="diffCanvas" class="w-full rounded border border-slate-800 bg-black" />
|
|
292
|
-
<p class="text-[10px] text-slate-500">
|
|
305
|
+
<p class="text-[10px] text-slate-500">{{ t('media.compare.diffHint') }}</p>
|
|
293
306
|
</div>
|
|
294
307
|
|
|
295
308
|
<!-- Hidden file input shared by replace/drop zone -->
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
// panels' drawer, or `executionId` for a run's "Infrastructure attempts" drawer (which
|
|
7
7
|
// surfaces that run's container/runner/env attempts). Loaded on mount + re-loadable.
|
|
8
8
|
import { onMounted } from 'vue'
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ProvisioningOperation,
|
|
11
|
+
ProvisioningOutcome,
|
|
12
|
+
ProvisioningSubsystem,
|
|
13
|
+
} from '~/types/provisioningLogs'
|
|
10
14
|
|
|
11
15
|
const props = defineProps<{ subsystem?: ProvisioningSubsystem; executionId?: string }>()
|
|
12
16
|
|
|
17
|
+
const { t, d } = useI18n()
|
|
18
|
+
|
|
13
19
|
const store = useProvisioningLogsStore()
|
|
14
20
|
const state = computed(() =>
|
|
15
21
|
props.executionId
|
|
@@ -24,17 +30,24 @@ function reload() {
|
|
|
24
30
|
|
|
25
31
|
onMounted(reload)
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Exhaustive enum→label maps of literal `t(...)` keys (keeps the typed-key drift guard
|
|
34
|
+
// live for these runtime-indexed lookups).
|
|
35
|
+
const OPERATION_LABEL = computed<Record<ProvisioningOperation, string>>(() => ({
|
|
36
|
+
provision: t('provisioning.operation.provision'),
|
|
37
|
+
teardown: t('provisioning.operation.teardown'),
|
|
38
|
+
status: t('provisioning.operation.status'),
|
|
39
|
+
dispatch: t('provisioning.operation.dispatch'),
|
|
40
|
+
release: t('provisioning.operation.release'),
|
|
41
|
+
'poll-failure': t('provisioning.operation.poll-failure'),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const OUTCOME_LABEL = computed<Record<ProvisioningOutcome, string>>(() => ({
|
|
45
|
+
success: t('provisioning.outcome.success'),
|
|
46
|
+
failure: t('provisioning.outcome.failure'),
|
|
47
|
+
}))
|
|
35
48
|
|
|
36
49
|
function when(epochMs: number): string {
|
|
37
|
-
return new Date(epochMs)
|
|
50
|
+
return d(new Date(epochMs), 'long')
|
|
38
51
|
}
|
|
39
52
|
</script>
|
|
40
53
|
|
|
@@ -42,7 +55,7 @@ function when(epochMs: number): string {
|
|
|
42
55
|
<div class="rounded-lg border border-slate-700 bg-slate-900/50">
|
|
43
56
|
<div class="flex items-center justify-between border-b border-slate-800 px-3 py-2">
|
|
44
57
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
45
|
-
|
|
58
|
+
{{ t('provisioning.title') }}
|
|
46
59
|
</p>
|
|
47
60
|
<UButton
|
|
48
61
|
icon="i-lucide-rotate-ccw"
|
|
@@ -51,7 +64,7 @@ function when(epochMs: number): string {
|
|
|
51
64
|
:loading="state.loading"
|
|
52
65
|
@click="reload"
|
|
53
66
|
>
|
|
54
|
-
|
|
67
|
+
{{ t('provisioning.refresh') }}
|
|
55
68
|
</UButton>
|
|
56
69
|
</div>
|
|
57
70
|
|
|
@@ -60,7 +73,7 @@ function when(epochMs: number): string {
|
|
|
60
73
|
v-else-if="!state.loading && state.entries.length === 0"
|
|
61
74
|
class="px-3 py-3 text-[12px] text-slate-500"
|
|
62
75
|
>
|
|
63
|
-
|
|
76
|
+
{{ t('provisioning.empty') }}
|
|
64
77
|
</p>
|
|
65
78
|
|
|
66
79
|
<ul v-else class="max-h-80 divide-y divide-slate-800 overflow-auto">
|
|
@@ -79,7 +92,7 @@ function when(epochMs: number): string {
|
|
|
79
92
|
? 'bg-emerald-950/60 text-emerald-300'
|
|
80
93
|
: 'bg-rose-950/60 text-rose-300'
|
|
81
94
|
"
|
|
82
|
-
>{{ entry.outcome }}</span
|
|
95
|
+
>{{ OUTCOME_LABEL[entry.outcome] }}</span
|
|
83
96
|
>
|
|
84
97
|
<span class="ml-auto text-[11px] text-slate-500">{{ when(entry.createdAt) }}</span>
|
|
85
98
|
</div>
|
|
@@ -8,15 +8,22 @@ import type { Recurrence } from '~/types/recurring'
|
|
|
8
8
|
const props = defineProps<{ modelValue: Recurrence }>()
|
|
9
9
|
const emit = defineEmits<{ 'update:modelValue': [Recurrence] }>()
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
// Exhaustive day-index→label map of literal `t(...)` keys (keeps the typed-key drift
|
|
14
|
+
// guard live for this short-form weekday list).
|
|
15
|
+
const WEEKDAY_LABELS = computed<Record<number, string>>(() => ({
|
|
16
|
+
0: t('recurring.weekday.sun'),
|
|
17
|
+
1: t('recurring.weekday.mon'),
|
|
18
|
+
2: t('recurring.weekday.tue'),
|
|
19
|
+
3: t('recurring.weekday.wed'),
|
|
20
|
+
4: t('recurring.weekday.thu'),
|
|
21
|
+
5: t('recurring.weekday.fri'),
|
|
22
|
+
6: t('recurring.weekday.sat'),
|
|
23
|
+
}))
|
|
24
|
+
const WEEKDAYS = computed(() =>
|
|
25
|
+
[0, 1, 2, 3, 4, 5, 6].map((value) => ({ value, label: WEEKDAY_LABELS.value[value]! })),
|
|
26
|
+
)
|
|
20
27
|
|
|
21
28
|
function patch(p: Partial<Recurrence>) {
|
|
22
29
|
emit('update:modelValue', { ...props.modelValue, ...p })
|
|
@@ -60,7 +67,7 @@ const timezoneOptions = computed(() =>
|
|
|
60
67
|
|
|
61
68
|
<template>
|
|
62
69
|
<div class="space-y-3">
|
|
63
|
-
<UFormField label="
|
|
70
|
+
<UFormField :label="t('recurring.runEvery')">
|
|
64
71
|
<div class="flex items-center gap-2">
|
|
65
72
|
<UInput
|
|
66
73
|
:model-value="modelValue.intervalHours"
|
|
@@ -69,11 +76,11 @@ const timezoneOptions = computed(() =>
|
|
|
69
76
|
class="w-24"
|
|
70
77
|
@update:model-value="patch({ intervalHours: Math.max(1, Number($event) || 1) })"
|
|
71
78
|
/>
|
|
72
|
-
<span class="text-xs text-slate-400">hours</span>
|
|
79
|
+
<span class="text-xs text-slate-400">{{ t('recurring.hours') }}</span>
|
|
73
80
|
</div>
|
|
74
81
|
</UFormField>
|
|
75
82
|
|
|
76
|
-
<UFormField label="
|
|
83
|
+
<UFormField :label="t('recurring.allowedDays')" :help="t('recurring.allowedDaysHelp')">
|
|
77
84
|
<div class="flex flex-wrap gap-1">
|
|
78
85
|
<UButton
|
|
79
86
|
v-for="d in WEEKDAYS"
|
|
@@ -91,7 +98,7 @@ const timezoneOptions = computed(() =>
|
|
|
91
98
|
<UFormField>
|
|
92
99
|
<UCheckbox
|
|
93
100
|
:model-value="windowEnabled"
|
|
94
|
-
label="
|
|
101
|
+
:label="t('recurring.windowToggle')"
|
|
95
102
|
@update:model-value="toggleWindow(Boolean($event))"
|
|
96
103
|
/>
|
|
97
104
|
</UFormField>
|
|
@@ -103,7 +110,7 @@ const timezoneOptions = computed(() =>
|
|
|
103
110
|
class="w-28"
|
|
104
111
|
@update:model-value="patch({ windowStartHour: Number($event) })"
|
|
105
112
|
/>
|
|
106
|
-
<span class="text-xs text-slate-400">to</span>
|
|
113
|
+
<span class="text-xs text-slate-400">{{ t('recurring.to') }}</span>
|
|
107
114
|
<USelect
|
|
108
115
|
:model-value="modelValue.windowEndHour ?? 24 % 24"
|
|
109
116
|
:items="hours"
|
|
@@ -112,7 +119,7 @@ const timezoneOptions = computed(() =>
|
|
|
112
119
|
/>
|
|
113
120
|
</div>
|
|
114
121
|
|
|
115
|
-
<UFormField label="
|
|
122
|
+
<UFormField :label="t('recurring.timezone')">
|
|
116
123
|
<USelect
|
|
117
124
|
:model-value="modelValue.timezone"
|
|
118
125
|
:items="timezoneOptions"
|