import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime"; import type { ScopedProjectRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; import { normalizeProjectPathForComparison } from "./lib/projectPaths"; import type { Project } from "./types"; export interface ProjectGroupingSettings { sidebarProjectGroupingMode: SidebarProjectGroupingMode; sidebarProjectGroupingOverrides: Record; } export type ProjectGroupingMode = SidebarProjectGroupingMode; function uniqueNonEmptyValues(values: ReadonlyArray): string[] { const seen = new Set(); const unique: string[] = []; for (const value of values) { const trimmed = value?.trim(); if (!trimmed || seen.has(trimmed)) { continue; } seen.add(trimmed); unique.push(trimmed); } return unique; } function deriveRepositoryRelativeProjectPath( project: Pick, ): string | null { const rootPath = project.repositoryIdentity?.rootPath?.trim(); if (!rootPath) { return null; } const normalizedProjectPath = normalizeProjectPathForComparison(project.cwd); const normalizedRootPath = normalizeProjectPathForComparison(rootPath); if (normalizedProjectPath.length === 0 || normalizedRootPath.length === 0) { return null; } if (normalizedProjectPath === normalizedRootPath) { return ""; } const separator = normalizedRootPath.includes("\\") ? "\\" : "/"; const rootPrefix = `${normalizedRootPath}${separator}`; if (!normalizedProjectPath.startsWith(rootPrefix)) { return null; } return normalizedProjectPath.slice(rootPrefix.length).replaceAll("\\", "/"); } export function derivePhysicalProjectKeyFromPath(environmentId: string, cwd: string): string { return `${environmentId}:${normalizeProjectPathForComparison(cwd)}`; } export function derivePhysicalProjectKey(project: Pick): string { return derivePhysicalProjectKeyFromPath(project.environmentId, project.cwd); } export function deriveProjectGroupingOverrideKey( project: Pick, ): string { return derivePhysicalProjectKey(project); } // Key under which a project's manual sort order (projectOrder) is stored. // Must stay aligned with the writer side in `uiStateStore.syncProjects` and // the drag handlers in `Sidebar` so readers and writers agree. export function getProjectOrderKey(project: Pick): string { return derivePhysicalProjectKey(project); } export function resolveProjectGroupingMode( project: Pick, settings: ProjectGroupingSettings, ): SidebarProjectGroupingMode { return ( settings.sidebarProjectGroupingOverrides?.[deriveProjectGroupingOverrideKey(project)] ?? settings.sidebarProjectGroupingMode ); } function deriveRepositoryScopedKey( project: Pick, groupingMode: SidebarProjectGroupingMode, ): string | null { const canonicalKey = project.repositoryIdentity?.canonicalKey; if (!canonicalKey) { return null; } if (groupingMode === "repository") { return canonicalKey; } const relativeProjectPath = deriveRepositoryRelativeProjectPath(project); if (relativeProjectPath === null) { return canonicalKey; } return relativeProjectPath.length === 0 ? canonicalKey : `${canonicalKey}::${relativeProjectPath}`; } export function deriveLogicalProjectKey( project: Pick, options?: { groupingMode?: SidebarProjectGroupingMode; }, ): string { const groupingMode = options?.groupingMode ?? "repository"; if (groupingMode === "separate") { return derivePhysicalProjectKey(project); } return ( deriveRepositoryScopedKey(project, groupingMode) ?? derivePhysicalProjectKey(project) ?? scopedProjectKey(scopeProjectRef(project.environmentId, project.id)) ); } export function deriveLogicalProjectKeyFromSettings( project: Pick, settings: ProjectGroupingSettings, ): string { return deriveLogicalProjectKey(project, { groupingMode: resolveProjectGroupingMode(project, settings), }); } export function deriveLogicalProjectKeyFromRef( projectRef: ScopedProjectRef, project: Pick | null | undefined, options?: { groupingMode?: SidebarProjectGroupingMode; }, ): string { return project ? deriveLogicalProjectKey(project, options) : scopedProjectKey(projectRef); } export function deriveProjectGroupLabel(input: { representative: Pick; members: ReadonlyArray>; }): string { const sharedDisplayNames = uniqueNonEmptyValues( input.members.map((member) => member.repositoryIdentity?.displayName), ); if (sharedDisplayNames.length === 1) { return sharedDisplayNames[0]!; } const sharedRepositoryNames = uniqueNonEmptyValues( input.members.map((member) => member.repositoryIdentity?.name), ); if (sharedRepositoryNames.length === 1) { return sharedRepositoryNames[0]!; } return input.representative.name; }