240 lines
7.0 KiB
TypeScript

/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */
import {
createTaskName,
getProjectsByType,
groupProjectsByDirectory,
} from '../util';
import { WorkspaceLayout } from '../interfaces';
import { ExclamationCircleIcon, EyeIcon } from '@heroicons/react/24/outline';
import { ReactNode } from 'react';
import { Tooltip } from '@nx/graph/ui-tooltips';
import { TaskGraphErrorTooltip } from './task-graph-error-tooltip';
interface SidebarProject {
projectGraphNode: ProjectGraphProjectNode;
isSelected: boolean;
error: string | null;
}
function ProjectListItem({
project,
toggleTask,
}: {
project: SidebarProject;
toggleTask: (taskId: string) => void;
}) {
return (
<li className="relative block cursor-default select-none pb-0 pl-2 pr-6 text-xs text-slate-600 dark:text-slate-400">
<div className="flex items-center">
<label
className="block w-full cursor-pointer truncate rounded-md p-2 font-mono font-normal transition hover:bg-slate-50 hover:dark:bg-slate-700"
data-project={project.projectGraphNode.name}
title={project.projectGraphNode.name}
data-active={project.isSelected.toString()}
onClick={() =>
!project.error ? toggleTask(project.projectGraphNode.name) : null
}
>
{project.projectGraphNode.name}
</label>
</div>
{project.error ? (
<Tooltip
content={<TaskGraphErrorTooltip error={project.error} />}
openAction="click"
strategy="fixed"
>
<span className="absolute inset-y-0 right-0 flex cursor-pointer items-center text-blue-500 dark:text-sky-500">
<ExclamationCircleIcon
className="h-5 w-5 text-yellow-500 dark:text-yellow-400"
aria-hidden="true"
/>
</span>
</Tooltip>
) : null}
{project.isSelected ? (
<span
title="This task is visible"
className="absolute inset-y-0 right-0 flex cursor-pointer items-center text-blue-500 dark:text-sky-500"
onClick={() => toggleTask(project.projectGraphNode.name)}
>
<EyeIcon className="h-5 w-5"></EyeIcon>
</span>
) : null}
</li>
);
}
function SubProjectList({
headerText = '',
projects,
toggleTask,
}: {
headerText: string;
projects: SidebarProject[];
toggleTask: (taskId: string) => void;
}) {
let sortedProjects = [...projects];
sortedProjects.sort((a, b) => {
return a.projectGraphNode.name.localeCompare(b.projectGraphNode.name);
});
return (
<>
{headerText !== '' ? (
<h3 className="mt-4 cursor-text py-2 text-sm font-semibold uppercase tracking-wide text-slate-800 dark:text-slate-200 lg:text-xs">
{headerText}
</h3>
) : null}
<ul className="mt-2 -ml-3">
{sortedProjects.map((project) => {
return (
<ProjectListItem
key={project.projectGraphNode.name}
project={project}
toggleTask={toggleTask}
></ProjectListItem>
);
})}
</ul>
</>
);
}
function mapToSidebarProjectWithTasks(
project: ProjectGraphProjectNode,
selectedProjects: string[],
selectedTarget: string,
errors: Record<string, string>
): SidebarProject {
const taskId = createTaskName(project.name, selectedTarget);
return {
projectGraphNode: project,
isSelected: selectedProjects.includes(project.name),
error: errors?.[taskId] ?? null,
};
}
export interface TaskListProps {
projects: ProjectGraphProjectNode[];
workspaceLayout: WorkspaceLayout;
selectedTarget: string;
selectedProjects: string[];
toggleProject: (projectName: string) => void;
children: ReactNode | ReactNode[];
errors: Record<string, string>;
}
export function TaskList({
projects,
workspaceLayout,
selectedTarget,
selectedProjects,
toggleProject,
children,
errors,
}: TaskListProps) {
const filteredProjects = projects
.filter((project) =>
(project.data as any).targets?.hasOwnProperty(selectedTarget)
)
.sort((a, b) => a.name.localeCompare(b.name));
const appProjects = getProjectsByType('app', filteredProjects);
const libProjects = getProjectsByType('lib', filteredProjects);
const e2eProjects = getProjectsByType('e2e', filteredProjects);
const appDirectoryGroups = groupProjectsByDirectory(
appProjects,
workspaceLayout
);
const libDirectoryGroups = groupProjectsByDirectory(
libProjects,
workspaceLayout
);
const e2eDirectoryGroups = groupProjectsByDirectory(
e2eProjects,
workspaceLayout
);
const sortedAppDirectories = Object.keys(appDirectoryGroups).sort();
const sortedLibDirectories = Object.keys(libDirectoryGroups).sort();
const sortedE2EDirectories = Object.keys(e2eDirectoryGroups).sort();
return (
<div id="project-lists" className="mt-8 border-t border-slate-400/10 px-4">
{children}
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
app projects
</h2>
{sortedAppDirectories.map((directoryName) => {
return (
<SubProjectList
key={'app-' + directoryName}
headerText={directoryName}
projects={appDirectoryGroups[directoryName].map((project) =>
mapToSidebarProjectWithTasks(
project,
selectedProjects,
selectedTarget,
errors
)
)}
toggleTask={toggleProject}
></SubProjectList>
);
})}
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
e2e projects
</h2>
{sortedE2EDirectories.map((directoryName) => {
return (
<SubProjectList
key={'e2e-' + directoryName}
headerText={directoryName}
projects={e2eDirectoryGroups[directoryName].map((project) =>
mapToSidebarProjectWithTasks(
project,
selectedProjects,
selectedTarget,
errors
)
)}
toggleTask={toggleProject}
></SubProjectList>
);
})}
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
lib projects
</h2>
{sortedLibDirectories.map((directoryName) => {
return (
<SubProjectList
key={'lib-' + directoryName}
headerText={directoryName}
projects={libDirectoryGroups[directoryName].map((project) =>
mapToSidebarProjectWithTasks(
project,
selectedProjects,
selectedTarget,
errors
)
)}
toggleTask={toggleProject}
></SubProjectList>
);
})}
</div>
);
}