Przeglądaj źródła

Use task templates in project creation

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 rok temu
rodzic
commit
700bc98c72
15 zmienionych plików z 197 dodań i 86 usunięć
  1. +1
    -0
      next.config.js
  2. +7
    -0
      src/app/(main)/projects/create/page.tsx
  3. +1
    -1
      src/app/api/projects/actions.ts
  4. +15
    -0
      src/app/api/projects/index.ts
  5. +1
    -0
      src/app/api/tasks/index.ts
  6. +22
    -4
      src/components/CreateProject/CreateProject.tsx
  7. +11
    -2
      src/components/CreateProject/CreateProjectWrapper.tsx
  8. +27
    -10
      src/components/CreateProject/ProjectClientDetails.tsx
  9. +9
    -5
      src/components/CreateProject/ResourceMilestone.tsx
  10. +54
    -16
      src/components/CreateProject/TaskSetup.tsx
  11. +3
    -3
      src/components/LoginPage/LoginPage.tsx
  12. +8
    -7
      src/components/ProjectSearch/ProjectSearch.tsx
  13. +3
    -2
      src/components/ProjectSearch/ProjectSearchWrapper.tsx
  14. +24
    -22
      src/components/TransferList/MultiSelectList.tsx
  15. +11
    -14
      src/components/TransferList/TransferList.tsx

+ 1
- 0
next.config.js Wyświetl plik

@@ -5,6 +5,7 @@ const withPWA = require("next-pwa")({
dest: "public", dest: "public",
register: true, register: true,
skipWaiting: true, skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
}); });


