Skip to content
Source
Sign inSign up

sidebarProjectGrouping.ts

246 lines8.6 KB

1import type {
2 EnvironmentId,
3 ScopedProjectRef,
4 SidebarProjectGroupingMode,
5} from "@t3tools/contracts";
6import {
7 buildProjectGroups,
8 derivePhysicalProjectKey,
9 resolveScratchProjectScopeKey,
10 type ProjectGroupingSettings,
11} from "@t3tools/client-runtime/state/project-grouping";
12import type { Project } from "./types";
13​
14export const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = {
15 repository: "Group by repository",
16 repository_path: "Group by repository path",
17 separate: "Keep separate",
18};
19​
20export function withProjectGroupingOverride(
21 overrides: Readonly<Record<string, SidebarProjectGroupingMode>> | undefined,
22 overrideKey: string,
23 selection: SidebarProjectGroupingMode | "inherit",
24): Record<string, SidebarProjectGroupingMode> {
25 const next = { ...overrides };
26 if (selection === "inherit") delete next[overrideKey];
27 else next[overrideKey] = selection;
28 return next;
30​
31export type EnvironmentPresence = "local-only" | "remote-only" | "mixed";
32​
33export interface SidebarProjectGroupMember extends Project {
34 physicalProjectKey: string;
35 environmentLabel: string | null;
37​
38export interface SidebarProjectSnapshot extends Project {
39 projectKey: string;
40 displayName: string;
41 groupedProjectCount: number;
42 environmentPresence: EnvironmentPresence;
43 allRemoteMembersAreDesktopLocal: boolean;
44 allRemoteMembersAreWsl: boolean;
45 memberProjects: readonly SidebarProjectGroupMember[];
46 memberProjectRefs: readonly ScopedProjectRef[];
47 remoteEnvironmentLabels: readonly string[];
48 pinned: boolean;
49 archived: boolean;
51​
52export function projectGroupsSpanEnvironments(
53 groups: ReadonlyArray<Pick<SidebarProjectSnapshot, "memberProjects">>,
54): boolean {
55 const environmentIds = new Set<EnvironmentId>();
56 for (const group of groups) {
57 for (const member of group.memberProjects) {
58 environmentIds.add(member.environmentId);
59 if (environmentIds.size > 1) return true;
60 }
61 }
62 return false;
64​
65export interface SidebarProjectPickerEntry {
66 group: SidebarProjectSnapshot;
67 targetProject: SidebarProjectGroupMember;
68 isPreferred: boolean;
70​
71export function getSidebarProjectSettingsKey(
72 group: SidebarProjectSnapshot,
73 settings: ProjectGroupingSettings,
74 projects: ReadonlyArray<Project>,
75 projectRef?: ScopedProjectRef,
76): string {
77 if (!group.isScratch) return group.projectKey;
78 const project =
79 group.memberProjects.find(
80 (member) =>
81 member.environmentId === projectRef?.environmentId && member.id === projectRef.projectId,
82 ) ??
83 group.memberProjects.find((member) => member.environmentId === projectRef?.environmentId) ??
84 group;
85 return (
86 buildPhysicalToLogicalProjectKeyMap({ projects, settings, primaryEnvironmentId: null }).get(
87 derivePhysicalProjectKey(project),
88 ) ?? group.projectKey
89 );
91​
92export function resolveSidebarProjectScopeKey(input: {
93 readonly groups: ReadonlyArray<SidebarProjectSnapshot>;
94 readonly key: string | null;
95 readonly settings?: ProjectGroupingSettings;
96 readonly canClearMissingScope?: boolean;
97}): string | null {
98 if (input.key === null) return null;
99 const group =
100 input.groups.find((candidate) => candidate.projectKey === input.key) ??
101 input.groups.find(
102 (candidate) =>
103 candidate.isScratch &&
104 resolveScratchProjectScopeKey(input.key, candidate.memberProjects, input.settings) ===
105 candidate.projectKey,
106 );
107 return group?.projectKey ?? (input.canClearMissingScope ? null : input.key);
109​
110export function resolveSidebarProjectScopeKeys(input: {
111 readonly groups: ReadonlyArray<SidebarProjectSnapshot>;
112 readonly keys: ReadonlyArray<string>;
113 readonly settings?: ProjectGroupingSettings;
114 readonly canClearMissingScope?: boolean;
115}): string[] {
116 return [
117 ...new Set(input.keys.flatMap((key) => resolveSidebarProjectScopeKey({ ...input, key }) ?? [])),
118 ];
120​
121export function buildPhysicalToLogicalProjectKeyMap(input: {
122 projects: ReadonlyArray<Project>;
123 settings: ProjectGroupingSettings;
124 primaryEnvironmentId: EnvironmentId | null;
125}): Map<string, string> {
126 const mapping = new Map<string, string>();
127 const groups = buildProjectGroups({
128 projects: input.projects,
129 settings: input.settings,
130 preferredEnvironmentId: input.primaryEnvironmentId,
131 });
132 for (const group of groups) {
133 for (const member of group.members) {
134 mapping.set(member.physicalProjectKey, group.key);
135 }
136 }
137 return mapping;
139​
140export function buildSidebarProjectSnapshots(input: {
141 projects: ReadonlyArray<Project>;
142 settings: ProjectGroupingSettings;
143 groupScratchProjects?: boolean;
144 primaryEnvironmentId: EnvironmentId | null;
145 resolveEnvironmentLabel: (environmentId: EnvironmentId) => string | null;
146 isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean;
147 isWslEnvironment?: (environmentId: EnvironmentId) => boolean;
148}): SidebarProjectSnapshot[] {
149 return buildProjectGroups({
150 projects: input.projects,
151 groupScratchProjects: input.groupScratchProjects ?? false,
152 settings: input.settings,
153 preferredEnvironmentId: input.primaryEnvironmentId,
154 }).map((group): SidebarProjectSnapshot => {
155 const members = group.members.map(
156 ({ physicalProjectKey, project }): SidebarProjectGroupMember => ({
157 ...project,
158 physicalProjectKey,
159 environmentLabel: input.resolveEnvironmentLabel(project.environmentId),
160 }),
161 );
162 const representative =
163 members.find(
164 (member) =>
165 member.environmentId === group.representative.environmentId &&
166 member.id === group.representative.id,
167 ) ?? members[0]!;
168​
169 const hasLocal =
170 input.primaryEnvironmentId !== null &&
171 members.some((member) => member.environmentId === input.primaryEnvironmentId);
172 const hasRemote =
173 input.primaryEnvironmentId !== null
174 ? members.some((member) => member.environmentId !== input.primaryEnvironmentId)
175 : false;
176 const remoteMembers = members.filter(
177 (member) =>
178 input.primaryEnvironmentId !== null && member.environmentId !== input.primaryEnvironmentId,
179 );
180 const remoteEnvironmentLabels = remoteMembers
181 .flatMap((member) => (member.environmentLabel ? [member.environmentLabel] : []))
182 .filter((label, index, labels) => labels.indexOf(label) === index);
183 const isDesktopLocal = input.isDesktopLocalEnvironment ?? (() => false);
184 const isWsl = input.isWslEnvironment ?? (() => false);
185 const allRemoteMembersAreDesktopLocal =
186 remoteMembers.length > 0 &&
187 remoteMembers.every((member) => isDesktopLocal(member.environmentId));
188 const allRemoteMembersAreWsl =
189 remoteMembers.length > 0 && remoteMembers.every((member) => isWsl(member.environmentId));
190​
191 return {
192 ...representative,
193 projectKey: group.key,
194 displayName: group.label,
195 groupedProjectCount: members.length,
196 environmentPresence:
197 hasLocal && hasRemote ? "mixed" : hasRemote ? "remote-only" : "local-only",
198 allRemoteMembersAreDesktopLocal,
199 allRemoteMembersAreWsl,
200 memberProjects: members,
201 memberProjectRefs: group.memberProjectRefs,
202 remoteEnvironmentLabels,
203 pinned: group.pinned,
204 archived: group.archived,
205 };
206 });
208​
209export function buildSidebarProjectPickerEntries(input: {
210 groups: ReadonlyArray<SidebarProjectSnapshot>;
211 preferredProjectRef: ScopedProjectRef | null;
212}) {
213 const preferredProjectRef = input.preferredProjectRef;
214 const entries = input.groups.flatMap((group): SidebarProjectPickerEntry[] => {
215 const isPreferred = preferredProjectRef
216 ? group.memberProjectRefs.some(
217 (projectRef) =>
218 projectRef.environmentId === preferredProjectRef.environmentId &&
219 projectRef.projectId === preferredProjectRef.projectId,
220 )
221 : false;
222 const preferredProject = preferredProjectRef
223 ? (group.memberProjects.find(
224 (project) =>
225 project.environmentId === preferredProjectRef.environmentId &&
226 project.id === preferredProjectRef.projectId,
227 ) ??
228 group.memberProjects.find(
229 (project) => project.environmentId === preferredProjectRef.environmentId,
230 ))
231 : null;
232 const targetProject =
233 preferredProject ??
234 group.memberProjects.find(
235 (project) => project.environmentId === group.environmentId && project.id === group.id,
236 ) ??
237 group.memberProjects[0];
238 if (!targetProject) return [];
239​
240 return [{ group, targetProject, isPreferred }];
241 });
242 // The current project leads, even ahead of pinned projects.
243 const preferred = entries.find((entry) => entry.isPreferred);
244 if (!preferred) return entries;
245 return [preferred, ...entries.filter((entry) => entry !== preferred)];