@cat-factory/app 0.36.0 → 0.37.1
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/auth/UserMenu.vue +11 -1
- package/app/components/brainstorm/BrainstormWindow.vue +2 -1
- package/app/components/clarity/ClarityReviewWindow.vue +2 -1
- package/app/components/layout/IntegrationBackTitle.vue +12 -7
- package/app/components/layout/IntegrationsHub.vue +191 -43
- package/app/components/layout/PersonalSetupModal.vue +141 -0
- package/app/components/pipeline/PipelineBuilder.vue +1 -1
- package/app/components/providers/VendorCredentialsModal.vue +7 -2
- package/app/composables/api/accounts.ts +36 -51
- package/app/composables/api/auth.ts +20 -19
- package/app/composables/api/board.ts +60 -40
- package/app/composables/api/bootstrap.ts +25 -22
- package/app/composables/api/client.ts +102 -0
- package/app/composables/api/context.ts +25 -6
- package/app/composables/api/documents.ts +36 -34
- package/app/composables/api/execution.ts +65 -48
- package/app/composables/api/followUps.ts +26 -26
- package/app/composables/api/fragments.ts +47 -34
- package/app/composables/api/github.ts +65 -45
- package/app/composables/api/humanReview.ts +7 -6
- package/app/composables/api/humanTest.ts +15 -11
- package/app/composables/api/kaizen.ts +8 -6
- package/app/composables/api/localSettings.ts +5 -4
- package/app/composables/api/models.ts +58 -51
- package/app/composables/api/notifications.ts +13 -7
- package/app/composables/api/presets.ts +34 -28
- package/app/composables/api/providerConnections.ts +68 -26
- package/app/composables/api/recurring.ts +40 -30
- package/app/composables/api/releaseHealth.ts +28 -26
- package/app/composables/api/reviews.ts +136 -114
- package/app/composables/api/sandbox.ts +52 -34
- package/app/composables/api/slack.ts +22 -25
- package/app/composables/api/spec.ts +3 -3
- package/app/composables/api/tasks.ts +42 -41
- package/app/composables/api/userSecrets.ts +12 -17
- package/app/composables/api/workspaces.ts +21 -15
- package/app/composables/useApi.ts +9 -1
- package/app/composables/useIntegrationBack.ts +9 -3
- package/app/composables/useSourceIntegration.ts +107 -0
- package/app/composables/useUpsertList.spec.ts +60 -0
- package/app/composables/useUpsertList.ts +57 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/auth.ts +2 -1
- package/app/stores/board.ts +2 -1
- package/app/stores/brainstorm.ts +2 -2
- package/app/stores/clarity.ts +6 -2
- package/app/stores/documents.ts +27 -62
- package/app/stores/execution.ts +3 -2
- package/app/stores/github.ts +1 -2
- package/app/stores/mergePresets.ts +2 -6
- package/app/stores/notifications.ts +9 -6
- package/app/stores/pipelines.ts +1 -1
- package/app/stores/recurringPipelines.ts +2 -7
- package/app/stores/sandbox.ts +1 -2
- package/app/stores/tasks.ts +25 -76
- package/app/stores/ui.ts +62 -19
- package/app/types/accountSettings.ts +11 -36
- package/app/types/accounts.ts +16 -71
- package/app/types/bootstrap.ts +13 -75
- package/app/types/brainstorm.ts +13 -38
- package/app/types/clarity.ts +12 -43
- package/app/types/consensus.ts +16 -89
- package/app/types/documents.ts +19 -94
- package/app/types/domain.ts +54 -586
- package/app/types/execution.ts +48 -515
- package/app/types/fragments.ts +15 -83
- package/app/types/github.ts +25 -161
- package/app/types/incidentEnrichment.ts +10 -25
- package/app/types/localModels.ts +11 -61
- package/app/types/localSettings.ts +9 -26
- package/app/types/merge.ts +10 -68
- package/app/types/model-presets.ts +7 -28
- package/app/types/models.ts +16 -164
- package/app/types/notifications.ts +18 -77
- package/app/types/openrouter.ts +8 -34
- package/app/types/providerConnections.ts +21 -41
- package/app/types/provisioningLogs.ts +9 -29
- package/app/types/recurring.ts +10 -63
- package/app/types/releaseHealth.ts +15 -39
- package/app/types/requirements.ts +14 -84
- package/app/types/sandbox.ts +45 -161
- package/app/types/services.ts +3 -22
- package/app/types/slack.ts +10 -47
- package/app/types/spec.ts +15 -68
- package/app/types/tasks.ts +15 -111
- package/app/types/tracker.ts +9 -24
- package/app/types/userSecrets.ts +12 -47
- package/package.json +9 -2
|
@@ -1,61 +1,69 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import {
|
|
2
|
+
connectTaskSourceContract,
|
|
3
|
+
createTaskFromIssueContract,
|
|
4
|
+
diagnoseTaskSourceContract,
|
|
5
|
+
disconnectTaskSourceContract,
|
|
6
|
+
getTrackerSettingsContract,
|
|
7
|
+
importTaskContract,
|
|
8
|
+
linkTaskContract,
|
|
9
|
+
listTaskConnectionsContract,
|
|
10
|
+
listTaskSourcesContract,
|
|
11
|
+
listTasksContract,
|
|
12
|
+
putTrackerSettingsContract,
|
|
13
|
+
searchTasksContract,
|
|
14
|
+
setTaskSourceEnabledContract,
|
|
15
|
+
spawnEpicContract,
|
|
16
|
+
} from '@cat-factory/contracts'
|
|
17
|
+
import type { TaskSourceKind } from '~/types/domain'
|
|
18
|
+
import type { PutTrackerSettingsInput } from '~/types/tracker'
|
|
11
19
|
import type { ApiContext } from './context'
|
|
12
20
|
|
|
13
21
|
/** Task sources (Jira, …): connect/import/search/link + the workspace tracker selection. */
|
|
14
|
-
export function tasksApi({
|
|
22
|
+
export function tasksApi({ send, ws }: ApiContext) {
|
|
15
23
|
return {
|
|
16
24
|
// ---- task sources (Jira, …) ------------------------------------------
|
|
17
25
|
// The configured trackers + their connect/import metadata + the workspace's
|
|
18
26
|
// per-source state (available + enabled). A 503 means the integration is off
|
|
19
27
|
// (the store hides its UI on any error here).
|
|
20
28
|
listTaskSources: (workspaceId: string) =>
|
|
21
|
-
|
|
29
|
+
send(listTaskSourcesContract, { pathPrefix: ws(workspaceId) }),
|
|
22
30
|
|
|
23
31
|
setTaskSourceEnabled: (workspaceId: string, source: TaskSourceKind, enabled: boolean) =>
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
send(setTaskSourceEnabledContract, {
|
|
33
|
+
pathPrefix: ws(workspaceId),
|
|
34
|
+
pathParams: { source },
|
|
26
35
|
body: { enabled },
|
|
27
36
|
}),
|
|
28
37
|
|
|
29
38
|
listTaskConnections: (workspaceId: string) =>
|
|
30
|
-
|
|
39
|
+
send(listTaskConnectionsContract, { pathPrefix: ws(workspaceId) }),
|
|
31
40
|
|
|
32
41
|
connectTaskSource: (
|
|
33
42
|
workspaceId: string,
|
|
34
43
|
source: TaskSourceKind,
|
|
35
44
|
credentials: Record<string, string>,
|
|
36
45
|
) =>
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
send(connectTaskSourceContract, {
|
|
47
|
+
pathPrefix: ws(workspaceId),
|
|
48
|
+
pathParams: { source },
|
|
39
49
|
body: { credentials },
|
|
40
50
|
}),
|
|
41
51
|
|
|
42
52
|
disconnectTaskSource: (workspaceId: string, source: TaskSourceKind) =>
|
|
43
|
-
|
|
53
|
+
send(disconnectTaskSourceContract, {
|
|
54
|
+
pathPrefix: ws(workspaceId),
|
|
55
|
+
pathParams: { source },
|
|
56
|
+
}),
|
|
44
57
|
|
|
45
58
|
// Live "check setup" probe: authenticates against the source and reads a slice
|
|
46
59
|
// of its issues API, returning a classified verdict the panel renders verbatim.
|
|
47
60
|
checkTaskSource: (workspaceId: string, source: TaskSourceKind) =>
|
|
48
|
-
|
|
49
|
-
method: 'POST',
|
|
50
|
-
}),
|
|
61
|
+
send(diagnoseTaskSourceContract, { pathPrefix: ws(workspaceId), pathParams: { source } }),
|
|
51
62
|
|
|
52
|
-
listTasks: (workspaceId: string) =>
|
|
63
|
+
listTasks: (workspaceId: string) => send(listTasksContract, { pathPrefix: ws(workspaceId) }),
|
|
53
64
|
|
|
54
65
|
importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
|
|
55
|
-
|
|
56
|
-
method: 'POST',
|
|
57
|
-
body,
|
|
58
|
-
}),
|
|
66
|
+
send(importTaskContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
|
|
59
67
|
|
|
60
68
|
searchTaskSource: (
|
|
61
69
|
workspaceId: string,
|
|
@@ -63,24 +71,21 @@ export function tasksApi({ http, ws }: ApiContext) {
|
|
|
63
71
|
query: string,
|
|
64
72
|
blockId?: string,
|
|
65
73
|
) =>
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
send(searchTasksContract, {
|
|
75
|
+
pathPrefix: ws(workspaceId),
|
|
76
|
+
pathParams: { source },
|
|
68
77
|
body: { query, ...(blockId ? { blockId } : {}) },
|
|
69
78
|
}),
|
|
70
79
|
|
|
71
80
|
linkTask: (
|
|
72
81
|
workspaceId: string,
|
|
73
82
|
body: { source: TaskSourceKind; externalId: string; blockId: string },
|
|
74
|
-
) =>
|
|
83
|
+
) => send(linkTaskContract, { pathPrefix: ws(workspaceId), body }),
|
|
75
84
|
|
|
76
85
|
createTaskFromIssue: (
|
|
77
86
|
workspaceId: string,
|
|
78
87
|
body: { source: TaskSourceKind; externalId: string; containerId: string },
|
|
79
|
-
) =>
|
|
80
|
-
http<{ block: Block; task: SourceTask }>(`${ws(workspaceId)}/tasks/create-block`, {
|
|
81
|
-
method: 'POST',
|
|
82
|
-
body,
|
|
83
|
-
}),
|
|
88
|
+
) => send(createTaskFromIssueContract, { pathPrefix: ws(workspaceId), body }),
|
|
84
89
|
|
|
85
90
|
// Spawn an epic + its children as an epic node + child tasks, with dependency edges
|
|
86
91
|
// seeded from the issues' blocked-by/depends-on links.
|
|
@@ -88,17 +93,13 @@ export function tasksApi({ http, ws }: ApiContext) {
|
|
|
88
93
|
workspaceId: string,
|
|
89
94
|
source: TaskSourceKind,
|
|
90
95
|
body: { ref: string; containerId: string; position?: { x: number; y: number } },
|
|
91
|
-
) =>
|
|
92
|
-
http<{ epic: Block; tasks: Block[] }>(
|
|
93
|
-
`${ws(workspaceId)}/task-sources/${source}/epics/spawn`,
|
|
94
|
-
{ method: 'POST', body },
|
|
95
|
-
),
|
|
96
|
+
) => send(spawnEpicContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
|
|
96
97
|
|
|
97
98
|
// ---- issue-tracker selection (workspace-level) ------------------------
|
|
98
99
|
getTrackerSettings: (workspaceId: string) =>
|
|
99
|
-
|
|
100
|
+
send(getTrackerSettingsContract, { pathPrefix: ws(workspaceId) }),
|
|
100
101
|
|
|
101
102
|
putTrackerSettings: (workspaceId: string, body: PutTrackerSettingsInput) =>
|
|
102
|
-
|
|
103
|
+
send(putTrackerSettingsContract, { pathPrefix: ws(workspaceId), body }),
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -1,31 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '~/types/userSecrets'
|
|
1
|
+
import {
|
|
2
|
+
listUserSecretsContract,
|
|
3
|
+
removeUserSecretContract,
|
|
4
|
+
storeUserSecretContract,
|
|
5
|
+
testUserSecretContract,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
7
|
+
import type { StoreUserSecretInput, TestUserSecretInput, UserSecretKind } from '~/types/userSecrets'
|
|
9
8
|
import type { ApiContext } from './context'
|
|
10
9
|
|
|
11
10
|
// Per-USER generic secrets (a GitHub PAT today). User-scoped (no workspace); the secret
|
|
12
11
|
// is write-only server-side and never returned — only status metadata + a `hasSecret`
|
|
13
12
|
// flag. `descriptors` drive the generic connect form; `test` probes before save.
|
|
14
|
-
export function userSecretsApi({
|
|
13
|
+
export function userSecretsApi({ send }: ApiContext) {
|
|
15
14
|
return {
|
|
16
|
-
listUserSecrets: () =>
|
|
17
|
-
http<{ secrets: UserSecretStatus[]; descriptors: UserSecretDescriptor[] }>('/user-secrets'),
|
|
15
|
+
listUserSecrets: () => send(listUserSecretsContract, {}),
|
|
18
16
|
|
|
19
17
|
storeUserSecret: (kind: UserSecretKind, body: StoreUserSecretInput) =>
|
|
20
|
-
|
|
18
|
+
send(storeUserSecretContract, { pathParams: { kind }, body }),
|
|
21
19
|
|
|
22
20
|
deleteUserSecret: (kind: UserSecretKind) =>
|
|
23
|
-
|
|
21
|
+
send(removeUserSecretContract, { pathParams: { kind } }),
|
|
24
22
|
|
|
25
23
|
testUserSecret: (kind: UserSecretKind, body: TestUserSecretInput) =>
|
|
26
|
-
|
|
27
|
-
method: 'POST',
|
|
28
|
-
body,
|
|
29
|
-
}),
|
|
24
|
+
send(testUserSecretContract, { pathParams: { kind }, body }),
|
|
30
25
|
}
|
|
31
26
|
}
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
createWorkspaceContract,
|
|
3
|
+
deleteWorkspaceContract,
|
|
4
|
+
getWorkspaceContract,
|
|
5
|
+
getWorkspaceSettingsContract,
|
|
6
|
+
listWorkspacesContract,
|
|
7
|
+
updateWorkspaceContract,
|
|
8
|
+
updateWorkspaceSettingsContract,
|
|
9
|
+
} from '@cat-factory/contracts'
|
|
10
|
+
import type { UpdateWorkspaceSettingsInput } from '~/types/domain'
|
|
7
11
|
import type { ApiContext } from './context'
|
|
8
12
|
|
|
9
13
|
/** Workspace CRUD + the full snapshot read. */
|
|
10
|
-
export function workspacesApi({
|
|
14
|
+
export function workspacesApi({ send, ws }: ApiContext) {
|
|
11
15
|
return {
|
|
12
16
|
// ---- workspaces -------------------------------------------------------
|
|
13
|
-
listWorkspaces: () =>
|
|
17
|
+
listWorkspaces: () => send(listWorkspacesContract, {}),
|
|
14
18
|
|
|
15
19
|
createWorkspace: (
|
|
16
20
|
body: { name?: string; description?: string; seed?: boolean; accountId?: string } = {},
|
|
17
|
-
) =>
|
|
21
|
+
) => send(createWorkspaceContract, { body }),
|
|
18
22
|
|
|
19
|
-
getWorkspace: (workspaceId: string) =>
|
|
23
|
+
getWorkspace: (workspaceId: string) =>
|
|
24
|
+
send(getWorkspaceContract, { pathParams: { workspaceId } }),
|
|
20
25
|
|
|
21
26
|
updateWorkspace: (workspaceId: string, body: { name?: string; description?: string | null }) =>
|
|
22
|
-
|
|
27
|
+
send(updateWorkspaceContract, { pathParams: { workspaceId }, body }),
|
|
23
28
|
|
|
24
29
|
renameWorkspace: (workspaceId: string, name: string) =>
|
|
25
|
-
|
|
30
|
+
send(updateWorkspaceContract, { pathParams: { workspaceId }, body: { name } }),
|
|
26
31
|
|
|
27
|
-
deleteWorkspace: (workspaceId: string) =>
|
|
32
|
+
deleteWorkspace: (workspaceId: string) =>
|
|
33
|
+
send(deleteWorkspaceContract, { pathParams: { workspaceId } }),
|
|
28
34
|
|
|
29
35
|
// ---- workspace runtime settings (human-wait escalation + per-service task limit) --
|
|
30
36
|
getWorkspaceSettings: (workspaceId: string) =>
|
|
31
|
-
|
|
37
|
+
send(getWorkspaceSettingsContract, { pathPrefix: ws(workspaceId) }),
|
|
32
38
|
|
|
33
39
|
updateWorkspaceSettings: (workspaceId: string, body: UpdateWorkspaceSettingsInput) =>
|
|
34
|
-
|
|
40
|
+
send(updateWorkspaceSettingsContract, { pathPrefix: ws(workspaceId), body }),
|
|
35
41
|
}
|
|
36
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FragmentOwnerKind } from '~/types/domain'
|
|
2
|
+
import { createApiClient, createSend, createSendWith } from './api/client'
|
|
2
3
|
import type { ApiContext } from './api/context'
|
|
3
4
|
import { accountsApi } from './api/accounts'
|
|
4
5
|
import { authApi } from './api/auth'
|
|
@@ -75,7 +76,14 @@ export function useApi() {
|
|
|
75
76
|
? `/accounts/${encodeURIComponent(id)}`
|
|
76
77
|
: `/workspaces/${encodeURIComponent(id)}`
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// The contract-driven client (wretch + sendByApiContract): one source of truth for
|
|
80
|
+
// path/method/request/response, shared with the backend via @cat-factory/contracts.
|
|
81
|
+
// API groups are migrated onto `send` incrementally; the rest still use `http`.
|
|
82
|
+
const client = createApiClient()
|
|
83
|
+
const send = createSend(client)
|
|
84
|
+
const sendWith = createSendWith(client)
|
|
85
|
+
|
|
86
|
+
const ctx: ApiContext = { http, client, send, sendWith, ws, scope, pwHeaders }
|
|
79
87
|
|
|
80
88
|
return {
|
|
81
89
|
...authApi(ctx),
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import type { Ref, WritableComputedRef } from 'vue'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The shared "back to
|
|
5
|
-
* Every panel reached from
|
|
4
|
+
* The shared "back to the hub" handler for an integration sub-panel's modal header.
|
|
5
|
+
* Every panel reached from a hub closes itself and reopens that hub when its
|
|
6
6
|
* {@link IntegrationBackTitle} Back control fires `@back`. Centralising it keeps the ~13
|
|
7
7
|
* panels from each re-implementing the two-step close-then-reopen inline — which also
|
|
8
8
|
* dodges a Vue SFC-compiler trap: a multi-statement inline template handler
|
|
9
9
|
* (`open = false` ⏎ `ui.openIntegrations()`) is rejected at build time, so callers had to
|
|
10
10
|
* resort to an obscure comma-operator expression. A named handler reads clearly instead.
|
|
11
11
|
*
|
|
12
|
+
* Returns to whichever hub the panel was reached from: the user-scoped "My setup" hub when
|
|
13
|
+
* `cameFromPersonal` is set, else the workspace Integrations hub. A shared panel (e.g. the
|
|
14
|
+
* vendor-credentials modal, reachable from both) thus lands the user back where they were.
|
|
15
|
+
*
|
|
12
16
|
* Pass the panel's `open` model (the writable ref/computed bound to its `UModal`).
|
|
13
17
|
*/
|
|
14
18
|
export function useIntegrationBack(open: Ref<boolean> | WritableComputedRef<boolean>) {
|
|
15
19
|
const ui = useUiStore()
|
|
16
20
|
return () => {
|
|
21
|
+
const toPersonal = ui.cameFromPersonal
|
|
17
22
|
open.value = false
|
|
18
|
-
ui.
|
|
23
|
+
if (toPersonal) ui.openPersonalSetup()
|
|
24
|
+
else ui.openIntegrations()
|
|
19
25
|
}
|
|
20
26
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type ComputedRef, type Ref, computed, ref } from 'vue'
|
|
2
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The source-integration lifecycle shared by the document-source and task-source stores:
|
|
6
|
+
* the opt-in `available` gate, the per-source `connections` list, the `descriptorFor` /
|
|
7
|
+
* `connectionFor` / `isConnected` lookups, and a `probe()` that resolves all of it (hiding
|
|
8
|
+
* the UI when the integration is off). Both stores previously hand-rolled this, with
|
|
9
|
+
* inconsistent error handling — one captured the probe failure, the other swallowed it.
|
|
10
|
+
* Standardising here means every integration now records WHY a probe failed
|
|
11
|
+
* (`probeError`: a 503 "turned off on this deployment" vs a 500 "the backend errored"), so
|
|
12
|
+
* a settings panel can explain the empty state instead of a blanket "install it first".
|
|
13
|
+
*
|
|
14
|
+
* The store supplies only what differs: how to fetch its sources + connections, and the
|
|
15
|
+
* connect/disconnect calls (which feed `upsertConnection` / `removeConnection`). Source-
|
|
16
|
+
* specific extras (diagnostics, per-source enable toggles, plan/spawn) stay in the store.
|
|
17
|
+
*/
|
|
18
|
+
export function useSourceIntegration<
|
|
19
|
+
Source extends string,
|
|
20
|
+
Conn extends { source: Source },
|
|
21
|
+
Desc extends { source: Source },
|
|
22
|
+
>(opts: {
|
|
23
|
+
/** Fetch the configured sources + live connections; throws when the integration is off. */
|
|
24
|
+
fetch: () => Promise<{ sources: Desc[]; connections: Conn[] }>
|
|
25
|
+
/** Gate the probe (e.g. skip until a workspace is selected). */
|
|
26
|
+
enabled?: () => boolean
|
|
27
|
+
}): {
|
|
28
|
+
available: Ref<boolean | null>
|
|
29
|
+
probeError: Ref<{ status: number | null; message: string } | null>
|
|
30
|
+
sources: Ref<Desc[]>
|
|
31
|
+
connections: Ref<Conn[]>
|
|
32
|
+
connectedSources: ComputedRef<Desc[]>
|
|
33
|
+
anyConnected: ComputedRef<boolean>
|
|
34
|
+
descriptorFor: (source: Source) => Desc | undefined
|
|
35
|
+
connectionFor: (source: Source) => Conn | undefined
|
|
36
|
+
isConnected: (source: Source) => boolean
|
|
37
|
+
upsertConnection: (conn: Conn) => void
|
|
38
|
+
removeConnection: (source: Source) => void
|
|
39
|
+
probe: () => Promise<void>
|
|
40
|
+
} {
|
|
41
|
+
/** null = unknown (not probed yet), true/false = integration on/off. */
|
|
42
|
+
const available = ref<boolean | null>(null)
|
|
43
|
+
/** Why the last probe failed, when it did (kept rather than swallowed). */
|
|
44
|
+
const probeError = ref<{ status: number | null; message: string } | null>(null)
|
|
45
|
+
const sources = ref<Desc[]>([]) as Ref<Desc[]>
|
|
46
|
+
const { items: connections, upsert: upsertConnection } = useUpsertList<Conn>({
|
|
47
|
+
key: (c) => c.source,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const connectedSources = computed(() =>
|
|
51
|
+
sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
|
|
52
|
+
)
|
|
53
|
+
const anyConnected = computed(() => connections.value.length > 0)
|
|
54
|
+
|
|
55
|
+
function descriptorFor(source: Source): Desc | undefined {
|
|
56
|
+
return sources.value.find((s) => s.source === source)
|
|
57
|
+
}
|
|
58
|
+
function connectionFor(source: Source): Conn | undefined {
|
|
59
|
+
return connections.value.find((c) => c.source === source)
|
|
60
|
+
}
|
|
61
|
+
function isConnected(source: Source): boolean {
|
|
62
|
+
return connectionFor(source) !== undefined
|
|
63
|
+
}
|
|
64
|
+
function removeConnection(source: Source) {
|
|
65
|
+
connections.value = connections.value.filter((c) => c.source !== source)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Probe the integration: resolves `available`, the sources and connections. */
|
|
69
|
+
async function probe() {
|
|
70
|
+
if (opts.enabled && !opts.enabled()) return
|
|
71
|
+
try {
|
|
72
|
+
const { sources: srcs, connections: conns } = await opts.fetch()
|
|
73
|
+
available.value = true
|
|
74
|
+
probeError.value = null
|
|
75
|
+
sources.value = srcs
|
|
76
|
+
connections.value = conns
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// 503 (integration disabled) or any error → hide the UI entry points, but keep the
|
|
79
|
+
// reason so a panel can explain it (503 = off here; 500 = the backend errored, e.g. an
|
|
80
|
+
// unapplied migration).
|
|
81
|
+
available.value = false
|
|
82
|
+
const err = e as { statusCode?: number; data?: { error?: { message?: string } } }
|
|
83
|
+
const serverMessage = err?.data?.error?.message
|
|
84
|
+
probeError.value = {
|
|
85
|
+
status: err?.statusCode ?? null,
|
|
86
|
+
message: serverMessage || (e instanceof Error ? e.message : String(e)),
|
|
87
|
+
}
|
|
88
|
+
sources.value = []
|
|
89
|
+
connections.value = []
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
available,
|
|
95
|
+
probeError,
|
|
96
|
+
sources,
|
|
97
|
+
connections,
|
|
98
|
+
connectedSources,
|
|
99
|
+
anyConnected,
|
|
100
|
+
descriptorFor,
|
|
101
|
+
connectionFor,
|
|
102
|
+
isConnected,
|
|
103
|
+
upsertConnection,
|
|
104
|
+
removeConnection,
|
|
105
|
+
probe,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
3
|
+
|
|
4
|
+
interface Item {
|
|
5
|
+
id: string
|
|
6
|
+
v: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('useUpsertList', () => {
|
|
10
|
+
it('appends new items by default and replaces in place by key', () => {
|
|
11
|
+
const { items, upsert } = useUpsertList<Item>({ key: (x) => x.id })
|
|
12
|
+
upsert({ id: 'a', v: 1 })
|
|
13
|
+
upsert({ id: 'b', v: 2 })
|
|
14
|
+
upsert({ id: 'a', v: 9 }) // replace, not duplicate
|
|
15
|
+
expect(items.value).toEqual([
|
|
16
|
+
{ id: 'a', v: 9 },
|
|
17
|
+
{ id: 'b', v: 2 },
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('prepends new items when prepend is set (newest-first)', () => {
|
|
22
|
+
const { items, upsert } = useUpsertList<Item>({ key: (x) => x.id, prepend: true })
|
|
23
|
+
upsert({ id: 'a', v: 1 })
|
|
24
|
+
upsert({ id: 'b', v: 2 })
|
|
25
|
+
expect(items.value.map((x) => x.id)).toEqual(['b', 'a'])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('removes by key and looks up by key', () => {
|
|
29
|
+
const { items, upsert, remove, get } = useUpsertList<Item>({ key: (x) => x.id })
|
|
30
|
+
upsert({ id: 'a', v: 1 })
|
|
31
|
+
upsert({ id: 'b', v: 2 })
|
|
32
|
+
expect(get('b')).toEqual({ id: 'b', v: 2 })
|
|
33
|
+
remove('a')
|
|
34
|
+
expect(items.value.map((x) => x.id)).toEqual(['b'])
|
|
35
|
+
remove('missing') // no-op
|
|
36
|
+
expect(items.value).toHaveLength(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('supports composite keys and hydrate-from-snapshot', () => {
|
|
40
|
+
interface Doc {
|
|
41
|
+
source: string
|
|
42
|
+
externalId: string
|
|
43
|
+
}
|
|
44
|
+
const { items, upsert, hydrate } = useUpsertList<Doc>({
|
|
45
|
+
key: (d) => `${d.source}:${d.externalId}`,
|
|
46
|
+
})
|
|
47
|
+
hydrate([{ source: 'jira', externalId: '1' }])
|
|
48
|
+
upsert({ source: 'jira', externalId: '1' }) // same composite key → replace
|
|
49
|
+
upsert({ source: 'gh', externalId: '1' }) // different source → new
|
|
50
|
+
expect(items.value).toHaveLength(2)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('seeds from initial without aliasing the caller array', () => {
|
|
54
|
+
const seed: Item[] = [{ id: 'a', v: 1 }]
|
|
55
|
+
const { items, upsert } = useUpsertList<Item>({ key: (x) => x.id, initial: seed })
|
|
56
|
+
upsert({ id: 'b', v: 2 })
|
|
57
|
+
expect(items.value).toHaveLength(2)
|
|
58
|
+
expect(seed).toHaveLength(1) // original untouched
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Ref, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A keyed list with find-by-key upsert — the pattern reimplemented in ~13 stores
|
|
5
|
+
* (`const i = list.findIndex((x) => x.id === item.id); if (i >= 0) list[i] = item else …`).
|
|
6
|
+
* Wraps a reactive `T[]` and exposes `upsert` (replace-in-place or insert), `remove`,
|
|
7
|
+
* `get`, and `hydrate` (replace from a server snapshot), all keyed by a caller-supplied
|
|
8
|
+
* `key` function. New items append by default, or `prepend: true` for newest-first inboxes.
|
|
9
|
+
*
|
|
10
|
+
* The returned `items` ref stays directly assignable, so a store can expose it under a
|
|
11
|
+
* domain name (`const { items: documents, upsert } = useUpsertList(...)`) and callers /
|
|
12
|
+
* tests can still do `store.documents = [...]`.
|
|
13
|
+
*/
|
|
14
|
+
export function useUpsertList<T>(opts: {
|
|
15
|
+
/** Stable identity for an item (e.g. `(x) => x.id`, or `(x) => `${x.source}:${x.externalId}``). */
|
|
16
|
+
key: (item: T) => unknown
|
|
17
|
+
/** Insert position for a brand-new item: `true` ⇒ unshift (newest-first), else push. */
|
|
18
|
+
prepend?: boolean
|
|
19
|
+
/** Seed contents (copied, not aliased). */
|
|
20
|
+
initial?: T[]
|
|
21
|
+
}): {
|
|
22
|
+
items: Ref<T[]>
|
|
23
|
+
upsert: (item: T) => void
|
|
24
|
+
remove: (keyValue: unknown) => void
|
|
25
|
+
get: (keyValue: unknown) => T | undefined
|
|
26
|
+
hydrate: (next: T[]) => void
|
|
27
|
+
indexOf: (keyValue: unknown) => number
|
|
28
|
+
} {
|
|
29
|
+
const items = ref<T[]>(opts.initial ? [...opts.initial] : []) as Ref<T[]>
|
|
30
|
+
|
|
31
|
+
function indexOf(keyValue: unknown): number {
|
|
32
|
+
return items.value.findIndex((x) => opts.key(x) === keyValue)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function upsert(item: T) {
|
|
36
|
+
const i = indexOf(opts.key(item))
|
|
37
|
+
if (i >= 0) items.value[i] = item
|
|
38
|
+
else if (opts.prepend) items.value.unshift(item)
|
|
39
|
+
else items.value.push(item)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function remove(keyValue: unknown) {
|
|
43
|
+
const i = indexOf(keyValue)
|
|
44
|
+
if (i >= 0) items.value.splice(i, 1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function get(keyValue: unknown): T | undefined {
|
|
48
|
+
const i = indexOf(keyValue)
|
|
49
|
+
return i >= 0 ? items.value[i] : undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hydrate(next: T[]) {
|
|
53
|
+
items.value = [...next]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { items, upsert, remove, get, hydrate, indexOf }
|
|
57
|
+
}
|
package/app/pages/index.vue
CHANGED
|
@@ -28,6 +28,7 @@ import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
|
|
|
28
28
|
import FragmentLibraryPanel from '~/components/fragments/FragmentLibraryPanel.vue'
|
|
29
29
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
30
30
|
import IntegrationsHub from '~/components/layout/IntegrationsHub.vue'
|
|
31
|
+
import PersonalSetupModal from '~/components/layout/PersonalSetupModal.vue'
|
|
31
32
|
import WorkspaceSettingsPanel from '~/components/settings/WorkspaceSettingsPanel.vue'
|
|
32
33
|
import AccountSettingsPanel from '~/components/settings/AccountSettingsPanel.vue'
|
|
33
34
|
import ObservabilityConnectionPanel from '~/components/settings/ObservabilityConnectionPanel.vue'
|
|
@@ -189,6 +190,7 @@ watch(
|
|
|
189
190
|
<FragmentLibraryPanel />
|
|
190
191
|
<CommandBar />
|
|
191
192
|
<IntegrationsHub />
|
|
193
|
+
<PersonalSetupModal />
|
|
192
194
|
<WorkspaceSettingsPanel />
|
|
193
195
|
<AccountSettingsPanel />
|
|
194
196
|
<ObservabilityConnectionPanel />
|
package/app/stores/auth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocalModeConfig } from '@cat-factory/contracts'
|
|
1
2
|
import { defineStore } from 'pinia'
|
|
2
3
|
import { computed, ref } from 'vue'
|
|
3
4
|
import type { AuthUser } from '~/types/domain'
|
|
@@ -29,7 +30,7 @@ export const useAuthStore = defineStore(
|
|
|
29
30
|
* `githubPatSetupUrl` is set when local mode has no GitHub PAT configured (drives the
|
|
30
31
|
* setup banner). Null on every other facade.
|
|
31
32
|
*/
|
|
32
|
-
const localMode = ref<
|
|
33
|
+
const localMode = ref<LocalModeConfig | null>(null)
|
|
33
34
|
/** True once the initial auth handshake has settled. */
|
|
34
35
|
const ready = ref(false)
|
|
35
36
|
|
package/app/stores/board.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
+
import type { UpdateBlockInput } from '@cat-factory/contracts'
|
|
3
4
|
import type { Block, BlockType, CreateTaskType, TaskTypeFields } from '~/types/domain'
|
|
4
5
|
import { useServicesStore } from '~/stores/services'
|
|
5
6
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
@@ -295,7 +296,7 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
/** Patch the user-editable fields of a block (title, features, threshold…). */
|
|
298
|
-
async function updateBlock(id: string, patch:
|
|
299
|
+
async function updateBlock(id: string, patch: UpdateBlockInput) {
|
|
299
300
|
const b = getBlock(id)
|
|
300
301
|
if (!b) return
|
|
301
302
|
Object.assign(b, patch) // optimistic
|
package/app/stores/brainstorm.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import type {
|
|
4
|
+
BrainstormItemStatus,
|
|
4
5
|
BrainstormSession,
|
|
5
6
|
BrainstormStage,
|
|
6
7
|
ResolveBrainstormExceededChoice,
|
|
7
|
-
ReviewItemStatus,
|
|
8
8
|
} from '~/types/brainstorm'
|
|
9
9
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
10
|
|
|
@@ -123,7 +123,7 @@ export const useBrainstormStore = defineStore('brainstorm', () => {
|
|
|
123
123
|
async function setItemStatus(
|
|
124
124
|
session: BrainstormSession,
|
|
125
125
|
itemId: string,
|
|
126
|
-
status:
|
|
126
|
+
status: BrainstormItemStatus,
|
|
127
127
|
) {
|
|
128
128
|
store(await api.setBrainstormItemStatus(workspace.requireId(), session.id, itemId, status))
|
|
129
129
|
}
|
package/app/stores/clarity.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ClarityItemStatus,
|
|
5
|
+
ClarityReview,
|
|
6
|
+
ResolveClarityExceededChoice,
|
|
7
|
+
} from '~/types/clarity'
|
|
4
8
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -120,7 +124,7 @@ export const useClarityStore = defineStore('clarity', () => {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Set an item's status (dismiss / reopen). */
|
|
123
|
-
async function setItemStatus(review: ClarityReview, itemId: string, status:
|
|
127
|
+
async function setItemStatus(review: ClarityReview, itemId: string, status: ClarityItemStatus) {
|
|
124
128
|
store(await api.setClarityItemStatus(workspace.requireId(), review.id, itemId, status))
|
|
125
129
|
}
|
|
126
130
|
|