@cat-factory/app 0.215.0 → 0.215.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.
- package/app/components/bootstrap/BootstrapModal.vue +65 -44
- package/app/components/github/AddServiceFromRepoModal.vue +49 -27
- package/app/components/github/GitHubOnboarding.vue +4 -23
- package/app/components/github/GitHubPanel.vue +10 -34
- package/app/components/vcs/VcsConnectSurfaces.vue +58 -0
- package/app/stores/github/vcsConnect.ts +62 -0
- package/app/stores/github.spec.ts +31 -0
- package/app/stores/github.ts +5 -26
- package/app/utils/vcs.spec.ts +101 -0
- package/app/utils/vcs.ts +63 -1
- package/i18n/locales/de.json +17 -17
- package/i18n/locales/en.json +17 -17
- package/i18n/locales/es.json +17 -17
- package/i18n/locales/fr.json +17 -17
- package/i18n/locales/he.json +17 -17
- package/i18n/locales/it.json +17 -17
- package/i18n/locales/ja.json +17 -17
- package/i18n/locales/pl.json +17 -17
- package/i18n/locales/tr.json +17 -17
- package/i18n/locales/uk.json +17 -17
- package/package.json +2 -2
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// architecture, or from scratch following a freeform prompt. The modal pairs the
|
|
6
6
|
// launch form with the managed base list.
|
|
7
7
|
import type { BootstrapStatus, FrameRepoType, ReferenceArchitecture } from '~/types/domain'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
8
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
9
|
+
import { appInstallationManageUrl, newRepoUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
11
10
|
|
|
12
11
|
const ui = useUiStore()
|
|
13
12
|
const bootstrap = useBootstrapStore()
|
|
@@ -137,35 +136,42 @@ watch(
|
|
|
137
136
|
{ immediate: true },
|
|
138
137
|
)
|
|
139
138
|
|
|
140
|
-
// A bootstrap run pushes into a
|
|
141
|
-
// first (the backend pre-flights the same and 409s otherwise). When the
|
|
142
|
-
// integration is on but unconnected, surface the
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
// The
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
// A bootstrap run pushes into a repo on the connected host, so the workspace must be
|
|
140
|
+
// connected first (the backend pre-flights the same and 409s otherwise). When the
|
|
141
|
+
// integration is on but unconnected, surface the connect prompt inline and block launch
|
|
142
|
+
// until it's bound.
|
|
143
|
+
const needsConnection = computed(() => github.available === true && !github.connected)
|
|
144
|
+
|
|
145
|
+
// The host this modal is about: the connected one, or the only one the deployment could
|
|
146
|
+
// connect while nothing is bound. Null where it offers several and none is connected, so
|
|
147
|
+
// `provider`'s own "what is connected" default can never send a GitLab deployment to github.com.
|
|
148
|
+
const hostProvider = computed(() => github.surfaceProvider)
|
|
149
|
+
const providerLabel = computed(() =>
|
|
150
|
+
hostProvider.value ? VCS_PROVIDER_LABELS[hostProvider.value] : '',
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// The account the repo must live under — the connected account. The run pushes into an
|
|
154
|
+
// existing repo here (cat-factory doesn't create it: a GitHub App can't create repos under a
|
|
155
|
+
// personal account, and we'd rather not hold the broad Administration permission). The repo
|
|
156
|
+
// must be empty or hold only a prepopulated README/.gitignore/license — the push
|
|
157
|
+
// force-overwrites that boilerplate. The convenience link opens the host's own new-repo page,
|
|
158
|
+
// prefilled, and is ABSENT for any host `~/utils/vcs` can't name (an unresolved provider, or a
|
|
159
|
+
// GitLab whose instance nothing on the wire states); the copy and the button both key off it,
|
|
160
|
+
// so what the intro promises and what renders cannot disagree.
|
|
153
161
|
const repoOwner = computed(() => github.connection?.accountLogin ?? '')
|
|
154
|
-
const createRepoUrl = computed(() =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return `https://github.com/new?${params.toString()}`
|
|
163
|
-
})
|
|
162
|
+
const createRepoUrl = computed(() =>
|
|
163
|
+
newRepoUrl(hostProvider.value, {
|
|
164
|
+
owner: repoOwner.value,
|
|
165
|
+
name: repoName.value.trim(),
|
|
166
|
+
description: description.value.trim(),
|
|
167
|
+
private: isPrivate.value,
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
164
170
|
|
|
165
171
|
const creatingRepo = ref(false)
|
|
166
172
|
|
|
167
173
|
// The "create repository" button behaves differently per tier. Restricted orgs
|
|
168
|
-
// (the default) open
|
|
174
|
+
// (the default) open the host's new-repo page — cat-factory needs no
|
|
169
175
|
// repo-creation permission. Privileged orgs (the connection reports
|
|
170
176
|
// `canCreateRepos`) create it programmatically via the backend, with no page.
|
|
171
177
|
async function openCreateRepo() {
|
|
@@ -173,7 +179,9 @@ async function openCreateRepo() {
|
|
|
173
179
|
if (!name || repoNameError.value) return
|
|
174
180
|
|
|
175
181
|
if (!github.canCreateRepos) {
|
|
176
|
-
|
|
182
|
+
// The button is hidden without a resolved host, so there is always a URL here; the guard
|
|
183
|
+
// keeps that a local fact rather than an assumption about the template.
|
|
184
|
+
if (createRepoUrl.value) window.open(createRepoUrl.value, '_blank', 'noopener')
|
|
177
185
|
return
|
|
178
186
|
}
|
|
179
187
|
|
|
@@ -207,20 +215,15 @@ async function openCreateRepo() {
|
|
|
207
215
|
// "not accessible to the GitHub App". Link straight to the connected
|
|
208
216
|
// installation's settings page, where the user adds the repo to its access list
|
|
209
217
|
// in one click — no install/connect round-trip (the workspace is already bound).
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!conn) return undefined
|
|
213
|
-
return conn.targetType === 'Organization'
|
|
214
|
-
? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
|
|
215
|
-
: `https://github.com/settings/installations/${conn.installationId}`
|
|
216
|
-
})
|
|
218
|
+
// Absent on a PAT connection, which grants no per-installation access (see `~/utils/vcs`).
|
|
219
|
+
const manageInstallUrl = computed(() => appInstallationManageUrl(github.connection))
|
|
217
220
|
|
|
218
221
|
function openManageInstall() {
|
|
219
222
|
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
const canLaunch = computed(() => {
|
|
223
|
-
if (
|
|
226
|
+
if (needsConnection.value) return false
|
|
224
227
|
if (!repoName.value.trim() || repoNameError.value) return false
|
|
225
228
|
return usingReference.value ? !!selectedArchId.value : instructions.value.trim().length > 0
|
|
226
229
|
})
|
|
@@ -420,22 +423,34 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
420
423
|
<UModal v-model:open="open" :title="t('bootstrap.title')" :ui="{ content: 'max-w-2xl' }">
|
|
421
424
|
<template #body>
|
|
422
425
|
<div class="space-y-6">
|
|
426
|
+
<!-- Three states, because each promises the user something different about the repo.
|
|
427
|
+
cat-factory creates it (privileged App tier, so a provider is always resolved);
|
|
428
|
+
the user creates it in one click on a host we can name; or the user creates it
|
|
429
|
+
themselves somewhere we cannot name, where promising a click below would be a lie
|
|
430
|
+
(the button is absent for exactly the same reason). -->
|
|
423
431
|
<p class="text-sm text-slate-400">
|
|
424
|
-
{{
|
|
432
|
+
{{
|
|
433
|
+
github.canCreateRepos
|
|
434
|
+
? t('vcs.bootstrap.introCanCreate', { provider: providerLabel })
|
|
435
|
+
: createRepoUrl
|
|
436
|
+
? t('vcs.bootstrap.introManual', { provider: providerLabel })
|
|
437
|
+
: t('vcs.bootstrap.introManualAny')
|
|
438
|
+
}}
|
|
425
439
|
</p>
|
|
426
440
|
|
|
427
|
-
<!-- not connected: a run
|
|
441
|
+
<!-- not connected: a run pushes to the host, so connect before launching. Offer
|
|
442
|
+
whichever methods the deployment serves, never just the GitHub App. -->
|
|
428
443
|
<div
|
|
429
|
-
v-if="
|
|
444
|
+
v-if="needsConnection"
|
|
430
445
|
class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
|
431
446
|
>
|
|
432
447
|
<div class="flex items-start gap-2">
|
|
433
448
|
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
434
449
|
<p class="text-sm text-amber-200/90">
|
|
435
|
-
{{ t('bootstrap.
|
|
450
|
+
{{ t('vcs.bootstrap.connectPrompt') }}
|
|
436
451
|
</p>
|
|
437
452
|
</div>
|
|
438
|
-
<
|
|
453
|
+
<VcsConnectSurfaces />
|
|
439
454
|
</div>
|
|
440
455
|
|
|
441
456
|
<!-- launch -->
|
|
@@ -484,7 +499,11 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
484
499
|
:placeholder="t('bootstrap.targetRepo.namePlaceholder')"
|
|
485
500
|
class="w-full"
|
|
486
501
|
/>
|
|
502
|
+
<!-- Creating the repo for the user needs no host name; sending them to the
|
|
503
|
+
host's own form needs one, so that variant waits until a host is
|
|
504
|
+
resolved rather than guessing which page to open. -->
|
|
487
505
|
<UButton
|
|
506
|
+
v-if="github.canCreateRepos || createRepoUrl"
|
|
488
507
|
color="neutral"
|
|
489
508
|
variant="subtle"
|
|
490
509
|
:icon="github.canCreateRepos ? 'i-lucide-plus' : 'i-lucide-external-link'"
|
|
@@ -493,14 +512,14 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
493
512
|
:title="
|
|
494
513
|
github.canCreateRepos
|
|
495
514
|
? t('bootstrap.createRepo.titleNow')
|
|
496
|
-
: t('bootstrap.
|
|
515
|
+
: t('vcs.bootstrap.createRepoTitle', { provider: providerLabel })
|
|
497
516
|
"
|
|
498
517
|
@click="openCreateRepo"
|
|
499
518
|
>
|
|
500
519
|
{{
|
|
501
520
|
github.canCreateRepos
|
|
502
521
|
? t('bootstrap.createRepo.now')
|
|
503
|
-
: t('bootstrap.
|
|
522
|
+
: t('vcs.bootstrap.createRepoOn', { provider: providerLabel })
|
|
504
523
|
}}
|
|
505
524
|
</UButton>
|
|
506
525
|
</div>
|
|
@@ -671,9 +690,11 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
671
690
|
v-if="showArchForm"
|
|
672
691
|
class="space-y-3 rounded-md border border-slate-700 bg-slate-900/80 p-3"
|
|
673
692
|
>
|
|
693
|
+
<!-- The options come from the connected projection, so a repo to pick means a
|
|
694
|
+
connection exists and `providerLabel` names it rather than guessing. -->
|
|
674
695
|
<UFormField
|
|
675
696
|
v-if="hasRepoOptions"
|
|
676
|
-
:label="t('bootstrap.
|
|
697
|
+
:label="t('vcs.bootstrap.archPickRepo', { provider: providerLabel })"
|
|
677
698
|
:description="t('bootstrap.arch.pickRepo.description')"
|
|
678
699
|
>
|
|
679
700
|
<USelect
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Add a board service backed by an EXISTING
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// Add a board service backed by an EXISTING repository — no bootstrap run. Unlike the
|
|
3
|
+
// bootstrap modal (which creates a repo and has an agent adapt it in a container), this
|
|
4
|
+
// just links a repo the workspace's connection can reach to a fresh, `ready` service
|
|
5
|
+
// frame. The workspace need not track the repo yet: the backend links + syncs it on
|
|
6
|
+
// import. On a GitHub App connection, a repo the App can't see yet is granted from here
|
|
7
|
+
// and searched for again; a PAT connection has no such page (see `~/utils/vcs`), so what
|
|
8
|
+
// is listed follows the token's own access.
|
|
8
9
|
//
|
|
9
10
|
// MONOREPO support: a repo flagged a monorepo can back SEVERAL services, each
|
|
10
11
|
// pinned to a subdirectory. When the selected repo is a monorepo, the user
|
|
@@ -12,11 +13,12 @@
|
|
|
12
13
|
// parent folder, in one pass — then adds them all at once. Directories that
|
|
13
14
|
// already back a service on this board are shown but not selectable.
|
|
14
15
|
import type { FrameRepoType, GitHubAvailableRepo } from '~/types/domain'
|
|
15
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
16
16
|
import RepoSearchEmpty from '~/components/github/RepoSearchEmpty.vue'
|
|
17
17
|
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
18
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
18
19
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
19
20
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
21
|
+
import { appInstallationManageUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
20
22
|
|
|
21
23
|
const { t } = useI18n()
|
|
22
24
|
|
|
@@ -64,7 +66,22 @@ watch(
|
|
|
64
66
|
)
|
|
65
67
|
|
|
66
68
|
// The integration is on but this workspace isn't bound yet — connect first.
|
|
67
|
-
const
|
|
69
|
+
const needsConnection = computed(() => github.available === true && !github.connected)
|
|
70
|
+
|
|
71
|
+
// Brand name of whatever the workspace connected, for the hint that names it. Only read where
|
|
72
|
+
// a connection exists (the hints below the picker), so `provider` is the right question there.
|
|
73
|
+
const providerLabel = computed(() => VCS_PROVIDER_LABELS[github.provider])
|
|
74
|
+
|
|
75
|
+
// Which remedy the picker's hint offers: an App installation sends the user to its repo-access
|
|
76
|
+
// list, a pasted token to the token's own scope. Asked of the CONNECTION rather than of the
|
|
77
|
+
// manage URL below, so a host whose settings page we could not build (an Enterprise install,
|
|
78
|
+
// say) never tells an App-connected user to go check their token's scope.
|
|
79
|
+
const isAppConnection = computed(() => github.connection?.method === 'app')
|
|
80
|
+
|
|
81
|
+
// The intro renders BEFORE a connection may exist, so it asks `surfaceProvider` instead and
|
|
82
|
+
// stays neutral where the deployment offers several and none is bound: naming one would be a
|
|
83
|
+
// guess, and `provider`'s own default would name GitHub on a GitLab-only deployment.
|
|
84
|
+
const introProvider = computed(() => github.surfaceProvider)
|
|
68
85
|
|
|
69
86
|
// Repos whose service is ALREADY mounted on THIS board can't be added again — adding here would
|
|
70
87
|
// be a no-op. A repo whose service lives on ANOTHER board in the org stays addable: adding it
|
|
@@ -201,15 +218,10 @@ function clearSelection() {
|
|
|
201
218
|
resetSelection()
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
// The App's installation settings page — where the user grants it access to a
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!conn) return undefined
|
|
209
|
-
return conn.targetType === 'Organization'
|
|
210
|
-
? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
|
|
211
|
-
: `https://github.com/settings/installations/${conn.installationId}`
|
|
212
|
-
})
|
|
221
|
+
// The App's installation settings page — where the user grants it access to a repo it can't
|
|
222
|
+
// see yet (mirrors the bootstrap modal's "grant access" link). Absent on a PAT connection,
|
|
223
|
+
// which has no installation to manage, so the affordance and its hint both drop out.
|
|
224
|
+
const manageInstallUrl = computed(() => appInstallationManageUrl(github.connection))
|
|
213
225
|
|
|
214
226
|
function openManageInstall() {
|
|
215
227
|
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
@@ -242,14 +254,14 @@ watch(
|
|
|
242
254
|
// multi-selects directories and adds them together via `addServices`.
|
|
243
255
|
const canAdd = computed(
|
|
244
256
|
() =>
|
|
245
|
-
!
|
|
257
|
+
!needsConnection.value &&
|
|
246
258
|
selectedRepoId.value !== undefined &&
|
|
247
259
|
!isMonorepo.value &&
|
|
248
260
|
!configuredBlockId.value,
|
|
249
261
|
)
|
|
250
262
|
const canAddServices = computed(
|
|
251
263
|
() =>
|
|
252
|
-
!
|
|
264
|
+
!needsConnection.value &&
|
|
253
265
|
selectedRepoId.value !== undefined &&
|
|
254
266
|
isMonorepo.value &&
|
|
255
267
|
selectedDirectories.value.length > 0,
|
|
@@ -357,27 +369,36 @@ function done() {
|
|
|
357
369
|
<template #body>
|
|
358
370
|
<div class="space-y-6">
|
|
359
371
|
<p class="text-sm text-slate-400">
|
|
360
|
-
{{
|
|
372
|
+
{{
|
|
373
|
+
introProvider
|
|
374
|
+
? t('vcs.addService.intro', { provider: VCS_PROVIDER_LABELS[introProvider] })
|
|
375
|
+
: t('vcs.addService.introAny')
|
|
376
|
+
}}
|
|
361
377
|
</p>
|
|
362
378
|
|
|
363
|
-
<!-- not connected: linking a repo needs
|
|
379
|
+
<!-- not connected: linking a repo needs a connection bound to this workspace, so
|
|
380
|
+
offer whichever connect methods the deployment serves (never just the App) -->
|
|
364
381
|
<div
|
|
365
|
-
v-if="
|
|
382
|
+
v-if="needsConnection"
|
|
366
383
|
class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
|
367
384
|
>
|
|
368
385
|
<div class="flex items-start gap-2">
|
|
369
386
|
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
370
387
|
<p class="text-sm text-amber-200/90">
|
|
371
|
-
{{ t('
|
|
388
|
+
{{ t('vcs.addService.connectFirst') }}
|
|
372
389
|
</p>
|
|
373
390
|
</div>
|
|
374
|
-
<
|
|
391
|
+
<VcsConnectSurfaces />
|
|
375
392
|
</div>
|
|
376
393
|
|
|
377
394
|
<template v-else>
|
|
378
395
|
<UFormField
|
|
379
396
|
:label="t('github.addService.repository')"
|
|
380
|
-
:description="
|
|
397
|
+
:description="
|
|
398
|
+
isAppConnection
|
|
399
|
+
? t('vcs.addService.repositoryHintApp')
|
|
400
|
+
: t('vcs.addService.repositoryHintToken', { provider: providerLabel })
|
|
401
|
+
"
|
|
381
402
|
required
|
|
382
403
|
>
|
|
383
404
|
<!-- The wrapper, not the UInputMenu itself, carries the anchor: a tutorial tour
|
|
@@ -521,9 +542,10 @@ function done() {
|
|
|
521
542
|
<ServiceFragments :block="configuredBlock" default-open />
|
|
522
543
|
</div>
|
|
523
544
|
|
|
524
|
-
|
|
545
|
+
<!-- App connections only: a pasted token has no installation whose repo access
|
|
546
|
+
could be edited, so there is no page to send the user to. -->
|
|
547
|
+
<div v-if="manageInstallUrl" class="flex flex-wrap items-center gap-2">
|
|
525
548
|
<UButton
|
|
526
|
-
v-if="manageInstallUrl"
|
|
527
549
|
color="neutral"
|
|
528
550
|
variant="subtle"
|
|
529
551
|
size="sm"
|
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
// Hard onboarding gate shown after login when the VCS integration is enabled but the workspace
|
|
3
3
|
// has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
|
|
4
4
|
// open pull/merge requests on the user's repos), so the board is withheld until the workspace
|
|
5
|
-
// connects one.
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// pick-an-existing-installation path, and <GitLabConnect> takes a pasted GitLab PAT. A "Sign
|
|
9
|
-
// out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
10
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
11
|
-
import GitLabConnect from '~/components/vcs/GitLabConnect.vue'
|
|
5
|
+
// connects one. <VcsConnectSurfaces> renders whichever connect methods the deployment serves; a
|
|
6
|
+
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
7
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
12
8
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
13
9
|
|
|
14
10
|
const { t } = useI18n()
|
|
@@ -41,22 +37,7 @@ const title = computed(() =>
|
|
|
41
37
|
</p>
|
|
42
38
|
</div>
|
|
43
39
|
|
|
44
|
-
<
|
|
45
|
-
<p class="mb-3 text-sm text-slate-400">{{ t('github.onboarding.appIntro') }}</p>
|
|
46
|
-
<GitHubConnect />
|
|
47
|
-
</template>
|
|
48
|
-
<USeparator
|
|
49
|
-
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
50
|
-
class="my-4"
|
|
51
|
-
:label="t('vcs.connect.or')"
|
|
52
|
-
/>
|
|
53
|
-
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
54
|
-
<p
|
|
55
|
-
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
56
|
-
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
57
|
-
>
|
|
58
|
-
{{ t('vcs.connect.noneConfigured') }}
|
|
59
|
-
</p>
|
|
40
|
+
<VcsConnectSurfaces :app-intro="t('github.onboarding.appIntro')" />
|
|
60
41
|
|
|
61
42
|
<p
|
|
62
43
|
v-if="auth.required && auth.user"
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// comment) go straight to the repo via the backend's installation token.
|
|
2
|
+
// Source-control panel: connect the workspace, manage the connection (disconnect / resync),
|
|
3
|
+
// and browse the projected repos, branches, pull requests and issues the backend caches.
|
|
4
|
+
// Mirrors the document-source connect/import surface. Writes (new branch, open/merge PR,
|
|
5
|
+
// comment) go straight to the repo via the backend's connection credential.
|
|
7
6
|
import type { GitHubPullRequest, GitHubRepo, VcsProvider } from '~/types/domain'
|
|
8
|
-
// Explicit
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the tag unambiguously.
|
|
12
|
-
import GitHubConnect from './GitHubConnect.vue'
|
|
7
|
+
// Explicit imports: the auto-import name for a component nested under a like-named directory
|
|
8
|
+
// (github/BranchProtectionPreflight) doesn't match the tag used below, so it would silently
|
|
9
|
+
// render as an empty element. Importing by path binds each tag unambiguously.
|
|
13
10
|
import BranchProtectionPreflight from './BranchProtectionPreflight.vue'
|
|
14
|
-
import
|
|
11
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
15
12
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
16
13
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
17
14
|
|
|
@@ -33,9 +30,7 @@ const back = useIntegrationBack(open)
|
|
|
33
30
|
// Provider-aware chrome: once connected, the panel is titled for the provider the workspace
|
|
34
31
|
// actually connected; before that it names the sole connectable provider, or stays neutral when
|
|
35
32
|
// the deployment serves several. Brand names are verbatim in every locale (see `~/utils/vcs`).
|
|
36
|
-
const chromeProvider = computed(() =>
|
|
37
|
-
github.connected ? github.provider : github.soleConnectProvider,
|
|
38
|
-
)
|
|
33
|
+
const chromeProvider = computed(() => github.surfaceProvider)
|
|
39
34
|
const panelTitle = computed(() =>
|
|
40
35
|
chromeProvider.value ? VCS_PROVIDER_LABELS[chromeProvider.value] : t('vcs.panel.title'),
|
|
41
36
|
)
|
|
@@ -283,26 +278,7 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
283
278
|
<template #body>
|
|
284
279
|
<div class="space-y-5">
|
|
285
280
|
<!-- not connected: connect -->
|
|
286
|
-
<
|
|
287
|
-
<!-- One connect surface per method the deployment actually serves: the App
|
|
288
|
-
installation picker only where a GitHub App is configured, the PAT box only
|
|
289
|
-
where the per-workspace GitLab connect is wired. -->
|
|
290
|
-
<template v-if="github.canConnectGitHubApp">
|
|
291
|
-
<p class="text-sm text-slate-400">{{ t('github.panel.connectIntro') }}</p>
|
|
292
|
-
<GitHubConnect />
|
|
293
|
-
</template>
|
|
294
|
-
<USeparator
|
|
295
|
-
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
296
|
-
:label="t('vcs.connect.or')"
|
|
297
|
-
/>
|
|
298
|
-
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
299
|
-
<p
|
|
300
|
-
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
301
|
-
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
302
|
-
>
|
|
303
|
-
{{ t('vcs.connect.noneConfigured') }}
|
|
304
|
-
</p>
|
|
305
|
-
</template>
|
|
281
|
+
<VcsConnectSurfaces v-if="!github.connected" :app-intro="t('github.panel.connectIntro')" />
|
|
306
282
|
|
|
307
283
|
<!-- connected: manage + browse -->
|
|
308
284
|
<template v-else>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The connect surfaces a deployment can actually serve, in one place. A deployment wires a
|
|
3
|
+
// GitHub App, a per-workspace GitLab PAT connect, both, or neither, and only
|
|
4
|
+
// `GET /vcs/connect-options` (via the store's `canConnect*`) can say which — a connection
|
|
5
|
+
// READ says nothing, because the `github` module builds for either provider.
|
|
6
|
+
//
|
|
7
|
+
// Every surface that can strand a user on "not connected yet" renders this: the source-control
|
|
8
|
+
// panel, the onboarding gate, and the two modals that need a connection before they can do
|
|
9
|
+
// anything (add-service-from-repo, bootstrap). They had diverged — both modals hardcoded the
|
|
10
|
+
// GitHub App picker, so a GitLab-only deployment offered an installation flow it cannot serve
|
|
11
|
+
// and no way to connect at all — which is why the fan-out lives here rather than being copied
|
|
12
|
+
// a fourth time.
|
|
13
|
+
//
|
|
14
|
+
// Explicit imports: the auto-import name for `github/GitHubConnect` doesn't match the
|
|
15
|
+
// `<GitHubConnect>` tag (see GitHubPanel), so both children are bound by path.
|
|
16
|
+
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
17
|
+
import GitLabConnect from '~/components/vcs/GitLabConnect.vue'
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
/**
|
|
21
|
+
* Copy shown above the GitHub App picker only, since it is the one surface whose flow needs
|
|
22
|
+
* explaining (pick an account, grant repo access). Omitted ⇒ no intro, which is what the
|
|
23
|
+
* modals want: their own prompt already said why a connection is needed.
|
|
24
|
+
*/
|
|
25
|
+
appIntro?: string
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const { t } = useI18n()
|
|
29
|
+
const github = useGitHubStore()
|
|
30
|
+
|
|
31
|
+
// Whether the deployment serves neither surface. Rendered as a statement rather than an empty
|
|
32
|
+
// box: "nothing is configured" and "we couldn't read what is configured" both land here, and a
|
|
33
|
+
// blank space would read as the connect UI still loading.
|
|
34
|
+
const nothingConfigured = computed(() => !github.canConnectGitHubApp && !github.canConnectGitLabPat)
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div class="space-y-3">
|
|
39
|
+
<template v-if="github.canConnectGitHubApp">
|
|
40
|
+
<p v-if="props.appIntro" class="text-sm text-slate-400">{{ props.appIntro }}</p>
|
|
41
|
+
<GitHubConnect />
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<USeparator
|
|
45
|
+
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
46
|
+
:label="t('vcs.connect.or')"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
50
|
+
|
|
51
|
+
<p
|
|
52
|
+
v-if="nothingConfigured"
|
|
53
|
+
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
54
|
+
>
|
|
55
|
+
{{ t('vcs.connect.noneConfigured') }}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
@@ -1,5 +1,67 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import type { VcsProvider } from '~/types/domain'
|
|
1
3
|
import type { GitHubStoreContext } from './context'
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Everything a component needs to know about WHICH provider it is dealing with. Kept here,
|
|
7
|
+
* beside the connect actions that populate the state it reads, so the three questions stay
|
|
8
|
+
* answered in one place: what is connected, what could be connected, and what a given surface
|
|
9
|
+
* should therefore call itself.
|
|
10
|
+
*/
|
|
11
|
+
export interface VcsProviderViews {
|
|
12
|
+
/** The provider backing the current connection; a connection predating the discriminator is App-era GitHub. */
|
|
13
|
+
provider: ComputedRef<VcsProvider>
|
|
14
|
+
canConnectGitHubApp: ComputedRef<boolean>
|
|
15
|
+
canConnectGitLabPat: ComputedRef<boolean>
|
|
16
|
+
soleConnectProvider: ComputedRef<VcsProvider | null>
|
|
17
|
+
surfaceProvider: ComputedRef<VcsProvider | null>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The derived provider questions above, off the probed connection + capability state. */
|
|
21
|
+
export function createVcsProviderViews(ctx: GitHubStoreContext): VcsProviderViews {
|
|
22
|
+
const { connection, connectOptions } = ctx
|
|
23
|
+
|
|
24
|
+
const provider = computed<VcsProvider>(() => connection.value?.provider ?? 'github')
|
|
25
|
+
|
|
26
|
+
/** Whether the deployment can serve a GitHub App connect / a per-workspace GitLab PAT connect. */
|
|
27
|
+
const canConnectGitHubApp = computed(() =>
|
|
28
|
+
connectOptions.value.some((o) => o.provider === 'github' && o.method === 'app'),
|
|
29
|
+
)
|
|
30
|
+
const canConnectGitLabPat = computed(() =>
|
|
31
|
+
connectOptions.value.some((o) => o.provider === 'gitlab' && o.method === 'pat'),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The single provider this deployment can connect, or null when it offers several (or none) —
|
|
36
|
+
* what the connect copy keys off so a one-provider deployment never says "choose a provider".
|
|
37
|
+
*/
|
|
38
|
+
const soleConnectProvider = computed<VcsProvider | null>(() => {
|
|
39
|
+
const providers = new Set(connectOptions.value.map((o) => o.provider))
|
|
40
|
+
return providers.size === 1 ? [...providers][0]! : null
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The provider a repo-facing surface is ABOUT: the connected one, or (with nothing bound yet)
|
|
45
|
+
* the only one this deployment could connect. Null when it offers several and none is
|
|
46
|
+
* connected, because naming one there would be a guess — such a surface says something neutral
|
|
47
|
+
* instead (`vcs.onboarding.titleAny` is the pattern). Distinct from {@link provider}, which
|
|
48
|
+
* answers "what is connected" and therefore defaults to `github`: reading THAT before a
|
|
49
|
+
* connection exists is what puts "Pick an existing GitHub repository" in front of a
|
|
50
|
+
* GitLab-only deployment.
|
|
51
|
+
*/
|
|
52
|
+
const surfaceProvider = computed<VcsProvider | null>(() =>
|
|
53
|
+
connection.value !== null ? provider.value : soleConnectProvider.value,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
provider,
|
|
58
|
+
canConnectGitHubApp,
|
|
59
|
+
canConnectGitLabPat,
|
|
60
|
+
soleConnectProvider,
|
|
61
|
+
surfaceProvider,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
3
65
|
/**
|
|
4
66
|
* The provider-neutral half of the connection lifecycle: which connect surfaces the deployment
|
|
5
67
|
* serves, and the per-workspace **PAT** connect (GitLab today). The GitHub-App installation
|
|
@@ -15,6 +15,7 @@ function connection(overrides: Partial<GitHubConnection> = {}): GitHubConnection
|
|
|
15
15
|
targetType: 'User',
|
|
16
16
|
connectedAt: 1,
|
|
17
17
|
provider: 'github',
|
|
18
|
+
method: 'app',
|
|
18
19
|
canCreateRepos: false,
|
|
19
20
|
canManageWorkflows: true,
|
|
20
21
|
...overrides,
|
|
@@ -56,6 +57,33 @@ describe('github store — VCS connect capability', () => {
|
|
|
56
57
|
expect(github.canConnectGitLabPat).toBe(true)
|
|
57
58
|
// A single-provider deployment names its provider, so the connect copy never says "choose".
|
|
58
59
|
expect(github.soleConnectProvider).toBe('gitlab')
|
|
60
|
+
// …and the repo-facing surfaces name it too, BEFORE anything is connected. `provider`
|
|
61
|
+
// answers "what is connected" and so defaults to github; a surface reading that one is how
|
|
62
|
+
// "Pick an existing GitHub repository" ends up on a GitLab-only deployment.
|
|
63
|
+
expect(github.provider).toBe('github')
|
|
64
|
+
expect(github.surfaceProvider).toBe('gitlab')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('names the connected provider once bound, whatever the deployment could connect', async () => {
|
|
68
|
+
stubApi({
|
|
69
|
+
getGitHubConnection: vi
|
|
70
|
+
.fn()
|
|
71
|
+
.mockResolvedValue({ connection: connection({ provider: 'gitlab', method: 'pat' }) }),
|
|
72
|
+
listVcsConnectOptions: vi.fn().mockResolvedValue({
|
|
73
|
+
options: [
|
|
74
|
+
{ provider: 'github', method: 'app' },
|
|
75
|
+
{ provider: 'gitlab', method: 'pat' },
|
|
76
|
+
] satisfies VcsConnectOption[],
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
const github = storeWithWorkspace()
|
|
80
|
+
|
|
81
|
+
await github.probe()
|
|
82
|
+
|
|
83
|
+
// Several connectable, so there is no sole provider — but one IS connected, and that is
|
|
84
|
+
// what every repo-facing surface is about.
|
|
85
|
+
expect(github.soleConnectProvider).toBeNull()
|
|
86
|
+
expect(github.surfaceProvider).toBe('gitlab')
|
|
59
87
|
})
|
|
60
88
|
|
|
61
89
|
it('reports no connect surface when the capability read fails, without hiding the integration', async () => {
|
|
@@ -91,6 +119,9 @@ describe('github store — VCS connect capability', () => {
|
|
|
91
119
|
expect(github.canConnectGitHubApp).toBe(true)
|
|
92
120
|
expect(github.canConnectGitLabPat).toBe(true)
|
|
93
121
|
expect(github.soleConnectProvider).toBeNull()
|
|
122
|
+
// Nothing connected and several on offer: naming one would be a guess, so the surfaces
|
|
123
|
+
// fall back to their neutral copy rather than picking a brand.
|
|
124
|
+
expect(github.surfaceProvider).toBeNull()
|
|
94
125
|
})
|
|
95
126
|
|
|
96
127
|
it('connects GitLab with a trimmed PAT and loads the projection', async () => {
|