@@ -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 = { | ||||
@@ -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> | ||||
@@ -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 | ||||
@@ -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, | ||||
@@ -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 = () => { | ||||
@@ -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}> | ||||
@@ -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; |
@@ -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}> | ||||
@@ -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")} | ||||
@@ -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> | ||||
@@ -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> | ||||
@@ -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(() => { | ||||
@@ -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; | ||||
@@ -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" }}> | ||||
@@ -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 | ||||