@cat-factory/app 0.316.0 → 0.317.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.
@@ -27,6 +27,7 @@ import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
27
27
  import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
28
28
  import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
29
29
  import { appInstallationManageUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
30
+ import { prefilledRepoMatch, prefillProviderMismatch } from '~/composables/useAddRepoDeepLink'
30
31
  import SectionLabel from '~/components/common/SectionLabel.vue'
31
32
  import IconButton from '~/components/common/IconButton.vue'
32
33
  import AppModal from '~/components/common/AppModal.vue'
@@ -305,10 +306,26 @@ watch(
305
306
  if (!isOpen) return
306
307
  resetSelection()
307
308
  void loadRepos()
309
+ if (ui.addServicePrefill) repoSearch.value = ui.addServicePrefill.repo
308
310
  },
309
311
  { immediate: true },
310
312
  )
311
313
 
314
+ // A deep link names one repository: select it as soon as the search lists it, once. A repository
315
+ // the connection cannot reach never lists, which leaves the search's own empty state (and its
316
+ // grant-access remedy) in front of the person.
317
+ const prefillMismatch = computed(() =>
318
+ prefillProviderMismatch(ui.addServicePrefill, github.connected ? github.provider : null),
319
+ )
320
+ watch(repoResults, (repos) => {
321
+ const prefill = ui.addServicePrefill
322
+ if (!prefill || prefillMismatch.value || selectedRepoId.value !== undefined) return
323
+ const match = prefilledRepoMatch(prefill, repos)
324
+ if (!match || toRepoItem(match).disabled) return
325
+ selectedRepoId.value = match.githubId
326
+ ui.addServicePrefill = null
327
+ })
328
+
312
329
  // A whole-repo service is added once (then configured inline). A monorepo instead
313
330
  // multi-selects directories and adds them together via `addServices`.
