@cat-factory/app 0.247.0 → 0.249.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/AddTaskModal.vue +1 -0
- package/app/components/board/nodes/BlockNode.vue +24 -0
- package/app/components/context/ContextAttachmentFields.vue +102 -0
- package/app/components/context/pastedLinkOffer.logic.spec.ts +35 -0
- package/app/components/context/pastedLinkOffer.logic.ts +50 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +50 -0
- package/app/components/documents/SpawnPreviewModal.vue +92 -16
- package/app/components/documents/StartFromDesignModal.vue +237 -0
- package/app/components/environments/EnvironmentSetupWizard.vue +1 -1
- package/app/components/environments/steps/JourneyStepNav.vue +1 -1
- package/app/components/layout/AccountDeploymentSettings.vue +122 -0
- package/app/components/layout/BoardToolbar.vue +1 -1
- package/app/components/layout/CommandBar.vue +1 -1
- package/app/components/layout/SideBar.vue +2 -2
- package/app/components/merge/MergeEffortChips.vue +1 -1
- package/app/components/panels/InspectorPanel.vue +1 -1
- package/app/components/panels/ResultWindowShell.vue +2 -2
- package/app/components/panels/StepResultViewHost.vue +1 -1
- package/app/components/panels/inspector/TaskRunSettings.vue +1 -1
- package/app/components/settings/MergeClassRulesEditor.vue +1 -1
- package/app/composables/api/documents.ts +17 -6
- package/app/composables/useDocumentSourceConnect.ts +88 -0
- package/app/modular/agent-kinds.ts +1 -1
- package/app/modular/external-tools.spec.ts +1 -0
- package/app/modular/journeys/environmentSetup.logic.ts +1 -1
- package/app/modular/journeys/environmentSetup.ts +1 -1
- package/app/modular/journeys/persistence.ts +1 -1
- package/app/modular/nav-contributions.spec.ts +2 -0
- package/app/modular/nav-contributions.ts +11 -1
- package/app/modular/nav-gates.ts +4 -0
- package/app/modular/panels/inspector.logic.ts +1 -1
- package/app/modular/registry.spec.ts +1 -0
- package/app/modular/registry.ts +1 -1
- package/app/modular/result-views.ts +1 -1
- package/app/modular/slots.ts +1 -1
- package/app/modular/tutorial-tours.spec.ts +5 -0
- package/app/modular/tutorial-tours.ts +77 -0
- package/app/pages/index.vue +4 -0
- package/app/plugins/modular.client.ts +1 -1
- package/app/stores/agents.ts +1 -1
- package/app/stores/documents.spec.ts +60 -0
- package/app/stores/documents.ts +52 -24
- package/app/stores/environmentWizard.ts +1 -1
- package/app/stores/publicApiKeys.spec.ts +1 -0
- package/app/stores/ui/modals.ts +15 -0
- package/app/utils/modular.ts +1 -1
- package/i18n/locales/de.json +82 -6
- package/i18n/locales/en.json +82 -6
- package/i18n/locales/es.json +82 -6
- package/i18n/locales/fr.json +82 -6
- package/i18n/locales/he.json +82 -6
- package/i18n/locales/it.json +82 -6
- package/i18n/locales/ja.json +82 -6
- package/i18n/locales/pl.json +82 -6
- package/i18n/locales/tr.json +82 -6
- package/i18n/locales/uk.json +82 -6
- package/package.json +2 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Start a task from a design link: paste a Figma/Zeplin URL on a service frame, and the resolved
|
|
3
|
+
// reference is staged onto the add-task form as context. One affordance, three steps that used to
|
|
4
|
+
// be four separate surfaces (Integrations hub → import modal → board → add task → attach).
|
|
5
|
+
//
|
|
6
|
+
// Three rules this surface exists to hold, each of which the obvious version gets wrong.
|
|
7
|
+
//
|
|
8
|
+
// - It resolves the reference BEFORE anything is created, through the same
|
|
9
|
+
// `POST /document-sources/:source/resolve-ref` the attach picker uses, so a share link's title
|
|
10
|
+
// segment and tracking params are trimmed where the person who pasted them can still see it,
|
|
11
|
+
// and a link the source cannot read is a correction rather than a toast over a task that
|
|
12
|
+
// already exists without its design.
|
|
13
|
+
// - It only asks the CONNECTED DESIGN sources, and it asks them in claim-confidence order
|
|
14
|
+
// (host-pinned first), because a host-blind prose parser will happily claim a Figma URL whose
|
|
15
|
+
// file key carries a UUID-shaped run. `connectedDesignSources` is already in registry order
|
|
16
|
+
// and every design source is host-pinned, so the order is the store's.
|
|
17
|
+
// - A WIDENED reference is stated separately from a trimmed one. Figma's own Copy link emits a
|
|
18
|
+
// complex instance id for any component instance, which the parser cannot read, so it falls
|
|
19
|
+
// back to the whole file: "I attached this frame" and "I attached the entire design" otherwise
|
|
20
|
+
// render identically. For a designer that widening IS the defect, not a detail.
|
|
21
|
+
import { refRowFor, classifyRefFailure, type RefState } from './ContextDocumentPicker.logic'
|
|
22
|
+
import type { DocumentSourceKind, ResolvedDocumentRef } from '~/types/domain'
|
|
23
|
+
|
|
24
|
+
const { t } = useI18n()
|
|
25
|
+
const ui = useUiStore()
|
|
26
|
+
const documents = useDocumentsStore()
|
|
27
|
+
|
|
28
|
+
const open = computed({
|
|
29
|
+
get: () => ui.startFromDesign !== null,
|
|
30
|
+
set: (v: boolean) => {
|
|
31
|
+
if (!v) ui.closeStartFromDesign()
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const pasted = ref('')
|
|
36
|
+
const state = ref<RefState>({ status: 'none' })
|
|
37
|
+
/** The source that claimed the paste, held beside the verdict so the staged item names it. */
|
|
38
|
+
const claimant = ref<DocumentSourceKind | null>(null)
|
|
39
|
+
/**
|
|
40
|
+
* The exact text {@link state} is the verdict FOR, so the same text is never re-resolved.
|
|
41
|
+
*
|
|
42
|
+
* This is what keeps Continue clickable. The input resolves on blur, and clicking Continue blurs
|
|
43
|
+
* it, so a person who resolved with Enter and then reached for the button re-entered `resolve()`
|
|
44
|
+
* on mousedown: the state fell back to `checking`, `row` went null, and Vue disabled the button
|
|
45
|
+
* before mouseup, so the click was never dispatched. The button came back enabled a moment later
|
|
46
|
+
* having done nothing, which reads as a dead button rather than as a race. (The `start-from-design`
|
|
47
|
+
* tour hit the same edge, its `advanceOn: 'target-click'` step waiting on a click that never fired.)
|
|
48
|
+
*
|
|
49
|
+
* Guarding on the TEXT rather than suppressing the blur is what makes it a fix instead of a
|
|
50
|
+
* workaround: re-resolving input that already has a verdict spends one HTTP call per connected
|
|
51
|
+
* design source to arrive back where it started, whoever caused it.
|
|
52
|
+
*/
|
|
53
|
+
const resolvedFor = ref<string | null>(null)
|
|
54
|
+
|
|
55
|
+
watch(open, (isOpen) => {
|
|
56
|
+
if (!isOpen) return
|
|
57
|
+
pasted.value = ''
|
|
58
|
+
state.value = { status: 'none' }
|
|
59
|
+
claimant.value = null
|
|
60
|
+
resolvedFor.value = null
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const row = computed(() => refRowFor(state.value, pasted.value))
|
|
64
|
+
const sources = computed(() => documents.connectedDesignSources)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The source the staged reference will be attached to.
|
|
68
|
+
*
|
|
69
|
+
* An UNCHECKED paste (the pre-flight itself failed: offline, a 502, a proxy's error page) is not
|
|
70
|
+
* a refusal, so it stays stageable with the import as the backstop it always was — but only when
|
|
71
|
+
* ONE design source is connected, because that is the only case where nothing has to be guessed.
|
|
72
|
+
* With two, the source is exactly what the resolve was going to tell us, and picking the
|
|
73
|
+
* first-registered one would attach a Zeplin screen to Figma's key space. Then the honest answer
|
|
74
|
+
* is to say the check could not be made and let the person retry.
|
|
75
|
+
*/
|
|
76
|
+
const target = computed<DocumentSourceKind | null>(() => {
|
|
77
|
+
if (claimant.value) return claimant.value
|
|
78
|
+
if (state.value.status !== 'unchecked') return null
|
|
79
|
+
return sources.value.length === 1 ? (sources.value[0] ?? null) : null
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ask each connected design source in turn and keep the FIRST that claims the paste.
|
|
84
|
+
*
|
|
85
|
+
* Sequential rather than parallel: the sources are ordered by how much a claim over a URL is
|
|
86
|
+
* worth, and running them together would make the winner whichever answered first. A refusal from
|
|
87
|
+
* one source is not a refusal of the paste, so only the LAST one is surfaced when nobody claims
|
|
88
|
+
* it — that is the state where the person genuinely has to change something.
|
|
89
|
+
*/
|
|
90
|
+
async function resolve() {
|
|
91
|
+
const text = pasted.value.trim()
|
|
92
|
+
// Already judged, so there is nothing to learn and a verdict to lose (see `resolvedFor`).
|
|
93
|
+
if (text === resolvedFor.value) return
|
|
94
|
+
claimant.value = null
|
|
95
|
+
if (!text) {
|
|
96
|
+
state.value = { status: 'none' }
|
|
97
|
+
resolvedFor.value = null
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
state.value = { status: 'checking' }
|
|
101
|
+
let last: RefState = { status: 'rejected', reason: 'document_ref_unrecognized' }
|
|
102
|
+
for (const source of sources.value) {
|
|
103
|
+
try {
|
|
104
|
+
const ref: ResolvedDocumentRef = await documents.resolveRef(source, text)
|
|
105
|
+
claimant.value = source
|
|
106
|
+
state.value = { status: 'ok', ref }
|
|
107
|
+
resolvedFor.value = text
|
|
108
|
+
return
|
|
109
|
+
} catch (e) {
|
|
110
|
+
last = classifyRefFailure(e)
|
|
111
|
+
// An outage leaves the paste UNJUDGED rather than refused, and asking the next source
|
|
112
|
+
// would turn one source being down into a different source's verdict.
|
|
113
|
+
if (last.status === 'unchecked') break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
state.value = last
|
|
117
|
+
// A refusal and an outage are verdicts too: re-running on the same text would reach the same
|
|
118
|
+
// one, and an `unchecked` paste is stageable, so it must survive the blur Continue causes.
|
|
119
|
+
resolvedFor.value = text
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Hand the resolved reference to the add-task form as a staged attachment.
|
|
124
|
+
*
|
|
125
|
+
* `needsImport` is true because nothing has been fetched yet: the form's own pre-create resolve
|
|
126
|
+
* (`useContextLinking().resolvePending`) performs the import, which is where an unreachable page
|
|
127
|
+
* becomes a correction the author can still make. Importing here as well would spend the fetch
|
|
128
|
+
* twice and move the failure back before the form exists.
|
|
129
|
+
*/
|
|
130
|
+
function stage() {
|
|
131
|
+
const frame = ui.startFromDesign
|
|
132
|
+
const resolved = row.value
|
|
133
|
+
const source = target.value
|
|
134
|
+
if (!frame || !resolved || !source) return
|
|
135
|
+
const descriptor = documents.descriptorFor(source)
|
|
136
|
+
ui.closeStartFromDesign()
|
|
137
|
+
ui.openAddTask(frame.frameId, {
|
|
138
|
+
context: [
|
|
139
|
+
{
|
|
140
|
+
kind: 'document',
|
|
141
|
+
source,
|
|
142
|
+
externalId: resolved.externalId,
|
|
143
|
+
title: resolved.label,
|
|
144
|
+
subtitle: descriptor?.label,
|
|
145
|
+
icon: descriptor?.icon,
|
|
146
|
+
needsImport: true,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<UModal v-model:open="open" :title="t('documents.startFromDesign.title')">
|
|
155
|
+
<template #body>
|
|
156
|
+
<div class="space-y-4">
|
|
157
|
+
<p class="text-sm text-slate-400">{{ t('documents.startFromDesign.intro') }}</p>
|
|
158
|
+
|
|
159
|
+
<!-- No connected design source: the flow cannot run, and saying which step is missing
|
|
160
|
+
beats an input that refuses every paste. The connect route is withheld from a member
|
|
161
|
+
for the same reason the picker's add tier is: connecting stores a credential. -->
|
|
162
|
+
<p v-if="sources.length === 0" class="text-sm text-amber-300">
|
|
163
|
+
{{ t('documents.startFromDesign.noSource') }}
|
|
164
|
+
</p>
|
|
165
|
+
|
|
166
|
+
<template v-else>
|
|
167
|
+
<UFormField :label="t('documents.startFromDesign.linkLabel')">
|
|
168
|
+
<UInput
|
|
169
|
+
v-model="pasted"
|
|
170
|
+
:placeholder="t('documents.startFromDesign.linkPlaceholder')"
|
|
171
|
+
class="w-full"
|
|
172
|
+
data-testid="start-from-design-link"
|
|
173
|
+
@blur="resolve"
|
|
174
|
+
@keyup.enter="resolve"
|
|
175
|
+
/>
|
|
176
|
+
</UFormField>
|
|
177
|
+
|
|
178
|
+
<div
|
|
179
|
+
v-if="state.status === 'checking'"
|
|
180
|
+
class="flex items-center gap-2 text-sm text-slate-400"
|
|
181
|
+
>
|
|
182
|
+
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
|
|
183
|
+
{{ t('documents.startFromDesign.checking') }}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div
|
|
187
|
+
v-else-if="row"
|
|
188
|
+
class="space-y-1 rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
189
|
+
data-testid="start-from-design-resolved"
|
|
190
|
+
>
|
|
191
|
+
<div class="flex items-center gap-2 text-sm text-white">
|
|
192
|
+
<UIcon name="i-lucide-frame" class="h-4 w-4 text-indigo-400" />
|
|
193
|
+
<span class="truncate">{{ row.label }}</span>
|
|
194
|
+
</div>
|
|
195
|
+
<p v-if="row.trimmed" class="text-[11px] text-slate-400">
|
|
196
|
+
{{ t('documents.startFromDesign.trimmed') }}
|
|
197
|
+
</p>
|
|
198
|
+
<!-- Its own line, in amber: a trim resolves the same page, a drop widens ONE frame to
|
|
199
|
+
the whole design file, and the second is what a designer needs to see. -->
|
|
200
|
+
<p v-if="row.droppedScope" class="text-[11px] text-amber-300">
|
|
201
|
+
{{ t('documents.startFromDesign.widened', { scope: row.droppedScope }) }}
|
|
202
|
+
</p>
|
|
203
|
+
<p v-if="row.unchecked && target" class="text-[11px] text-slate-400">
|
|
204
|
+
{{ t('documents.startFromDesign.unchecked') }}
|
|
205
|
+
</p>
|
|
206
|
+
<p v-else-if="row.unchecked" class="text-[11px] text-amber-300">
|
|
207
|
+
{{ t('documents.startFromDesign.uncheckedAmbiguous') }}
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<p
|
|
212
|
+
v-else-if="state.status === 'rejected'"
|
|
213
|
+
class="text-sm text-amber-300"
|
|
214
|
+
data-testid="start-from-design-rejected"
|
|
215
|
+
>
|
|
216
|
+
{{ t('documents.startFromDesign.rejected') }}
|
|
217
|
+
</p>
|
|
218
|
+
</template>
|
|
219
|
+
|
|
220
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
221
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeStartFromDesign()">
|
|
222
|
+
{{ t('common.cancel') }}
|
|
223
|
+
</UButton>
|
|
224
|
+
<UButton
|
|
225
|
+
color="primary"
|
|
226
|
+
icon="i-lucide-arrow-right"
|
|
227
|
+
:disabled="!row || !target"
|
|
228
|
+
data-testid="start-from-design-continue"
|
|
229
|
+
@click="stage"
|
|
230
|
+
>
|
|
231
|
+
{{ t('documents.startFromDesign.continue') }}
|
|
232
|
+
</UButton>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</template>
|
|
236
|
+
</UModal>
|
|
237
|
+
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The environment setup wizard shell (shared-stacks slice 7; converted to a
|
|
3
3
|
// modular-vue journey in slice 3 of the modular-vue adoption —
|
|
4
|
-
// docs/
|
|
4
|
+
// backend/docs/adr/0049-modular-vue-adoption.md).
|
|
5
5
|
//
|
|
6
6
|
// This component is now purely the MODAL + STEPPER CHROME. The step sequence,
|
|
7
7
|
// forward/back navigation, and resume are owned by the `environment-setup`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Shared footer chrome for a modular-vue journey step (slice 3 of the modular-vue
|
|
3
|
-
// adoption — docs/
|
|
3
|
+
// adoption — backend/docs/adr/0049-modular-vue-adoption.md). Renders the Back control
|
|
4
4
|
// (wired to the host-provided `goBack`, present only when the current entry
|
|
5
5
|
// declared `allowBack` and there's a prior step) plus a `primary` slot for the
|
|
6
6
|
// step's own advance affordance, so gating stays local to each step.
|
|
@@ -39,9 +39,14 @@ watch(
|
|
|
39
39
|
|
|
40
40
|
const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
|
|
41
41
|
const linear = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
|
|
42
|
+
// The deployment's registered Figma app, which is what turns "Connect with Figma" on for every
|
|
43
|
+
// board in the account. Without it the Figma document source still connects, by personal access
|
|
44
|
+
// token — which is the step this exists to spare a designer.
|
|
45
|
+
const figma = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
|
|
42
46
|
const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
|
|
43
47
|
const savingSlack = ref(false)
|
|
44
48
|
const savingLinear = ref(false)
|
|
49
|
+
const savingFigma = ref(false)
|
|
45
50
|
const savingWeb = ref(false)
|
|
46
51
|
|
|
47
52
|
const summary = computed(() => store.view?.summary ?? null)
|
|
@@ -289,6 +294,62 @@ async function clearLinear() {
|
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
296
|
|
|
297
|
+
async function saveFigma() {
|
|
298
|
+
if (!figma.clientId.trim() || !figma.clientSecret.trim() || !figma.redirectUrl.trim()) {
|
|
299
|
+
toast.add({ title: t('layout.accountDeployment.figma.validation'), color: 'error' })
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
savingFigma.value = true
|
|
303
|
+
try {
|
|
304
|
+
await store.save(props.accountId, {
|
|
305
|
+
secrets: {
|
|
306
|
+
figmaOAuth: {
|
|
307
|
+
clientId: figma.clientId.trim(),
|
|
308
|
+
clientSecret: figma.clientSecret.trim(),
|
|
309
|
+
redirectUrl: figma.redirectUrl.trim(),
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
figma.clientId = ''
|
|
314
|
+
figma.clientSecret = ''
|
|
315
|
+
figma.redirectUrl = ''
|
|
316
|
+
toast.add({
|
|
317
|
+
title: t('layout.accountDeployment.figma.saved'),
|
|
318
|
+
icon: 'i-lucide-check',
|
|
319
|
+
color: 'success',
|
|
320
|
+
})
|
|
321
|
+
} catch (e) {
|
|
322
|
+
toast.add({
|
|
323
|
+
title: t('layout.accountDeployment.figma.saveFailed'),
|
|
324
|
+
description: e instanceof Error ? e.message : String(e),
|
|
325
|
+
color: 'error',
|
|
326
|
+
})
|
|
327
|
+
} finally {
|
|
328
|
+
savingFigma.value = false
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function clearFigma() {
|
|
333
|
+
if (!(await confirmAction('clear', 'Figma'))) return
|
|
334
|
+
savingFigma.value = true
|
|
335
|
+
try {
|
|
336
|
+
await store.save(props.accountId, { secrets: { figmaOAuth: null } })
|
|
337
|
+
toast.add({
|
|
338
|
+
title: t('layout.accountDeployment.figma.cleared'),
|
|
339
|
+
icon: 'i-lucide-check',
|
|
340
|
+
color: 'success',
|
|
341
|
+
})
|
|
342
|
+
} catch (e) {
|
|
343
|
+
toast.add({
|
|
344
|
+
title: t('layout.accountDeployment.figma.clearFailed'),
|
|
345
|
+
description: e instanceof Error ? e.message : String(e),
|
|
346
|
+
color: 'error',
|
|
347
|
+
})
|
|
348
|
+
} finally {
|
|
349
|
+
savingFigma.value = false
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
292
353
|
async function saveWeb() {
|
|
293
354
|
const brave = web.braveApiKey.trim()
|
|
294
355
|
const searxng = web.searxngUrl.trim()
|
|
@@ -479,6 +540,67 @@ async function clearWeb() {
|
|
|
479
540
|
</div>
|
|
480
541
|
</section>
|
|
481
542
|
|
|
543
|
+
<!-- Figma app OAuth (the document source's designer-doable connect) -->
|
|
544
|
+
<section class="space-y-2 border-t border-slate-800 pt-6">
|
|
545
|
+
<div class="flex items-center gap-2">
|
|
546
|
+
<h4 class="text-sm font-semibold text-slate-200">
|
|
547
|
+
{{ t('layout.accountDeployment.figma.title') }}
|
|
548
|
+
</h4>
|
|
549
|
+
<UBadge
|
|
550
|
+
:color="summary?.figmaOAuthConfigured ? 'success' : 'neutral'"
|
|
551
|
+
variant="subtle"
|
|
552
|
+
size="xs"
|
|
553
|
+
>
|
|
554
|
+
{{
|
|
555
|
+
summary?.figmaOAuthConfigured
|
|
556
|
+
? t('layout.accountDeployment.configured')
|
|
557
|
+
: t('layout.accountDeployment.notSet')
|
|
558
|
+
}}
|
|
559
|
+
</UBadge>
|
|
560
|
+
</div>
|
|
561
|
+
<p class="text-[11px] text-slate-400">
|
|
562
|
+
{{ t('layout.accountDeployment.figma.description') }}
|
|
563
|
+
</p>
|
|
564
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
565
|
+
<UInput
|
|
566
|
+
v-model="figma.clientId"
|
|
567
|
+
:placeholder="t('layout.accountDeployment.figma.clientId')"
|
|
568
|
+
size="sm"
|
|
569
|
+
/>
|
|
570
|
+
<SecretInput
|
|
571
|
+
v-model="figma.clientSecret"
|
|
572
|
+
:placeholder="t('layout.accountDeployment.figma.clientSecret')"
|
|
573
|
+
size="sm"
|
|
574
|
+
/>
|
|
575
|
+
<UInput
|
|
576
|
+
v-model="figma.redirectUrl"
|
|
577
|
+
:placeholder="t('layout.accountDeployment.figma.redirectUrl')"
|
|
578
|
+
size="sm"
|
|
579
|
+
/>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="flex gap-2">
|
|
582
|
+
<UButton
|
|
583
|
+
color="primary"
|
|
584
|
+
size="xs"
|
|
585
|
+
icon="i-lucide-save"
|
|
586
|
+
:loading="savingFigma"
|
|
587
|
+
@click="saveFigma"
|
|
588
|
+
>
|
|
589
|
+
{{ t('common.save') }}
|
|
590
|
+
</UButton>
|
|
591
|
+
<UButton
|
|
592
|
+
v-if="summary?.figmaOAuthConfigured"
|
|
593
|
+
color="neutral"
|
|
594
|
+
variant="ghost"
|
|
595
|
+
size="xs"
|
|
596
|
+
:loading="savingFigma"
|
|
597
|
+
@click="clearFigma"
|
|
598
|
+
>
|
|
599
|
+
{{ t('layout.accountDeployment.clear') }}
|
|
600
|
+
</UButton>
|
|
601
|
+
</div>
|
|
602
|
+
</section>
|
|
603
|
+
|
|
482
604
|
<!-- Web search keys -->
|
|
483
605
|
<section class="space-y-2 border-t border-slate-800 pt-6">
|
|
484
606
|
<div class="flex items-center gap-2">
|
|
@@ -5,7 +5,7 @@ import IconButton from '~/components/common/IconButton.vue'
|
|
|
5
5
|
|
|
6
6
|
const ui = useUiStore()
|
|
7
7
|
const board = useBoardStore()
|
|
8
|
-
// Toolbar contributions from the shared nav manifest (docs/
|
|
8
|
+
// Toolbar contributions from the shared nav manifest (backend/docs/adr/0049-modular-vue-adoption.md,
|
|
9
9
|
// slice 1). First-party contributes none — this is the reactive extension point a consumer
|
|
10
10
|
// deployment uses to add a board-toolbar action via `registerAppModule`, gated + rendered like
|
|
11
11
|
// the sidebar/command entries with zero edits here.
|
|
@@ -26,7 +26,7 @@ const library = useFragmentLibraryStore()
|
|
|
26
26
|
const access = useWorkspaceAccess()
|
|
27
27
|
|
|
28
28
|
// The static destination catalog + its RBAC/availability gating now comes from
|
|
29
|
-
// the shared nav manifest (docs/
|
|
29
|
+
// the shared nav manifest (backend/docs/adr/0049-modular-vue-adoption.md, slice 1),
|
|
30
30
|
// rendered here as command entries. The DYNAMIC per-connection commands below
|
|
31
31
|
// (github/slack/doc/task connect + import) stay local to the palette — they vary
|
|
32
32
|
// per live connection, so they are not part of the static manifest this slice.
|
|
@@ -31,7 +31,7 @@ const providerConnections = useProviderConnectionsStore()
|
|
|
31
31
|
const ui = useUiStore()
|
|
32
32
|
|
|
33
33
|
// The nav catalog + its reactive RBAC/availability gating now lives in the shared
|
|
34
|
-
// modular-vue manifest (docs/
|
|
34
|
+
// modular-vue manifest (backend/docs/adr/0049-modular-vue-adoption.md, slice 1): every
|
|
35
35
|
// destination is declared once in `nav-contributions.ts`, gated by `navSlotFilter`
|
|
36
36
|
// over a reactive `gates` service, and rendered here (and in CommandBar / BoardToolbar)
|
|
37
37
|
// from `useReactiveSlots`. Sections + items appear/disappear reactively as a permission
|
|
@@ -230,7 +230,7 @@ watch(
|
|
|
230
230
|
</button>
|
|
231
231
|
|
|
232
232
|
<!-- Sections + items come from the shared nav manifest, already gated by the
|
|
233
|
-
reactive slotFilter (docs/
|
|
233
|
+
reactive slotFilter (backend/docs/adr/0049-modular-vue-adoption.md, slice 1) — which
|
|
234
234
|
also drops the `advanced` items in basic interface mode. An empty section is
|
|
235
235
|
dropped upstream, so there is no per-section `v-if` here.
|
|
236
236
|
In the rail the section HEADERS go (they'd wrap to nothing at 3.5rem) but the
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Tagging is never mandatory: the parent can always act without a selection, and an untagged
|
|
7
7
|
// merge records a null tag. This component only chooses; the parent performs the action.
|
|
8
8
|
//
|
|
9
|
-
// See docs/
|
|
9
|
+
// See backend/docs/adr/0046-merge-track-record.md.
|
|
10
10
|
import { computed } from 'vue'
|
|
11
11
|
import { REVIEW_EFFORTS } from '@cat-factory/contracts'
|
|
12
12
|
import type { ChangeClass, MergeClassRollup, ReviewEffort } from '~/types/merge'
|
|
@@ -524,7 +524,7 @@ const showOriginalDescription = ref(false)
|
|
|
524
524
|
`Block | null` is rejected at compile time. `unknown` is the real
|
|
525
525
|
runtime contract; `as any` is the minimal unblock until the binding
|
|
526
526
|
types the prop explicitly (filed upstream — see the slice-4 residuals
|
|
527
|
-
in docs/
|
|
527
|
+
in backend/docs/adr/0049-modular-vue-adoption.md). -->
|
|
528
528
|
<PanelsOutlet
|
|
529
529
|
:group="inspectorPanels"
|
|
530
530
|
:subject="(block ?? null) as any"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Shared modal shell for the agent-run result windows (slice 5 of the modular-vue
|
|
3
|
-
// adoption — docs/
|
|
4
|
-
// docs/
|
|
3
|
+
// adoption — backend/docs/adr/0049-modular-vue-adoption.md; progress in
|
|
4
|
+
// backend/docs/adr/0049-modular-vue-adoption.md).
|
|
5
5
|
//
|
|
6
6
|
// Every result window (the merger verdict, the tester report, the requirements-review
|
|
7
7
|
// loop, the gates, …) used to hand-roll the SAME modal chrome — `<Teleport>`, a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Universal dedicated-result-view host (slice 2 of the modular-vue adoption —
|
|
3
|
-
// docs/
|
|
3
|
+
// backend/docs/adr/0049-modular-vue-adoption.md). An agent archetype can declare a
|
|
4
4
|
// `resultView` id (see `~/utils/catalog`); when a step of that kind is opened,
|
|
5
5
|
// `ui.resultView` is set and this host mounts the matching registered window
|
|
6
6
|
// instead of the generic `AgentStepDetail` prose panel.
|
|
@@ -195,7 +195,7 @@ function setResolveOnMerge(value: WritebackOverride | null) {
|
|
|
195
195
|
board.updateBlock(props.block.id, { trackerResolveOnMerge: value })
|
|
196
196
|
}
|
|
197
197
|
// Only consulted for runs started through the public API — a task started here keeps its in-app
|
|
198
|
-
// clarification window regardless (docs/
|
|
198
|
+
// clarification window regardless (backend/docs/adr/0047-headless-clarification-loop.md).
|
|
199
199
|
function setQuestionsOnPark(value: WritebackOverride | null) {
|
|
200
200
|
board.updateBlock(props.block.id, { trackerQuestionsOnPark: value })
|
|
201
201
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// `unknown` is deliberately not listed: no rule may ever match it (an unclassifiable diff must
|
|
8
8
|
// fall back to the score ceilings), so offering one would be a lie. See
|
|
9
|
-
// docs/
|
|
9
|
+
// backend/docs/adr/0046-merge-track-record.md.
|
|
10
10
|
import { computed } from 'vue'
|
|
11
11
|
import { autoMergeShare, frictionlessShare, RULEABLE_CHANGE_CLASSES } from '@cat-factory/contracts'
|
|
12
12
|
import type { MergeClassRule, MergeClassRules } from '~/types/merge'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connectDocumentSourceContract,
|
|
3
3
|
disconnectDocumentSourceContract,
|
|
4
|
+
documentSourceOAuthUrlContract,
|
|
4
5
|
importDocumentContract,
|
|
5
6
|
linkDocumentContract,
|
|
6
7
|
linkDocumentForKindContract,
|
|
@@ -41,6 +42,14 @@ export function documentsApi({ send, ws }: ApiContext) {
|
|
|
41
42
|
body: { credentials },
|
|
42
43
|
}),
|
|
43
44
|
|
|
45
|
+
// The vendor authorization URL for a source's OAuth connect. Admin-tier: what it hands back
|
|
46
|
+
// is the first half of a credential write, completed by the public callback.
|
|
47
|
+
documentSourceOAuthUrl: (workspaceId: string, source: DocumentSourceKind) =>
|
|
48
|
+
send(documentSourceOAuthUrlContract, {
|
|
49
|
+
pathPrefix: ws(workspaceId),
|
|
50
|
+
pathParams: { source },
|
|
51
|
+
}),
|
|
52
|
+
|
|
44
53
|
disconnectDocumentSource: (workspaceId: string, source: DocumentSourceKind) =>
|
|
45
54
|
send(disconnectDocumentSourceContract, {
|
|
46
55
|
pathPrefix: ws(workspaceId),
|
|
@@ -77,12 +86,14 @@ export function documentsApi({ send, ws }: ApiContext) {
|
|
|
77
86
|
body: { query },
|
|
78
87
|
}),
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
// `frameId` makes the plan TARGET-AWARE: modules and tasks for a service that already exists
|
|
90
|
+
// rather than an architecture. The same frame is sent to the spawn, so the preview and the
|
|
91
|
+
// write agree about the target.
|
|
92
|
+
planDocument: (
|
|
93
|
+
workspaceId: string,
|
|
94
|
+
source: DocumentSourceKind,
|
|
95
|
+
body: { externalId: string; frameId?: string },
|
|
96
|
+
) => send(planDocumentContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
|
|
86
97
|
|
|
87
98
|
spawnDocument: (
|
|
88
99
|
workspaceId: string,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { computed, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import { isDesignSource } from '@cat-factory/contracts'
|
|
3
|
+
import type {
|
|
4
|
+
DocumentConnection,
|
|
5
|
+
DocumentSourceDescriptor,
|
|
6
|
+
DocumentSourceKind,
|
|
7
|
+
} from '~/types/domain'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How this workspace CONNECTS to a document source, and what it may connect with.
|
|
11
|
+
*
|
|
12
|
+
* Split out of `stores/documents.ts` when the OAuth half landed, because "which credential can
|
|
13
|
+
* this board offer for this source" turned into a question with several parts: a source declares
|
|
14
|
+
* an OAuth half in code, the deployment may or may not have registered an app for it, and the
|
|
15
|
+
* typed-credential form is the fallback either way. The store's other concerns (the imported
|
|
16
|
+
* documents, their freshness, the doc-kind role links) never read any of it.
|
|
17
|
+
*
|
|
18
|
+
* It takes bound accessors rather than the store, so it stays testable on its own and cannot
|
|
19
|
+
* reach for state it has no business in.
|
|
20
|
+
*/
|
|
21
|
+
export interface DocumentSourceConnectDeps {
|
|
22
|
+
workspaceId: () => string
|
|
23
|
+
/**
|
|
24
|
+
* The sources this DEPLOYMENT can OAuth, as the backend reports them beside the descriptors.
|
|
25
|
+
*
|
|
26
|
+
* Owned by the caller (the probe writes it) rather than fetched here, because it arrives on the
|
|
27
|
+
* same response as the descriptors and a second read would be a second round trip that could
|
|
28
|
+
* disagree with the first.
|
|
29
|
+
*/
|
|
30
|
+
oauthSources: Ref<DocumentSourceKind[]>
|
|
31
|
+
/** The connected subset of the descriptors, in registry order. */
|
|
32
|
+
connectedSources: ComputedRef<DocumentSourceDescriptor[]>
|
|
33
|
+
/** Fold a new/updated connection into the caller's list. */
|
|
34
|
+
onConnected: (connection: DocumentConnection) => void
|
|
35
|
+
/** Drop a source's connection from the caller's list. */
|
|
36
|
+
onDisconnected: (source: DocumentSourceKind) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useDocumentSourceConnect(deps: DocumentSourceConnectDeps) {
|
|
40
|
+
const api = useApi()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether this deployment can run the OAuth connect for a source RIGHT NOW.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately not `descriptor.oauth !== undefined`: that says the source supports the flow,
|
|
46
|
+
* which is true of Figma on every deployment, including the ones that have registered no app.
|
|
47
|
+
* A button rendered off the descriptor alone could only 503.
|
|
48
|
+
*/
|
|
49
|
+
function canConnectWithOAuth(source: DocumentSourceKind): boolean {
|
|
50
|
+
return deps.oauthSources.value.includes(source)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every CONNECTED design source, in the order the backend registered them.
|
|
55
|
+
*
|
|
56
|
+
* `isDesignSource` comes from contracts rather than a local list, for the reason the backend
|
|
57
|
+
* reads it there: whether a source describes a design is a fact both sides have to agree about,
|
|
58
|
+
* and a second copy here would drift the moment a source is added.
|
|
59
|
+
*/
|
|
60
|
+
const connectedDesignSources = computed(() =>
|
|
61
|
+
deps.connectedSources.value.map((s) => s.source).filter(isDesignSource),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
/** Connect the workspace to a source with its credential bag. */
|
|
65
|
+
async function connect(source: DocumentSourceKind, credentials: Record<string, string>) {
|
|
66
|
+
deps.onConnected(await api.connectDocumentSource(deps.workspaceId(), source, credentials))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Disconnect the workspace from a source. */
|
|
70
|
+
async function disconnect(source: DocumentSourceKind) {
|
|
71
|
+
await api.disconnectDocumentSource(deps.workspaceId(), source)
|
|
72
|
+
deps.onDisconnected(source)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Send the browser to a source's vendor consent screen.
|
|
77
|
+
*
|
|
78
|
+
* A full navigation rather than a popup: the vendor lands back on the app's own OAuth callback,
|
|
79
|
+
* which stores the grant and redirects here, so the returning page re-probes and sees the
|
|
80
|
+
* connection. A popup would leave the opener holding stale state with nothing to tell it.
|
|
81
|
+
*/
|
|
82
|
+
async function beginOAuthConnect(source: DocumentSourceKind) {
|
|
83
|
+
const { url } = await api.documentSourceOAuthUrl(deps.workspaceId(), source)
|
|
84
|
+
window.location.assign(url)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { canConnectWithOAuth, connectedDesignSources, connect, disconnect, beginOAuthConnect }
|
|
88
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AgentArchetype, CustomAgentKind } from '~/types/domain'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Custom agent-kind projection (slice 2 of the modular-vue adoption —
|
|
5
|
-
* docs/
|
|
5
|
+
* backend/docs/adr/0049-modular-vue-adoption.md).
|
|
6
6
|
*
|
|
7
7
|
* A deployment's BACKEND-registered agent kinds arrive in the workspace snapshot as
|
|
8
8
|
* `customAgentKinds` (wire data), folded into the shared per-workspace capability manifest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure navigation logic for the environment-setup journey (slice 3 of the
|
|
3
|
-
* modular-vue adoption — docs/
|
|
3
|
+
* modular-vue adoption — backend/docs/adr/0049-modular-vue-adoption.md).
|
|
4
4
|
*
|
|
5
5
|
* The journey owns only the WIZARD NAVIGATION — the ordered steps
|
|
6
6
|
* (pick → review → preflight → save), the forward transitions, and the
|