@cat-factory/app 0.145.0 → 0.146.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/app/components/board/TaskDependencyEdges.vue +37 -73
- package/app/components/brainstorm/BrainstormWindow.vue +2 -7
- package/app/components/clarity/ClarityReviewWindow.vue +2 -2
- package/app/components/consensus/ConsensusSessionWindow.vue +2 -2
- package/app/components/docs/DocInterviewWindow.vue +1 -1
- package/app/components/environments/EnvSetupStepper.vue +70 -0
- package/app/components/environments/EnvironmentSetupWizard.vue +9 -59
- package/app/components/forkDecision/ForkDecisionWindow.vue +1 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +1 -1
- package/app/components/initiative/InitiativeTrackerWindow.vue +1 -1
- package/app/components/prReview/PrReviewWindow.vue +2 -2
- package/app/components/requirements/RequirementsReviewWindow.vue +2 -2
- package/app/components/spec/ServiceSpecWindow.vue +2 -2
- package/app/composables/useResultView.ts +26 -3
- package/app/modular/journeys/environmentSetup.logic.spec.ts +46 -15
- package/app/modular/journeys/environmentSetup.logic.ts +102 -35
- package/app/modular/journeys/environmentSetup.ts +17 -68
- package/app/stores/pipelines.ts +50 -19
- package/app/stores/ui/modals.ts +599 -532
- package/i18n/locales/de.json +1 -0
- package/i18n/locales/en.json +1 -0
- package/i18n/locales/es.json +1 -0
- package/i18n/locales/fr.json +1 -0
- package/i18n/locales/he.json +1 -0
- package/i18n/locales/it.json +1 -0
- package/i18n/locales/ja.json +1 -0
- package/i18n/locales/pl.json +1 -0
- package/i18n/locales/tr.json +1 -0
- package/i18n/locales/uk.json +1 -0
- package/package.json +4 -4
|
@@ -108,88 +108,52 @@ function border(cx: number, cy: number, hw: number, hh: number, tx: number, ty:
|
|
|
108
108
|
return { x: cx + dx * t, y: cy + dy * t }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Resolve the on-screen, origin-relative border-to-border segment between two blocks,
|
|
112
|
+
* or null when either end is missing or both collapsed into the same frame. */
|
|
113
|
+
function segmentBetween(sourceId: string, targetId: string, origin: DOMRect) {
|
|
114
|
+
const a = anchorEl(sourceId)
|
|
115
|
+
const b = anchorEl(targetId)
|
|
116
|
+
if (!a || !b || a === b) return null // missing, or both collapsed into the same frame
|
|
117
|
+
const ra = a.getBoundingClientRect()
|
|
118
|
+
const rb = b.getBoundingClientRect()
|
|
119
|
+
const ax = ra.left + ra.width / 2 - origin.left
|
|
120
|
+
const ay = ra.top + ra.height / 2 - origin.top
|
|
121
|
+
const bx = rb.left + rb.width / 2 - origin.left
|
|
122
|
+
const by = rb.top + rb.height / 2 - origin.top
|
|
123
|
+
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
124
|
+
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
125
|
+
return { x1: start.x, y1: start.y, x2: end.x, y2: end.y }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Map a list of {id, source, target} links to their drawable (arrowhead-agnostic) segments. */
|
|
129
|
+
function linkSegments(
|
|
130
|
+
links: { id: string; source: string; target: string }[],
|
|
131
|
+
origin: DOMRect,
|
|
132
|
+
): { id: string; x1: number; y1: number; x2: number; y2: number }[] {
|
|
133
|
+
const out: { id: string; x1: number; y1: number; x2: number; y2: number }[] = []
|
|
134
|
+
for (const link of links) {
|
|
135
|
+
const seg = segmentBetween(link.source, link.target, origin)
|
|
136
|
+
if (seg) out.push({ id: link.id, ...seg })
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
function recompute() {
|
|
112
142
|
const el = svg.value
|
|
113
143
|
if (!el) return
|
|
114
144
|
const origin = el.getBoundingClientRect()
|
|
115
|
-
const next: Seg[] = []
|
|
116
145
|
|
|
146
|
+
const next: Seg[] = []
|
|
117
147
|
for (const d of taskDeps.value) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const ra = a.getBoundingClientRect()
|
|
123
|
-
const rb = b.getBoundingClientRect()
|
|
124
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
125
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
126
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
127
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
128
|
-
|
|
129
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
130
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
131
|
-
|
|
132
|
-
next.push({
|
|
133
|
-
id: d.id,
|
|
134
|
-
x1: start.x,
|
|
135
|
-
y1: start.y,
|
|
136
|
-
x2: end.x,
|
|
137
|
-
y2: end.y,
|
|
138
|
-
done: board.getBlock(d.source)?.status === 'done',
|
|
139
|
-
})
|
|
148
|
+
const seg = segmentBetween(d.source, d.target, origin)
|
|
149
|
+
if (!seg) continue
|
|
150
|
+
next.push({ id: d.id, ...seg, done: board.getBlock(d.source)?.status === 'done' })
|
|
140
151
|
}
|
|
141
152
|
segments.value = next
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const b = anchorEl(link.target)
|
|
147
|
-
if (!a || !b || a === b) continue
|
|
148
|
-
const ra = a.getBoundingClientRect()
|
|
149
|
-
const rb = b.getBoundingClientRect()
|
|
150
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
151
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
152
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
153
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
154
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
155
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
156
|
-
members.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
157
|
-
}
|
|
158
|
-
memberSegments.value = members
|
|
159
|
-
|
|
160
|
-
const fes: FrontendSeg[] = []
|
|
161
|
-
for (const link of frontendLinks.value) {
|
|
162
|
-
const a = anchorEl(link.source)
|
|
163
|
-
const b = anchorEl(link.target)
|
|
164
|
-
if (!a || !b || a === b) continue
|
|
165
|
-
const ra = a.getBoundingClientRect()
|
|
166
|
-
const rb = b.getBoundingClientRect()
|
|
167
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
168
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
169
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
170
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
171
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
172
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
173
|
-
fes.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
174
|
-
}
|
|
175
|
-
frontendSegments.value = fes
|
|
176
|
-
|
|
177
|
-
const conns: ConnectionSeg[] = []
|
|
178
|
-
for (const link of connectionLinks.value) {
|
|
179
|
-
const a = anchorEl(link.source)
|
|
180
|
-
const b = anchorEl(link.target)
|
|
181
|
-
if (!a || !b || a === b) continue
|
|
182
|
-
const ra = a.getBoundingClientRect()
|
|
183
|
-
const rb = b.getBoundingClientRect()
|
|
184
|
-
const ax = ra.left + ra.width / 2 - origin.left
|
|
185
|
-
const ay = ra.top + ra.height / 2 - origin.top
|
|
186
|
-
const bx = rb.left + rb.width / 2 - origin.left
|
|
187
|
-
const by = rb.top + rb.height / 2 - origin.top
|
|
188
|
-
const start = border(ax, ay, ra.width / 2, ra.height / 2, bx, by)
|
|
189
|
-
const end = border(bx, by, rb.width / 2, rb.height / 2, ax, ay)
|
|
190
|
-
conns.push({ id: link.id, x1: start.x, y1: start.y, x2: end.x, y2: end.y })
|
|
191
|
-
}
|
|
192
|
-
connectionSegments.value = conns
|
|
154
|
+
memberSegments.value = linkSegments(epicLinks.value, origin)
|
|
155
|
+
frontendSegments.value = linkSegments(frontendLinks.value, origin)
|
|
156
|
+
connectionSegments.value = linkSegments(connectionLinks.value, origin)
|
|
193
157
|
}
|
|
194
158
|
|
|
195
159
|
const { pause, resume } = useRafFn(recompute, { immediate: false })
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
const board = useBoardStore()
|
|
24
24
|
const brainstorm = useBrainstormStore()
|
|
25
|
-
const ui = useUiStore()
|
|
26
25
|
const toast = useToast()
|
|
27
26
|
const { t } = useI18n()
|
|
28
27
|
const access = useWorkspaceAccess()
|
|
@@ -32,15 +31,11 @@ const redoComment = ref('')
|
|
|
32
31
|
const showRedo = ref(false)
|
|
33
32
|
|
|
34
33
|
const { open, blockId, stage, close } = useResultView('brainstorm', {
|
|
35
|
-
|
|
36
|
-
// const below is initialised — so read the stage straight off the store here (referencing
|
|
37
|
-
// `stage.value` would hit its temporal dead zone and throw on every open).
|
|
38
|
-
onOpen: (id) => {
|
|
34
|
+
onOpen: ({ blockId, stage }) => {
|
|
39
35
|
drafts.value = {}
|
|
40
36
|
redoComment.value = ''
|
|
41
37
|
showRedo.value = false
|
|
42
|
-
|
|
43
|
-
if (openStage) void brainstorm.load(id, openStage)
|
|
38
|
+
if (stage) void brainstorm.load(blockId, stage)
|
|
44
39
|
},
|
|
45
40
|
})
|
|
46
41
|
const activeStage = computed<BrainstormStage>(() => stage.value ?? 'requirements')
|
|
@@ -43,12 +43,12 @@ const showRedo = ref(false)
|
|
|
43
43
|
// fresh each open, so a non-immediate per-window watch used to leave it empty for whichever
|
|
44
44
|
// route (a pipeline step / "Review & approve") didn't warm the cache by selecting the block.
|
|
45
45
|
const { open, blockId, close } = useResultView('clarity-review', {
|
|
46
|
-
onOpen: (
|
|
46
|
+
onOpen: ({ blockId }) => {
|
|
47
47
|
drafts.value = {}
|
|
48
48
|
seededReply.value = {}
|
|
49
49
|
redoComment.value = ''
|
|
50
50
|
showRedo.value = false
|
|
51
|
-
void clarity.load(
|
|
51
|
+
void clarity.load(blockId)
|
|
52
52
|
},
|
|
53
53
|
// Flush any typed-but-unblurred answer before the view tears down (X, backdrop, Escape) so
|
|
54
54
|
// closing the window never silently drops it (UX-33).
|
|
@@ -20,8 +20,8 @@ const consensus = useConsensusStore()
|
|
|
20
20
|
const models = useModelsStore()
|
|
21
21
|
|
|
22
22
|
const { open, blockId, close } = useResultView('consensus-session', {
|
|
23
|
-
onOpen: (
|
|
24
|
-
void consensus.load(
|
|
23
|
+
onOpen: ({ blockId }) => {
|
|
24
|
+
void consensus.load(blockId)
|
|
25
25
|
},
|
|
26
26
|
})
|
|
27
27
|
|
|
@@ -16,7 +16,7 @@ const { t } = useI18n()
|
|
|
16
16
|
const access = useWorkspaceAccess()
|
|
17
17
|
|
|
18
18
|
const { open, blockId, close } = useResultView('doc-interview', {
|
|
19
|
-
onOpen: (
|
|
19
|
+
onOpen: ({ blockId }) => void docInterview.load(blockId),
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The environment-setup wizard's stepper header, driven entirely by
|
|
3
|
+
// `useJourneyProgress` (modular-react#83, production-feedback item 4). The ordered
|
|
4
|
+
// steps, the live position, and the "Step X of N" total all come from
|
|
5
|
+
// `resolveStepSequence` walking the journey's transition graph — there is no
|
|
6
|
+
// hand-maintained step-order array to keep in sync (the old `ENV_STEP_ORDER` +
|
|
7
|
+
// `crumbState` is gone). Rendered inside `<JourneyHost>`, which hands it the live
|
|
8
|
+
// `instanceId`; each step's label is the i18n key carried in the journey's `steps`
|
|
9
|
+
// metadata.
|
|
10
|
+
import type { InstanceId } from '@modular-vue/journeys'
|
|
11
|
+
import { useJourneyProgress } from '@modular-vue/journeys'
|
|
12
|
+
import { environmentSetupJourney } from '~/modular/journeys/environmentSetup'
|
|
13
|
+
import type { EnvSetupInput } from '~/modular/journeys/environmentSetup.logic'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
/** The live journey instance from `<JourneyHost>`, or null before it starts. */
|
|
17
|
+
instanceId: InstanceId | null
|
|
18
|
+
/** The launch input; frozen for the host's lifetime, so the resolved spine is
|
|
19
|
+
* stable (`useJourneyProgress` reads `sequence.input` once at setup). */
|
|
20
|
+
input: EnvSetupInput
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const { t, te } = useI18n()
|
|
24
|
+
|
|
25
|
+
const { index, total, steps } = useJourneyProgress(
|
|
26
|
+
() => props.instanceId,
|
|
27
|
+
environmentSetupJourney,
|
|
28
|
+
{ sequence: { input: props.input } },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** Resolve a step's progress-label key (from the journey's `steps` metadata)
|
|
32
|
+
* through i18n, tolerating a locale that omits it rather than leaking the key. */
|
|
33
|
+
function stepLabel(key: string | undefined): string {
|
|
34
|
+
return key && te(key) ? t(key) : ''
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="space-y-2" data-testid="env-setup-stepper">
|
|
40
|
+
<p v-if="total" class="text-[11px] font-medium text-slate-400" data-testid="env-setup-progress">
|
|
41
|
+
{{ t('environmentWizard.progress', { index: index + 1, total }) }}
|
|
42
|
+
</p>
|
|
43
|
+
<ol class="flex items-center gap-2 text-[11px]">
|
|
44
|
+
<li
|
|
45
|
+
v-for="(step, i) in steps"
|
|
46
|
+
:key="step.entry"
|
|
47
|
+
class="flex items-center gap-2"
|
|
48
|
+
:data-testid="`env-setup-crumb-${step.entry}`"
|
|
49
|
+
>
|
|
50
|
+
<span
|
|
51
|
+
class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
52
|
+
:class="{
|
|
53
|
+
'bg-primary-500 text-white': i === index,
|
|
54
|
+
'bg-emerald-600/70 text-white': i < index,
|
|
55
|
+
'bg-slate-700 text-slate-300': i > index,
|
|
56
|
+
}"
|
|
57
|
+
>{{ i + 1 }}</span
|
|
58
|
+
>
|
|
59
|
+
<span :class="i === index ? 'text-slate-100' : 'text-slate-500'">
|
|
60
|
+
{{ stepLabel(step.progressLabel) }}
|
|
61
|
+
</span>
|
|
62
|
+
<UIcon
|
|
63
|
+
v-if="i < steps.length - 1"
|
|
64
|
+
name="i-lucide-chevron-right"
|
|
65
|
+
class="h-3 w-3 text-slate-600"
|
|
66
|
+
/>
|
|
67
|
+
</li>
|
|
68
|
+
</ol>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
// journey (`~/modular/journeys/environmentSetup`), hosted here by `<JourneyHost>`:
|
|
9
9
|
// it starts the journey on open (RESUMING the in-flight instance for the same
|
|
10
10
|
// frame when one was left mid-flow, via the Pinia persistence adapter), renders
|
|
11
|
-
// the current step through `<JourneyOutlet>`, and abandons it on close.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// the current step through `<JourneyOutlet>`, and abandons it on close. The
|
|
12
|
+
// stepper header + "Step X of N" are derived from the journey's transition graph
|
|
13
|
+
// by `<EnvSetupStepper>` (via `useJourneyProgress`), not a hand-maintained step
|
|
14
|
+
// list. Each step component drives the `environmentWizard` store for its
|
|
15
|
+
// data/actions and fires the journey's exits to advance. On `complete` (the save
|
|
16
|
+
// step's Done) the journey clears its persisted blob and we close the modal.
|
|
15
17
|
import { computed } from 'vue'
|
|
16
18
|
import { JourneyHost, JourneyOutlet } from '@modular-vue/journeys'
|
|
17
19
|
import { environmentSetupHandle } from '~/modular/journeys/environmentSetup'
|
|
18
|
-
import { ENV_STEP_ORDER, type EnvStep } from '~/modular/journeys/environmentSetup.logic'
|
|
19
20
|
|
|
20
21
|
const ui = useUiStore()
|
|
21
22
|
const { t } = useI18n()
|
|
@@ -30,23 +31,6 @@ const open = computed({
|
|
|
30
31
|
// Read once at journey start; keyed for resume. Frozen for the host's lifetime,
|
|
31
32
|
// so the modal remounts the host per open (it renders under `v-if` upstream).
|
|
32
33
|
const input = computed(() => ({ frameId: ui.environmentWizardFrameId }))
|
|
33
|
-
|
|
34
|
-
const STEP_LABEL = computed<Record<EnvStep, string>>(() => ({
|
|
35
|
-
pick: t('environmentWizard.steps.pick'),
|
|
36
|
-
review: t('environmentWizard.steps.review'),
|
|
37
|
-
preflight: t('environmentWizard.steps.preflight'),
|
|
38
|
-
save: t('environmentWizard.steps.save'),
|
|
39
|
-
}))
|
|
40
|
-
|
|
41
|
-
/** The crumb index of a step, relative to the current journey step. */
|
|
42
|
-
function crumbState(
|
|
43
|
-
entry: EnvStep,
|
|
44
|
-
currentEntry: EnvStep | undefined,
|
|
45
|
-
): 'current' | 'done' | 'todo' {
|
|
46
|
-
if (!currentEntry) return 'todo'
|
|
47
|
-
if (entry === currentEntry) return 'current'
|
|
48
|
-
return ENV_STEP_ORDER.indexOf(entry) < ENV_STEP_ORDER.indexOf(currentEntry) ? 'done' : 'todo'
|
|
49
|
-
}
|
|
50
34
|
</script>
|
|
51
35
|
|
|
52
36
|
<template>
|
|
@@ -63,43 +47,9 @@ function crumbState(
|
|
|
63
47
|
:input="input"
|
|
64
48
|
@finished="ui.closeEnvironmentSetup()"
|
|
65
49
|
>
|
|
66
|
-
<template #default="{ instanceId
|
|
67
|
-
<!-- stepper header,
|
|
68
|
-
<
|
|
69
|
-
<li
|
|
70
|
-
v-for="(s, i) in ENV_STEP_ORDER"
|
|
71
|
-
:key="s"
|
|
72
|
-
class="flex items-center gap-2"
|
|
73
|
-
:data-testid="`env-setup-crumb-${s}`"
|
|
74
|
-
>
|
|
75
|
-
<span
|
|
76
|
-
class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
77
|
-
:class="{
|
|
78
|
-
'bg-primary-500 text-white':
|
|
79
|
-
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current',
|
|
80
|
-
'bg-emerald-600/70 text-white':
|
|
81
|
-
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'done',
|
|
82
|
-
'bg-slate-700 text-slate-300':
|
|
83
|
-
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'todo',
|
|
84
|
-
}"
|
|
85
|
-
>{{ i + 1 }}</span
|
|
86
|
-
>
|
|
87
|
-
<span
|
|
88
|
-
:class="
|
|
89
|
-
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current'
|
|
90
|
-
? 'text-slate-100'
|
|
91
|
-
: 'text-slate-500'
|
|
92
|
-
"
|
|
93
|
-
>
|
|
94
|
-
{{ STEP_LABEL[s] }}
|
|
95
|
-
</span>
|
|
96
|
-
<UIcon
|
|
97
|
-
v-if="i < ENV_STEP_ORDER.length - 1"
|
|
98
|
-
name="i-lucide-chevron-right"
|
|
99
|
-
class="h-3 w-3 text-slate-600"
|
|
100
|
-
/>
|
|
101
|
-
</li>
|
|
102
|
-
</ol>
|
|
50
|
+
<template #default="{ instanceId }">
|
|
51
|
+
<!-- stepper header, derived from the journey graph -->
|
|
52
|
+
<EnvSetupStepper :instance-id="instanceId" :input="input" />
|
|
103
53
|
|
|
104
54
|
<!-- the current step (pick / review / preflight / save) -->
|
|
105
55
|
<div class="mt-4">
|
|
@@ -26,7 +26,7 @@ const { t } = useI18n()
|
|
|
26
26
|
// Hybrid: state rides the coder step (like follow-ups), but warm it from the GET on open too.
|
|
27
27
|
// No `stepRef`: this is a pre-run decision, so there's no "restart from here".
|
|
28
28
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('fork-decision', {
|
|
29
|
-
onOpen: (
|
|
29
|
+
onOpen: ({ blockId }) => void forkDecision.load(blockId),
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -17,7 +17,7 @@ const initiatives = useInitiativesStore()
|
|
|
17
17
|
const { t } = useI18n()
|
|
18
18
|
|
|
19
19
|
const { open, blockId, close } = useResultView('initiative-planning', {
|
|
20
|
-
onOpen: (
|
|
20
|
+
onOpen: ({ blockId }) => void initiatives.load(blockId),
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -26,7 +26,7 @@ const { t } = useI18n()
|
|
|
26
26
|
const toast = useToast()
|
|
27
27
|
|
|
28
28
|
const { open, blockId, close } = useResultView('initiative-tracker', {
|
|
29
|
-
onOpen: (
|
|
29
|
+
onOpen: ({ blockId }) => void initiatives.load(blockId),
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -30,8 +30,8 @@ const access = useWorkspaceAccess()
|
|
|
30
30
|
const { t } = useI18n()
|
|
31
31
|
|
|
32
32
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('pr-review', {
|
|
33
|
-
onOpen: (
|
|
34
|
-
if (instanceId
|
|
33
|
+
onOpen: ({ instanceId }) => {
|
|
34
|
+
if (instanceId) void prReview.load(instanceId)
|
|
35
35
|
},
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -55,7 +55,7 @@ const docCollapsedOverride = ref<boolean | null>(null)
|
|
|
55
55
|
// fresh each open, so a non-immediate per-window watch used to leave it empty for whichever
|
|
56
56
|
// route (a pipeline step / "Review & approve") didn't warm the cache by selecting the block.
|
|
57
57
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('requirements-review', {
|
|
58
|
-
onOpen: (
|
|
58
|
+
onOpen: ({ blockId }) => {
|
|
59
59
|
drafts.value = {}
|
|
60
60
|
seededReply.value = {}
|
|
61
61
|
recommendMode.value = new Set()
|
|
@@ -64,7 +64,7 @@ const { open, blockId, instanceId, stepIndex, close } = useResultView('requireme
|
|
|
64
64
|
redoComment.value = ''
|
|
65
65
|
showRedo.value = false
|
|
66
66
|
docCollapsedOverride.value = null
|
|
67
|
-
void requirements.load(
|
|
67
|
+
void requirements.load(blockId)
|
|
68
68
|
},
|
|
69
69
|
// Closing the window (X, backdrop, Escape) must not silently drop an answer the user typed
|
|
70
70
|
// but never blurred out of. Flush before the view tears down; flushDrafts captures the
|
|
@@ -25,10 +25,10 @@ const mode = ref<ViewMode>('structured')
|
|
|
25
25
|
const selected = ref<{ m: number; g: number } | null>(null)
|
|
26
26
|
|
|
27
27
|
const { open, blockId, close } = useResultView('service-spec', {
|
|
28
|
-
onOpen: (
|
|
28
|
+
onOpen: ({ blockId }) => {
|
|
29
29
|
mode.value = 'structured'
|
|
30
30
|
selected.value = null
|
|
31
|
-
void serviceSpec.load(
|
|
31
|
+
void serviceSpec.load(blockId)
|
|
32
32
|
},
|
|
33
33
|
})
|
|
34
34
|
|
|
@@ -24,9 +24,24 @@
|
|
|
24
24
|
* overlay stack (top overlay closes first, focus/scroll managed too). A listener here would
|
|
25
25
|
* double-fire `close`, so it was removed once the last window converted onto the shell.
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* The fully-resolved view context handed to `onOpen`. Every field is already initialised by
|
|
29
|
+
* the time `onOpen` fires, so a loader takes exactly what it needs from here and never reaches
|
|
30
|
+
* back into the store or the composable's own return refs. That matters because `onOpen` fires
|
|
31
|
+
* synchronously from the `immediate` watch below — DURING the caller's `setup`, before the
|
|
32
|
+
* `const { … } = useResultView(…)` destructure has been assigned — so any callback that closed
|
|
33
|
+
* over those refs would hit their temporal dead zone and throw on every open.
|
|
34
|
+
*/
|
|
35
|
+
export interface OpenResultView {
|
|
36
|
+
blockId: string
|
|
37
|
+
instanceId: string | null
|
|
38
|
+
stepIndex: number | null
|
|
39
|
+
stage: 'requirements' | 'architecture' | null
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
export function useResultView(
|
|
28
43
|
viewId: string,
|
|
29
|
-
opts?: { onOpen?: (
|
|
44
|
+
opts?: { onOpen?: (view: OpenResultView) => void; onClose?: () => void },
|
|
30
45
|
) {
|
|
31
46
|
const ui = useUiStore()
|
|
32
47
|
|
|
@@ -44,12 +59,20 @@ export function useResultView(
|
|
|
44
59
|
ui.closeResultView()
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
// The load-on-open contract: fire immediately on mount and on any later block switch.
|
|
62
|
+
// The load-on-open contract: fire immediately on mount and on any later block switch. The
|
|
63
|
+
// callback receives the fully-resolved context (see OpenResultView) rather than the return
|
|
64
|
+
// refs, which aren't assigned yet at the initial synchronous fire.
|
|
48
65
|
if (opts?.onOpen) {
|
|
49
66
|
watch(
|
|
50
67
|
blockId,
|
|
51
68
|
(id) => {
|
|
52
|
-
if (id)
|
|
69
|
+
if (id)
|
|
70
|
+
opts.onOpen!({
|
|
71
|
+
blockId: id,
|
|
72
|
+
instanceId: instanceId.value,
|
|
73
|
+
stepIndex: stepIndex.value,
|
|
74
|
+
stage: stage.value,
|
|
75
|
+
})
|
|
53
76
|
},
|
|
54
77
|
{ immediate: true },
|
|
55
78
|
)
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolveStepSequence } from '@modular-vue/journeys'
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
+
ENV_MODULE_ID,
|
|
5
|
+
environmentSetupJourney,
|
|
4
6
|
envInitialState,
|
|
5
|
-
envNextAfter,
|
|
6
7
|
envStartStep,
|
|
7
8
|
} from './environmentSetup.logic'
|
|
9
|
+
import en from '../../../i18n/locales/en.json'
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/** Dot-path lookup into the real `en.json`, mirroring which keys actually ship. */
|
|
12
|
+
function hasKey(path: string): boolean {
|
|
13
|
+
return (
|
|
14
|
+
path.split('.').reduce<unknown>((node, seg) => {
|
|
15
|
+
return node && typeof node === 'object' ? (node as Record<string, unknown>)[seg] : undefined
|
|
16
|
+
}, en) !== undefined
|
|
17
|
+
)
|
|
18
|
+
}
|
|
17
19
|
|
|
20
|
+
// Pure navigation graph for the environment-setup journey (slice 3). The step
|
|
21
|
+
// order is derived from the annotated transition graph (production-feedback item
|
|
22
|
+
// 4), so `resolveStepSequenceResult` walking the real journey definition is the
|
|
23
|
+
// guard against a silent reorder or dropped step — no hand-maintained order array
|
|
24
|
+
// to pin separately.
|
|
25
|
+
describe('environment-setup journey logic', () => {
|
|
18
26
|
it('seeds state from the launch input', () => {
|
|
19
27
|
expect(envInitialState({ frameId: 'blk_1' })).toEqual({ frameId: 'blk_1' })
|
|
20
28
|
expect(envInitialState({ frameId: null })).toEqual({ frameId: null })
|
|
@@ -25,10 +33,33 @@ describe('environment-setup journey logic', () => {
|
|
|
25
33
|
expect(envStartStep({ frameId: null }, { frameId: null })).toBe('pick')
|
|
26
34
|
})
|
|
27
35
|
|
|
28
|
-
it('
|
|
29
|
-
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
32
|
-
expect(
|
|
36
|
+
it('derives the pick → review → preflight → save order from the transition graph', () => {
|
|
37
|
+
const steps = resolveStepSequence(environmentSetupJourney, { input: { frameId: null } })
|
|
38
|
+
expect(steps.map((s) => s.entry)).toEqual(['pick', 'review', 'preflight', 'save'])
|
|
39
|
+
expect(steps.every((s) => s.module === ENV_MODULE_ID)).toBe(true)
|
|
40
|
+
expect(steps.map((s) => s.progressLabel)).toEqual([
|
|
41
|
+
'environmentWizard.steps.pick',
|
|
42
|
+
'environmentWizard.steps.review',
|
|
43
|
+
'environmentWizard.steps.preflight',
|
|
44
|
+
'environmentWizard.steps.save',
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('starts the derived sequence at review when a frame is preselected', () => {
|
|
49
|
+
const steps = resolveStepSequence(environmentSetupJourney, { input: { frameId: 'blk_1' } })
|
|
50
|
+
expect(steps.map((s) => s.entry)).toEqual(['review', 'preflight', 'save'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// The stepper resolves each label through a VARIABLE key (`t(step.progressLabel)`), so
|
|
54
|
+
// the tier-1 typed-message-keys check + `vue-i18n-extract` can no longer see these keys
|
|
55
|
+
// as used and can't catch a typo or a dropped key. Pin the missing-key guard here instead:
|
|
56
|
+
// every `progressLabel` the real definition emits must resolve to a shipping `en.json` key.
|
|
57
|
+
it('carries a shipping en.json label key for every derived step', () => {
|
|
58
|
+
const steps = resolveStepSequence(environmentSetupJourney, { input: { frameId: null } })
|
|
59
|
+
for (const step of steps) {
|
|
60
|
+
const key = step.progressLabel
|
|
61
|
+
expect(key, `missing i18n key for step "${step.entry}"`).toBeTruthy()
|
|
62
|
+
expect(hasKey(key ?? ''), `key "${key}" not in en.json`).toBe(true)
|
|
63
|
+
}
|
|
33
64
|
})
|
|
34
65
|
})
|