314
331
  const canAdd = computed(
@@ -511,6 +528,19 @@ function done() {
511
528
  </div>
512
529
  </UFormField>
513
530
 
531
+ <p
532
+ v-if="prefillMismatch"
533
+ class="text-sm text-app-warning-200/90"
534
+ data-testid="add-service-prefill-mismatch"
535
+ >
536
+ {{
537
+ t('github.addService.prefillProviderMismatch', {
538
+ repo: ui.addServicePrefill?.repo,
539
+ connected: providerLabel,
540
+ })
541
+ }}
542
+ </p>
543
+
514
544
  <UFormField
515
545
  :label="t('github.addService.repoType')"
516
546
  :description="t('github.addService.repoTypeHint')"
@@ -0,0 +1,66 @@
1
+ import { createPinia, setActivePinia } from 'pinia'
2
+ import { beforeEach, describe, expect, it } from 'vitest'
3
+ import type { GitHubAvailableRepo } from '~/types/domain'
4
+ import {
5
+ captureAddRepoDeepLink,
6
+ prefilledRepoMatch,
7
+ prefillProviderMismatch,
8
+ } from './useAddRepoDeepLink'
9
+
10
+ function setUrl(search: string): void {
11
+ history.replaceState(null, '', `/${search}`)
12
+ }
13
+
14
+ function repo(owner: string, name: string): GitHubAvailableRepo {
15
+ return { owner, name, githubId: owner.length + name.length } as GitHubAvailableRepo
16
+ }
17
+
18
+ describe('captureAddRepoDeepLink', () => {
19
+ beforeEach(() => {
20
+ setActivePinia(createPinia())
21
+ setUrl('')
22
+ })
23
+
24
+ it('parses the link an integration builds, and strips only its own params', () => {
25
+ setUrl('?addRepo=kibertoad%2Fsimpler-service3&provider=github&ws=ws_1&keep=yes')
26
+ expect(captureAddRepoDeepLink()).toEqual({
27
+ repo: 'kibertoad/simpler-service3',
28
+ provider: 'github',
29
+ workspaceId: 'ws_1',
30
+ })
31
+ expect(window.location.search).toBe('?keep=yes')
32
+ expect(captureAddRepoDeepLink()).toBeNull()
33
+ })
34
+
35
+ it('takes a GitLab project under nested groups', () => {
36
+ setUrl('?addRepo=acme/platform/payments')
37
+ expect(captureAddRepoDeepLink()).toMatchObject({ repo: 'acme/platform/payments' })
38
+ })
39
+
40
+ it('drops a value that is not a repository path, and still strips it', () => {
41
+ setUrl('?addRepo=payments')
42
+ expect(captureAddRepoDeepLink()).toBeNull()
43
+ expect(window.location.search).toBe('')
44
+ })
45
+ })
46
+
47
+ describe('prefilledRepoMatch', () => {
48
+ it('matches the full path regardless of case, never a longer name', () => {
49
+ const wanted = repo('Kibertoad', 'simpler-service3')
50
+ const found = prefilledRepoMatch({ repo: 'kibertoad/simpler-service3', provider: null }, [
51
+ repo('kibertoad', 'simpler-service30'),
52
+ wanted,
53
+ ])
54
+ expect(found).toBe(wanted)
55
+ })
56
+ })
57
+
58
+ describe('prefillProviderMismatch', () => {
59
+ it('flags a link for another host only when the board is connected', () => {
60
+ const prefill = { repo: 'acme/payments', provider: 'gitlab' }
61
+ expect(prefillProviderMismatch(prefill, 'github')).toBe(true)
62
+ expect(prefillProviderMismatch(prefill, 'gitlab')).toBe(false)
63
+ expect(prefillProviderMismatch(prefill, null)).toBe(false)
64
+ expect(prefillProviderMismatch({ ...prefill, provider: null }, 'github')).toBe(false)
65
+ })
66
+ })
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Boot-time replay of an ADD-REPOSITORY deep link: `?addRepo=<owner>/<name>&provider=<id>&ws=<id>`.
3
+ *
4
+ * An integration that finds a repository the platform has no service for (sainte-beuve filing a
5
+ * review, say) cannot create the service itself, because adding one is a person's choice of board,
6
+ * role and layout. It sends the person here instead: the add-service window opens with the
7
+ * repository already searched for, and selected once the connection lists it. `provider` is the
8
+ * host the link means, so a link for a GitLab project is not silently matched against GitHub;
9
+ * `ws` pins the board the same way a run link does. Integrations build it on `GET
10
+ * /api/v1/deployment`; the format is published at
11
+ * https://catfactory.ai/extend/public-api.html#sending-a-person-to-add-a-repository
12
+ *
13
+ * Signing in first keeps the query (`postSignInUrl`), so a signed-out person lands here too.
14
+ */
15
+ import { watch } from 'vue'
16
+ import type { GitHubAvailableRepo } from '~/types/domain'
17
+ import type { VcsProvider } from '~/types/vcs'
18
+ import { useUiStore } from '../stores/ui'
19
+ import { useWorkspaceStore } from '../stores/workspace'
20
+
21
+ /** What the add-service window is opened with: the repository to look for, and its host. */
22
+ export interface AddServicePrefill {
23
+ repo: string
24
+ provider: string | null
25
+ }
26
+
27
+ interface PendingAddRepoLink extends AddServicePrefill {
28
+ workspaceId: string | null
29
+ }
30
+
31
+ const ADD_REPO_LINK_PARAMS = ['addRepo', 'provider', 'ws'] as const
32
+
33
+ /** `owner/name`, where a GitLab owner may itself be a nested group path. */
34
+ const REPO_PATH = /^[\w.-]+(?:\/[\w.-]+)+$/
35
+
36
+ /**
37
+ * Read (and strip) an add-repository link from the current URL, or null when there is none or its
38
+ * `addRepo` is not a repository path. Called before `workspace.init()` so `ws` can pin the board.
39
+ */
40
+ export function captureAddRepoDeepLink(): PendingAddRepoLink | null {
41
+ if (typeof window === 'undefined') return null
42
+ const params = new URLSearchParams(window.location.search)
43
+ const repo = params.get('addRepo')?.trim()
44
+ if (!repo) return null
45
+ const link = { repo, provider: params.get('provider'), workspaceId: params.get('ws') }
46
+ for (const key of ADD_REPO_LINK_PARAMS) params.delete(key)
47
+ const qs = params.toString()
48
+ history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
49
+ return REPO_PATH.test(repo) ? link : null
50
+ }
51
+
52
+ /** The listed repository a prefill names, matched on its full path regardless of case. */
53
+ export function prefilledRepoMatch(
54
+ prefill: AddServicePrefill,
55
+ repos: readonly GitHubAvailableRepo[],
56
+ ): GitHubAvailableRepo | undefined {
57
+ const wanted = prefill.repo.toLowerCase()
58
+ return repos.find((r) => `${r.owner}/${r.name}`.toLowerCase() === wanted)
59
+ }
60
+
61
+ /** Whether the link names a host other than the one this board is connected to. */
62
+ export function prefillProviderMismatch(
63
+ prefill: AddServicePrefill | null,
64
+ connected: VcsProvider | null,
65
+ ): boolean {
66
+ return prefill?.provider != null && connected !== null && prefill.provider !== connected
67
+ }
68
+
69
+ /** Capture an add-repository link and, once the board has hydrated, open the window with it. */
70
+ export function useAddRepoDeepLink(): void {
71
+ const link = captureAddRepoDeepLink()
72
+ if (!link) return
73
+
74
+ const workspace = useWorkspaceStore()
75
+ const ui = useUiStore()
76
+ if (link.workspaceId) workspace.workspaceId = link.workspaceId
77
+
78
+ // One-shot, for the same reasons `useRunDeepLink` spells out: a later refresh must not reopen a
79
+ // window the person closed, and `immediate` runs the callback before `stop` is bound.
80
+ let applied = false
81
+ let stop: (() => void) | undefined
82
+ stop = watch(
83
+ () => workspace.ready,
84
+ (ready) => {
85
+ if (!ready || applied) return
86
+ applied = true
87
+ ui.openAddService({ repo: link.repo, provider: link.provider })
88
+ stop?.()
89
+ },
90
+ { immediate: true },
91
+ )
92
+ }
@@ -183,6 +183,8 @@ onMounted(() => {
183
183
  // board it names is the one that opens. This is what makes the observability link on an
184
184
  // engine-published PR verification report resolve.
185
185
  useRunDeepLink()
186
+ // `?addRepo=<owner>/<name>`: an integration sending a person to add a service for a repository.
187
+ useAddRepoDeepLink()
186
188
  // `?view=queue` / `?view=board`: a link that opens one of the board's two views.
187
189
  views.consumeDeepLink()
188
190
  void workspace.init()
@@ -10,6 +10,7 @@ import type {
10
10
  InfrastructureTab,
11
11
  ProviderConnectionKind,
12
12
  } from '~/types/providerConnections'
13
+ import type { AddServicePrefill } from '~/composables/useAddRepoDeepLink'
13
14
  import type { PendingContext } from '~/composables/useContextLinking'
14
15
  import {
15
16
  infraSetupDismissalKey,
@@ -523,6 +524,8 @@ function createOverlayModals() {
523
524
  const bootstrapOpen = ref(false)
524
525
  // "Add a service from an existing GitHub repo" modal (no bootstrap run).
525
526
  const addServiceOpen = ref(false)
527
+ // The repository an add-repository deep link opened the window for, consumed on open.
528
+ const addServicePrefill = ref<AddServicePrefill | null>(null)
526
529
  // Prompt-fragment library panel (manage the board's best-practice catalog +
527
530
  // linked guideline repos; ADR 0006).
528
531
  const fragmentLibraryOpen = ref(false)
@@ -546,11 +549,13 @@ function createOverlayModals() {
546
549
  function closeBootstrap() {
547
550
  bootstrapOpen.value = false
548
551
  }
549
- function openAddService() {
552
+ function openAddService(prefill: AddServicePrefill | null = null) {
553
+ addServicePrefill.value = prefill
550
554
  addServiceOpen.value = true
551
555
  }
552
556
  function closeAddService() {
553
557
  addServiceOpen.value = false
558
+ addServicePrefill.value = null
554
559
  }
555
560
  function openFragmentLibrary() {
556
561
  fragmentLibraryOpen.value = true
@@ -601,6 +606,7 @@ function createOverlayModals() {
601
606
  return {
602
607
  bootstrapOpen,
603
608
  addServiceOpen,
609
+ addServicePrefill,
604
610
  fragmentLibraryOpen,
605
611
  foundationalServicesOpen,
606
612
  commandBarOpen,
@@ -3813,7 +3813,8 @@
3813
3813
  "servicesAddedDescription": "{count} Service zum Board hinzugefügt. | {count} Services zum Board hinzugefügt.",
3814
3814
  "frontendLinkedNote": "{directory} wurde als Frontend hinzugefügt und mit den übrigen verknüpft. Benennen Sie die Umgebungsvariablen für die Backend-URLs im Inspector.",
3815
3815
  "frontendWiringFailedNote": "Die Frontend-Einstellungen wurden nicht gespeichert. Öffnen Sie den Inspector jeder Frontend-App, um ihr Unterverzeichnis und ihre Backend-Services zu setzen."
3816
- }
3816
+ },
3817
+ "prefillProviderMismatch": "Der Link verweist auf {repo} bei einem anderen Anbieter, dieses Board ist aber mit {connected} verbunden. Suche das Repository von Hand oder öffne den Link auf einem Board, das mit seinem Anbieter verbunden ist."
3817
3818
  },
3818
3819
  "repoTree": {
3819
3820
  "root": "root",
@@ -4830,7 +4830,8 @@
4830
4830
  },
4831
4831
  "frontendLinkedNote": "{directory} was added as the frontend and linked to the others. Name its backend URL environment variables in its inspector.",
4832
4832
  "frontendWiringFailedNote": "The frontend settings did not save. Open each frontend app's inspector to set its subdirectory and its backend services."
4833
- }
4833
+ },
4834
+ "prefillProviderMismatch": "The link asked for {repo} on a different host, and this board is connected to {connected}. Search for the repository by hand, or open the link on a board connected to its host."
4834
4835
  },
4835
4836
  "repoTree": {
4836
4837
  "root": "root",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "Los ajustes del frontend no se guardaron. Abre el inspector de cada aplicación frontend para definir su subdirectorio y sus servicios backend."
4646
4646
  },
4647
4647
  "repoType": "Tipo de repositorio",
4648
- "repoTypeHint": "Qué es este repositorio: un servicio backend, una aplicación frontend, una biblioteca compartida o un repositorio de documentación (solo documentos/spikes)."
4648
+ "repoTypeHint": "Qué es este repositorio: un servicio backend, una aplicación frontend, una biblioteca compartida o un repositorio de documentación (solo documentos/spikes).",
4649
+ "prefillProviderMismatch": "El enlace pedía {repo} en otro proveedor, y este tablero está conectado a {connected}. Busca el repositorio a mano o abre el enlace en un tablero conectado a su proveedor."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "raíz",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "Les réglages du frontend n'ont pas été enregistrés. Ouvrez l'inspecteur de chaque application frontend pour définir son sous-répertoire et ses services backend."
4646
4646
  },
4647
4647
  "repoType": "Type de dépôt",
4648
- "repoTypeHint": "Ce qu'est ce dépôt : un service backend, une application frontend, une bibliothèque partagée ou un dépôt de documentation (documents/spikes uniquement)."
4648
+ "repoTypeHint": "Ce qu'est ce dépôt : un service backend, une application frontend, une bibliothèque partagée ou un dépôt de documentation (documents/spikes uniquement).",
4649
+ "prefillProviderMismatch": "Le lien visait {repo} chez un autre hébergeur, et ce tableau est connecté à {connected}. Cherchez le dépôt à la main, ou ouvrez le lien sur un tableau connecté à son hébergeur."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "racine",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "הגדרות הפרונט-אנד לא נשמרו. פתח את האינספקטור של כל אפליקציית פרונט-אנד כדי להגדיר את תת-הספרייה ואת שירותי הבק-אנד שלה."
4646
4646
  },
4647
4647
  "repoType": "סוג המאגר",
4648
- "repoTypeHint": "מה המאגר הזה: שירות בק-אנד, אפליקציית פרונט-אנד, ספרייה משותפת או מאגר תיעוד (מסמכים/ספייקים בלבד)."
4648
+ "repoTypeHint": "מה המאגר הזה: שירות בק-אנד, אפליקציית פרונט-אנד, ספרייה משותפת או מאגר תיעוד (מסמכים/ספייקים בלבד).",
4649
+ "prefillProviderMismatch": "הקישור ביקש את {repo} אצל ספק אחר, והלוח הזה מחובר ל-{connected}. חפשו את המאגר ידנית, או פתחו את הקישור בלוח שמחובר לספק שלו."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "שורש",
@@ -3813,7 +3813,8 @@
3813
3813
  "servicesAddedDescription": "{count} servizio aggiunto alla board. | {count} servizi aggiunti alla board.",
3814
3814
  "frontendLinkedNote": "{directory} è stata aggiunta come frontend e collegata alle altre. Assegna il nome alle sue variabili d'ambiente con gli URL dei backend nel suo inspector.",
3815
3815
  "frontendWiringFailedNote": "Le impostazioni del frontend non sono state salvate. Apri l'inspector di ogni app frontend per impostarne la sottodirectory e i servizi backend."
3816
- }
3816
+ },
3817
+ "prefillProviderMismatch": "Il link chiedeva {repo} su un altro provider, e questa board è collegata a {connected}. Cerca il repository a mano, oppure apri il link su una board collegata al suo provider."
3817
3818
  },
