mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
import type { Repo, ProjectGroup, ProjectGroupCreatedFrom } from './types'
|
|
|
|
export const UNGROUPED_PROJECT_GROUP_KEY = 'project-group:ungrouped'
|
|
|
|
function createProjectGroupId(): string {
|
|
const randomUUID = globalThis.crypto?.randomUUID
|
|
if (randomUUID) {
|
|
return randomUUID.call(globalThis.crypto)
|
|
}
|
|
return `project-group-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
}
|
|
|
|
export function normalizeProjectGroupName(name: string, fallback = 'Untitled group'): string {
|
|
const trimmed = name.trim()
|
|
return trimmed.length > 0 ? trimmed : fallback
|
|
}
|
|
|
|
export function createProjectGroup(input: {
|
|
name: string
|
|
parentPath?: string | null
|
|
connectionId?: string | null
|
|
parentGroupId?: string | null
|
|
createdFrom: ProjectGroupCreatedFrom
|
|
tabOrder: number
|
|
now?: number
|
|
}): ProjectGroup {
|
|
const now = input.now ?? Date.now()
|
|
return {
|
|
id: createProjectGroupId(),
|
|
name: normalizeProjectGroupName(input.name),
|
|
parentPath: input.parentPath ?? null,
|
|
connectionId: input.connectionId ?? null,
|
|
parentGroupId: input.parentGroupId ?? null,
|
|
createdFrom: input.createdFrom,
|
|
tabOrder: input.tabOrder,
|
|
isCollapsed: false,
|
|
color: null,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
}
|
|
}
|
|
|
|
export function normalizeProjectGroups(value: unknown): ProjectGroup[] {
|
|
if (!Array.isArray(value)) {
|
|
return []
|
|
}
|
|
const groups: ProjectGroup[] = []
|
|
const seen = new Set<string>()
|
|
for (const candidate of value) {
|
|
if (!candidate || typeof candidate !== 'object') {
|
|
continue
|
|
}
|
|
const raw = candidate as Partial<ProjectGroup>
|
|
if (typeof raw.id !== 'string' || seen.has(raw.id)) {
|
|
continue
|
|
}
|
|
seen.add(raw.id)
|
|
const now = Date.now()
|
|
groups.push({
|
|
id: raw.id,
|
|
name: normalizeProjectGroupName(typeof raw.name === 'string' ? raw.name : ''),
|
|
parentPath: typeof raw.parentPath === 'string' ? raw.parentPath : null,
|
|
connectionId:
|
|
typeof raw.connectionId === 'string'
|
|
? raw.connectionId
|
|
: raw.connectionId === null
|
|
? null
|
|
: null,
|
|
parentGroupId: typeof raw.parentGroupId === 'string' ? raw.parentGroupId : null,
|
|
createdFrom:
|
|
raw.createdFrom === 'manual' ||
|
|
raw.createdFrom === 'folder-scan' ||
|
|
raw.createdFrom === 'migration'
|
|
? raw.createdFrom
|
|
: 'manual',
|
|
tabOrder:
|
|
typeof raw.tabOrder === 'number' && Number.isFinite(raw.tabOrder) ? raw.tabOrder : 0,
|
|
isCollapsed: raw.isCollapsed === true,
|
|
color: typeof raw.color === 'string' ? raw.color : null,
|
|
createdAt:
|
|
typeof raw.createdAt === 'number' && Number.isFinite(raw.createdAt) ? raw.createdAt : now,
|
|
updatedAt:
|
|
typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : now
|
|
})
|
|
}
|
|
groups.sort(
|
|
(left, right) => left.tabOrder - right.tabOrder || left.name.localeCompare(right.name)
|
|
)
|
|
const groupIds = new Set(groups.map((group) => group.id))
|
|
for (const group of groups) {
|
|
if (group.parentGroupId === group.id || !groupIds.has(group.parentGroupId ?? '')) {
|
|
group.parentGroupId = null
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
export function clearMissingProjectGroupMemberships(repos: Repo[], groups: ProjectGroup[]): Repo[] {
|
|
const groupIds = new Set(groups.map((group) => group.id))
|
|
return repos.map((repo) =>
|
|
repo.projectGroupId && !groupIds.has(repo.projectGroupId)
|
|
? { ...repo, projectGroupId: null }
|
|
: repo
|
|
)
|
|
}
|
|
|
|
export function getProjectGroupSubtreeIds(
|
|
groups: readonly Pick<ProjectGroup, 'id' | 'parentGroupId'>[],
|
|
rootGroupId: string
|
|
): Set<string> {
|
|
const childGroupsByParentId = new Map<string, string[]>()
|
|
for (const group of groups) {
|
|
if (!group.parentGroupId) {
|
|
continue
|
|
}
|
|
const children = childGroupsByParentId.get(group.parentGroupId) ?? []
|
|
children.push(group.id)
|
|
childGroupsByParentId.set(group.parentGroupId, children)
|
|
}
|
|
|
|
const subtreeIds = new Set<string>()
|
|
const pending = [rootGroupId]
|
|
while (pending.length > 0) {
|
|
const groupId = pending.pop()!
|
|
if (subtreeIds.has(groupId)) {
|
|
continue
|
|
}
|
|
subtreeIds.add(groupId)
|
|
// Why: imported project-group trees can be very wide; `push(...children)`
|
|
// can exceed V8's argument limit while collecting descendants.
|
|
for (const childGroupId of childGroupsByParentId.get(groupId) ?? []) {
|
|
pending.push(childGroupId)
|
|
}
|
|
}
|
|
return subtreeIds
|
|
}
|
|
|
|
export function getNextProjectGroupOrder(repos: readonly Repo[], groupId: string | null): number {
|
|
let max = -1
|
|
for (const repo of repos) {
|
|
if ((repo.projectGroupId ?? null) !== groupId) {
|
|
continue
|
|
}
|
|
const order = repo.projectGroupOrder
|
|
if (typeof order === 'number' && Number.isFinite(order)) {
|
|
max = Math.max(max, order)
|
|
}
|
|
}
|
|
return max + 1
|
|
}
|