Files
orca/src/shared/source-control-section-order.ts
T
Kelvin Amoaba 6c2423d25d fix(mobile): list Source Control changes with desktop's sections, order and row rules (#34017)
* fix(mobile): list Source Control changes with desktop's sections, sort and row rules

Conflicts get their own section in the user's group order, submodule rows lose stage and
discard like desktop, paths sort with the shared collator, and review notes go out as desktop formats them.

* fix(mobile): read the group order the host now publishes and keep submodule rows read-only

The host's paired settings projection now carries sourceControlGroupOrder, the
normalized status projection keeps submodule metadata, and conflict rows reuse the
shared conflict-kind labels.

* refactor(mobile): share Source Control grouping and row rules with desktop

Desktop and mobile group rows through one shared helper, mobile's unstage button
follows desktop's rule, and the group order resets when the host changes.
2026-10-27 04:14:56 +00:00

154 lines
5.3 KiB
TypeScript

import { compareGitStatusEntries } from './git-status-sort'
import { normalizeSourceControlGroupOrder } from './source-control-group-order'
import type { GitStatusEntry } from './git-status-types'
import type { SourceControlGroupOrder } from './ui-chrome-types'
export const SOURCE_CONTROL_AREAS = ['unstaged', 'staged', 'untracked'] as const
export type SourceControlSectionArea = (typeof SOURCE_CONTROL_AREAS)[number]
export type SourceControlDisplaySectionArea = Exclude<SourceControlSectionArea, 'untracked'>
export type SourceControlDisplaySectionId = SourceControlDisplaySectionArea | 'conflicts'
export type SourceControlEntryGroups<T extends GitStatusEntry = GitStatusEntry> = Record<
SourceControlSectionArea,
T[]
>
export type SourceControlDisplaySection<T extends GitStatusEntry = GitStatusEntry> = {
id: SourceControlDisplaySectionId
area: SourceControlDisplaySectionArea
items: T[]
}
export type SourceControlConflictReviewEntry = {
path: string
conflictKind: NonNullable<GitStatusEntry['conflictKind']>
}
export type SourceControlSectionViewAction =
| { kind: 'conflict-review'; entries: SourceControlConflictReviewEntry[] }
| { kind: 'combined-diff'; area?: SourceControlSectionArea; entries: GitStatusEntry[] }
const ORDER_BY_PRESET: Record<
ReturnType<typeof normalizeSourceControlGroupOrder>,
readonly SourceControlDisplaySectionArea[]
> = {
'changes-first': ['unstaged', 'staged'],
'staged-first': ['staged', 'unstaged']
}
/** Rows by staging area, each sorted conflicts-first then by path, as Source Control lists them. */
export function groupSourceControlEntries<T extends GitStatusEntry>(
entries: readonly T[]
): SourceControlEntryGroups<T> {
const groups: SourceControlEntryGroups<T> = { staged: [], unstaged: [], untracked: [] }
for (const entry of entries) {
groups[entry.area].push(entry)
}
for (const area of SOURCE_CONTROL_AREAS) {
groups[area].sort(compareGitStatusEntries)
}
return groups
}
export function resolveSourceControlGroupOrder(
value: SourceControlGroupOrder | null | undefined
): readonly SourceControlDisplaySectionArea[] {
return ORDER_BY_PRESET[normalizeSourceControlGroupOrder(value)]
}
export function isPinnedConflictEntry(entry: GitStatusEntry): boolean {
return entry.conflictStatus === 'unresolved' || entry.conflictStatus === 'resolved_locally'
}
export function getConflictReviewEntries(
entries: readonly GitStatusEntry[]
): SourceControlConflictReviewEntry[] {
return entries
.filter((entry) => entry.conflictStatus === 'unresolved' && entry.conflictKind)
.map((entry) => ({
path: entry.path,
conflictKind: entry.conflictKind!
}))
}
export function getSourceControlSectionViewAction(
section: SourceControlDisplaySection
): SourceControlSectionViewAction | null {
if (section.id === 'conflicts') {
const entries = getConflictReviewEntries(section.items)
if (entries.length > 0) {
return { kind: 'conflict-review', entries }
}
if (section.items.length === 0) {
return null
}
const [firstItem] = section.items
const area = section.items.every((item) => item.area === firstItem?.area)
? firstItem?.area
: undefined
return area
? { kind: 'combined-diff', area, entries: section.items }
: { kind: 'combined-diff', entries: section.items }
}
return { kind: 'combined-diff', area: section.area, entries: section.items }
}
export type SplitSourceControlGroups<T extends GitStatusEntry = GitStatusEntry> = {
pinnedConflicts: T[]
normalGroups: SourceControlEntryGroups<T>
}
export function splitPinnedSourceControlConflicts<T extends GitStatusEntry>(
groups: SourceControlEntryGroups<T>
): SplitSourceControlGroups<T> {
const pinnedConflicts = SOURCE_CONTROL_AREAS.flatMap((area) =>
groups[area].filter(isPinnedConflictEntry)
)
// Preserve groups identity without pinned conflicts so status polls do not rebuild downstream trees.
if (pinnedConflicts.length === 0) {
return { pinnedConflicts, normalGroups: groups }
}
return {
pinnedConflicts,
normalGroups: {
staged: groups.staged.filter((entry) => !isPinnedConflictEntry(entry)),
unstaged: groups.unstaged.filter((entry) => !isPinnedConflictEntry(entry)),
untracked: groups.untracked.filter((entry) => !isPinnedConflictEntry(entry))
}
}
}
export function buildSourceControlDisplaySectionsFromSplit<T extends GitStatusEntry>(
split: SplitSourceControlGroups<T>,
order: readonly SourceControlDisplaySectionArea[]
): SourceControlDisplaySection<T>[] {
const { pinnedConflicts, normalGroups } = split
const sections: SourceControlDisplaySection<T>[] = []
if (pinnedConflicts.length > 0) {
sections.push({ id: 'conflicts', area: 'unstaged', items: pinnedConflicts })
}
const changes = [...normalGroups.unstaged, ...normalGroups.untracked].sort(
compareGitStatusEntries
)
for (const area of order) {
const items = area === 'unstaged' ? changes : normalGroups[area]
if (items.length > 0) {
sections.push({ id: area, area, items })
}
}
return sections
}
export function buildSourceControlDisplaySections<T extends GitStatusEntry>(
groups: SourceControlEntryGroups<T>,
order: readonly SourceControlDisplaySectionArea[]
): SourceControlDisplaySection<T>[] {
return buildSourceControlDisplaySectionsFromSplit(
splitPinnedSourceControlConflicts(groups),
order
)
}