@cat-factory/app 0.73.1 → 0.74.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.
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// it in a container), this just links a repo the App can access to a fresh,
|
|
5
5
|
// `ready` service frame. The workspace need not track the repo yet: the backend
|
|
6
6
|
// links + syncs it on import. If the App can't see the wanted repo, the user
|
|
7
|
-
// grants it access from here, then
|
|
7
|
+
// grants it access from here, then searches for it again.
|
|
8
8
|
//
|
|
9
9
|
// MONOREPO support: a repo flagged a monorepo can back SEVERAL services, each
|
|
10
10
|
// pinned to a subdirectory. When the selected repo is a monorepo, the user
|
|
11
11
|
// browses its tree and picks the service's directory before adding (and may add
|
|
12
12
|
// more than one, a subset of the repo's services).
|
|
13
13
|
import { refDebounced } from '@vueuse/core'
|
|
14
|
-
import type { FrameRepoType } from '~/types/domain'
|
|
14
|
+
import type { FrameRepoType, GitHubAvailableRepo } from '~/types/domain'
|
|
15
15
|
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
16
16
|
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
17
17
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
@@ -42,7 +42,10 @@ const adding = ref(false)
|
|
|
42
42
|
async function loadRepos() {
|
|
43
43
|
try {
|
|
44
44
|
await github.probe()
|
|
45
|
-
|
|
45
|
+
// Load only the linked-repo projection (to flag repos already on the board). The
|
|
46
|
+
// available-repos list is NOT prefetched — it's searched server-side on demand once
|
|
47
|
+
// the user types (see the repoQueryRaw watcher), so a wide install isn't shipped whole.
|
|
48
|
+
if (github.connected) await github.load()
|
|
46
49
|
} catch {
|
|
47
50
|
// Integration off / unreachable → the picker stays empty, GitHubConnect shows.
|
|
48
51
|
}
|
|
@@ -66,76 +69,57 @@ const onBoardIds = computed(
|
|
|
66
69
|
() => new Set(github.repos.filter((r) => r.blockId).map((r) => r.githubId)),
|
|
67
70
|
)
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
disabled: onBoard,
|
|
83
|
-
}
|
|
84
|
-
}),
|
|
85
|
-
)
|
|
72
|
+
// Map an available repo to a combobox item. The label carries the private/monorepo/
|
|
73
|
+
// on-board suffixes; an on-board whole-repo is disabled (it already backs a service).
|
|
74
|
+
function toRepoItem(r: GitHubAvailableRepo) {
|
|
75
|
+
const onBoard = onBoardIds.value.has(r.githubId) && !r.isMonorepo
|
|
76
|
+
const suffix = [
|
|
77
|
+
r.private ? t('github.addService.repoLabel.private') : '',
|
|
78
|
+
r.isMonorepo ? t('github.addService.repoLabel.monorepo') : '',
|
|
79
|
+
onBoard ? t('github.addService.repoLabel.onBoard') : '',
|
|
80
|
+
].join('')
|
|
81
|
+
return { label: `${r.owner}/${r.name}${suffix}`, value: r.githubId, disabled: onBoard }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const repoItems = computed(() => github.availableRepos.map(toRepoItem))
|
|
86
85
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// rows; but when the whole list is small enough to browse (<= BROWSE_ALL_MAX) the gate is
|
|
93
|
-
// dropped and every repo is offered up-front (typing then narrows), so a handful of repos
|
|
94
|
-
// stays pickable without having to type — matching the old always-open dropdown.
|
|
86
|
+
// A wide App install (or a PAT) can expose hundreds of repos, far too many to prefetch and
|
|
87
|
+
// filter in the browser — so the picker searches SERVER-SIDE. Nothing is fetched on open;
|
|
88
|
+
// once the user types at least MIN_SEARCH_LEN characters the (debounced) query is sent to
|
|
89
|
+
// the backend, which returns only the `owner/name` matches. Below the gate the list stays
|
|
90
|
+
// empty and the field prompts for more characters.
|
|
95
91
|
const MIN_SEARCH_LEN = 3
|
|
96
|
-
const BROWSE_ALL_MAX = 25
|
|
97
92
|
const repoSearch = ref('')
|
|
98
93
|
const repoSearchDebounced = refDebounced(repoSearch, 250)
|
|
99
|
-
// Trimmed
|
|
94
|
+
// Trimmed for display + the min-length gate; the backend matches case-insensitively.
|
|
100
95
|
const repoQueryRaw = computed(() => repoSearchDebounced.value.trim())
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
96
|
+
|
|
97
|
+
// True while the query is too short to search — the picker shows the "type N chars" hint.
|
|
98
|
+
const belowMinChars = computed(() => repoQueryRaw.value.length < MIN_SEARCH_LEN)
|
|
99
|
+
|
|
100
|
+
// The selected repo, captured when picked (below). The loaded list is volatile — a later
|
|
101
|
+
// search replaces it — so the selection can't be derived from `availableRepos` after the fact.
|
|
102
|
+
const selectedRepo = ref<GitHubAvailableRepo | undefined>(undefined)
|
|
103
|
+
|
|
104
|
+
// Fetch matches server-side as the debounced query changes; below the gate clear the list
|
|
105
|
+
// so stale matches don't linger (the hint shows instead). Granting the App access needs no
|
|
106
|
+
// manual refresh — the next search hits GitHub live, with no cached list to invalidate.
|
|
107
|
+
watch(repoQueryRaw, (q) => {
|
|
108
|
+
void github.loadAvailableRepos(q.length >= MIN_SEARCH_LEN ? q : '')
|
|
114
109
|
})
|
|
115
110
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
// The server already filtered, so the matches ARE the loaded repos (empty below the gate).
|
|
112
|
+
const queryMatches = computed(() => (belowMinChars.value ? [] : repoItems.value))
|
|
113
|
+
|
|
114
|
+
// Items fed to the combobox: the matches plus the current selection kept present, so the
|
|
115
|
+
// menu still renders the selected repo's label after a later search replaces the list.
|
|
119
116
|
const repoMenuItems = computed(() => {
|
|
120
117
|
const matches = queryMatches.value
|
|
121
118
|
if (selectedRepoId.value === undefined) return matches
|
|
122
119
|
if (matches.some((r) => r.value === selectedRepoId.value)) return matches
|
|
123
|
-
|
|
124
|
-
return selected ? [selected, ...matches] : matches
|
|
120
|
+
return selectedRepo.value ? [toRepoItem(selectedRepo.value), ...matches] : matches
|
|
125
121
|
})
|
|
126
122
|
|
|
127
|
-
// The count summary under the field is shown only when it's meaningful: there are matches
|
|
128
|
-
// to count AND the user is actually searching (or browsing a small list). After a
|
|
129
|
-
// selection resets the search term this is false, so the field doesn't claim "Showing 0"
|
|
130
|
-
// or nag "type 3 characters" right under the repo the user just picked. The zero-match and
|
|
131
|
-
// min-length messages are owned by the combobox's own empty state instead.
|
|
132
|
-
const showResultCount = computed(() => !belowMinChars.value && queryMatches.value.length > 0)
|
|
133
|
-
|
|
134
|
-
const hasRepos = computed(() => github.availableRepos.length > 0)
|
|
135
|
-
const selectedRepo = computed(() =>
|
|
136
|
-
github.availableRepos.find((r) => r.githubId === selectedRepoId.value),
|
|
137
|
-
)
|
|
138
|
-
|
|
139
123
|
// ---- monorepo flag + directory picker ------------------------------------
|
|
140
124
|
|
|
141
125
|
// The monorepo flag is MODAL-LOCAL state, sent as part of the add-service request
|
|
@@ -151,8 +135,14 @@ function toggleMonorepo(value: boolean) {
|
|
|
151
135
|
selectedDirectory.value = undefined
|
|
152
136
|
}
|
|
153
137
|
|
|
154
|
-
// On repo change,
|
|
155
|
-
|
|
138
|
+
// On repo change, capture the picked repo (from the volatile loaded list, before a later
|
|
139
|
+
// search replaces it), seed the monorepo toggle from its persisted flag, and clear the rest.
|
|
140
|
+
watch(selectedRepoId, (id) => {
|
|
141
|
+
if (id === undefined) selectedRepo.value = undefined
|
|
142
|
+
else {
|
|
143
|
+
const found = github.availableRepos.find((r) => r.githubId === id)
|
|
144
|
+
if (found) selectedRepo.value = found
|
|
145
|
+
}
|
|
156
146
|
isMonorepo.value = selectedRepo.value?.isMonorepo === true
|
|
157
147
|
selectedDirectory.value = undefined
|
|
158
148
|
configuredBlockId.value = undefined
|
|
@@ -286,10 +276,7 @@ function done() {
|
|
|
286
276
|
:description="t('github.addService.repositoryHint')"
|
|
287
277
|
required
|
|
288
278
|
>
|
|
289
|
-
<div
|
|
290
|
-
{{ t('github.addService.noReposAvailable') }}
|
|
291
|
-
</div>
|
|
292
|
-
<div v-else class="space-y-1.5">
|
|
279
|
+
<div class="space-y-1.5">
|
|
293
280
|
<UInputMenu
|
|
294
281
|
v-model="selectedRepoId"
|
|
295
282
|
v-model:search-term="repoSearch"
|
|
@@ -317,19 +304,11 @@ function done() {
|
|
|
317
304
|
t('github.addService.searchMinChars', { min: MIN_SEARCH_LEN }, MIN_SEARCH_LEN)
|
|
318
305
|
}}
|
|
319
306
|
</span>
|
|
320
|
-
<span v-else>{{
|
|
307
|
+
<span v-else-if="!github.loadingAvailable">{{
|
|
321
308
|
t('github.addService.noMatches', { query: repoQueryRaw })
|
|
322
309
|
}}</span>
|
|
323
310
|
</template>
|
|
324
311
|
</UInputMenu>
|
|
325
|
-
<p v-if="showResultCount" class="text-xs text-slate-500">
|
|
326
|
-
{{
|
|
327
|
-
t('github.addService.showingCount', {
|
|
328
|
-
shown: queryMatches.length,
|
|
329
|
-
total: repoItems.length,
|
|
330
|
-
})
|
|
331
|
-
}}
|
|
332
|
-
</p>
|
|
333
312
|
</div>
|
|
334
313
|
</UFormField>
|
|
335
314
|
|
|
@@ -402,16 +381,6 @@ function done() {
|
|
|
402
381
|
>
|
|
403
382
|
{{ t('github.addService.grantAccess') }}
|
|
404
383
|
</UButton>
|
|
405
|
-
<UButton
|
|
406
|
-
color="neutral"
|
|
407
|
-
variant="ghost"
|
|
408
|
-
size="sm"
|
|
409
|
-
icon="i-lucide-refresh-cw"
|
|
410
|
-
:loading="github.loadingAvailable"
|
|
411
|
-
@click="github.loadAvailableRepos()"
|
|
412
|
-
>
|
|
413
|
-
{{ t('github.addService.refreshList') }}
|
|
414
|
-
</UButton>
|
|
415
384
|
</div>
|
|
416
385
|
|
|
417
386
|
<div class="flex justify-end gap-2">
|
|
@@ -71,9 +71,14 @@ export function githubApi({ send, ws }: ApiContext) {
|
|
|
71
71
|
send(createGitHubRepoContract, { pathPrefix: ws(workspaceId), body }),
|
|
72
72
|
|
|
73
73
|
// Repos the connected installation can access, annotated with whether this
|
|
74
|
-
// workspace links each (drives the per-workspace repo picker).
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
// workspace links each (drives the per-workspace repo picker). An optional `q`
|
|
75
|
+
// filters `owner/name` server-side so the add-service picker searches instead of
|
|
76
|
+
// prefetching the whole (possibly huge) installation; omitting it browses all.
|
|
77
|
+
listGitHubAvailableRepos: (workspaceId: string, q?: string) =>
|
|
78
|
+
send(listGitHubAvailableReposContract, {
|
|
79
|
+
pathPrefix: ws(workspaceId),
|
|
80
|
+
queryParams: { q },
|
|
81
|
+
}),
|
|
77
82
|
|
|
78
83
|
// Set the exact set of repos this workspace links.
|
|
79
84
|
setGitHubLinkedRepos: (workspaceId: string, repoGithubIds: number[]) =>
|
package/app/stores/github.ts
CHANGED
|
@@ -131,12 +131,21 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
131
131
|
if (connected.value && repos.value.length === 0) await load()
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Load the repos the installation can access, with this workspace's link state.
|
|
136
|
+
* With a `q` the backend filters `owner/name` server-side (the add-service picker
|
|
137
|
+
* searches instead of prefetching a huge installation); without one it browses all
|
|
138
|
+
* (the repo-link panel). A blank/short `q` clears the list rather than fetching.
|
|
139
|
+
*/
|
|
140
|
+
async function loadAvailableRepos(q?: string) {
|
|
136
141
|
if (!connected.value) return
|
|
142
|
+
if (q !== undefined && q.trim() === '') {
|
|
143
|
+
availableRepos.value = []
|
|
144
|
+
return
|
|
145
|
+
}
|
|
137
146
|
loadingAvailable.value = true
|
|
138
147
|
try {
|
|
139
|
-
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId())
|
|
148
|
+
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
140
149
|
} finally {
|
|
141
150
|
loadingAvailable.value = false
|
|
142
151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "^3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.
|
|
37
|
+
"@cat-factory/contracts": "0.81.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|