3818
3819
  "repoTree": {
3819
3820
  "root": "root",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "フロントエンドの設定を保存できませんでした。各フロントエンドアプリのインスペクターを開き、サブディレクトリとバックエンドサービスを設定してください。"
4646
4646
  },
4647
4647
  "repoType": "リポジトリの種類",
4648
- "repoTypeHint": "このリポジトリの種類: バックエンドサービス、フロントエンドアプリ、共有ライブラリ、またはドキュメントリポジトリ(ドキュメント/スパイクのみ)。"
4648
+ "repoTypeHint": "このリポジトリの種類: バックエンドサービス、フロントエンドアプリ、共有ライブラリ、またはドキュメントリポジトリ(ドキュメント/スパイクのみ)。",
4649
+ "prefillProviderMismatch": "このリンクは別のホストにある {repo} を指していますが、このボードは {connected} に接続されています。リポジトリを手動で検索するか、そのホストに接続されたボードでリンクを開いてください。"
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "ルート",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "Ustawienia frontendu nie zostały zapisane. Otwórz inspektor każdej aplikacji frontendowej, aby ustawić jej podkatalog i usługi backendowe."
4646
4646
  },
4647
4647
  "repoType": "Typ repozytorium",
4648
- "repoTypeHint": "Czym jest to repozytorium: usługą backendową, aplikacją frontendową, współdzieloną biblioteką lub repozytorium dokumentacji (tylko dokumenty/spike'i)."
4648
+ "repoTypeHint": "Czym jest to repozytorium: usługą backendową, aplikacją frontendową, współdzieloną biblioteką lub repozytorium dokumentacji (tylko dokumenty/spike'i).",
4649
+ "prefillProviderMismatch": "Link wskazywał {repo} u innego dostawcy, a ta tablica jest połączona z {connected}. Wyszukaj repozytorium ręcznie albo otwórz link na tablicy połączonej z jego dostawcą."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "katalog główny",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "Frontend ayarları kaydedilmedi. Alt dizinini ve backend servislerini ayarlamak için her frontend uygulamasının inspector’ını açın."
4646
4646
  },
