@cat-factory/app 0.6.0 → 0.7.2

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.
@@ -1,178 +1,178 @@
1
- <script setup lang="ts">
2
- // Inspector section shown when the selected task block backs a recurring pipeline.
3
- // Lets the user edit the cadence, pause/resume, run now, and review the run history
4
- // (lazily loaded; retained ~1 week on the backend).
5
- import type { Block } from '~/types/domain'
6
- import type { Recurrence } from '~/types/recurring'
7
-
8
- const props = defineProps<{ block: Block }>()
9
- const recurring = useRecurringPipelinesStore()
10
- const pipelines = usePipelinesStore()
11
- const toast = useToast()
12
-
13
- const schedule = computed(() => recurring.byBlock(props.block.id))
14
- const runs = computed(() =>
15
- schedule.value ? (recurring.runsBySchedule[schedule.value.id] ?? []) : [],
16
- )
17
-
18
- const editing = ref(false)
19
- const draft = ref<Recurrence | null>(null)
20
- const busy = ref(false)
21
-
22
- // Load history whenever a schedule is shown.
23
- watch(
24
- () => schedule.value?.id,
25
- (id) => {
26
- if (id) recurring.loadRuns(id).catch(() => {})
27
- },
28
- { immediate: true },
29
- )
30
-
31
- const pipelineName = computed(
32
- () => pipelines.getPipeline(schedule.value?.pipelineId ?? '')?.name ?? schedule.value?.pipelineId,
33
- )
34
-
35
- function describeCadence(r: Recurrence): string {
36
- const every = r.intervalHours % 24 === 0 ? `${r.intervalHours / 24}d` : `${r.intervalHours}h`
37
- const days =
38
- r.weekdays.length === 0
39
- ? 'any day'
40
- : r.weekdays.map((d) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d]).join(' ')
41
- const window =
42
- r.windowStartHour === null && r.windowEndHour === null
43
- ? ''
44
- : ` · ${String(r.windowStartHour ?? 0).padStart(2, '0')}:00–${String(r.windowEndHour ?? 24).padStart(2, '0')}:00`
45
- return `Every ${every} · ${days}${window} · ${r.timezone}`
46
- }
47
-
48
- function startEdit() {
49
- if (!schedule.value) return
50
- draft.value = { ...schedule.value.recurrence }
51
- editing.value = true
52
- }
53
-
54
- async function saveEdit() {
55
- if (!schedule.value || !draft.value) return
56
- busy.value = true
57
- try {
58
- await recurring.update(schedule.value.id, { recurrence: draft.value })
59
- editing.value = false
60
- } catch (e) {
61
- toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
62
- } finally {
63
- busy.value = false
64
- }
65
- }
66
-
67
- async function toggleEnabled() {
68
- if (!schedule.value) return
69
- busy.value = true
70
- try {
71
- await recurring.update(schedule.value.id, { enabled: !schedule.value.enabled })
72
- } catch (e) {
73
- toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
74
- } finally {
75
- busy.value = false
76
- }
77
- }
78
-
79
- async function runNow() {
80
- if (!schedule.value) return
81
- busy.value = true
82
- try {
83
- await recurring.runNow(schedule.value.id)
84
- } catch (e) {
85
- toast.add({ title: 'Could not run now', description: errMsg(e), color: 'error' })
86
- } finally {
87
- busy.value = false
88
- }
89
- }
90
-
91
- function errMsg(e: unknown) {
92
- return e instanceof Error ? e.message : String(e)
93
- }
94
-
95
- const RUN_COLOR: Record<string, string> = {
96
- running: 'text-amber-400',
97
- done: 'text-emerald-400',
98
- failed: 'text-rose-400',
99
- skipped: 'text-slate-500',
100
- }
101
- function fmtTime(ms: number) {
102
- return new Date(ms).toLocaleString()
103
- }
104
- </script>
105
-
106
- <template>
107
- <div
108
- v-if="schedule"
109
- class="space-y-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
110
- >
111
- <div class="flex items-center justify-between">
112
- <span
113
- class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-indigo-300"
114
- >
115
- <UIcon name="i-lucide-repeat" class="h-3.5 w-3.5" />
116
- Recurring pipeline
117
- </span>
118
- <UBadge :color="schedule.enabled ? 'primary' : 'neutral'" variant="subtle" size="xs">
119
- {{ schedule.enabled ? 'Active' : 'Paused' }}
120
- </UBadge>
121
- </div>
122
-
123
- <p class="text-[11px] text-slate-400">
124
- <span class="text-slate-300">{{ pipelineName }}</span>
125
- </p>
126
-
127
- <template v-if="!editing">
128
- <p class="text-[11px] text-slate-400">{{ describeCadence(schedule.recurrence) }}</p>
129
- <p class="text-[11px] text-slate-500">Next run: {{ fmtTime(schedule.nextRunAt) }}</p>
130
- <div class="flex flex-wrap gap-1.5 pt-1">
131
- <UButton size="xs" variant="soft" icon="i-lucide-play" :loading="busy" @click="runNow">
132
- Run now
133
- </UButton>
134
- <UButton
135
- size="xs"
136
- variant="soft"
137
- color="neutral"
138
- :icon="schedule.enabled ? 'i-lucide-pause' : 'i-lucide-play'"
139
- :loading="busy"
140
- @click="toggleEnabled"
141
- >
142
- {{ schedule.enabled ? 'Pause' : 'Resume' }}
143
- </UButton>
144
- <UButton
145
- size="xs"
146
- variant="ghost"
147
- color="neutral"
148
- icon="i-lucide-pencil"
149
- @click="startEdit"
150
- >
151
- Edit cadence
152
- </UButton>
153
- </div>
154
- </template>
155
-
156
- <template v-else-if="draft">
157
- <RecurringRecurrenceEditor v-model="draft" />
158
- <div class="flex justify-end gap-1.5 pt-1">
159
- <UButton size="xs" variant="ghost" color="neutral" @click="editing = false">Cancel</UButton>
160
- <UButton size="xs" color="primary" :loading="busy" @click="saveEdit">Save</UButton>
161
- </div>
162
- </template>
163
-
164
- <!-- run history -->
165
- <div v-if="runs.length" class="space-y-1 border-t border-slate-800 pt-2">
166
- <span class="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
167
- Recent runs
168
- </span>
169
- <div v-for="run in runs" :key="run.id" class="flex items-center gap-2 text-[11px]">
170
- <span :class="RUN_COLOR[run.status] ?? 'text-slate-400'" class="w-14 shrink-0 capitalize">
171
- {{ run.status }}
172
- </span>
173
- <span class="truncate text-slate-500">{{ fmtTime(run.startedAt) }}</span>
174
- <span v-if="run.outcome" class="ml-auto truncate text-slate-500">{{ run.outcome }}</span>
175
- </div>
176
- </div>
177
- </div>
178
- </template>
1
+ <script setup lang="ts">
2
+ // Inspector section shown when the selected task block backs a recurring pipeline.
3
+ // Lets the user edit the cadence, pause/resume, run now, and review the run history
4
+ // (lazily loaded; retained ~1 week on the backend).
5
+ import type { Block } from '~/types/domain'
6
+ import type { Recurrence } from '~/types/recurring'
7
+
8
+ const props = defineProps<{ block: Block }>()
9
+ const recurring = useRecurringPipelinesStore()
10
+ const pipelines = usePipelinesStore()
11
+ const toast = useToast()
12
+
13
+ const schedule = computed(() => recurring.byBlock(props.block.id))
14
+ const runs = computed(() =>
15
+ schedule.value ? (recurring.runsBySchedule[schedule.value.id] ?? []) : [],
16
+ )
17
+
18
+ const editing = ref(false)
19
+ const draft = ref<Recurrence | null>(null)
20
+ const busy = ref(false)
21
+
22
+ // Load history whenever a schedule is shown.
23
+ watch(
24
+ () => schedule.value?.id,
25
+ (id) => {
26
+ if (id) recurring.loadRuns(id).catch(() => {})
27
+ },
28
+ { immediate: true },
29
+ )
30
+
31
+ const pipelineName = computed(
32
+ () => pipelines.getPipeline(schedule.value?.pipelineId ?? '')?.name ?? schedule.value?.pipelineId,
33
+ )
34
+
35
+ function describeCadence(r: Recurrence): string {
36
+ const every = r.intervalHours % 24 === 0 ? `${r.intervalHours / 24}d` : `${r.intervalHours}h`
37
+ const days =
38
+ r.weekdays.length === 0
39
+ ? 'any day'
40
+ : r.weekdays.map((d) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d]).join(' ')
41
+ const window =
42
+ r.windowStartHour === null && r.windowEndHour === null
43
+ ? ''
44
+ : ` · ${String(r.windowStartHour ?? 0).padStart(2, '0')}:00–${String(r.windowEndHour ?? 24).padStart(2, '0')}:00`
45
+ return `Every ${every} · ${days}${window} · ${r.timezone}`
46
+ }
47
+
48
+ function startEdit() {
49
+ if (!schedule.value) return
50
+ draft.value = { ...schedule.value.recurrence }
51
+ editing.value = true
52
+ }
53
+
54
+ async function saveEdit() {
55
+ if (!schedule.value || !draft.value) return
56
+ busy.value = true
57
+ try {
58
+ await recurring.update(schedule.value.id, { recurrence: draft.value })
59
+ editing.value = false
60
+ } catch (e) {
61
+ toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
62
+ } finally {
63
+ busy.value = false
64
+ }
65
+ }
66
+
67
+ async function toggleEnabled() {
68
+ if (!schedule.value) return
69
+ busy.value = true
70
+ try {
71
+ await recurring.update(schedule.value.id, { enabled: !schedule.value.enabled })
72
+ } catch (e) {
73
+ toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
74
+ } finally {
75
+ busy.value = false
76
+ }
77
+ }
78
+
79
+ async function runNow() {
80
+ if (!schedule.value) return
81
+ busy.value = true
82
+ try {
83
+ await recurring.runNow(schedule.value.id)
84
+ } catch (e) {
85
+ toast.add({ title: 'Could not run now', description: errMsg(e), color: 'error' })
86
+ } finally {
87
+ busy.value = false
88
+ }
89
+ }
90
+
91
+ function errMsg(e: unknown) {
92
+ return e instanceof Error ? e.message : String(e)
93
+ }
94
+
95
+ const RUN_COLOR: Record<string, string> = {
96
+ running: 'text-amber-400',
97
+ done: 'text-emerald-400',
98
+ failed: 'text-rose-400',
99
+ skipped: 'text-slate-500',
100
+ }
101
+ function fmtTime(ms: number) {
102
+ return new Date(ms).toLocaleString()
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <div
108
+ v-if="schedule"
109
+ class="space-y-2 rounded-lg border border-indigo-900/50 bg-indigo-950/20 p-3"
110
+ >
111
+ <div class="flex items-center justify-between">
112
+ <span
113
+ class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-indigo-300"
114
+ >
115
+ <UIcon name="i-lucide-repeat" class="h-3.5 w-3.5" />
116
+ Recurring pipeline
117
+ </span>
118
+ <UBadge :color="schedule.enabled ? 'primary' : 'neutral'" variant="subtle" size="xs">
119
+ {{ schedule.enabled ? 'Active' : 'Paused' }}
120
+ </UBadge>
121
+ </div>
122
+
123
+ <p class="text-[11px] text-slate-400">
124
+ <span class="text-slate-300">{{ pipelineName }}</span>
125
+ </p>
126
+
127
+ <template v-if="!editing">
128
+ <p class="text-[11px] text-slate-400">{{ describeCadence(schedule.recurrence) }}</p>
129
+ <p class="text-[11px] text-slate-500">Next run: {{ fmtTime(schedule.nextRunAt) }}</p>
130
+ <div class="flex flex-wrap gap-1.5 pt-1">
131
+ <UButton size="xs" variant="soft" icon="i-lucide-play" :loading="busy" @click="runNow">
132
+ Run now
133
+ </UButton>
134
+ <UButton
135
+ size="xs"
136
+ variant="soft"
137
+ color="neutral"
138
+ :icon="schedule.enabled ? 'i-lucide-pause' : 'i-lucide-play'"
139
+ :loading="busy"
140
+ @click="toggleEnabled"
141
+ >
142
+ {{ schedule.enabled ? 'Pause' : 'Resume' }}
143
+ </UButton>
144
+ <UButton
145
+ size="xs"
146
+ variant="ghost"
147
+ color="neutral"
148
+ icon="i-lucide-pencil"
149
+ @click="startEdit"
150
+ >
151
+ Edit cadence
152
+ </UButton>
153
+ </div>
154
+ </template>
155
+
156
+ <template v-else-if="draft">
157
+ <RecurringRecurrenceEditor v-model="draft" />
158
+ <div class="flex justify-end gap-1.5 pt-1">
159
+ <UButton size="xs" variant="ghost" color="neutral" @click="editing = false">Cancel</UButton>
160
+ <UButton size="xs" color="primary" :loading="busy" @click="saveEdit">Save</UButton>
161
+ </div>
162
+ </template>
163
+
164
+ <!-- run history -->
165
+ <div v-if="runs.length" class="space-y-1 border-t border-slate-800 pt-2">
166
+ <span class="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
167
+ Recent runs
168
+ </span>
169
+ <div v-for="run in runs" :key="run.id" class="flex items-center gap-2 text-[11px]">
170
+ <span :class="RUN_COLOR[run.status] ?? 'text-slate-400'" class="w-14 shrink-0 capitalize">
171
+ {{ run.status }}
172
+ </span>
173
+ <span class="truncate text-slate-500">{{ fmtTime(run.startedAt) }}</span>
174
+ <span v-if="run.outcome" class="ml-auto truncate text-slate-500">{{ run.outcome }}</span>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </template>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted } from 'vue'
3
3
  import type { Block } from '~/types/domain'
