Scope GitHub Project rows to selected repos (#6174)
* Scope GitHub Project rows to selected repos Adds the selected-repo Project scoping implementation and documents the UI behavior in docs/github-project-selected-repo-popup.md. * rm design doc * fix: address review findings
This commit is contained in:
parent
3e955c0377
commit
aec7bb6aa0
|
|
@ -7762,91 +7762,84 @@ export default function TaskPage(): React.JSX.Element {
|
|||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{/* Why: the repo combobox filters Items mode by repo. In
|
||||
Project mode the row set comes from the project's
|
||||
view filter (server-side), so this control would be
|
||||
inert — hide it to avoid suggesting it does
|
||||
something. */}
|
||||
{githubMode !== 'project' && (
|
||||
<>
|
||||
<div className="min-w-0 max-w-[220px] shrink-0">
|
||||
<TaskProjectSourceCombobox
|
||||
groups={taskPickerGroups}
|
||||
selected={repoSelection}
|
||||
getRepoHostLabel={getTaskPickerRepoHostLabel}
|
||||
onChange={(next) => {
|
||||
const normalized = normalizeTaskRepoSelection(eligibleRepos, next)
|
||||
setRepoSelection(normalized)
|
||||
void updateSettings({ defaultRepoSelection: [...normalized] }).catch(
|
||||
() => {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.TaskPage.dfd72673e7',
|
||||
'Failed to save project selection.'
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}}
|
||||
onSelectAll={() => {
|
||||
const allIds = new Set(taskPickerRepos.map((r) => r.id))
|
||||
setRepoSelection(allIds)
|
||||
void updateSettings({ defaultRepoSelection: null }).catch(() => {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.TaskPage.dfd72673e7',
|
||||
'Failed to save project selection.'
|
||||
)
|
||||
{/* Why: Project rows are now repo-scoped too, so the
|
||||
selection must stay visible in both GitHub modes. */}
|
||||
<div className="min-w-0 max-w-[220px] shrink-0">
|
||||
<TaskProjectSourceCombobox
|
||||
groups={taskPickerGroups}
|
||||
selected={repoSelection}
|
||||
getRepoHostLabel={getTaskPickerRepoHostLabel}
|
||||
onChange={(next) => {
|
||||
const normalized = normalizeTaskRepoSelection(eligibleRepos, next)
|
||||
setRepoSelection(normalized)
|
||||
void updateSettings({ defaultRepoSelection: [...normalized] }).catch(
|
||||
() => {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.TaskPage.dfd72673e7',
|
||||
'Failed to save project selection.'
|
||||
)
|
||||
})
|
||||
}}
|
||||
triggerClassName="h-8 w-auto max-w-[220px] rounded-md border border-border/50 bg-muted/50 px-2 text-xs font-medium shadow-sm transition hover:bg-muted/50 focus:ring-2 focus:ring-ring/20 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
if (!selectedGitHubRepoExternalLink?.url) {
|
||||
return
|
||||
}
|
||||
void window.api.shell.openUrl(selectedGitHubRepoExternalLink.url)
|
||||
}}
|
||||
aria-label={
|
||||
selectedGitHubRepoExternalLink
|
||||
? translate(
|
||||
'auto.components.TaskPage.8d1e17a3ef',
|
||||
'Open {{value0}} in GitHub',
|
||||
{ value0: selectedGitHubRepoExternalLink.label }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.TaskPage.d1132848f8',
|
||||
'Select one GitHub project to open in GitHub'
|
||||
)
|
||||
}
|
||||
className="h-8 w-8 rounded-md border-border/50 bg-muted/50 text-foreground shadow-sm transition hover:bg-muted/50"
|
||||
>
|
||||
<ExternalLink className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{selectedGitHubRepoExternalLink
|
||||
)
|
||||
}
|
||||
)
|
||||
}}
|
||||
onSelectAll={() => {
|
||||
const allIds = new Set(taskPickerRepos.map((r) => r.id))
|
||||
setRepoSelection(allIds)
|
||||
void updateSettings({ defaultRepoSelection: null }).catch(() => {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.TaskPage.dfd72673e7',
|
||||
'Failed to save project selection.'
|
||||
)
|
||||
)
|
||||
})
|
||||
}}
|
||||
triggerClassName="h-8 w-auto max-w-[220px] rounded-md border border-border/50 bg-muted/50 px-2 text-xs font-medium shadow-sm transition hover:bg-muted/50 focus:ring-2 focus:ring-ring/20 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
if (!selectedGitHubRepoExternalLink?.url) {
|
||||
return
|
||||
}
|
||||
void window.api.shell.openUrl(selectedGitHubRepoExternalLink.url)
|
||||
}}
|
||||
aria-label={
|
||||
selectedGitHubRepoExternalLink
|
||||
? translate(
|
||||
'auto.components.TaskPage.8d1e17a3ef',
|
||||
'Open {{value0}} in GitHub',
|
||||
{ value0: selectedGitHubRepoExternalLink.label }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.TaskPage.bc46d8204e',
|
||||
'Select one project to open in GitHub'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
'auto.components.TaskPage.d1132848f8',
|
||||
'Select one GitHub project to open in GitHub'
|
||||
)
|
||||
}
|
||||
className="h-8 w-8 rounded-md border-border/50 bg-muted/50 text-foreground shadow-sm transition hover:bg-muted/50"
|
||||
>
|
||||
<ExternalLink className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{selectedGitHubRepoExternalLink
|
||||
? translate(
|
||||
'auto.components.TaskPage.8d1e17a3ef',
|
||||
'Open {{value0}} in GitHub',
|
||||
{ value0: selectedGitHubRepoExternalLink.label }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.TaskPage.bc46d8204e',
|
||||
'Select one project to open in GitHub'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
@ -8635,7 +8628,7 @@ export default function TaskPage(): React.JSX.Element {
|
|||
)
|
||||
) : taskSource === 'github' && githubMode === 'project' ? (
|
||||
<div className="mt-3 flex min-h-0 min-w-0 max-h-full flex-col overflow-hidden rounded-md border border-border/50 bg-muted/50 shadow-sm">
|
||||
<ProjectViewWrapper />
|
||||
<ProjectViewWrapper selectedRepoIds={repoSelection} />
|
||||
</div>
|
||||
) : taskSource === 'github' ? (
|
||||
<div className="flex min-h-0 min-w-0 max-h-full flex-col overflow-hidden rounded-md rounded-t-none border border-t-0 border-border/50 bg-muted/50 shadow-sm">
|
||||
|
|
|
|||
|
|
@ -47,19 +47,25 @@ import type { GitHubWorkItem } from '../../../../shared/types'
|
|||
import ProjectPicker, { type ResolvedProjectSelection } from './ProjectPicker'
|
||||
import ProjectViewList from './ProjectViewList'
|
||||
import ProjectItemSlugDialog from './ProjectItemSlugDialog'
|
||||
import { filterProjectTableRowsByOpenRepos } from './project-row-filtering'
|
||||
import {
|
||||
filterProjectTableRowsBySelectedRepos,
|
||||
resolveSelectedProjectRowRepo
|
||||
} from './project-row-filtering'
|
||||
import {
|
||||
resolveMissingRepoProjectDialogState,
|
||||
resolveRepoBackedProjectDialogState
|
||||
} from './project-dialog-state'
|
||||
import {
|
||||
getSelectedRepoFingerprint,
|
||||
getNextVisibleProjectTableCache,
|
||||
getVisibleProjectTable,
|
||||
type CachedVisibleProjectTable
|
||||
} from './project-visible-table-cache'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type Props = Record<string, never>
|
||||
type Props = {
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
}
|
||||
|
||||
const ORCA_FEATURE_REQUEST_URL = 'https://github.com/stablyai/orca/issues/new'
|
||||
|
||||
|
|
@ -80,7 +86,7 @@ function getProjectViewSourceScope(settings: Parameters<typeof getActiveRuntimeT
|
|||
return target.kind === 'environment' ? `runtime:${target.environmentId}` : 'local'
|
||||
}
|
||||
|
||||
export default function ProjectViewWrapper(_props: Props = {} as Props): React.JSX.Element {
|
||||
export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const projectViewCache = useAppStore((s) => s.projectViewCache)
|
||||
const fetchProjectViewTable = useAppStore((s) => s.fetchProjectViewTable)
|
||||
|
|
@ -329,9 +335,16 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
const table: GitHubProjectTable | null = currentCacheKey
|
||||
? (projectViewCache[currentCacheKey]?.data ?? null)
|
||||
: null
|
||||
const selectedRepoFingerprint = useMemo(
|
||||
() => getSelectedRepoFingerprint(selectedRepoIds),
|
||||
[selectedRepoIds]
|
||||
)
|
||||
const filteredTable = useMemo(
|
||||
() => (table && slugIndexReady ? filterProjectTableRowsByOpenRepos(table, lookupSlug) : null),
|
||||
[table, slugIndexReady, lookupSlug]
|
||||
() =>
|
||||
table && slugIndexReady
|
||||
? filterProjectTableRowsBySelectedRepos(table, lookupSlug, slugIndexReady, selectedRepoIds)
|
||||
: null,
|
||||
[table, slugIndexReady, lookupSlug, selectedRepoIds]
|
||||
)
|
||||
const lastFilteredTableRef = useRef<CachedVisibleProjectTable | null>(null)
|
||||
// Why: this cache only prevents a blank table while the repo slug index
|
||||
|
|
@ -339,6 +352,7 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
// a second render after every filtered-table change.
|
||||
lastFilteredTableRef.current = getNextVisibleProjectTableCache({
|
||||
currentCacheKey,
|
||||
selectedRepoFingerprint,
|
||||
sourceTable: table,
|
||||
slugIndexReady,
|
||||
filteredTable,
|
||||
|
|
@ -346,6 +360,7 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
})
|
||||
const visibleTable = getVisibleProjectTable({
|
||||
currentCacheKey,
|
||||
selectedRepoFingerprint,
|
||||
slugIndexReady,
|
||||
filteredTable,
|
||||
cachedTable: lastFilteredTableRef.current
|
||||
|
|
@ -401,7 +416,11 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
} | null>(null)
|
||||
const liveRepoIds = useMemo(() => new Set(repos.map((repo) => repo.id)), [repos])
|
||||
|
||||
const resolvedDialogRepoItem = resolveRepoBackedProjectDialogState(dialogRepoItem, liveRepoIds)
|
||||
const resolvedDialogRepoItem = resolveRepoBackedProjectDialogState(
|
||||
dialogRepoItem,
|
||||
liveRepoIds,
|
||||
selectedRepoIds
|
||||
)
|
||||
if (resolvedDialogRepoItem !== dialogRepoItem) {
|
||||
// Why: repo-backed Project dialogs cannot edit after their repo leaves
|
||||
// Orca; clear them before the modal tree receives stale repo ids.
|
||||
|
|
@ -412,7 +431,8 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
slugIndexReady,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug
|
||||
lookupSlug,
|
||||
selectedRepoIds
|
||||
})
|
||||
if (resolvedMissingRepoDialogs.slugDialog !== slugDialog) {
|
||||
// Why: once a previously missing repo is registered, Project rows should
|
||||
|
|
@ -483,6 +503,13 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
[]
|
||||
)
|
||||
|
||||
const openProjectRowUrlWithToast = useCallback((row: GitHubProjectRow, message: string) => {
|
||||
if (row.content.url) {
|
||||
void window.api.shell.openUrl(row.content.url)
|
||||
}
|
||||
toast.message(message)
|
||||
}, [])
|
||||
|
||||
const handleOpenDialog = useCallback(
|
||||
(row: GitHubProjectRow) => {
|
||||
if (!currentCacheKey || !table) {
|
||||
|
|
@ -496,19 +523,69 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
}
|
||||
return
|
||||
}
|
||||
const matches = lookupSlug(`${origin.owner}/${origin.repo}`)
|
||||
const matched = matches.length === 1 ? matches[0] : null
|
||||
if (matched) {
|
||||
const workItem = buildWorkItem(row, matched.id)
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row,
|
||||
lookupSlug,
|
||||
slugIndexReady,
|
||||
selectedRepoIds
|
||||
})
|
||||
if (resolution.status === 'loading') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.f352abf7c3',
|
||||
'Repository list is updating.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (resolution.status === 'selected_match') {
|
||||
const workItem = buildWorkItem(row, resolution.repo.id)
|
||||
if (workItem) {
|
||||
setDialogRepoItem({ workItem, repoPath: matched.path, repoId: matched.id, origin })
|
||||
setDialogRepoItem({
|
||||
workItem,
|
||||
repoPath: resolution.repo.path,
|
||||
repoId: resolution.repo.id,
|
||||
origin
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// Unknown repo — use the simplified slug-mode dialog.
|
||||
setSlugDialog({ origin })
|
||||
if (resolution.status === 'no_global_match') {
|
||||
// Unknown repo — use the simplified slug-mode dialog.
|
||||
setSlugDialog({ origin })
|
||||
return
|
||||
}
|
||||
if (resolution.status === 'unselected_match') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.1ce21b8cff',
|
||||
'This item is outside the selected repositories.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (resolution.status === 'ambiguous_selected_match') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.030de75bc5',
|
||||
'This item matches multiple selected repositories.'
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
[currentCacheKey, table, buildOrigin, lookupSlug, buildWorkItem]
|
||||
[
|
||||
currentCacheKey,
|
||||
table,
|
||||
buildOrigin,
|
||||
lookupSlug,
|
||||
slugIndexReady,
|
||||
selectedRepoIds,
|
||||
openProjectRowUrlWithToast,
|
||||
buildWorkItem
|
||||
]
|
||||
)
|
||||
|
||||
const handleStartWork = useCallback(
|
||||
|
|
@ -520,9 +597,23 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
if (!origin) {
|
||||
return
|
||||
}
|
||||
const matches = lookupSlug(`${origin.owner}/${origin.repo}`)
|
||||
const matched = matches.length === 1 ? matches[0] : null
|
||||
if (!matched) {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row,
|
||||
lookupSlug,
|
||||
slugIndexReady,
|
||||
selectedRepoIds
|
||||
})
|
||||
if (resolution.status === 'loading') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.f352abf7c3',
|
||||
'Repository list is updating.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (resolution.status === 'no_global_match') {
|
||||
setRepoNotInOrca({
|
||||
owner: origin.owner,
|
||||
repo: origin.repo,
|
||||
|
|
@ -530,13 +621,36 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
})
|
||||
return
|
||||
}
|
||||
const workItem = buildWorkItem(row, matched.id)
|
||||
if (resolution.status === 'unselected_match') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.1ce21b8cff',
|
||||
'This item is outside the selected repositories.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (resolution.status === 'ambiguous_selected_match') {
|
||||
openProjectRowUrlWithToast(
|
||||
row,
|
||||
translate(
|
||||
'auto.components.github.project.ProjectViewWrapper.030de75bc5',
|
||||
'This item matches multiple selected repositories.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (resolution.status !== 'selected_match') {
|
||||
return
|
||||
}
|
||||
const workItem = buildWorkItem(row, resolution.repo.id)
|
||||
if (!workItem) {
|
||||
return
|
||||
}
|
||||
void launchWorkItemDirect({
|
||||
item: workItem,
|
||||
repoId: matched.id,
|
||||
repoId: resolution.repo.id,
|
||||
launchSource: 'task_page',
|
||||
telemetrySource: 'sidebar',
|
||||
openModalFallback: () => {
|
||||
|
|
@ -550,7 +664,16 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J
|
|||
}
|
||||
})
|
||||
},
|
||||
[currentCacheKey, table, buildOrigin, lookupSlug, buildWorkItem]
|
||||
[
|
||||
currentCacheKey,
|
||||
table,
|
||||
buildOrigin,
|
||||
lookupSlug,
|
||||
slugIndexReady,
|
||||
selectedRepoIds,
|
||||
openProjectRowUrlWithToast,
|
||||
buildWorkItem
|
||||
]
|
||||
)
|
||||
|
||||
const handleEditAssignees = useCallback(
|
||||
|
|
|
|||
|
|
@ -8,18 +8,34 @@ describe('resolveRepoBackedProjectDialogState', () => {
|
|||
it('keeps a repo-backed dialog when the repo still exists', () => {
|
||||
const dialog = { repoId: 'repo-1', label: 'Issue 1' }
|
||||
|
||||
expect(resolveRepoBackedProjectDialogState(dialog, new Set(['repo-1']))).toBe(dialog)
|
||||
expect(
|
||||
resolveRepoBackedProjectDialogState(dialog, new Set(['repo-1']), new Set(['repo-1']))
|
||||
).toBe(dialog)
|
||||
})
|
||||
|
||||
it('clears a repo-backed dialog when its repo is removed', () => {
|
||||
expect(
|
||||
resolveRepoBackedProjectDialogState({ repoId: 'repo-1' }, new Set(['repo-2']))
|
||||
resolveRepoBackedProjectDialogState(
|
||||
{ repoId: 'repo-1' },
|
||||
new Set(['repo-2']),
|
||||
new Set(['repo-1'])
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('clears a repo-backed dialog when its repo is no longer selected', () => {
|
||||
expect(
|
||||
resolveRepoBackedProjectDialogState(
|
||||
{ repoId: 'repo-1' },
|
||||
new Set(['repo-1']),
|
||||
new Set(['repo-2'])
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveMissingRepoProjectDialogState', () => {
|
||||
it('waits for the slug index before closing missing-repo dialogs', () => {
|
||||
it('clears fallback dialogs while the slug index is rebuilding', () => {
|
||||
const slugDialog = { origin: { owner: 'stablyai', repo: 'orca' } }
|
||||
const repoNotInOrca = { owner: 'stablyai', repo: 'orca', url: null }
|
||||
|
||||
|
|
@ -28,9 +44,10 @@ describe('resolveMissingRepoProjectDialogState', () => {
|
|||
slugIndexReady: false,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug: () => ['repo-1']
|
||||
lookupSlug: () => [{ id: 'repo-1' }],
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
).toEqual({ slugDialog, repoNotInOrca })
|
||||
).toEqual({ slugDialog: null, repoNotInOrca: null })
|
||||
})
|
||||
|
||||
it('clears slug fallback dialogs once the repo slug resolves', () => {
|
||||
|
|
@ -40,7 +57,8 @@ describe('resolveMissingRepoProjectDialogState', () => {
|
|||
slugIndexReady: true,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug: (slug) => (slug === 'stablyai/orca' ? ['repo-1'] : [])
|
||||
lookupSlug: (slug) => (slug === 'stablyai/orca' ? [{ id: 'repo-1' }] : []),
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(result.slugDialog).toBeNull()
|
||||
|
|
@ -54,10 +72,39 @@ describe('resolveMissingRepoProjectDialogState', () => {
|
|||
slugIndexReady: true,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug: (slug) => (slug === 'stablyai/orca' ? ['repo-1'] : [])
|
||||
lookupSlug: (slug) => (slug === 'stablyai/orca' ? [{ id: 'repo-1' }] : []),
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(result.slugDialog).toBe(slugDialog)
|
||||
expect(result.repoNotInOrca).toBeNull()
|
||||
})
|
||||
|
||||
it('clears fallback dialogs when the repo is globally known but not selected', () => {
|
||||
const slugDialog = { origin: { owner: 'stablyai', repo: 'orca' } }
|
||||
const repoNotInOrca = { owner: 'stablyai', repo: 'orca', url: null }
|
||||
const result = resolveMissingRepoProjectDialogState({
|
||||
slugIndexReady: true,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug: () => [{ id: 'repo-2' }],
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(result).toEqual({ slugDialog: null, repoNotInOrca: null })
|
||||
})
|
||||
|
||||
it('keeps missing-repo fallback dialogs when there are no global matches', () => {
|
||||
const slugDialog = { origin: { owner: 'stablyai', repo: 'orca' } }
|
||||
const repoNotInOrca = { owner: 'stablyai', repo: 'orca', url: null }
|
||||
const result = resolveMissingRepoProjectDialogState({
|
||||
slugIndexReady: true,
|
||||
slugDialog,
|
||||
repoNotInOrca,
|
||||
lookupSlug: () => [],
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(result).toEqual({ slugDialog, repoNotInOrca })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,17 +14,30 @@ type RepoNotInOrcaDialogState = {
|
|||
repo: string
|
||||
}
|
||||
|
||||
type LookupSlug = (slug: string) => readonly unknown[]
|
||||
type RepoMatch = {
|
||||
id: string
|
||||
}
|
||||
|
||||
function hasRepoMatch(lookupSlug: LookupSlug, owner: string, repo: string): boolean {
|
||||
return lookupSlug(`${owner}/${repo}`).length > 0
|
||||
type LookupSlug = (slug: string) => readonly RepoMatch[]
|
||||
|
||||
function shouldCloseFallbackDialog(args: {
|
||||
lookupSlug: LookupSlug
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
owner: string
|
||||
repo: string
|
||||
}): boolean {
|
||||
const matches = args.lookupSlug(`${args.owner}/${args.repo}`)
|
||||
const selectedMatchCount = matches.filter((match) => args.selectedRepoIds.has(match.id)).length
|
||||
const unselectedMatchCount = matches.length - selectedMatchCount
|
||||
return selectedMatchCount > 0 || unselectedMatchCount > 0
|
||||
}
|
||||
|
||||
export function resolveRepoBackedProjectDialogState<T extends RepoBackedProjectDialogState>(
|
||||
dialog: T | null,
|
||||
liveRepoIds: ReadonlySet<string>
|
||||
liveRepoIds: ReadonlySet<string>,
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
): T | null {
|
||||
if (dialog && !liveRepoIds.has(dialog.repoId)) {
|
||||
if (dialog && (!liveRepoIds.has(dialog.repoId) || !selectedRepoIds.has(dialog.repoId))) {
|
||||
return null
|
||||
}
|
||||
return dialog
|
||||
|
|
@ -38,21 +51,34 @@ export function resolveMissingRepoProjectDialogState<
|
|||
slugDialog: TSlugDialog | null
|
||||
repoNotInOrca: TRepoNotInOrca | null
|
||||
lookupSlug: LookupSlug
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
}): {
|
||||
slugDialog: TSlugDialog | null
|
||||
repoNotInOrca: TRepoNotInOrca | null
|
||||
} {
|
||||
const { lookupSlug, repoNotInOrca, slugDialog, slugIndexReady } = args
|
||||
const { lookupSlug, repoNotInOrca, selectedRepoIds, slugDialog, slugIndexReady } = args
|
||||
if (!slugIndexReady) {
|
||||
return { slugDialog, repoNotInOrca }
|
||||
return { slugDialog: null, repoNotInOrca: null }
|
||||
}
|
||||
return {
|
||||
slugDialog:
|
||||
slugDialog && hasRepoMatch(lookupSlug, slugDialog.origin.owner, slugDialog.origin.repo)
|
||||
slugDialog &&
|
||||
shouldCloseFallbackDialog({
|
||||
lookupSlug,
|
||||
selectedRepoIds,
|
||||
owner: slugDialog.origin.owner,
|
||||
repo: slugDialog.origin.repo
|
||||
})
|
||||
? null
|
||||
: slugDialog,
|
||||
repoNotInOrca:
|
||||
repoNotInOrca && hasRepoMatch(lookupSlug, repoNotInOrca.owner, repoNotInOrca.repo)
|
||||
repoNotInOrca &&
|
||||
shouldCloseFallbackDialog({
|
||||
lookupSlug,
|
||||
selectedRepoIds,
|
||||
owner: repoNotInOrca.owner,
|
||||
repo: repoNotInOrca.repo
|
||||
})
|
||||
? null
|
||||
: repoNotInOrca
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github-project-types'
|
||||
import type { Repo } from '../../../../shared/types'
|
||||
import { filterProjectTableRowsByOpenRepos } from './project-row-filtering'
|
||||
import {
|
||||
filterProjectTableRowsByOpenRepos,
|
||||
filterProjectTableRowsBySelectedRepos,
|
||||
resolveSelectedProjectRowRepo
|
||||
} from './project-row-filtering'
|
||||
|
||||
function repo(id: string): Repo {
|
||||
return {
|
||||
|
|
@ -92,3 +96,113 @@ describe('filterProjectTableRowsByOpenRepos', () => {
|
|||
expect(filtered.totalCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterProjectTableRowsBySelectedRepos', () => {
|
||||
it('keeps a row when at least one slug match is selected', () => {
|
||||
const rows = [row('visible', 'acme/orca'), row('hidden', 'acme/tool')]
|
||||
const filtered = filterProjectTableRowsBySelectedRepos(
|
||||
table(rows),
|
||||
(slug) => (slug?.toLowerCase() === 'acme/orca' ? [repo('repo-1')] : [repo('repo-2')]),
|
||||
true,
|
||||
new Set(['repo-1'])
|
||||
)
|
||||
|
||||
expect(filtered.rows.map((r) => r.id)).toEqual(['visible'])
|
||||
expect(filtered.totalCount).toBe(1)
|
||||
})
|
||||
|
||||
it('filters a row when only unselected repos match', () => {
|
||||
const rows = [row('hidden', 'acme/orca')]
|
||||
const filtered = filterProjectTableRowsBySelectedRepos(
|
||||
table(rows),
|
||||
() => [repo('repo-2')],
|
||||
true,
|
||||
new Set(['repo-1'])
|
||||
)
|
||||
|
||||
expect(filtered.rows).toEqual([])
|
||||
expect(filtered.totalCount).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps a row with multiple selected matches for action ambiguity handling', () => {
|
||||
const rows = [row('ambiguous', 'acme/orca')]
|
||||
const filtered = filterProjectTableRowsBySelectedRepos(
|
||||
table(rows),
|
||||
() => [repo('repo-1'), repo('repo-2'), repo('repo-3')],
|
||||
true,
|
||||
new Set(['repo-1', 'repo-2'])
|
||||
)
|
||||
|
||||
expect(filtered.rows.map((r) => r.id)).toEqual(['ambiguous'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveSelectedProjectRowRepo', () => {
|
||||
it('reports loading without reading stale slug matches', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('loading', 'acme/orca'),
|
||||
lookupSlug: () => {
|
||||
throw new Error('should not read stale matches')
|
||||
},
|
||||
slugIndexReady: false,
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(resolution.status).toBe('loading')
|
||||
})
|
||||
|
||||
it('reports invalid slug for rows without a repository', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('missing-slug', null),
|
||||
lookupSlug: () => [repo('repo-1')],
|
||||
slugIndexReady: true,
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(resolution.status).toBe('invalid_slug')
|
||||
})
|
||||
|
||||
it('reports no global match when Orca has no repo for the slug', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('missing', 'acme/orca'),
|
||||
lookupSlug: () => [],
|
||||
slugIndexReady: true,
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(resolution.status).toBe('no_global_match')
|
||||
})
|
||||
|
||||
it('reports global-only matches when the repo is not selected', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('unselected', 'acme/orca'),
|
||||
lookupSlug: () => [repo('repo-2')],
|
||||
slugIndexReady: true,
|
||||
selectedRepoIds: new Set(['repo-1'])
|
||||
})
|
||||
|
||||
expect(resolution.status).toBe('unselected_match')
|
||||
})
|
||||
|
||||
it('returns the selected match when exactly one matching repo is selected', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('selected', 'acme/orca'),
|
||||
lookupSlug: () => [repo('repo-1'), repo('repo-2')],
|
||||
slugIndexReady: true,
|
||||
selectedRepoIds: new Set(['repo-2'])
|
||||
})
|
||||
|
||||
expect(resolution).toMatchObject({ status: 'selected_match', repo: { id: 'repo-2' } })
|
||||
})
|
||||
|
||||
it('reports ambiguity when multiple matching repos are selected', () => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row: row('ambiguous', 'acme/orca'),
|
||||
lookupSlug: () => [repo('repo-1'), repo('repo-2')],
|
||||
slugIndexReady: true,
|
||||
selectedRepoIds: new Set(['repo-1', 'repo-2'])
|
||||
})
|
||||
|
||||
expect(resolution.status).toBe('ambiguous_selected_match')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,52 @@ import type { Repo } from '../../../../shared/types'
|
|||
|
||||
export type ProjectRowSlugLookup = (slug: string | null | undefined) => readonly Repo[]
|
||||
|
||||
export type SelectedProjectRowResolution =
|
||||
| { status: 'loading' }
|
||||
| { status: 'invalid_slug' }
|
||||
| { status: 'no_global_match' }
|
||||
| { status: 'unselected_match'; globalMatches: readonly Repo[] }
|
||||
| { status: 'selected_match'; repo: Repo; globalMatches: readonly Repo[] }
|
||||
| {
|
||||
status: 'ambiguous_selected_match'
|
||||
selectedMatches: readonly Repo[]
|
||||
globalMatches: readonly Repo[]
|
||||
}
|
||||
|
||||
export function resolveSelectedProjectRowRepo(input: {
|
||||
row: GitHubProjectRow
|
||||
lookupSlug: ProjectRowSlugLookup
|
||||
slugIndexReady: boolean
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
}): SelectedProjectRowResolution {
|
||||
if (!input.slugIndexReady) {
|
||||
return { status: 'loading' }
|
||||
}
|
||||
|
||||
const repository = input.row.content.repository
|
||||
if (!repository) {
|
||||
return { status: 'invalid_slug' }
|
||||
}
|
||||
const [owner, repo] = repository.split('/')
|
||||
if (!owner || !repo) {
|
||||
return { status: 'invalid_slug' }
|
||||
}
|
||||
|
||||
const globalMatches = input.lookupSlug(repository)
|
||||
if (globalMatches.length === 0) {
|
||||
return { status: 'no_global_match' }
|
||||
}
|
||||
|
||||
const selectedMatches = globalMatches.filter((match) => input.selectedRepoIds.has(match.id))
|
||||
if (selectedMatches.length === 0) {
|
||||
return { status: 'unselected_match', globalMatches }
|
||||
}
|
||||
if (selectedMatches.length === 1) {
|
||||
return { status: 'selected_match', repo: selectedMatches[0], globalMatches }
|
||||
}
|
||||
return { status: 'ambiguous_selected_match', selectedMatches, globalMatches }
|
||||
}
|
||||
|
||||
export function projectRowHasOpenRepo(
|
||||
row: GitHubProjectRow,
|
||||
lookupSlug: ProjectRowSlugLookup
|
||||
|
|
@ -20,3 +66,26 @@ export function filterProjectTableRowsByOpenRepos(
|
|||
}
|
||||
return { ...table, rows, totalCount: rows.length }
|
||||
}
|
||||
|
||||
export function filterProjectTableRowsBySelectedRepos(
|
||||
table: GitHubProjectTable,
|
||||
lookupSlug: ProjectRowSlugLookup,
|
||||
slugIndexReady: boolean,
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
): GitHubProjectTable {
|
||||
const rows = table.rows.filter((row) => {
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row,
|
||||
lookupSlug,
|
||||
slugIndexReady,
|
||||
selectedRepoIds
|
||||
})
|
||||
return (
|
||||
resolution.status === 'selected_match' || resolution.status === 'ambiguous_selected_match'
|
||||
)
|
||||
})
|
||||
if (rows.length === table.rows.length && table.totalCount === rows.length) {
|
||||
return table
|
||||
}
|
||||
return { ...table, rows, totalCount: rows.length }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { GitHubProjectTable } from '../../../../shared/github-project-types'
|
||||
import {
|
||||
getSelectedRepoFingerprint,
|
||||
getNextVisibleProjectTableCache,
|
||||
getVisibleProjectTable
|
||||
getVisibleProjectTable,
|
||||
getVisibleProjectTableCacheKey
|
||||
} from './project-visible-table-cache'
|
||||
|
||||
function table(id: string): GitHubProjectTable {
|
||||
|
|
@ -17,20 +19,22 @@ describe('project visible table cache', () => {
|
|||
expect(
|
||||
getNextVisibleProjectTableCache({
|
||||
currentCacheKey: 'project:view',
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-1'])),
|
||||
sourceTable,
|
||||
slugIndexReady: true,
|
||||
filteredTable,
|
||||
previous: null
|
||||
})
|
||||
).toEqual({ cacheKey: 'project:view', table: filteredTable })
|
||||
).toEqual({ cacheKey: 'project:view:selected:["repo-1"]', table: filteredTable })
|
||||
})
|
||||
|
||||
it('keeps the previous cache while the slug index is rebuilding', () => {
|
||||
const previous = { cacheKey: 'project:view', table: table('previous') }
|
||||
const previous = { cacheKey: 'project:view:selected:["repo-1"]', table: table('previous') }
|
||||
|
||||
expect(
|
||||
getNextVisibleProjectTableCache({
|
||||
currentCacheKey: 'project:view',
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-1'])),
|
||||
sourceTable: table('source'),
|
||||
slugIndexReady: false,
|
||||
filteredTable: null,
|
||||
|
|
@ -45,6 +49,7 @@ describe('project visible table cache', () => {
|
|||
expect(
|
||||
getNextVisibleProjectTableCache({
|
||||
currentCacheKey: null,
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-1'])),
|
||||
sourceTable: null,
|
||||
slugIndexReady: false,
|
||||
filteredTable: null,
|
||||
|
|
@ -54,11 +59,12 @@ describe('project visible table cache', () => {
|
|||
})
|
||||
|
||||
it('shows a matching cached table while the slug index is rebuilding', () => {
|
||||
const cachedTable = { cacheKey: 'project:view', table: table('cached') }
|
||||
const cachedTable = { cacheKey: 'project:view:selected:["repo-1"]', table: table('cached') }
|
||||
|
||||
expect(
|
||||
getVisibleProjectTable({
|
||||
currentCacheKey: 'project:view',
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-1'])),
|
||||
slugIndexReady: false,
|
||||
filteredTable: null,
|
||||
cachedTable
|
||||
|
|
@ -67,15 +73,36 @@ describe('project visible table cache', () => {
|
|||
})
|
||||
|
||||
it('does not show stale cached data for a different cache key', () => {
|
||||
const cachedTable = { cacheKey: 'other:view', table: table('cached') }
|
||||
const cachedTable = { cacheKey: 'other:view:selected:["repo-1"]', table: table('cached') }
|
||||
|
||||
expect(
|
||||
getVisibleProjectTable({
|
||||
currentCacheKey: 'project:view',
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-1'])),
|
||||
slugIndexReady: false,
|
||||
filteredTable: null,
|
||||
cachedTable
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('does not show stale cached data for a different repo selection', () => {
|
||||
const cachedTable = { cacheKey: 'project:view:selected:["repo-1"]', table: table('cached') }
|
||||
|
||||
expect(
|
||||
getVisibleProjectTable({
|
||||
currentCacheKey: 'project:view',
|
||||
selectedRepoFingerprint: getSelectedRepoFingerprint(new Set(['repo-2'])),
|
||||
slugIndexReady: false,
|
||||
filteredTable: null,
|
||||
cachedTable
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('layers selection only onto the renderer visible-table cache key', () => {
|
||||
const storeCacheKey = 'organization:stablyai:1:view-id:local'
|
||||
|
||||
expect(getVisibleProjectTableCacheKey(storeCacheKey, '["repo-1"]')).not.toBe(storeCacheKey)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,24 +5,41 @@ export type CachedVisibleProjectTable = {
|
|||
table: GitHubProjectTable
|
||||
}
|
||||
|
||||
export function getSelectedRepoFingerprint(selectedRepoIds: ReadonlySet<string>): string {
|
||||
return JSON.stringify([...selectedRepoIds].sort())
|
||||
}
|
||||
|
||||
export function getVisibleProjectTableCacheKey(
|
||||
currentCacheKey: string | null,
|
||||
selectedRepoFingerprint: string
|
||||
): string | null {
|
||||
return currentCacheKey ? `${currentCacheKey}:selected:${selectedRepoFingerprint}` : null
|
||||
}
|
||||
|
||||
export function getNextVisibleProjectTableCache(input: {
|
||||
currentCacheKey: string | null
|
||||
selectedRepoFingerprint: string
|
||||
sourceTable: GitHubProjectTable | null
|
||||
slugIndexReady: boolean
|
||||
filteredTable: GitHubProjectTable | null
|
||||
previous: CachedVisibleProjectTable | null
|
||||
}): CachedVisibleProjectTable | null {
|
||||
if (!input.currentCacheKey || !input.sourceTable) {
|
||||
const visibleCacheKey = getVisibleProjectTableCacheKey(
|
||||
input.currentCacheKey,
|
||||
input.selectedRepoFingerprint
|
||||
)
|
||||
if (!visibleCacheKey || !input.sourceTable) {
|
||||
return null
|
||||
}
|
||||
if (input.slugIndexReady && input.filteredTable) {
|
||||
return { cacheKey: input.currentCacheKey, table: input.filteredTable }
|
||||
return { cacheKey: visibleCacheKey, table: input.filteredTable }
|
||||
}
|
||||
return input.previous
|
||||
}
|
||||
|
||||
export function getVisibleProjectTable(input: {
|
||||
currentCacheKey: string | null
|
||||
selectedRepoFingerprint: string
|
||||
slugIndexReady: boolean
|
||||
filteredTable: GitHubProjectTable | null
|
||||
cachedTable: CachedVisibleProjectTable | null
|
||||
|
|
@ -30,5 +47,9 @@ export function getVisibleProjectTable(input: {
|
|||
if (input.slugIndexReady || !input.currentCacheKey) {
|
||||
return input.filteredTable
|
||||
}
|
||||
return input.cachedTable?.cacheKey === input.currentCacheKey ? input.cachedTable.table : null
|
||||
const visibleCacheKey = getVisibleProjectTableCacheKey(
|
||||
input.currentCacheKey,
|
||||
input.selectedRepoFingerprint
|
||||
)
|
||||
return input.cachedTable?.cacheKey === visibleCacheKey ? input.cachedTable.table : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ describe('TaskPage source switching host boundary', () => {
|
|||
)
|
||||
const modalSection = sourceBetween(
|
||||
TASK_PAGE_SOURCE,
|
||||
'<ProjectViewWrapper />',
|
||||
'<ProjectViewWrapper selectedRepoIds={repoSelection} />',
|
||||
'<GitLabItemDialog'
|
||||
)
|
||||
|
||||
expect(modalSection).toContain('selectedRepoIds={repoSelection}')
|
||||
expect(detailSection).toContain('workItem={dialogWorkItem}')
|
||||
expect(detailSection).toContain('<GitHubItemDialog')
|
||||
expect(detailSection).toContain('sourceContext={dialogSourceContext}')
|
||||
|
|
|
|||
|
|
@ -1846,7 +1846,10 @@
|
|||
"22df63c393": "Sub-issue data is unavailable for your token.",
|
||||
"067119985c": "GitHub search, e.g. assignee:@me is:open",
|
||||
"1850fceac8": "{{value0}}/{{value1}} isn't added to Orca. Add it to start work, or open in GitHub.",
|
||||
"1aa7c952b9": "Project view"
|
||||
"1aa7c952b9": "Project view",
|
||||
"f352abf7c3": "Repository list is updating.",
|
||||
"1ce21b8cff": "This item is outside the selected repositories.",
|
||||
"030de75bc5": "This item matches multiple selected repositories."
|
||||
},
|
||||
"slug": {
|
||||
"dialog": {
|
||||
|
|
|
|||
|
|
@ -1846,7 +1846,10 @@
|
|||
"22df63c393": "Los datos de la subemisión no están disponibles para su token.",
|
||||
"067119985c": "Búsqueda de GitHub, p. cesionario:@yo es:abierto",
|
||||
"1850fceac8": "{{value0}}/{{value1}} no se agrega a Orca. Agréguelo para comenzar a trabajar o ábralo en GitHub.",
|
||||
"1aa7c952b9": "Project view"
|
||||
"1aa7c952b9": "Project view",
|
||||
"f352abf7c3": "Repository list is updating.",
|
||||
"1ce21b8cff": "This item is outside the selected repositories.",
|
||||
"030de75bc5": "This item matches multiple selected repositories."
|
||||
},
|
||||
"slug": {
|
||||
"dialog": {
|
||||
|
|
@ -3819,7 +3822,8 @@
|
|||
"1b0a156717": "Agents"
|
||||
},
|
||||
"WorktreeCardReviewDetailSection": {
|
||||
"reviewHeader": "{{value0}} #{{value1}}"
|
||||
"reviewHeader": "{{value0}} #{{value1}}",
|
||||
"copyLink": "Copy link"
|
||||
},
|
||||
"WorktreeCardMeta": {
|
||||
"3e65e11cc6": "Metadatos del espacio de trabajo",
|
||||
|
|
@ -3846,7 +3850,13 @@
|
|||
"checkingAutomationAvailability": "Comprobando disponibilidad de la automatización...",
|
||||
"automationMissing": "La automatización ya no está disponible.",
|
||||
"automationRunMissing": "El historial de ejecuciones ya no está disponible.",
|
||||
"automationAvailabilityUnavailable": "No se pudo comprobar la disponibilidad de la automatización."
|
||||
"automationAvailabilityUnavailable": "No se pudo comprobar la disponibilidad de la automatización.",
|
||||
"moreIssueActions": "More issue actions",
|
||||
"copyLink": "Copy link",
|
||||
"copyLinkSuccess": "{{value0}} copied",
|
||||
"copyLinkFailure": "Failed to copy link",
|
||||
"issueLinkLabel": "Issue link",
|
||||
"reviewLinkLabel": "{{value0}} link"
|
||||
},
|
||||
"WorktreeCardMetadataStatusBadges": {
|
||||
"fe188062a1": "Estado: Abierto",
|
||||
|
|
|
|||
|
|
@ -1846,7 +1846,10 @@
|
|||
"22df63c393": "トークンではサブ Issue データを利用できません。",
|
||||
"067119985c": "GitHub 検索、例:担当者:@me は:開いています",
|
||||
"1850fceac8": "{{value0}}/{{value1}} は Orca に追加されません。これを追加して作業を開始するか、GitHub で開きます。",
|
||||
"1aa7c952b9": "Project view"
|
||||
"1aa7c952b9": "Project view",
|
||||
"f352abf7c3": "Repository list is updating.",
|
||||
"1ce21b8cff": "This item is outside the selected repositories.",
|
||||
"030de75bc5": "This item matches multiple selected repositories."
|
||||
},
|
||||
"slug": {
|
||||
"dialog": {
|
||||
|
|
@ -3800,7 +3803,8 @@
|
|||
"1b0a156717": "Agents"
|
||||
},
|
||||
"WorktreeCardReviewDetailSection": {
|
||||
"reviewHeader": "{{value0}} #{{value1}}"
|
||||
"reviewHeader": "{{value0}} #{{value1}}",
|
||||
"copyLink": "Copy link"
|
||||
},
|
||||
"WorktreeCardMeta": {
|
||||
"3e65e11cc6": "ワークスペースのメタデータ",
|
||||
|
|
@ -3827,7 +3831,13 @@
|
|||
"checkingAutomationAvailability": "自動化の利用可否を確認中...",
|
||||
"automationMissing": "自動化は利用できなくなりました。",
|
||||
"automationRunMissing": "実行履歴は利用できなくなりました。",
|
||||
"automationAvailabilityUnavailable": "自動化の利用可否を確認できませんでした。"
|
||||
"automationAvailabilityUnavailable": "自動化の利用可否を確認できませんでした。",
|
||||
"moreIssueActions": "More issue actions",
|
||||
"copyLink": "Copy link",
|
||||
"copyLinkSuccess": "{{value0}} copied",
|
||||
"copyLinkFailure": "Failed to copy link",
|
||||
"issueLinkLabel": "Issue link",
|
||||
"reviewLinkLabel": "{{value0}} link"
|
||||
},
|
||||
"WorktreeCardMetadataStatusBadges": {
|
||||
"fe188062a1": "状態: オープン",
|
||||
|
|
|
|||
|
|
@ -1846,7 +1846,10 @@
|
|||
"22df63c393": "토큰에 대한 하위 이슈 데이터를 사용할 수 없습니다.",
|
||||
"067119985c": "GitHub 검색, 예: assignee:@me is:open",
|
||||
"1850fceac8": "{{value0}}/{{value1}}이(가) Orca에 추가되어 있지 않습니다. 작업을 시작하려면 추가하거나 GitHub에서 여세요.",
|
||||
"1aa7c952b9": "프로젝트 보기"
|
||||
"1aa7c952b9": "프로젝트 보기",
|
||||
"f352abf7c3": "Repository list is updating.",
|
||||
"1ce21b8cff": "This item is outside the selected repositories.",
|
||||
"030de75bc5": "This item matches multiple selected repositories."
|
||||
},
|
||||
"slug": {
|
||||
"dialog": {
|
||||
|
|
@ -3800,7 +3803,8 @@
|
|||
"1b0a156717": "Agents"
|
||||
},
|
||||
"WorktreeCardReviewDetailSection": {
|
||||
"reviewHeader": "{{value0}} #{{value1}}"
|
||||
"reviewHeader": "{{value0}} #{{value1}}",
|
||||
"copyLink": "Copy link"
|
||||
},
|
||||
"WorktreeCardMeta": {
|
||||
"3e65e11cc6": "워크스페이스 메타데이터",
|
||||
|
|
@ -3827,7 +3831,13 @@
|
|||
"checkingAutomationAvailability": "자동화 사용 가능 여부 확인 중...",
|
||||
"automationMissing": "자동화를 더 이상 사용할 수 없습니다.",
|
||||
"automationRunMissing": "실행 기록을 더 이상 사용할 수 없습니다.",
|
||||
"automationAvailabilityUnavailable": "자동화 사용 가능 여부를 확인할 수 없습니다."
|
||||
"automationAvailabilityUnavailable": "자동화 사용 가능 여부를 확인할 수 없습니다.",
|
||||
"moreIssueActions": "More issue actions",
|
||||
"copyLink": "Copy link",
|
||||
"copyLinkSuccess": "{{value0}} copied",
|
||||
"copyLinkFailure": "Failed to copy link",
|
||||
"issueLinkLabel": "Issue link",
|
||||
"reviewLinkLabel": "{{value0}} link"
|
||||
},
|
||||
"WorktreeCardMetadataStatusBadges": {
|
||||
"fe188062a1": "상태: 열림",
|
||||
|
|
|
|||
|
|
@ -1846,7 +1846,10 @@
|
|||
"22df63c393": "您的 Token 无法获取子议题数据。",
|
||||
"067119985c": "GitHub 搜索,例如 assignee:@me is:open",
|
||||
"1850fceac8": "{{value0}}/{{value1}} 未添加到 Orca。添加它以开始工作,或在 GitHub 中打开。",
|
||||
"1aa7c952b9": "项目视图"
|
||||
"1aa7c952b9": "项目视图",
|
||||
"f352abf7c3": "Repository list is updating.",
|
||||
"1ce21b8cff": "This item is outside the selected repositories.",
|
||||
"030de75bc5": "This item matches multiple selected repositories."
|
||||
},
|
||||
"slug": {
|
||||
"dialog": {
|
||||
|
|
@ -3800,7 +3803,8 @@
|
|||
"1b0a156717": "Agents"
|
||||
},
|
||||
"WorktreeCardReviewDetailSection": {
|
||||
"reviewHeader": "{{value0}} #{{value1}}"
|
||||
"reviewHeader": "{{value0}} #{{value1}}",
|
||||
"copyLink": "Copy link"
|
||||
},
|
||||
"WorktreeCardMeta": {
|
||||
"3e65e11cc6": "工作区元数据",
|
||||
|
|
@ -3827,7 +3831,13 @@
|
|||
"checkingAutomationAvailability": "正在检查自动化可用性...",
|
||||
"automationMissing": "自动化已不可用。",
|
||||
"automationRunMissing": "运行历史已不可用。",
|
||||
"automationAvailabilityUnavailable": "无法检查自动化可用性。"
|
||||
"automationAvailabilityUnavailable": "无法检查自动化可用性。",
|
||||
"moreIssueActions": "More issue actions",
|
||||
"copyLink": "Copy link",
|
||||
"copyLinkSuccess": "{{value0}} copied",
|
||||
"copyLinkFailure": "Failed to copy link",
|
||||
"issueLinkLabel": "Issue link",
|
||||
"reviewLinkLabel": "{{value0}} link"
|
||||
},
|
||||
"WorktreeCardMetadataStatusBadges": {
|
||||
"fe188062a1": "状态:开放",
|
||||
|
|
|
|||
Loading…
Reference in New Issue