@cat-factory/app 0.6.0 → 0.7.3
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/LICENSE +21 -21
- package/app/components/board/ContextPicker.vue +367 -367
- package/app/components/gates/GateResultView.vue +90 -12
- package/app/components/layout/SideBar.vue +11 -0
- package/app/components/observability/StepMetricsBar.vue +102 -102
- package/app/components/observability/StepModelActivity.vue +49 -0
- package/app/components/panels/ObservabilityPanel.vue +1 -1
- package/app/components/panels/StepMetadataCard.vue +4 -16
- package/app/components/panels/StepRunMeta.vue +105 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -178
- package/app/components/panels/inspector/TaskRunSettings.vue +77 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -124
- package/app/components/settings/IssueTrackerWritebackPanel.vue +103 -0
- package/app/components/testing/TestReportWindow.vue +17 -8
- package/app/composables/useBlockQueries.ts +154 -154
- package/app/composables/useContextLinking.ts +65 -65
- package/app/composables/useFrameResize.ts +54 -54
- package/app/pages/index.vue +2 -0
- package/app/stores/documents.ts +176 -176
- package/app/stores/services.ts +87 -87
- package/app/stores/tracker.ts +39 -27
- package/app/stores/ui.ts +12 -0
- package/app/types/documents.ts +104 -104
- package/app/types/domain.ts +5 -1
- package/app/types/execution.ts +18 -0
- package/app/types/github.ts +173 -173
- package/app/types/services.ts +27 -27
- package/app/types/tasks.ts +82 -82
- package/app/types/tracker.ts +27 -18
- package/app/utils/agentOutput.spec.ts +128 -128
- package/app/utils/agentOutput.ts +173 -173
- package/app/utils/observability.ts +52 -52
- package/package.json +6 -1
package/app/stores/documents.ts
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
3
|
-
import type {
|
|
4
|
-
DocumentBoardPlan,
|
|
5
|
-
DocumentConnection,
|
|
6
|
-
DocumentSearchResult,
|
|
7
|
-
DocumentSourceDescriptor,
|
|
8
|
-
DocumentSourceKind,
|
|
9
|
-
SourceDocument,
|
|
10
|
-
} from '~/types/domain'
|
|
11
|
-
import { useWorkspaceStore } from '~/stores/workspace'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Document-source integration state: the sources the backend offers (and their
|
|
15
|
-
* connect metadata), the workspace's per-source connections, and the pages it
|
|
16
|
-
* has imported — plus the actions that connect/import/plan/spawn/link against the
|
|
17
|
-
* backend. `available` mirrors the backend's opt-in gate: a 503 from the source
|
|
18
|
-
* probe means the integration is off, and the UI hides its entry points (just as
|
|
19
|
-
* `auth.required` gates the login UI). The abstraction is source-agnostic; every
|
|
20
|
-
* action is keyed by a `DocumentSourceKind`. Per-workspace, like the board
|
|
21
|
-
* itself; nothing is persisted client-side.
|
|
22
|
-
*/
|
|
23
|
-
export const useDocumentsStore = defineStore('documents', () => {
|
|
24
|
-
const api = useApi()
|
|
25
|
-
const workspace = useWorkspaceStore()
|
|
26
|
-
|
|
27
|
-
/** null = unknown (not probed yet), true/false = integration on/off. */
|
|
28
|
-
const available = ref<boolean | null>(null)
|
|
29
|
-
/** The configured sources and their connect/import descriptors. */
|
|
30
|
-
const sources = ref<DocumentSourceDescriptor[]>([])
|
|
31
|
-
/** Live connections, one per connected source. */
|
|
32
|
-
const connections = ref<DocumentConnection[]>([])
|
|
33
|
-
const documents = ref<SourceDocument[]>([])
|
|
34
|
-
const loading = ref(false)
|
|
35
|
-
|
|
36
|
-
/** Sources the workspace currently has a live connection to. */
|
|
37
|
-
const connectedSources = computed(() =>
|
|
38
|
-
sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
|
|
39
|
-
)
|
|
40
|
-
const anyConnected = computed(() => connections.value.length > 0)
|
|
41
|
-
|
|
42
|
-
function descriptorFor(source: DocumentSourceKind): DocumentSourceDescriptor | undefined {
|
|
43
|
-
return sources.value.find((s) => s.source === source)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function connectionFor(source: DocumentSourceKind): DocumentConnection | undefined {
|
|
47
|
-
return connections.value.find((c) => c.source === source)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isConnected(source: DocumentSourceKind): boolean {
|
|
51
|
-
return connectionFor(source) !== undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Imported documents currently attached to a given block. */
|
|
55
|
-
function docsForBlock(blockId: string): SourceDocument[] {
|
|
56
|
-
return documents.value.filter((d) => d.linkedBlockId === blockId)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Merge a document returned by the backend into the local cache. */
|
|
60
|
-
function upsertDoc(doc: SourceDocument) {
|
|
61
|
-
const i = documents.value.findIndex(
|
|
62
|
-
(d) => d.source === doc.source && d.externalId === doc.externalId,
|
|
63
|
-
)
|
|
64
|
-
if (i >= 0) documents.value[i] = doc
|
|
65
|
-
else documents.value.unshift(doc)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function upsertConnection(conn: DocumentConnection) {
|
|
69
|
-
const i = connections.value.findIndex((c) => c.source === conn.source)
|
|
70
|
-
if (i >= 0) connections.value[i] = conn
|
|
71
|
-
else connections.value.push(conn)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Probe the integration: resolves `available`, the sources and connections. */
|
|
75
|
-
async function probe() {
|
|
76
|
-
if (!workspace.workspaceId) return
|
|
77
|
-
try {
|
|
78
|
-
const [{ sources: srcs }, { connections: conns }] = await Promise.all([
|
|
79
|
-
api.listDocumentSources(workspace.requireId()),
|
|
80
|
-
api.listDocumentConnections(workspace.requireId()),
|
|
81
|
-
])
|
|
82
|
-
available.value = true
|
|
83
|
-
sources.value = srcs
|
|
84
|
-
connections.value = conns
|
|
85
|
-
} catch {
|
|
86
|
-
// 503 (integration disabled) or any error → hide the UI entry points.
|
|
87
|
-
available.value = false
|
|
88
|
-
sources.value = []
|
|
89
|
-
connections.value = []
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Connect the workspace to a source with its credential bag. */
|
|
94
|
-
async function connect(source: DocumentSourceKind, credentials: Record<string, string>) {
|
|
95
|
-
const conn = await api.connectDocumentSource(workspace.requireId(), source, credentials)
|
|
96
|
-
upsertConnection(conn)
|
|
97
|
-
available.value = true
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Disconnect the workspace from a source. */
|
|
101
|
-
async function disconnect(source: DocumentSourceKind) {
|
|
102
|
-
await api.disconnectDocumentSource(workspace.requireId(), source)
|
|
103
|
-
connections.value = connections.value.filter((c) => c.source !== source)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Load the imported documents for the workspace (across sources). */
|
|
107
|
-
async function loadDocuments() {
|
|
108
|
-
documents.value = await api.listDocuments(workspace.requireId())
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Import (fetch + persist) a page by id or URL from a source. */
|
|
112
|
-
async function importDocument(source: DocumentSourceKind, ref: string): Promise<SourceDocument> {
|
|
113
|
-
loading.value = true
|
|
114
|
-
try {
|
|
115
|
-
const doc = await api.importDocument(workspace.requireId(), source, { ref })
|
|
116
|
-
upsertDoc(doc)
|
|
117
|
-
return doc
|
|
118
|
-
} finally {
|
|
119
|
-
loading.value = false
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Search a connected source's catalogue by free text (title/content). */
|
|
124
|
-
async function search(
|
|
125
|
-
source: DocumentSourceKind,
|
|
126
|
-
query: string,
|
|
127
|
-
): Promise<DocumentSearchResult[]> {
|
|
128
|
-
const { results } = await api.searchDocumentSource(workspace.requireId(), source, query)
|
|
129
|
-
return results
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Preview the board structure a page would expand into (no writes). */
|
|
133
|
-
function plan(source: DocumentSourceKind, externalId: string): Promise<DocumentBoardPlan> {
|
|
134
|
-
return api.planDocument(workspace.requireId(), source, externalId)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Apply a page's structure to the board, then refresh the board snapshot. */
|
|
138
|
-
async function spawn(source: DocumentSourceKind, externalId: string, frameId?: string) {
|
|
139
|
-
const { result } = await api.spawnDocument(workspace.requireId(), source, {
|
|
140
|
-
externalId,
|
|
141
|
-
frameId,
|
|
142
|
-
})
|
|
143
|
-
await workspace.refresh()
|
|
144
|
-
return result
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Attach an imported page to a block as agent context. */
|
|
148
|
-
async function linkToBlock(blockId: string, source: DocumentSourceKind, externalId: string) {
|
|
149
|
-
const doc = await api.linkDocument(workspace.requireId(), { source, externalId, blockId })
|
|
150
|
-
upsertDoc(doc)
|
|
151
|
-
return doc
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
available,
|
|
156
|
-
sources,
|
|
157
|
-
connections,
|
|
158
|
-
documents,
|
|
159
|
-
loading,
|
|
160
|
-
connectedSources,
|
|
161
|
-
anyConnected,
|
|
162
|
-
descriptorFor,
|
|
163
|
-
connectionFor,
|
|
164
|
-
isConnected,
|
|
165
|
-
docsForBlock,
|
|
166
|
-
probe,
|
|
167
|
-
connect,
|
|
168
|
-
disconnect,
|
|
169
|
-
loadDocuments,
|
|
170
|
-
importDocument,
|
|
171
|
-
search,
|
|
172
|
-
plan,
|
|
173
|
-
spawn,
|
|
174
|
-
linkToBlock,
|
|
175
|
-
}
|
|
176
|
-
})
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
DocumentBoardPlan,
|
|
5
|
+
DocumentConnection,
|
|
6
|
+
DocumentSearchResult,
|
|
7
|
+
DocumentSourceDescriptor,
|
|
8
|
+
DocumentSourceKind,
|
|
9
|
+
SourceDocument,
|
|
10
|
+
} from '~/types/domain'
|
|
11
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Document-source integration state: the sources the backend offers (and their
|
|
15
|
+
* connect metadata), the workspace's per-source connections, and the pages it
|
|
16
|
+
* has imported — plus the actions that connect/import/plan/spawn/link against the
|
|
17
|
+
* backend. `available` mirrors the backend's opt-in gate: a 503 from the source
|
|
18
|
+
* probe means the integration is off, and the UI hides its entry points (just as
|
|
19
|
+
* `auth.required` gates the login UI). The abstraction is source-agnostic; every
|
|
20
|
+
* action is keyed by a `DocumentSourceKind`. Per-workspace, like the board
|
|
21
|
+
* itself; nothing is persisted client-side.
|
|
22
|
+
*/
|
|
23
|
+
export const useDocumentsStore = defineStore('documents', () => {
|
|
24
|
+
const api = useApi()
|
|
25
|
+
const workspace = useWorkspaceStore()
|
|
26
|
+
|
|
27
|
+
/** null = unknown (not probed yet), true/false = integration on/off. */
|
|
28
|
+
const available = ref<boolean | null>(null)
|
|
29
|
+
/** The configured sources and their connect/import descriptors. */
|
|
30
|
+
const sources = ref<DocumentSourceDescriptor[]>([])
|
|
31
|
+
/** Live connections, one per connected source. */
|
|
32
|
+
const connections = ref<DocumentConnection[]>([])
|
|
33
|
+
const documents = ref<SourceDocument[]>([])
|
|
34
|
+
const loading = ref(false)
|
|
35
|
+
|
|
36
|
+
/** Sources the workspace currently has a live connection to. */
|
|
37
|
+
const connectedSources = computed(() =>
|
|
38
|
+
sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
|
|
39
|
+
)
|
|
40
|
+
const anyConnected = computed(() => connections.value.length > 0)
|
|
41
|
+
|
|
42
|
+
function descriptorFor(source: DocumentSourceKind): DocumentSourceDescriptor | undefined {
|
|
43
|
+
return sources.value.find((s) => s.source === source)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function connectionFor(source: DocumentSourceKind): DocumentConnection | undefined {
|
|
47
|
+
return connections.value.find((c) => c.source === source)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isConnected(source: DocumentSourceKind): boolean {
|
|
51
|
+
return connectionFor(source) !== undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Imported documents currently attached to a given block. */
|
|
55
|
+
function docsForBlock(blockId: string): SourceDocument[] {
|
|
56
|
+
return documents.value.filter((d) => d.linkedBlockId === blockId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Merge a document returned by the backend into the local cache. */
|
|
60
|
+
function upsertDoc(doc: SourceDocument) {
|
|
61
|
+
const i = documents.value.findIndex(
|
|
62
|
+
(d) => d.source === doc.source && d.externalId === doc.externalId,
|
|
63
|
+
)
|
|
64
|
+
if (i >= 0) documents.value[i] = doc
|
|
65
|
+
else documents.value.unshift(doc)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function upsertConnection(conn: DocumentConnection) {
|
|
69
|
+
const i = connections.value.findIndex((c) => c.source === conn.source)
|
|
70
|
+
if (i >= 0) connections.value[i] = conn
|
|
71
|
+
else connections.value.push(conn)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Probe the integration: resolves `available`, the sources and connections. */
|
|
75
|
+
async function probe() {
|
|
76
|
+
if (!workspace.workspaceId) return
|
|
77
|
+
try {
|
|
78
|
+
const [{ sources: srcs }, { connections: conns }] = await Promise.all([
|
|
79
|
+
api.listDocumentSources(workspace.requireId()),
|
|
80
|
+
api.listDocumentConnections(workspace.requireId()),
|
|
81
|
+
])
|
|
82
|
+
available.value = true
|
|
83
|
+
sources.value = srcs
|
|
84
|
+
connections.value = conns
|
|
85
|
+
} catch {
|
|
86
|
+
// 503 (integration disabled) or any error → hide the UI entry points.
|
|
87
|
+
available.value = false
|
|
88
|
+
sources.value = []
|
|
89
|
+
connections.value = []
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Connect the workspace to a source with its credential bag. */
|
|
94
|
+
async function connect(source: DocumentSourceKind, credentials: Record<string, string>) {
|
|
95
|
+
const conn = await api.connectDocumentSource(workspace.requireId(), source, credentials)
|
|
96
|
+
upsertConnection(conn)
|
|
97
|
+
available.value = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Disconnect the workspace from a source. */
|
|
101
|
+
async function disconnect(source: DocumentSourceKind) {
|
|
102
|
+
await api.disconnectDocumentSource(workspace.requireId(), source)
|
|
103
|
+
connections.value = connections.value.filter((c) => c.source !== source)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Load the imported documents for the workspace (across sources). */
|
|
107
|
+
async function loadDocuments() {
|
|
108
|
+
documents.value = await api.listDocuments(workspace.requireId())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Import (fetch + persist) a page by id or URL from a source. */
|
|
112
|
+
async function importDocument(source: DocumentSourceKind, ref: string): Promise<SourceDocument> {
|
|
113
|
+
loading.value = true
|
|
114
|
+
try {
|
|
115
|
+
const doc = await api.importDocument(workspace.requireId(), source, { ref })
|
|
116
|
+
upsertDoc(doc)
|
|
117
|
+
return doc
|
|
118
|
+
} finally {
|
|
119
|
+
loading.value = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Search a connected source's catalogue by free text (title/content). */
|
|
124
|
+
async function search(
|
|
125
|
+
source: DocumentSourceKind,
|
|
126
|
+
query: string,
|
|
127
|
+
): Promise<DocumentSearchResult[]> {
|
|
128
|
+
const { results } = await api.searchDocumentSource(workspace.requireId(), source, query)
|
|
129
|
+
return results
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Preview the board structure a page would expand into (no writes). */
|
|
133
|
+
function plan(source: DocumentSourceKind, externalId: string): Promise<DocumentBoardPlan> {
|
|
134
|
+
return api.planDocument(workspace.requireId(), source, externalId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Apply a page's structure to the board, then refresh the board snapshot. */
|
|
138
|
+
async function spawn(source: DocumentSourceKind, externalId: string, frameId?: string) {
|
|
139
|
+
const { result } = await api.spawnDocument(workspace.requireId(), source, {
|
|
140
|
+
externalId,
|
|
141
|
+
frameId,
|
|
142
|
+
})
|
|
143
|
+
await workspace.refresh()
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Attach an imported page to a block as agent context. */
|
|
148
|
+
async function linkToBlock(blockId: string, source: DocumentSourceKind, externalId: string) {
|
|
149
|
+
const doc = await api.linkDocument(workspace.requireId(), { source, externalId, blockId })
|
|
150
|
+
upsertDoc(doc)
|
|
151
|
+
return doc
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
available,
|
|
156
|
+
sources,
|
|
157
|
+
connections,
|
|
158
|
+
documents,
|
|
159
|
+
loading,
|
|
160
|
+
connectedSources,
|
|
161
|
+
anyConnected,
|
|
162
|
+
descriptorFor,
|
|
163
|
+
connectionFor,
|
|
164
|
+
isConnected,
|
|
165
|
+
docsForBlock,
|
|
166
|
+
probe,
|
|
167
|
+
connect,
|
|
168
|
+
disconnect,
|
|
169
|
+
loadDocuments,
|
|
170
|
+
importDocument,
|
|
171
|
+
search,
|
|
172
|
+
plan,
|
|
173
|
+
spawn,
|
|
174
|
+
linkToBlock,
|
|
175
|
+
}
|
|
176
|
+
})
|
package/app/stores/services.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
3
|
-
import type { Service, WorkspaceMount } from '~/types/services'
|
|
4
|
-
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* In-org shared services. A `Service` is account-owned (a service frame + its subtree + repo)
|
|
8
|
-
* and can be mounted onto several teams' boards; a `WorkspaceMount` places one onto THIS
|
|
9
|
-
* board with its own frame layout. Hydrated from the workspace snapshot:
|
|
10
|
-
* - `mounts` — the services this board mounts (drives the per-board frame layout),
|
|
11
|
-
* - `catalog` — the org's services this board can mount from (each with a `mountCount`).
|
|
12
|
-
*/
|
|
13
|
-
export const useServicesStore = defineStore('services', () => {
|
|
14
|
-
const api = useApi()
|
|
15
|
-
|
|
16
|
-
const mounts = ref<WorkspaceMount[]>([])
|
|
17
|
-
const catalog = ref<Service[]>([])
|
|
18
|
-
|
|
19
|
-
function hydrate(nextMounts: WorkspaceMount[], nextCatalog: Service[]) {
|
|
20
|
-
mounts.value = [...nextMounts]
|
|
21
|
-
catalog.value = [...nextCatalog]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Mount row keyed by service id. */
|
|
25
|
-
const byServiceId = computed<Record<string, WorkspaceMount>>(() => {
|
|
26
|
-
const map: Record<string, WorkspaceMount> = {}
|
|
27
|
-
for (const m of mounts.value) map[m.serviceId] = m
|
|
28
|
-
return map
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
/** Catalog service keyed by its frame block id (resolve a frame → its service). */
|
|
32
|
-
const serviceByFrameBlock = computed<Record<string, Service>>(() => {
|
|
33
|
-
const map: Record<string, Service> = {}
|
|
34
|
-
for (const s of catalog.value) map[s.frameBlockId] = s
|
|
35
|
-
return map
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
/** Org services NOT yet mounted on this board (the "add existing service" picker's options). */
|
|
39
|
-
const mountable = computed<Service[]>(() => {
|
|
40
|
-
const mounted = new Set(mounts.value.map((m) => m.serviceId))
|
|
41
|
-
return catalog.value.filter((s) => !mounted.has(s.id))
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
/** A frame is "shared" when its service is mounted on more than one board. */
|
|
45
|
-
function isSharedFrame(frameBlockId: string): boolean {
|
|
46
|
-
return (serviceByFrameBlock.value[frameBlockId]?.mountCount ?? 0) > 1
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function mount(serviceId: string, position?: { x: number; y: number }) {
|
|
50
|
-
const ws = useWorkspaceStore()
|
|
51
|
-
const created = await api.mountService(ws.requireId(), serviceId, position ? { position } : {})
|
|
52
|
-
await ws.refresh()
|
|
53
|
-
return created
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function unmount(serviceId: string) {
|
|
57
|
-
const ws = useWorkspaceStore()
|
|
58
|
-
await api.unmountService(ws.requireId(), serviceId)
|
|
59
|
-
await ws.refresh()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Persist a mounted frame's per-board layout (called on frame drag/resize end). */
|
|
63
|
-
async function updateLayout(
|
|
64
|
-
serviceId: string,
|
|
65
|
-
position?: { x: number; y: number },
|
|
66
|
-
size?: { w: number; h: number } | null,
|
|
67
|
-
) {
|
|
68
|
-
const ws = useWorkspaceStore()
|
|
69
|
-
const updated = await api.updateMountLayout(ws.requireId(), serviceId, { position, size })
|
|
70
|
-
const local = mounts.value.find((m) => m.serviceId === serviceId)
|
|
71
|
-
if (local) Object.assign(local, updated)
|
|
72
|
-
return updated
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
mounts,
|
|
77
|
-
catalog,
|
|
78
|
-
byServiceId,
|
|
79
|
-
serviceByFrameBlock,
|
|
80
|
-
mountable,
|
|
81
|
-
isSharedFrame,
|
|
82
|
-
hydrate,
|
|
83
|
-
mount,
|
|
84
|
-
unmount,
|
|
85
|
-
updateLayout,
|
|
86
|
-
}
|
|
87
|
-
})
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { Service, WorkspaceMount } from '~/types/services'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-org shared services. A `Service` is account-owned (a service frame + its subtree + repo)
|
|
8
|
+
* and can be mounted onto several teams' boards; a `WorkspaceMount` places one onto THIS
|
|
9
|
+
* board with its own frame layout. Hydrated from the workspace snapshot:
|
|
10
|
+
* - `mounts` — the services this board mounts (drives the per-board frame layout),
|
|
11
|
+
* - `catalog` — the org's services this board can mount from (each with a `mountCount`).
|
|
12
|
+
*/
|
|
13
|
+
export const useServicesStore = defineStore('services', () => {
|
|
14
|
+
const api = useApi()
|
|
15
|
+
|
|
16
|
+
const mounts = ref<WorkspaceMount[]>([])
|
|
17
|
+
const catalog = ref<Service[]>([])
|
|
18
|
+
|
|
19
|
+
function hydrate(nextMounts: WorkspaceMount[], nextCatalog: Service[]) {
|
|
20
|
+
mounts.value = [...nextMounts]
|
|
21
|
+
catalog.value = [...nextCatalog]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Mount row keyed by service id. */
|
|
25
|
+
const byServiceId = computed<Record<string, WorkspaceMount>>(() => {
|
|
26
|
+
const map: Record<string, WorkspaceMount> = {}
|
|
27
|
+
for (const m of mounts.value) map[m.serviceId] = m
|
|
28
|
+
return map
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/** Catalog service keyed by its frame block id (resolve a frame → its service). */
|
|
32
|
+
const serviceByFrameBlock = computed<Record<string, Service>>(() => {
|
|
33
|
+
const map: Record<string, Service> = {}
|
|
34
|
+
for (const s of catalog.value) map[s.frameBlockId] = s
|
|
35
|
+
return map
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** Org services NOT yet mounted on this board (the "add existing service" picker's options). */
|
|
39
|
+
const mountable = computed<Service[]>(() => {
|
|
40
|
+
const mounted = new Set(mounts.value.map((m) => m.serviceId))
|
|
41
|
+
return catalog.value.filter((s) => !mounted.has(s.id))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/** A frame is "shared" when its service is mounted on more than one board. */
|
|
45
|
+
function isSharedFrame(frameBlockId: string): boolean {
|
|
46
|
+
return (serviceByFrameBlock.value[frameBlockId]?.mountCount ?? 0) > 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function mount(serviceId: string, position?: { x: number; y: number }) {
|
|
50
|
+
const ws = useWorkspaceStore()
|
|
51
|
+
const created = await api.mountService(ws.requireId(), serviceId, position ? { position } : {})
|
|
52
|
+
await ws.refresh()
|
|
53
|
+
return created
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function unmount(serviceId: string) {
|
|
57
|
+
const ws = useWorkspaceStore()
|
|
58
|
+
await api.unmountService(ws.requireId(), serviceId)
|
|
59
|
+
await ws.refresh()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Persist a mounted frame's per-board layout (called on frame drag/resize end). */
|
|
63
|
+
async function updateLayout(
|
|
64
|
+
serviceId: string,
|
|
65
|
+
position?: { x: number; y: number },
|
|
66
|
+
size?: { w: number; h: number } | null,
|
|
67
|
+
) {
|
|
68
|
+
const ws = useWorkspaceStore()
|
|
69
|
+
const updated = await api.updateMountLayout(ws.requireId(), serviceId, { position, size })
|
|
70
|
+
const local = mounts.value.find((m) => m.serviceId === serviceId)
|
|
71
|
+
if (local) Object.assign(local, updated)
|
|
72
|
+
return updated
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
mounts,
|
|
77
|
+
catalog,
|
|
78
|
+
byServiceId,
|
|
79
|
+
serviceByFrameBlock,
|
|
80
|
+
mountable,
|
|
81
|
+
isSharedFrame,
|
|
82
|
+
hydrate,
|
|
83
|
+
mount,
|
|
84
|
+
unmount,
|
|
85
|
+
updateLayout,
|
|
86
|
+
}
|
|
87
|
+
})
|
package/app/stores/tracker.ts
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
|
|
4
|
-
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The workspace's issue-tracker selection (GitHub Issues or Jira) — where the
|
|
8
|
-
* tech-debt recurring pipeline files its ticket. Hydrated from the snapshot;
|
|
9
|
-
* edited inline when configuring a tech-debt recurring pipeline.
|
|
10
|
-
*/
|
|
11
|
-
export const useTrackerStore = defineStore('tracker', () => {
|
|
12
|
-
const api = useApi()
|
|
13
|
-
|
|
14
|
-
const settings = ref<TrackerSettings>({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The workspace's issue-tracker selection (GitHub Issues or Jira) — where the
|
|
8
|
+
* tech-debt recurring pipeline files its ticket. Hydrated from the snapshot;
|
|
9
|
+
* edited inline when configuring a tech-debt recurring pipeline.
|
|
10
|
+
*/
|
|
11
|
+
export const useTrackerStore = defineStore('tracker', () => {
|
|
12
|
+
const api = useApi()
|
|
13
|
+
|
|
14
|
+
const settings = ref<TrackerSettings>({
|
|
15
|
+
tracker: null,
|
|
16
|
+
jiraProjectKey: null,
|
|
17
|
+
writebackCommentOnPrOpen: false,
|
|
18
|
+
writebackResolveOnMerge: false,
|
|
19
|
+
updatedAt: 0,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function hydrate(value: TrackerSettings | undefined) {
|
|
23
|
+
settings.value = value ?? {
|
|
24
|
+
tracker: null,
|
|
25
|
+
jiraProjectKey: null,
|
|
26
|
+
writebackCommentOnPrOpen: false,
|
|
27
|
+
writebackResolveOnMerge: false,
|
|
28
|
+
updatedAt: 0,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function save(input: PutTrackerSettingsInput) {
|
|
33
|
+
const ws = useWorkspaceStore()
|
|
34
|
+
settings.value = await api.putTrackerSettings(ws.requireId(), input)
|
|
35
|
+
return settings.value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { settings, hydrate, save }
|
|
39
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -70,6 +70,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
70
70
|
// Workspace-settings panel: the run-timing escalation threshold + per-service task limit.
|
|
71
71
|
const workspaceSettingsOpen = ref(false)
|
|
72
72
|
const datadogOpen = ref(false)
|
|
73
|
+
// Workspace-settings panel: issue-tracker writeback toggles (comment on PR open,
|
|
74
|
+
// close linked issue on merge).
|
|
75
|
+
const issueWritebackOpen = ref(false)
|
|
73
76
|
const modelDefaultsOpen = ref(false)
|
|
74
77
|
// Workspace-settings panel: the default service-fragment selection new services inherit.
|
|
75
78
|
const serviceFragmentDefaultsOpen = ref(false)
|
|
@@ -287,6 +290,12 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
287
290
|
function closeWorkspaceSettings() {
|
|
288
291
|
workspaceSettingsOpen.value = false
|
|
289
292
|
}
|
|
293
|
+
function openIssueWriteback() {
|
|
294
|
+
issueWritebackOpen.value = true
|
|
295
|
+
}
|
|
296
|
+
function closeIssueWriteback() {
|
|
297
|
+
issueWritebackOpen.value = false
|
|
298
|
+
}
|
|
290
299
|
function openDatadog() {
|
|
291
300
|
datadogOpen.value = true
|
|
292
301
|
}
|
|
@@ -360,6 +369,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
360
369
|
fragmentLibraryOpen,
|
|
361
370
|
commandBarOpen,
|
|
362
371
|
mergeThresholdsOpen,
|
|
372
|
+
issueWritebackOpen,
|
|
363
373
|
workspaceSettingsOpen,
|
|
364
374
|
datadogOpen,
|
|
365
375
|
modelDefaultsOpen,
|
|
@@ -411,6 +421,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
411
421
|
toggleCommandBar,
|
|
412
422
|
openMergeThresholds,
|
|
413
423
|
closeMergeThresholds,
|
|
424
|
+
openIssueWriteback,
|
|
425
|
+
closeIssueWriteback,
|
|
414
426
|
openWorkspaceSettings,
|
|
415
427
|
closeWorkspaceSettings,
|
|
416
428
|
openDatadog,
|