mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Remove repo source control AI draft sync effect
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types'
|
||||
import { createRepoAiDraftState, resolveRepoAiDraftState } from './RepositorySourceControlAiSection'
|
||||
|
||||
describe('RepositorySourceControlAiSection draft state', () => {
|
||||
it('refreshes clean drafts when persisted repo overrides change', () => {
|
||||
const state = createRepoAiDraftState('repo-1', {
|
||||
instructionsByOperation: {
|
||||
commitMessage: 'old'
|
||||
}
|
||||
})
|
||||
const persisted: RepoSourceControlAiOverrides = {
|
||||
instructionsByOperation: {
|
||||
commitMessage: 'new'
|
||||
}
|
||||
}
|
||||
|
||||
expect(resolveRepoAiDraftState(state, 'repo-1', persisted)).toEqual({
|
||||
repoId: 'repo-1',
|
||||
value: persisted,
|
||||
baseSerialized: JSON.stringify(persisted)
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves dirty drafts across external repo override changes', () => {
|
||||
const state = createRepoAiDraftState('repo-1', {
|
||||
instructionsByOperation: {
|
||||
commitMessage: 'old'
|
||||
}
|
||||
})
|
||||
state.value = {
|
||||
instructionsByOperation: {
|
||||
commitMessage: 'local edit'
|
||||
}
|
||||
}
|
||||
|
||||
const persisted: RepoSourceControlAiOverrides = {
|
||||
instructionsByOperation: {
|
||||
commitMessage: 'external edit'
|
||||
}
|
||||
}
|
||||
|
||||
expect(resolveRepoAiDraftState(state, 'repo-1', persisted)).toBe(state)
|
||||
})
|
||||
|
||||
it('resets the draft when the selected repo changes', () => {
|
||||
const state = createRepoAiDraftState('repo-1', {
|
||||
prCreationDefaults: {
|
||||
draft: true
|
||||
}
|
||||
})
|
||||
state.value = {
|
||||
prCreationDefaults: {
|
||||
draft: false
|
||||
}
|
||||
}
|
||||
|
||||
const persisted: RepoSourceControlAiOverrides = {
|
||||
prCreationDefaults: {
|
||||
useTemplate: true
|
||||
}
|
||||
}
|
||||
|
||||
expect(resolveRepoAiDraftState(state, 'repo-2', persisted)).toEqual({
|
||||
repoId: 'repo-2',
|
||||
value: persisted,
|
||||
baseSerialized: JSON.stringify(persisted)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable max-lines -- Why: repo Source Control AI settings keep one
|
||||
draft/save flow across model, instruction, and PR-default override groups. */
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { Repo } from '../../../../shared/types'
|
||||
import type {
|
||||
RepoSourceControlAiOverrides,
|
||||
@@ -98,6 +98,39 @@ function serializeRepoAiDraft(value: RepoSourceControlAiOverrides): string {
|
||||
return JSON.stringify(normalizeRepoAiDraft(value))
|
||||
}
|
||||
|
||||
export function createRepoAiDraftState(
|
||||
repoId: string,
|
||||
value: RepoSourceControlAiOverrides
|
||||
): RepoAiDraftState {
|
||||
const normalized = normalizeRepoAiDraft(value)
|
||||
return {
|
||||
repoId,
|
||||
value: normalized,
|
||||
baseSerialized: serializeRepoAiDraft(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRepoAiDraftState(
|
||||
current: RepoAiDraftState,
|
||||
repoId: string,
|
||||
persistedRepoAi: RepoSourceControlAiOverrides,
|
||||
persistedSerialized = serializeRepoAiDraft(persistedRepoAi)
|
||||
): RepoAiDraftState {
|
||||
const currentSerialized = serializeRepoAiDraft(current.value)
|
||||
if (
|
||||
current.repoId !== repoId ||
|
||||
currentSerialized === current.baseSerialized ||
|
||||
currentSerialized === persistedSerialized
|
||||
) {
|
||||
return {
|
||||
repoId,
|
||||
value: persistedRepoAi,
|
||||
baseSerialized: persistedSerialized
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
export function RepositorySourceControlAiSection({
|
||||
repo,
|
||||
updateRepo
|
||||
@@ -137,44 +170,47 @@ export function RepositorySourceControlAiSection({
|
||||
)
|
||||
// Why: repo.sourceControlAi is saved as one nested value; a local draft keeps
|
||||
// textarea keystrokes and sibling controls from racing over IPC/RPC.
|
||||
const [draftState, setDraftState] = useState<RepoAiDraftState>(() => ({
|
||||
repoId: repo.id,
|
||||
value: persistedRepoAi,
|
||||
baseSerialized: persistedSerialized
|
||||
}))
|
||||
const [draftState, setDraftState] = useState<RepoAiDraftState>(() =>
|
||||
createRepoAiDraftState(repo.id, persistedRepoAi)
|
||||
)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setDraftState((current) => {
|
||||
const currentSerialized = serializeRepoAiDraft(current.value)
|
||||
if (
|
||||
current.repoId !== repo.id ||
|
||||
currentSerialized === current.baseSerialized ||
|
||||
currentSerialized === persistedSerialized
|
||||
) {
|
||||
return {
|
||||
repoId: repo.id,
|
||||
value: persistedRepoAi,
|
||||
baseSerialized: persistedSerialized
|
||||
}
|
||||
}
|
||||
return current
|
||||
})
|
||||
setSaveError(null)
|
||||
}, [persistedRepoAi, persistedSerialized, repo.id])
|
||||
const resolvedDraftState = resolveRepoAiDraftState(
|
||||
draftState,
|
||||
repo.id,
|
||||
persistedRepoAi,
|
||||
persistedSerialized
|
||||
)
|
||||
if (resolvedDraftState !== draftState) {
|
||||
// Why: repo settings may be refreshed externally; clean drafts should
|
||||
// follow that source before paint, while dirty edits stay in place.
|
||||
setDraftState(resolvedDraftState)
|
||||
if (saveError !== null) {
|
||||
setSaveError(null)
|
||||
}
|
||||
}
|
||||
|
||||
const repoAi = draftState.value
|
||||
const repoAi = resolvedDraftState.value
|
||||
const draftSerialized = useMemo(() => serializeRepoAiDraft(repoAi), [repoAi])
|
||||
const isDirty = draftState.repoId !== repo.id || draftSerialized !== draftState.baseSerialized
|
||||
const isDirty =
|
||||
resolvedDraftState.repoId !== repo.id || draftSerialized !== resolvedDraftState.baseSerialized
|
||||
|
||||
const updateDraftRepoAi = (
|
||||
update: (current: RepoSourceControlAiOverrides) => RepoSourceControlAiOverrides
|
||||
): void => {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
value: normalizeRepoAiDraft(update(current.value))
|
||||
}))
|
||||
setDraftState((current) => {
|
||||
const resolved = resolveRepoAiDraftState(
|
||||
current,
|
||||
repo.id,
|
||||
persistedRepoAi,
|
||||
persistedSerialized
|
||||
)
|
||||
return {
|
||||
...resolved,
|
||||
value: normalizeRepoAiDraft(update(resolved.value))
|
||||
}
|
||||
})
|
||||
setSaveError(null)
|
||||
}
|
||||
|
||||
@@ -182,7 +218,7 @@ export function RepositorySourceControlAiSection({
|
||||
if (!isDirty || isSaving) {
|
||||
return
|
||||
}
|
||||
const next = normalizeRepoAiDraft(draftState.value)
|
||||
const next = normalizeRepoAiDraft(resolvedDraftState.value)
|
||||
const nextSerialized = serializeRepoAiDraft(next)
|
||||
setIsSaving(true)
|
||||
setSaveError(null)
|
||||
@@ -211,11 +247,7 @@ export function RepositorySourceControlAiSection({
|
||||
}
|
||||
|
||||
const discardDraft = (): void => {
|
||||
setDraftState({
|
||||
repoId: repo.id,
|
||||
value: persistedRepoAi,
|
||||
baseSerialized: persistedSerialized
|
||||
})
|
||||
setDraftState(createRepoAiDraftState(repo.id, persistedRepoAi))
|
||||
setSaveError(null)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user