const nextConfig = { const nextConfig = {


+ 7
- 0
src/app/(main)/projects/create/page.tsx Wyświetl plik

@@ -1,3 +1,5 @@
import { fetchProjectCategories } from "@/app/api/projects";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "@/components/CreateProject"; import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n"; import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@@ -10,6 +12,11 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => { const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects"); const { t } = await getServerI18n("projects");


// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();

return ( return (
<> <>
<Typography variant="h4">{t("Create Project")}</Typography> <Typography variant="h4">{t("Create Project")}</Typography>


+ 1
- 1
src/app/api/projects/actions.ts Wyświetl plik

@@ -8,7 +8,7 @@ export interface CreateProjectInputs {
// Project details // Project details
projectCode: string; projectCode: string;
projectName: string; projectName: string;
projectCategory: string;
projectCategoryId: number;
projectDescription: string; projectDescription: string;


// Client details // Client details


+ 15
- 0
src/app/api/projects/index.ts Wyświetl plik

@@ -10,7 +10,13 @@ export interface ProjectResult {
client: string; client: string;
} }


export interface ProjectCategory {
id: number;
label: string;
}

export const preloadProjects = () => { export const preloadProjects = () => {
fetchProjectCategories();
fetchProjects(); fetchProjects();
}; };


@@ -18,6 +24,15 @@ export const fetchProjects = cache(async () => {
return mockProjects; return mockProjects;
}); });


export const fetchProjectCategories = cache(async () => {
return mockProjectCategories;
});

const mockProjectCategories: ProjectCategory[] = [
{ id: 1, label: "Confirmed Project" },
{ id: 2, label: "Project to be bidded" },
];

const mockProjects: ProjectResult[] = [ const mockProjects: ProjectResult[] = [
{ {
id: 1, id: 1,


+ 1
- 0
src/app/api/tasks/index.ts Wyświetl plik

@@ -19,6 +19,7 @@ export interface TaskTemplate {
id: number; id: number;
code: string; code: string;
name: string; name: string;
tasks: Task[];
} }


export const preloadTaskTemplates = () => { export const preloadTaskTemplates = () => {


+ 22
- 4
src/components/CreateProject/CreateProject.tsx Wyświetl plik

@@ -13,7 +13,7 @@ import ProjectClientDetails from "./ProjectClientDetails";
import TaskSetup from "./TaskSetup"; import TaskSetup from "./TaskSetup";
import StaffAllocation from "./StaffAllocation"; import StaffAllocation from "./StaffAllocation";
import ResourceMilestone from "./ResourceMilestone"; import ResourceMilestone from "./ResourceMilestone";
import { Task } from "@/app/api/tasks";
import { Task, TaskTemplate } from "@/app/api/tasks";
import { import {
FieldErrors, FieldErrors,
FormProvider, FormProvider,
@@ -23,9 +23,12 @@ import {
} from "react-hook-form"; } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions"; import { CreateProjectInputs } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material"; import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";


export interface Props { export interface Props {
allTasks: Task[]; allTasks: Task[];
projectCategories: ProjectCategory[];
taskTemplates: TaskTemplate[];
} }


const hasErrorsInTab = ( const hasErrorsInTab = (
@@ -40,7 +43,11 @@ const hasErrorsInTab = (
} }
}; };


const CreateProject: React.FC<Props> = ({ allTasks }) => {
const CreateProject: React.FC<Props> = ({
allTasks,
projectCategories,
taskTemplates,
}) => {
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -101,8 +108,19 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => {
<Tab label={t("Staff Allocation")} iconPosition="end" /> <Tab label={t("Staff Allocation")} iconPosition="end" />
<Tab label={t("Resource and Milestone")} iconPosition="end" /> <Tab label={t("Resource and Milestone")} iconPosition="end" />
</Tabs> </Tabs>
{<ProjectClientDetails isActive={tabIndex === 0} />}
{<TaskSetup allTasks={allTasks} isActive={tabIndex === 1} />}
{
<ProjectClientDetails
projectCategories={projectCategories}
isActive={tabIndex === 0}
/>
}
{
<TaskSetup
allTasks={allTasks}
taskTemplates={taskTemplates}
isActive={tabIndex === 1}
/>
}
{<StaffAllocation isActive={tabIndex === 2} />} {<StaffAllocation isActive={tabIndex === 2} />}
{<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />} {<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />}
<Stack direction="row" justifyContent="flex-end" gap={1}> <Stack direction="row" justifyContent="flex-end" gap={1}>


+ 11
- 2
src/components/CreateProject/CreateProjectWrapper.tsx Wyświetl plik

@@ -1,10 +1,19 @@
import { fetchAllTasks } from "@/app/api/tasks";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "./CreateProject"; import CreateProject from "./CreateProject";
import { fetchProjectCategories } from "@/app/api/projects";


const CreateProjectWrapper: React.FC = async () => { const CreateProjectWrapper: React.FC = async () => {
const tasks = await fetchAllTasks(); const tasks = await fetchAllTasks();
const taskTemplates = await fetchTaskTemplates();
const projectCategories = await fetchProjectCategories();


return <CreateProject allTasks={tasks} />;
return (
<CreateProject
allTasks={tasks}
projectCategories={projectCategories}
taskTemplates={taskTemplates}
/>
);
}; };


export default CreateProjectWrapper; export default CreateProjectWrapper;

+ 27
- 10
src/components/CreateProject/ProjectClientDetails.tsx Wyświetl plik

@@ -15,16 +15,24 @@ import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt"; import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions"; import { CreateProjectInputs } from "@/app/api/projects/actions";
import { ProjectCategory } from "@/app/api/projects";


const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({
interface Props {
isActive: boolean;
projectCategories: ProjectCategory[];
}

const ProjectClientDetails: React.FC<Props> = ({
isActive, isActive,
projectCategories,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
register, register,
formState: { errors }, formState: { errors },
control,
} = useFormContext<CreateProjectInputs>(); } = useFormContext<CreateProjectInputs>();


return ( return (
@@ -55,14 +63,23 @@ const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>{t("Project Category")}</InputLabel> <InputLabel>{t("Project Category")}</InputLabel>
<Select
label={t("Project Category")}
value={"Temporary Project"}
>
<MenuItem value={"Temporary Project"}>
{t("Temporary Project")}
</MenuItem>
</Select>
<Controller
defaultValue={projectCategories[0].id}
control={control}
name="projectCategoryId"
render={({ field }) => (
<Select label={t("Project Category")} {...field}>
{projectCategories.map((category, index) => (
<MenuItem
key={`${category.id}-${index}`}
value={category.id}
>
{t(category.label)}
</MenuItem>
))}
</Select>
)}
/>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>


+ 9
- 5
src/components/CreateProject/ResourceMilestone.tsx Wyświetl plik

@@ -81,11 +81,15 @@ const ResourceMilestone: React.FC<Props> = ({
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<ResourceSection
tasks={currentTasks}
manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
<MilestoneSection taskGroupId={currentTaskGroupId} />
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && (
<ResourceSection
tasks={currentTasks}
manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
)}
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && <MilestoneSection taskGroupId={currentTaskGroupId} />}
<CardActions sx={{ justifyContent: "flex-end" }}> <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}> <Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")} {t("Reset")}


+ 54
- 16
src/components/CreateProject/TaskSetup.tsx Wyświetl plik

@@ -7,31 +7,65 @@ import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import TransferList from "../TransferList"; import TransferList from "../TransferList";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import React, { useMemo } from "react";
import React, { useCallback, useMemo, useState } from "react";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt"; import RestartAlt from "@mui/icons-material/RestartAlt";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import InputLabel from "@mui/material/InputLabel"; import InputLabel from "@mui/material/InputLabel";
import { Task } from "@/app/api/tasks";
import { Task, TaskTemplate } from "@/app/api/tasks";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions"; import { CreateProjectInputs } from "@/app/api/projects/actions";
import isNumber from "lodash/isNumber";


interface Props { interface Props {
allTasks: Task[]; allTasks: Task[];
taskTemplates: TaskTemplate[];
isActive: boolean; isActive: boolean;
} }


const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => {
const TaskSetup: React.FC<Props> = ({
allTasks: tasks,
taskTemplates,
isActive,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getValues, setValue } = useFormContext<CreateProjectInputs>();
const currentTasks = getValues("tasks");
const { setValue, watch } = useFormContext<CreateProjectInputs>();
const currentTasks = watch("tasks");


const items = useMemo(
() => tasks.map((t) => ({ id: t.id, label: t.name, group: t.taskGroup })),
[tasks],
const onReset = useCallback(() => {
setValue("tasks", {});
}, [setValue]);

const [selectedTaskTemplateId, setSelectedTaskTemplateId] = useState<
"All" | number
>("All");
const onSelectTaskTemplate = useCallback(
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
setSelectedTaskTemplateId(e.target.value);
onReset();
}
},
[onReset],
); );

const items = useMemo(() => {
const taskList =
selectedTaskTemplateId === "All"
? tasks
: taskTemplates.find(
(template) => template.id === selectedTaskTemplateId,
)?.tasks || tasks;

return taskList.map((t) => ({
id: t.id,
label: t.name,
group: t.taskGroup,
}));
}, [tasks, selectedTaskTemplateId, taskTemplates]);

const selectedItems = useMemo(() => { const selectedItems = useMemo(() => {
return tasks return tasks
.filter((t) => currentTasks[t.id]) .filter((t) => currentTasks[t.id])
@@ -53,20 +87,24 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => {
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>{t("Task List Source")}</InputLabel> <InputLabel>{t("Task List Source")}</InputLabel>
<Select
<Select<"All" | number>
label={t("Task List Source")} label={t("Task List Source")}
value={"M1009 - Consultancy Project X Temp"}
value={selectedTaskTemplateId}
onChange={onSelectTaskTemplate}
> >
<MenuItem value={"M1009 - Consultancy Project X Temp"}>
{"M1009 - Consultancy Project X Temp"}
</MenuItem>
<MenuItem value={"All"}>{t("All tasks")}</MenuItem>
{taskTemplates.map((template, index) => (
<MenuItem key={`${template.id}-${index}`} value={template.id}>
{template.name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
</Grid> </Grid>
<TransferList <TransferList
allItems={items} allItems={items}
initiallySelectedItems={selectedItems}
selectedItems={selectedItems}
onChange={(selectedTasks) => { onChange={(selectedTasks) => {
const newTasks = selectedTasks.reduce<CreateProjectInputs["tasks"]>( const newTasks = selectedTasks.reduce<CreateProjectInputs["tasks"]>(
(acc, item) => { (acc, item) => {
@@ -85,7 +123,7 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => {
selectedItemsLabel={t("Project Task List")} selectedItemsLabel={t("Project Task List")}
/> />
<CardActions sx={{ justifyContent: "flex-end" }}> <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
<Button variant="text" startIcon={<RestartAlt />} onClick={onReset}>
{t("Reset")} {t("Reset")}
</Button> </Button>
</CardActions> </CardActions>


+ 3
- 3
src/components/LoginPage/LoginPage.tsx Wyświetl plik

@@ -10,10 +10,10 @@ const LoginPage = () => {
<Grid item sm sx={{ backgroundColor: 'neutral.900'}}> <Grid item sm sx={{ backgroundColor: 'neutral.900'}}>
</Grid> </Grid>
<Grid item xs={12} sm={8} lg={5}> <Grid item xs={12} sm={8} lg={5}>
<Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', svg: { maxHeight: 120 } }}>
<Logo />
</Box>
<Paper square sx={{ height: "100%" }}> <Paper square sx={{ height: "100%" }}>
<Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', svg: { maxHeight: 120 } }}>
<Logo />
</Box>
<LoginForm /> <LoginForm />
</Paper> </Paper>
</Grid> </Grid>


+ 8
- 7
src/components/ProjectSearch/ProjectSearch.tsx Wyświetl plik

@@ -1,23 +1,24 @@
"use client"; "use client";


import { ProjectResult } from "@/app/api/projects";
import { ProjectCategory, ProjectResult } from "@/app/api/projects";
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox"; import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults"; import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote"; import EditNote from "@mui/icons-material/EditNote";
import uniq from 'lodash/uniq';


interface Props { interface Props {
projects: ProjectResult[]; projects: ProjectResult[];
projectCategories: ProjectCategory[];
} }


type SearchQuery = Partial<Omit<ProjectResult, "id">>; type SearchQuery = Partial<Omit<ProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery; type SearchParamNames = keyof SearchQuery;


const ProjectSearch: React.FC<Props> = ({ projects }) => {
const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
const { t } = useTranslation("projects"); const { t } = useTranslation("projects");


// If project searching is done on the server-side, then no need for this.
const [filteredProjects, setFilteredProjects] = useState(projects); const [filteredProjects, setFilteredProjects] = useState(projects);


const searchCriteria: Criterion<SearchParamNames>[] = useMemo( const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -28,22 +29,22 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => {
label: t("Client name"), label: t("Client name"),
paramName: "client", paramName: "client",
type: "select", type: "select",
options: ["Client A", "Client B", "Client C"],
options: uniq(projects.map((project) => project.client)),
}, },
{ {
label: t("Project category"), label: t("Project category"),
paramName: "category", paramName: "category",
type: "select", type: "select",
options: ["Confirmed Project", "Project to be bidded"],
options: projectCategories.map((category) => category.label),
}, },
{ {
label: t("Team"), label: t("Team"),
paramName: "team", paramName: "team",
type: "select", type: "select",
options: ["TW", "WY"],
options: uniq(projects.map((project) => project.team)),
}, },
], ],
[t],
[t, projectCategories, projects],
); );


const onReset = useCallback(() => { const onReset = useCallback(() => {


+ 3
- 2
src/components/ProjectSearch/ProjectSearchWrapper.tsx Wyświetl plik

@@ -1,4 +1,4 @@
import { fetchProjects } from "@/app/api/projects";
import { fetchProjectCategories, fetchProjects } from "@/app/api/projects";
import React from "react"; import React from "react";
import ProjectSearch from "./ProjectSearch"; import ProjectSearch from "./ProjectSearch";
import ProjectSearchLoading from "./ProjectSearchLoading"; import ProjectSearchLoading from "./ProjectSearchLoading";
@@ -8,9 +8,10 @@ interface SubComponents {
} }


const ProjectSearchWrapper: React.FC & SubComponents = async () => { const ProjectSearchWrapper: React.FC & SubComponents = async () => {
const projectCategories = await fetchProjectCategories();
const projects = await fetchProjects(); const projects = await fetchProjects();


return <ProjectSearch projects={projects} />;
return <ProjectSearch projects={projects} projectCategories={projectCategories} />;
}; };


ProjectSearchWrapper.Loading = ProjectSearchLoading; ProjectSearchWrapper.Loading = ProjectSearchLoading;


+ 24
- 22
src/components/TransferList/MultiSelectList.tsx Wyświetl plik

@@ -11,11 +11,11 @@ import {
ListSubheader, ListSubheader,
MenuItem, MenuItem,
Select, Select,
SelectProps,
SelectChangeEvent,
Stack, Stack,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback } from "react";
import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList"; import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
@@ -23,7 +23,7 @@ import groupBy from "lodash/groupBy";


export const MultiSelectList: React.FC<TransferListProps> = ({ export const MultiSelectList: React.FC<TransferListProps> = ({
allItems, allItems,
initiallySelectedItems,
selectedItems,
selectedItemsLabel, selectedItemsLabel,
allItemsLabel, allItemsLabel,
onChange, onChange,
@@ -39,33 +39,31 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
(a: number, b: number) => sortMap[a].index - sortMap[b].index, (a: number, b: number) => sortMap[a].index - sortMap[b].index,
[sortMap], [sortMap],
); );
const [selectedItems, setSelectedItems] = useState(
initiallySelectedItems.map((item) => item.id),

const handleChange = useCallback(
(event: SelectChangeEvent<number[]>) => {
const {
target: { value },
} = event;
const selectedValues =
typeof value === "string" ? [Number(value)] : value;

onChange(allItems.filter((item) => selectedValues.includes(item.id)));
},
[allItems, onChange],
); );
const handleChange = useCallback<
NonNullable<SelectProps<typeof selectedItems>["onChange"]>
>((event) => {
const {
target: { value },
} = event;
setSelectedItems(typeof value === "string" ? [Number(value)] : value);
}, []);


const handleToggleAll = useCallback( const handleToggleAll = useCallback(
() => () => { () => () => {
if (selectedItems.length === allItems.length) { if (selectedItems.length === allItems.length) {
setSelectedItems([]);
onChange([]);
} else { } else {
setSelectedItems(allItems.map((item) => item.id));
onChange(allItems);
} }
}, },
[allItems, selectedItems.length],
[allItems, onChange, selectedItems.length],
); );


useEffect(() => {
onChange(selectedItems.map((item) => sortMap[item]));
}, [onChange, selectedItems, sortMap]);

const { t } = useTranslation(); const { t } = useTranslation();
const groups: LabelGroup[] = uniqBy( const groups: LabelGroup[] = uniqBy(
[ [
@@ -85,7 +83,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
<InputLabel>{selectedItemsLabel}</InputLabel> <InputLabel>{selectedItemsLabel}</InputLabel>
<Select <Select
multiple multiple
value={selectedItems}
value={selectedItems.map((item) => item.id)}
onChange={handleChange} onChange={handleChange}
renderValue={(values) => { renderValue={(values) => {
return ( return (
@@ -153,7 +151,11 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
return ( return (
<MenuItem key={item.id} value={item.id} disableRipple> <MenuItem key={item.id} value={item.id} disableRipple>
<Checkbox <Checkbox
checked={selectedItems.includes(item.id)}
checked={Boolean(
selectedItems.find(
(selected) => selected.id === item.id,
),
)}
disableRipple disableRipple
/> />
<ListItemText sx={{ whiteSpace: "normal" }}> <ListItemText sx={{ whiteSpace: "normal" }}>


+ 11
- 14
src/components/TransferList/TransferList.tsx Wyświetl plik

@@ -33,7 +33,7 @@ export interface LabelWithId {


export interface TransferListProps { export interface TransferListProps {
allItems: LabelWithId[]; allItems: LabelWithId[];
initiallySelectedItems: LabelWithId[];
selectedItems: LabelWithId[];
onChange: (selectedItems: LabelWithId[]) => void; onChange: (selectedItems: LabelWithId[]) => void;
allItemsLabel: string; allItemsLabel: string;
selectedItemsLabel: string; selectedItemsLabel: string;
@@ -144,7 +144,7 @@ const ItemList: React.FC<ItemListProps> = ({


const TransferList: React.FC<TransferListProps> = ({ const TransferList: React.FC<TransferListProps> = ({
allItems, allItems,
initiallySelectedItems,
selectedItems,
allItemsLabel, allItemsLabel,
selectedItemsLabel, selectedItemsLabel,
onChange, onChange,
@@ -163,12 +163,15 @@ const TransferList: React.FC<TransferListProps> = ({


const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]); const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]);
const [leftList, setLeftList] = React.useState<LabelWithId[]>( const [leftList, setLeftList] = React.useState<LabelWithId[]>(
differenceBy(allItems, initiallySelectedItems, "id"),
);
const [rightList, setRightList] = React.useState<LabelWithId[]>(
initiallySelectedItems,
differenceBy(allItems, selectedItems, "id"),
); );


React.useEffect(() => {
setLeftList(differenceBy(allItems, selectedItems, "id"));
}, [allItems, selectedItems]);

const rightList = selectedItems;

const leftListChecked = intersection(checkedList, leftList); const leftListChecked = intersection(checkedList, leftList);
const rightListChecked = intersection(checkedList, rightList); const rightListChecked = intersection(checkedList, rightList);


@@ -196,23 +199,17 @@ const TransferList: React.FC<TransferListProps> = ({
); );


const handleCheckedRight = () => { const handleCheckedRight = () => {
setRightList([...rightList, ...leftListChecked].sort(compareFn));
onChange([...selectedItems, ...leftListChecked].sort(compareFn));
setLeftList(differenceBy(leftList, leftListChecked, "id").sort(compareFn)); setLeftList(differenceBy(leftList, leftListChecked, "id").sort(compareFn));
setCheckedList(differenceBy(checkedList, leftListChecked, "id")); setCheckedList(differenceBy(checkedList, leftListChecked, "id"));
}; };


const handleCheckedLeft = () => { const handleCheckedLeft = () => {
setLeftList([...leftList, ...rightListChecked].sort(compareFn)); setLeftList([...leftList, ...rightListChecked].sort(compareFn));
setRightList(
differenceBy(rightList, rightListChecked, "id").sort(compareFn),
);
onChange(differenceBy(rightList, rightListChecked, "id").sort(compareFn));
setCheckedList(differenceBy(checkedList, rightListChecked, "id")); setCheckedList(differenceBy(checkedList, rightListChecked, "id"));
}; };


React.useEffect(() => {
onChange(rightList);
}, [onChange, rightList]);

return ( return (
<Stack spacing={2} direction="row" alignItems="center" position="relative"> <Stack spacing={2} direction="row" alignItems="center" position="relative">
<ItemList <ItemList


Ładowanie…
Anuluj
Zapisz