4
+ import type { WritebackOverride } from '~/types/tracker'
4
5
 
5
6
  const props = defineProps<{ block: Block }>()
6
7
 
@@ -8,6 +9,7 @@ const board = useBoardStore()
8
9
  const mergePresets = useMergePresetsStore()
9
10
  const pipelines = usePipelinesStore()
10
11
  const accounts = useAccountsStore()
12
+ const tracker = useTrackerStore()
11
13
 
12
14
  // ---- responsible product person --------------------------------------------
13
15
  // The account member (a `product` role-holder) accountable for this task; they are
@@ -87,6 +89,41 @@ const pipelineMenu = computed(() => [
87
89
  function setPipeline(id: string) {
88
90
  board.updateBlock(props.block.id, { pipelineId: id })
89
91
  }
92
+
93
+ // ---- issue-tracker writeback overrides -------------------------------------
94
+ // Per-task overrides for the two workspace writeback toggles (comment on PR open,
95
+ // close linked issue on merge). null override ⇒ inherit the workspace default.
96
+ function setCommentOnPrOpen(value: WritebackOverride | null) {
97
+ board.updateBlock(props.block.id, { trackerCommentOnPrOpen: value })
98
+ }
99
+ function setResolveOnMerge(value: WritebackOverride | null) {
100
+ board.updateBlock(props.block.id, { trackerResolveOnMerge: value })
101
+ }
102
+ function writebackMenu(set: (value: WritebackOverride | null) => void) {
103
+ return [
104
+ [
105
+ { label: 'Inherit workspace', icon: 'i-lucide-rotate-ccw', onSelect: () => set(null) },
106
+ { label: 'On', icon: 'i-lucide-check', onSelect: () => set('on') },
107
+ { label: 'Off', icon: 'i-lucide-x', onSelect: () => set('off') },
108
+ ],
109
+ ]
110
+ }
111
+
112
+ function writebackLabel(
113
+ override: WritebackOverride | null | undefined,
114
+ wsDefault: boolean,
115
+ ): string {
116
+ if (override === 'on') return 'On'
117
+ if (override === 'off') return 'Off'
118
+ return `Inherit (${wsDefault ? 'on' : 'off'})`
119
+ }
120
+
121
+ const commentOnPrOpenLabel = computed(() =>
122
+ writebackLabel(props.block.trackerCommentOnPrOpen, tracker.settings.writebackCommentOnPrOpen),
123
+ )
124
+ const resolveOnMergeLabel = computed(() =>
125
+ writebackLabel(props.block.trackerResolveOnMerge, tracker.settings.writebackResolveOnMerge),
126
+ )
90
127
  </script>
91
128
 
92
129
  <template>
@@ -152,6 +189,46 @@ function setPipeline(id: string) {
152
189
  </div>
153
190
  </div>
154
191
 
192
+ <!-- issue-tracker writeback overrides -->
193
+ <div>
194
+ <div class="mb-1 flex items-center justify-between">
195
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
196
+ Issue writeback
197
+ </span>
198
+ </div>
199
+ <div class="space-y-1.5">
200
+ <div class="flex items-center justify-between">
201
+ <span class="text-[11px] text-slate-400">Comment on PR open</span>
202
+ <UDropdownMenu :items="writebackMenu(setCommentOnPrOpen)">
203
+ <UButton
204
+ size="xs"
205
+ variant="ghost"
206
+ color="neutral"
207
+ trailing-icon="i-lucide-chevron-down"
208
+ >
209
+ {{ commentOnPrOpenLabel }}
210
+ </UButton>
211
+ </UDropdownMenu>
212
+ </div>
213
+ <div class="flex items-center justify-between">
214
+ <span class="text-[11px] text-slate-400">Close on merge</span>
215
+ <UDropdownMenu :items="writebackMenu(setResolveOnMerge)">
216
+ <UButton
217
+ size="xs"
218
+ variant="ghost"
219
+ color="neutral"
220
+ trailing-icon="i-lucide-chevron-down"
221
+ >
222
+ {{ resolveOnMergeLabel }}
223
+ </UButton>
224
+ </UDropdownMenu>
225
+ </div>
226
+ </div>
227
+ <div class="mt-1 text-[11px] text-slate-500">
228
+ Writes back to this task's linked tracker issue. Overrides the workspace default.
229
+ </div>
230
+ </div>
231
+
155
232
  <!-- responsible product person -->
156
233
  <div>
157
234
  <div class="mb-1 flex items-center justify-between">