4647
4647
  "repoType": "Depo türü",
4648
- "repoTypeHint": "Bu deponun türü: bir backend servisi, bir frontend uygulaması, paylaşılan bir kütüphane veya bir doküman deposu (yalnızca doküman/spike)."
4648
+ "repoTypeHint": "Bu deponun türü: bir backend servisi, bir frontend uygulaması, paylaşılan bir kütüphane veya bir doküman deposu (yalnızca doküman/spike).",
4649
+ "prefillProviderMismatch": "Bağlantı başka bir sağlayıcıdaki {repo} deposunu istiyordu, bu pano ise {connected} ile bağlı. Depoyu elle arayın veya bağlantıyı onun sağlayıcısına bağlı bir panoda açın."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "kök",
@@ -4645,7 +4645,8 @@
4645
4645
  "frontendWiringFailedNote": "Налаштування фронтенду не збережено. Відкрийте інспектор кожного фронтенд-застосунку, щоб указати його підкаталог і бекенд-сервіси."
4646
4646
  },
4647
4647
  "repoType": "Тип репозиторію",
4648
- "repoTypeHint": "Що це за репозиторій: бекенд-сервіс, фронтенд-застосунок, спільна бібліотека або репозиторій документації (лише документи/спайки)."
4648
+ "repoTypeHint": "Що це за репозиторій: бекенд-сервіс, фронтенд-застосунок, спільна бібліотека або репозиторій документації (лише документи/спайки).",
4649
+ "prefillProviderMismatch": "Посилання вказувало на {repo} в іншого провайдера, а ця дошка підключена до {connected}. Знайдіть репозиторій вручну або відкрийте посилання на дошці, підключеній до його провайдера."
4649
4650
  },
4650
4651
  "repoTree": {
4651
4652
  "root": "корінь",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.316.0",
3
+ "version": "0.317.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@cat-factory/contracts": "0.370.0",
21
+ "@cat-factory/contracts": "0.371.0",
22
22
  "@modular-frontend/core": "0.6.0",
23
23
  "@modular-vue/core": "^1.5.0",
24
24
  "@modular-vue/journeys": "^1.4.0",