@cat-factory/app 0.315.3 → 0.315.6
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/README.md +25 -4
- package/app/components/assistant/AssistantModal.vue +3 -2
- package/app/components/board/AddTaskModal.vue +3 -2
- package/app/components/board/CreateInitiativeModal.vue +3 -2
- package/app/components/board/RecurringPipelineModal.vue +3 -2
- package/app/components/board/ReviewFrictionDialog.vue +4 -3
- package/app/components/bootstrap/AdoptionReviewModal.vue +2 -0
- package/app/components/bootstrap/BootstrapModal.vue +4 -3
- package/app/components/common/AppModal.vue +50 -0
- package/app/components/common/ConfirmDialog.vue +5 -3
- package/app/components/common/KeyboardShortcutsHelp.vue +6 -4
- package/app/components/documents/DocumentImportModal.vue +3 -2
- package/app/components/documents/DocumentSourceConnectModal.vue +7 -2
- package/app/components/documents/DocumentTemplatesModal.vue +3 -2
- package/app/components/documents/SpawnPreviewModal.vue +3 -2
- package/app/components/documents/StartFromDesignModal.vue +3 -2
- package/app/components/environments/EnvironmentSetupWizard.vue +2 -0
- package/app/components/foundational/FoundationalServicePanel.vue +2 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +2 -0
- package/app/components/github/AddServiceFromRepoModal.vue +5 -4
- package/app/components/github/GitHubPanel.vue +4 -3
- package/app/components/layout/BoardSwitcher.vue +7 -2
- package/app/components/layout/CommandBar.vue +3 -2
- package/app/components/layout/IntegrationsHub.vue +3 -6
- package/app/components/layout/ModelProvidersHub.vue +3 -6
- package/app/components/layout/PersonalSetupModal.vue +3 -6
- package/app/components/layout/RolePrompt.vue +3 -2
- package/app/components/notifications/NotificationSettingsPanel.vue +3 -6
- package/app/components/panels/DecisionModal.vue +3 -2
- package/app/components/panels/InspectorPanel.vue +6 -1
- package/app/components/panels/inspector/ServiceTestConfig.vue +6 -4
- package/app/components/pipeline/AgentPromptEditor.vue +2 -0
- package/app/components/pipeline/PipelineBuilder.vue +11 -2
- package/app/components/pipeline/PipelineHealthModal.vue +3 -2
- package/app/components/providers/AiPresetMismatchDialog.vue +3 -6
- package/app/components/providers/AiProviderOnboardingModal.vue +3 -6
- package/app/components/providers/PersonalCredentialModal.vue +3 -2
- package/app/components/providers/VendorCredentialsModal.vue +3 -6
- package/app/components/queue/DescribeWorkModal.vue +3 -2
- package/app/components/sandbox/SandboxPanel.vue +2 -0
- package/app/components/settings/AccountSettingsPanel.vue +1 -0
- package/app/components/settings/ApiTokensPanel.vue +3 -2
- package/app/components/settings/InfrastructureWindow.vue +3 -6
- package/app/components/settings/LocalModelEndpointsPanel.vue +3 -6
- package/app/components/settings/ModelConfigurationPanel.vue +9 -1
- package/app/components/settings/ModelPresetHealthModal.vue +3 -2
- package/app/components/settings/ObservabilityConnectionPanel.vue +3 -6
- package/app/components/settings/OpenRouterCatalogPanel.vue +3 -2
- package/app/components/settings/RiskPolicyHealthModal.vue +3 -2
- package/app/components/settings/UserSecretsSection.vue +3 -6
- package/app/components/settings/WorkspaceSettingsPanel.vue +1 -0
- package/app/components/setup/SetupChecklistPanel.vue +3 -2
- package/app/components/slack/SlackPanel.vue +3 -2
- package/app/components/tasks/BugHuntModal.vue +2 -0
- package/app/components/tasks/TaskImportModal.vue +3 -2
- package/app/components/tasks/TaskSourceConnectModal.vue +3 -2
- package/app/components/theme/ThemeImportModal.vue +4 -2
- package/app/components/tutorial/TutorialCatalogue.vue +4 -3
- package/app/components/tutorial/TutorialPrompt.vue +3 -2
- package/app/stores/github/connection.ts +2 -4
- package/app/stores/github/context.ts +5 -1
- package/app/stores/github/projectionLoad.ts +127 -0
- package/app/stores/github/repoActions.ts +7 -2
- package/app/stores/github.spec.ts +355 -1
- package/app/stores/github.ts +21 -22
- package/app/stores/ui/builderModelConfigHandoff.spec.ts +88 -0
- package/app/stores/ui/modals.ts +78 -12
- package/app/utils/overlaySize.ts +22 -0
- package/i18n/locales/de.json +1 -0
- package/i18n/locales/en.json +1 -0
- package/i18n/locales/es.json +1 -0
- package/i18n/locales/fr.json +1 -0
- package/i18n/locales/he.json +1 -0
- package/i18n/locales/it.json +1 -0
- package/i18n/locales/ja.json +1 -0
- package/i18n/locales/pl.json +1 -0
- package/i18n/locales/tr.json +1 -0
- package/i18n/locales/uk.json +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
2
3
|
// Startup advisory for built-in merge presets that drifted from the catalog. Opened once per
|
|
3
4
|
// session from the board page when `useRiskPolicyHealth` reports any issue. Lists:
|
|
4
5
|
// • new built-in presets the workspace doesn't have yet (ADD them);
|
|
@@ -48,7 +49,7 @@ const reseedableCount = computed(
|
|
|
48
49
|
</script>
|
|
49
50
|
|
|
50
51
|
<template>
|
|
51
|
-
<
|
|
52
|
+
<AppModal size="form" v-model:open="open" :title="t('riskPolicy.health.title')">
|
|
52
53
|
<template #body>
|
|
53
54
|
<div v-if="!hasIssues" class="py-6 text-center text-sm text-muted">
|
|
54
55
|
<UIcon name="i-lucide-check-circle-2" class="mx-auto mb-2 h-8 w-8 text-app-success-400" />
|
|
@@ -156,5 +157,5 @@ const reseedableCount = computed(
|
|
|
156
157
|
</UButton>
|
|
157
158
|
</div>
|
|
158
159
|
</template>
|
|
159
|
-
</
|
|
160
|
+
</AppModal>
|
|
160
161
|
</template>
|
|
@@ -11,6 +11,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
|
11
11
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
12
12
|
import ConnectionWarnings from '~/components/settings/ConnectionWarnings.vue'
|
|
13
13
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
14
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
14
15
|
|
|
15
16
|
const { t } = useI18n()
|
|
16
17
|
const ui = useUiStore()
|
|
@@ -142,11 +143,7 @@ async function remove() {
|
|
|
142
143
|
</script>
|
|
143
144
|
|
|
144
145
|
<template>
|
|
145
|
-
<
|
|
146
|
-
v-model:open="open"
|
|
147
|
-
:title="t('settings.userSecrets.title')"
|
|
148
|
-
:ui="{ content: 'max-w-xl' }"
|
|
149
|
-
>
|
|
146
|
+
<AppModal size="form" v-model:open="open" :title="t('settings.userSecrets.title')">
|
|
150
147
|
<template #title>
|
|
151
148
|
<IntegrationBackTitle :title="t('settings.userSecrets.title')" @back="back" />
|
|
152
149
|
</template>
|
|
@@ -247,5 +244,5 @@ async function remove() {
|
|
|
247
244
|
</div>
|
|
248
245
|
</div>
|
|
249
246
|
</template>
|
|
250
|
-
</
|
|
247
|
+
</AppModal>
|
|
251
248
|
</template>
|
|
@@ -3,6 +3,7 @@ import type { SetupRow, SetupRowCause, SetupRowId, SetupRowStatus } from '~/util
|
|
|
3
3
|
import type { SetupRemedy } from '~/utils/setupAudience'
|
|
4
4
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
5
5
|
import Spinner from '~/components/common/Spinner.vue'
|
|
6
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The setup checklist: every prerequisite for "a task can run and merge", its state, and one
|
|
@@ -210,7 +211,7 @@ const also = computed(() =>
|
|
|
210
211
|
</script>
|
|
211
212
|
|
|
212
213
|
<template>
|
|
213
|
-
<
|
|
214
|
+
<AppModal size="wide" v-model:open="open" :title="t('setup.title')">
|
|
214
215
|
<template #title>
|
|
215
216
|
<span class="flex items-baseline gap-3">
|
|
216
217
|
{{ t('setup.title') }}
|
|
@@ -336,5 +337,5 @@ const also = computed(() =>
|
|
|
336
337
|
</ul>
|
|
337
338
|
</div>
|
|
338
339
|
</template>
|
|
339
|
-
</
|
|
340
|
+
</AppModal>
|
|
340
341
|
</template>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
18
18
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
19
19
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
20
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
20
21
|
|
|
21
22
|
const ui = useUiStore()
|
|
22
23
|
const slack = useSlackStore()
|
|
@@ -202,7 +203,7 @@ async function saveMapping() {
|
|
|
202
203
|
</script>
|
|
203
204
|
|
|
204
205
|
<template>
|
|
205
|
-
<
|
|
206
|
+
<AppModal size="form" v-model:open="open" :title="t('slack.panel.title')">
|
|
206
207
|
<template #title>
|
|
207
208
|
<IntegrationBackTitle :title="t('slack.panel.title')" @back="back" />
|
|
208
209
|
</template>
|
|
@@ -372,5 +373,5 @@ async function saveMapping() {
|
|
|
372
373
|
</template>
|
|
373
374
|
</div>
|
|
374
375
|
</template>
|
|
375
|
-
</
|
|
376
|
+
</AppModal>
|
|
376
377
|
</template>
|
|
@@ -335,6 +335,8 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
|
|
|
335
335
|
</script>
|
|
336
336
|
|
|
337
337
|
<template>
|
|
338
|
+
<!-- workbench-width-ok: a ranked list of tracker bugs with scan controls (768px). Its shell
|
|
339
|
+
waits on #2359. -->
|
|
338
340
|
<UModal v-model:open="open" :title="t('bugHunt.title')" :ui="{ content: 'max-w-3xl' }">
|
|
339
341
|
<template #title>
|
|
340
342
|
<IntegrationBackTitle :title="t('bugHunt.title')" @back="back" />
|
|
@@ -19,6 +19,7 @@ import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
|
19
19
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
20
20
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
21
21
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
22
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
22
23
|
|
|
23
24
|
const { t } = useI18n()
|
|
24
25
|
const ui = useUiStore()
|
|
@@ -120,7 +121,7 @@ async function doSpawnEpic() {
|
|
|
120
121
|
</script>
|
|
121
122
|
|
|
122
123
|
<template>
|
|
123
|
-
<
|
|
124
|
+
<AppModal size="form" v-model:open="open" :title="title">
|
|
124
125
|
<template #title>
|
|
125
126
|
<IntegrationBackTitle :title="title" @back="back" />
|
|
126
127
|
</template>
|
|
@@ -215,5 +216,5 @@ async function doSpawnEpic() {
|
|
|
215
216
|
</div>
|
|
216
217
|
</div>
|
|
217
218
|
</template>
|
|
218
|
-
</
|
|
219
|
+
</AppModal>
|
|
219
220
|
</template>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// available (Jira connected / the GitHub App installed) — there is nothing to offer
|
|
13
13
|
// before that.
|
|
14
14
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
15
16
|
|
|
16
17
|
const { t } = useI18n()
|
|
17
18
|
const ui = useUiStore()
|
|
@@ -116,7 +117,7 @@ async function toggleEnabled(enabled: boolean) {
|
|
|
116
117
|
</script>
|
|
117
118
|
|
|
118
119
|
<template>
|
|
119
|
-
<
|
|
120
|
+
<AppModal size="form" v-model:open="open" :title="descriptor?.label ?? t('tasks.connect.title')">
|
|
120
121
|
<template #title>
|
|
121
122
|
<IntegrationBackTitle :title="descriptor?.label ?? t('tasks.connect.title')" @back="back" />
|
|
122
123
|
</template>
|
|
@@ -212,5 +213,5 @@ async function toggleEnabled(enabled: boolean) {
|
|
|
212
213
|
</div>
|
|
213
214
|
</div>
|
|
214
215
|
</template>
|
|
215
|
-
</
|
|
216
|
+
</AppModal>
|
|
216
217
|
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { useThemeStore } from '~/stores/theme'
|
|
4
4
|
import { decodeThemeLink, type ThemeLinkDecodeResult } from '~/utils/theme/link'
|
|
5
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
5
6
|
|
|
6
7
|
// Import a theme built in the Nuxt UI theme editor (https://ui.nuxt.com/theme): the user pastes
|
|
7
8
|
// the editor's share link (or the raw document JSON), names it, and the theme is stored in this
|
|
@@ -60,7 +61,8 @@ async function submit() {
|
|
|
60
61
|
</script>
|
|
61
62
|
|
|
62
63
|
<template>
|
|
63
|
-
<
|
|
64
|
+
<AppModal
|
|
65
|
+
size="prompt"
|
|
64
66
|
v-model:open="open"
|
|
65
67
|
:title="t('appearance.import.title')"
|
|
66
68
|
:description="t('appearance.import.description')"
|
|
@@ -111,5 +113,5 @@ async function submit() {
|
|
|
111
113
|
</div>
|
|
112
114
|
</UForm>
|
|
113
115
|
</template>
|
|
114
|
-
</
|
|
116
|
+
</AppModal>
|
|
115
117
|
</template>
|
|
@@ -14,6 +14,7 @@ import { buildCatalogueRows, summarizeProgress } from './TutorialCatalogue.logic
|
|
|
14
14
|
import type { TutorialCatalogueRow } from './TutorialCatalogue.logic'
|
|
15
15
|
import { TUTORIAL_ACTION_KEYS, TUTORIAL_STATUS_KEYS } from '~/utils/tutorial'
|
|
16
16
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
17
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
17
18
|
|
|
18
19
|
const { t } = useI18n()
|
|
19
20
|
const tutorial = useTutorialStore()
|
|
@@ -51,11 +52,11 @@ const statusColor = (row: TutorialCatalogueRow) =>
|
|
|
51
52
|
</script>
|
|
52
53
|
|
|
53
54
|
<template>
|
|
54
|
-
<
|
|
55
|
+
<AppModal
|
|
56
|
+
size="form"
|
|
55
57
|
v-model:open="open"
|
|
56
58
|
:title="t('tutorial.catalogue.title')"
|
|
57
59
|
:description="t('tutorial.catalogue.intro')"
|
|
58
|
-
:ui="{ content: 'max-w-2xl' }"
|
|
59
60
|
>
|
|
60
61
|
<template #body>
|
|
61
62
|
<div class="space-y-4" data-testid="tutorial-catalogue">
|
|
@@ -162,5 +163,5 @@ const statusColor = (row: TutorialCatalogueRow) =>
|
|
|
162
163
|
</UButton>
|
|
163
164
|
</div>
|
|
164
165
|
</template>
|
|
165
|
-
</
|
|
166
|
+
</AppModal>
|
|
166
167
|
</template>
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// prompt never auto-opens again), while closing without answering defers to next launch.
|
|
16
16
|
import { TUTORIAL_ACTION_KEYS } from '~/utils/tutorial'
|
|
17
17
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
18
|
+
import AppModal from '~/components/common/AppModal.vue'
|
|
18
19
|
|
|
19
20
|
const { t } = useI18n()
|
|
20
21
|
const tutorial = useTutorialStore()
|
|
@@ -34,7 +35,7 @@ const undecided = computed(() => tutorial.decision === null)
|
|
|
34
35
|
</script>
|
|
35
36
|
|
|
36
37
|
<template>
|
|
37
|
-
<
|
|
38
|
+
<AppModal size="prompt" v-model:open="open" :title="t('tutorial.prompt.title')">
|
|
38
39
|
<template #body>
|
|
39
40
|
<!-- NOTE for spec authors: this marks the modal's BODY, so it is the "prompt is
|
|
40
41
|
showing" signal, not a container for everything in the prompt. The footer's
|
|
@@ -125,5 +126,5 @@ const undecided = computed(() => tutorial.decision === null)
|
|
|
125
126
|
</div>
|
|
126
127
|
</div>
|
|
127
128
|
</template>
|
|
128
|
-
</
|
|
129
|
+
</AppModal>
|
|
129
130
|
</template>
|
|
@@ -14,7 +14,7 @@ import type { GitHubStoreContext } from './context'
|
|
|
14
14
|
*/
|
|
15
15
|
export function createGitHubConnectionActions(ctx: GitHubStoreContext) {
|
|
16
16
|
const { api, workspace, available, connection, installations, loadingInstallations } = ctx
|
|
17
|
-
const {
|
|
17
|
+
const { availableRepos, branches, syncing, load, clearProjection } = ctx
|
|
18
18
|
|
|
19
19
|
/** The URL a workspace owner visits to install the App against this workspace. */
|
|
20
20
|
function getInstallUrl(): Promise<string> {
|
|
@@ -48,10 +48,8 @@ export function createGitHubConnectionActions(ctx: GitHubStoreContext) {
|
|
|
48
48
|
// Backends predating the discriminator omit `provider`; those connections are GitHub App ones.
|
|
49
49
|
await DISCONNECT_BY_PROVIDER[connection.value?.provider ?? 'github'](workspace.requireId())
|
|
50
50
|
connection.value = null
|
|
51
|
-
|
|
51
|
+
clearProjection()
|
|
52
52
|
availableRepos.value = []
|
|
53
|
-
pulls.value = []
|
|
54
|
-
issues.value = []
|
|
55
53
|
branches.value = {}
|
|
56
54
|
}
|
|
57
55
|
|
|
@@ -45,6 +45,10 @@ export interface GitHubStoreContext {
|
|
|
45
45
|
syncing: Ref<boolean>
|
|
46
46
|
/** Whether an App installation is bound (the store's `connected` computed). */
|
|
47
47
|
connected: Readonly<Ref<boolean>>
|
|
48
|
-
/** Reload the cached repos/PRs/issues projection (the store's `load`). */
|
|
48
|
+
/** Reload the cached repos/PRs/issues projection after a write (the store's `load`). */
|
|
49
49
|
load: () => Promise<void>
|
|
50
|
+
/** Record a direct write to the projection, so a read that started before it cannot land. */
|
|
51
|
+
markWritten: () => void
|
|
52
|
+
/** Empty the repos/PRs/issues projection and mark it unloaded, so `ensureLoaded` reads it again. */
|
|
53
|
+
clearProjection: () => void
|
|
50
54
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import type { GitHubConnection, GitHubIssue, GitHubPullRequest, GitHubRepo } from '~/types/domain'
|
|
3
|
+
import type { GitHubStoreContext } from '~/stores/github/context'
|
|
4
|
+
|
|
5
|
+
/** What the projection load reads and writes. The store owns every ref. */
|
|
6
|
+
interface ProjectionLoadDeps {
|
|
7
|
+
api: GitHubStoreContext['api']
|
|
8
|
+
workspace: GitHubStoreContext['workspace']
|
|
9
|
+
connection: Readonly<Ref<GitHubConnection | null>>
|
|
10
|
+
repos: Ref<GitHubRepo[]>
|
|
11
|
+
pulls: Ref<GitHubPullRequest[]>
|
|
12
|
+
issues: Ref<GitHubIssue[]>
|
|
13
|
+
loading: Ref<boolean>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ProjectionLoad {
|
|
17
|
+
/** Read the projection again after a write, never sharing a read that started before it. */
|
|
18
|
+
load: () => Promise<void>
|
|
19
|
+
/** Read the projection again, sharing a read already in flight for the same connection. */
|
|
20
|
+
refresh: () => Promise<void>
|
|
21
|
+
/** Read the projection unless it is already loaded for the current connection. */
|
|
22
|
+
ensureLoaded: () => Promise<void>
|
|
23
|
+
/** Record a direct write to the projection, so a read that started before it cannot land. */
|
|
24
|
+
markWritten: () => void
|
|
25
|
+
/** Empty the lists and forget the load in one step. */
|
|
26
|
+
clearProjection: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface InFlightRead {
|
|
30
|
+
key: string
|
|
31
|
+
promise: Promise<void>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The repos / pull requests / issues projection read, and the record of what it is loaded for.
|
|
36
|
+
*
|
|
37
|
+
* One generation counter answers every question about whether a read may land. A read takes the
|
|
38
|
+
* next number when it starts, and every direct write to the projection (`markWritten()`, which
|
|
39
|
+
* `clearProjection()` calls too) takes one as well. A read lands only if its number is still the
|
|
40
|
+
* latest and it was started for the connection that is current when it finishes, so a result that
|
|
41
|
+
* a newer read, a write, a disconnect or a board switch overtook is dropped.
|
|
42
|
+
*
|
|
43
|
+
* The projection counts as loaded for a key made of the workspace and the connection. A connected
|
|
44
|
+
* workspace with no linked repos is loaded: the empty list is its answer. A different workspace or
|
|
45
|
+
* a new connection is a different key, so a reinstall is read again without anyone telling this
|
|
46
|
+
* module that it happened.
|
|
47
|
+
*
|
|
48
|
+
* There are three doors. `ensureLoaded()` reads only when nothing is loaded, `refresh()` is a
|
|
49
|
+
* deliberate re-read that still shares a read in flight, and `load()` is the re-read after a write,
|
|
50
|
+
* which starts its own read so it never returns what the write replaced.
|
|
51
|
+
*/
|
|
52
|
+
export function createGitHubProjectionLoad(deps: ProjectionLoadDeps): ProjectionLoad {
|
|
53
|
+
const { api, workspace, connection, repos, pulls, issues, loading } = deps
|
|
54
|
+
|
|
55
|
+
let generation = 0
|
|
56
|
+
let loadedKey: string | null = null
|
|
57
|
+
let inFlight: InFlightRead | null = null
|
|
58
|
+
|
|
59
|
+
/** What the projection is read for. Null while there is nothing to read: no board or no connection. */
|
|
60
|
+
function currentKey(): string | null {
|
|
61
|
+
const conn = connection.value
|
|
62
|
+
if (!workspace.workspaceId || !conn) return null
|
|
63
|
+
return `${workspace.workspaceId}:${conn.provider}:${conn.installationId}:${conn.connectedAt}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function read(mine: number, key: string, askedFor: string): Promise<void> {
|
|
67
|
+
const [r, p, i] = await Promise.all([
|
|
68
|
+
api.listGitHubRepos(askedFor),
|
|
69
|
+
api.listGitHubPullRequests(askedFor),
|
|
70
|
+
api.listGitHubIssues(askedFor),
|
|
71
|
+
])
|
|
72
|
+
if (mine !== generation || key !== currentKey()) return
|
|
73
|
+
repos.value = r
|
|
74
|
+
pulls.value = p
|
|
75
|
+
issues.value = i
|
|
76
|
+
loadedKey = key
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function start(key: string): Promise<void> {
|
|
80
|
+
const mine = ++generation
|
|
81
|
+
loading.value = true
|
|
82
|
+
const entry: InFlightRead = {
|
|
83
|
+
key,
|
|
84
|
+
promise: read(mine, key, workspace.requireId()).finally(() => {
|
|
85
|
+
// Only the read that still owns the slot settles the flag. One that a write, a clear or a
|
|
86
|
+
// newer read replaced must not report the projection as settled.
|
|
87
|
+
if (inFlight === entry) {
|
|
88
|
+
inFlight = null
|
|
89
|
+
loading.value = false
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
inFlight = entry
|
|
94
|
+
return entry.promise
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function markWritten(): void {
|
|
98
|
+
generation++
|
|
99
|
+
inFlight = null
|
|
100
|
+
loading.value = false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
load: () => {
|
|
105
|
+
const key = currentKey()
|
|
106
|
+
return key ? start(key) : Promise.resolve()
|
|
107
|
+
},
|
|
108
|
+
refresh: () => {
|
|
109
|
+
const key = currentKey()
|
|
110
|
+
if (!key) return Promise.resolve()
|
|
111
|
+
return inFlight?.key === key ? inFlight.promise : start(key)
|
|
112
|
+
},
|
|
113
|
+
ensureLoaded: () => {
|
|
114
|
+
const key = currentKey()
|
|
115
|
+
if (!key || loadedKey === key) return Promise.resolve()
|
|
116
|
+
return inFlight?.key === key ? inFlight.promise : start(key)
|
|
117
|
+
},
|
|
118
|
+
markWritten,
|
|
119
|
+
clearProjection: () => {
|
|
120
|
+
markWritten()
|
|
121
|
+
loadedKey = null
|
|
122
|
+
repos.value = []
|
|
123
|
+
pulls.value = []
|
|
124
|
+
issues.value = []
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -16,7 +16,7 @@ import { pullKey, type GitHubStoreContext } from './context'
|
|
|
16
16
|
*/
|
|
17
17
|
export function createGitHubRepoActions(ctx: GitHubStoreContext) {
|
|
18
18
|
const { api, workspace, connected, repos, availableRepos, loadingAvailable, savingRepos } = ctx
|
|
19
|
-
const { branches, repoFiles, upsertPull, getPull, load } = ctx
|
|
19
|
+
const { branches, repoFiles, upsertPull, getPull, load, markWritten } = ctx
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Load the repos the installation can access, with this workspace's link state.
|
|
@@ -55,6 +55,7 @@ export function createGitHubRepoActions(ctx: GitHubStoreContext) {
|
|
|
55
55
|
savingRepos.value = true
|
|
56
56
|
try {
|
|
57
57
|
repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
|
|
58
|
+
markWritten()
|
|
58
59
|
// Reflect the new link state in the picker and refresh PRs/issues.
|
|
59
60
|
const linked = new Set(repoGithubIds)
|
|
60
61
|
availableRepos.value = availableRepos.value.map((r) => ({
|
|
@@ -117,6 +118,7 @@ export function createGitHubRepoActions(ctx: GitHubStoreContext) {
|
|
|
117
118
|
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
118
119
|
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
119
120
|
upsertPull(pr)
|
|
121
|
+
markWritten()
|
|
120
122
|
return pr
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -128,7 +130,10 @@ export function createGitHubRepoActions(ctx: GitHubStoreContext) {
|
|
|
128
130
|
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
129
131
|
// Optimistically reflect the merge until the next sync confirms it.
|
|
130
132
|
const existing = getPull(pullKey(repoGithubId, number))
|
|
131
|
-
if (existing)
|
|
133
|
+
if (existing) {
|
|
134
|
+
upsertPull({ ...existing, state: 'closed', merged: true })
|
|
135
|
+
markWritten()
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
function comment(repoGithubId: number, number: number, body: string) {
|