Skip to content

Commit

Permalink
[SiteSettings] Redesign project management (#3565)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Feb 11, 2025
1 parent 4612156 commit 195323a
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 51 deletions.
3 changes: 3 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
},
"siteSettings": {
"projectList": "Projects",
"activeProjects": "Active Projects: {{ val }}",
"archivedProjects": "Archived Projects: {{ val }}",
"manageProject": "Manage this project",
"projectRoles": "Project roles",
"addProjectUsers": "Add project users",
"archiveProjectText": "This project will not be accessible to any users.",
Expand Down
135 changes: 109 additions & 26 deletions src/components/SiteSettings/ProjectManagement/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { List, ListItem, Typography } from "@mui/material";
import { ReactElement, useEffect, useState } from "react";
import { Settings } from "@mui/icons-material";
import {
Divider,
List,
ListItem,
ListItemText,
ListSubheader,
Stack,
Typography,
} from "@mui/material";
import { ReactElement, ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

import { Project } from "api/models";
import { getAllProjects } from "backend";
import { IconButtonWithTooltip } from "components/Buttons";
import ExportButton from "components/ProjectExport/ExportButton";
import ProjectArchive from "components/ProjectSettings/ProjectArchive";
import ProjectUsersButtonWithConfirmation from "components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation";
import theme from "types/theme";

/** Component for managing all projects in the database. */
export default function ProjectManagement(): ReactElement {
const [allProjects, setAllProjects] = useState<Project[]>([]);
const [activeProjects, setActiveProjects] = useState<Project[]>([]);
Expand Down Expand Up @@ -36,43 +48,114 @@ export default function ProjectManagement(): ReactElement {
);
}, [allProjects]);

return ProjectList(activeProjects, archivedProjects, updateProjectList);
return (
<ProjectList
activeProjects={activeProjects}
archivedProjects={archivedProjects}
updateProjects={updateProjectList}
/>
);
}

interface ProjectListProps {
activeProjects: Project[];
archivedProjects: Project[];
/** Function to trigger an update of all projects in the parent component. */
updateProjects: () => Promise<void>;
}

// Extract and export for unit testing.
export function ProjectList(
activeProjects: Project[],
archivedProjects: Project[],
updateProjects: () => Promise<void>
): ReactElement {
function getListItems(projects: Project[]): ReactElement[] {
return projects.map((project) => (
<ListItem key={project.id}>
<Typography
variant="h6"
color={project.isActive ? "inherit" : "textSecondary"}
style={{ marginRight: theme.spacing(1) }}
>
{project.name}
</Typography>
{/* Export Lift file */}
export function ProjectList(props: ProjectListProps): ReactElement {
const [manageProjectId, setManageProjectId] = useState("");

const { t } = useTranslation();

const toggleManageProjectId = (projId: string): void => {
setManageProjectId((prev) => (projId === prev ? "" : projId));
};

function getProjectManagement(project: Project): ReactNode {
if (project.id !== manageProjectId) {
return;
}

return (
<Stack direction="row">
{/* Export LIFT file */}
<ExportButton projectId={project.id} />
{/* Manage project users. */}

{/* Manage project users */}
<ProjectUsersButtonWithConfirmation projectId={project.id} />
{/* Archive active project or restore archived project. */}

{/* Archive active project or restore archived project */}
<ProjectArchive
archive={project.isActive}
projectId={project.id}
updateParent={updateProjects}
updateParent={props.updateProjects}
/>
</ListItem>
));
</Stack>
);
}

/** Generates an array of items to fill a project List.
* Adds a Divider and a ListItem For each project in the given Project array. */
function getListItems(projects: Project[]): ReactElement[] {
const items: ReactElement[] = [];
for (const project of projects) {
items.push(<Divider key={`divider-${project.id}`} />);
items.push(
<ListItem key={project.id}>
<ListItemText>
<Stack>
<Stack direction="row">
{/* Project name */}
<Typography
color={project.isActive ? "inherit" : "textSecondary"}
style={{ marginRight: theme.spacing(1) }}
variant="h6"
>
{project.name}
</Typography>

{/* Button to open project management options */}
<IconButtonWithTooltip
icon={<Settings />}
onClick={() => toggleManageProjectId(project.id)}
textId="siteSettings.manageProject"
/>
</Stack>

{/* Project management options */}
{getProjectManagement(project)}
</Stack>
</ListItemText>
</ListItem>
);
}
return items;
}

return (
<List>
{getListItems(activeProjects)}
{getListItems(archivedProjects)}
{/* Active projects */}
<ListSubheader>
<Typography variant="h5">
{t("siteSettings.activeProjects", {
val: props.activeProjects.length,
})}
</Typography>
</ListSubheader>
{getListItems(props.activeProjects)}

{/* Archived projects */}
<ListSubheader>
<Typography variant="h5">
{t("siteSettings.archivedProjects", {
val: props.archivedProjects.length,
})}
</Typography>
</ListSubheader>
{getListItems(props.archivedProjects)}
</List>
);
}
35 changes: 10 additions & 25 deletions src/components/SiteSettings/ProjectManagement/tests/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { ListItem } from "@mui/material";
import { act } from "react";
import renderer from "react-test-renderer";

import ExportButton from "components/ProjectExport/ExportButton";
import ProjectArchive from "components/ProjectSettings/ProjectArchive";
import { ProjectList } from "components/SiteSettings/ProjectManagement";
import ProjectUsersButtonWithConfirmation from "components/SiteSettings/ProjectManagement/ProjectUsersButtonWithConfirmation";
import { randomProject } from "types/project";

const mockProjects = [randomProject(), randomProject(), randomProject()];

jest.mock("components/ProjectExport/ExportButton", () => "div");

let testRenderer: renderer.ReactTestRenderer;

beforeAll(() => {
renderer.act(() => {
testRenderer = renderer.create(ProjectList(mockProjects, [], jest.fn()));
beforeAll(async () => {
await act(async () => {
testRenderer = renderer.create(
<ProjectList
activeProjects={mockProjects}
archivedProjects={[]}
updateProjects={jest.fn()}
/>
);
});
});

Expand All @@ -24,21 +26,4 @@ describe("ProjectList", () => {
const projectList = testRenderer.root.findAllByType(ListItem);
expect(projectList.length).toEqual(mockProjects.length);
});

it("Has the right number of export buttons", () => {
const exportButtons = testRenderer.root.findAllByType(ExportButton);
expect(exportButtons.length).toEqual(mockProjects.length);
});

it("Has the right number of archive/restore buttons", () => {
const projectButtons = testRenderer.root.findAllByType(ProjectArchive);
expect(projectButtons.length).toEqual(mockProjects.length);
});

it("Has the right number of project roles buttons", () => {
const projectButtons = testRenderer.root.findAllByType(
ProjectUsersButtonWithConfirmation
);
expect(projectButtons.length).toEqual(mockProjects.length);
});
});

0 comments on commit 195323a

Please sign in to comment.