| @@ -5,6 +5,7 @@ const withPWA = require("next-pwa")({ | |||
| dest: "public", | |||
| register: true, | |||
| skipWaiting: true, | |||
| disable: process.env.NODE_ENV === 'development' | |||
| }); | |||
| 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 { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import Typography from "@mui/material/Typography"; | |||
| @@ -10,6 +12,11 @@ export const metadata: Metadata = { | |||
| const Projects: React.FC = async () => { | |||
| const { t } = await getServerI18n("projects"); | |||
| // Preload necessary dependencies | |||
| fetchAllTasks(); | |||
| fetchTaskTemplates(); | |||
| fetchProjectCategories(); | |||
| return ( | |||
| <> | |||
| <Typography variant="h4">{t("Create Project")}</Typography> | |||
| @@ -8,7 +8,7 @@ export interface CreateProjectInputs { | |||
| // Project details | |||
| projectCode: string; | |||
| projectName: string; | |||
| projectCategory: string; | |||
| projectCategoryId: number; | |||
| projectDescription: string; | |||
| // Client details | |||
| @@ -10,7 +10,13 @@ export interface ProjectResult { | |||
| client: string; | |||
| } | |||
| export interface ProjectCategory { | |||
| id: number; | |||
| label: string; | |||
| } | |||
| export const preloadProjects = () => { | |||
| fetchProjectCategories(); | |||
| fetchProjects(); | |||
| }; | |||
| @@ -18,6 +24,15 @@ export const fetchProjects = cache(async () => { | |||
| 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[] = [ | |||
| { | |||
| id: 1, | |||
| @@ -19,6 +19,7 @@ export interface TaskTemplate { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| tasks: Task[]; | |||
| } | |||
| export const preloadTaskTemplates = () => { | |||
| @@ -13,7 +13,7 @@ import ProjectClientDetails from "./ProjectClientDetails"; | |||
| import TaskSetup from "./TaskSetup"; | |||
| import StaffAllocation from "./StaffAllocation"; | |||
| import ResourceMilestone from "./ResourceMilestone"; | |||
| import { Task } from "@/app/api/tasks"; | |||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
| import { | |||
| FieldErrors, | |||
| FormProvider, | |||
| @@ -23,9 +23,12 @@ import { | |||
| } from "react-hook-form"; | |||
| import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
| import { Error } from "@mui/icons-material"; | |||
| import { ProjectCategory } from "@/app/api/projects"; | |||
| export interface Props { | |||
| allTasks: Task[]; | |||
| projectCategories: ProjectCategory[]; | |||
| taskTemplates: TaskTemplate[]; | |||
| } | |||
| 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 { t } = useTranslation(); | |||
| const router = useRouter(); | |||
| @@ -101,8 +108,19 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => { | |||
| <Tab label={t("Staff Allocation")} iconPosition="end" /> | |||
| <Tab label={t("Resource and Milestone")} iconPosition="end" /> | |||
| </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} />} | |||
| {<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||
| <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 { fetchProjectCategories } from "@/app/api/projects"; | |||
| const CreateProjectWrapper: React.FC = async () => { | |||
| 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; | |||
| @@ -15,16 +15,24 @@ import { useTranslation } from "react-i18next"; | |||
| import CardActions from "@mui/material/CardActions"; | |||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| 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 { ProjectCategory } from "@/app/api/projects"; | |||
| const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({ | |||
| interface Props { | |||
| isActive: boolean; | |||
| projectCategories: ProjectCategory[]; | |||
| } | |||
| const ProjectClientDetails: React.FC<Props> = ({ | |||
| isActive, | |||
| projectCategories, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| register, | |||
| formState: { errors }, | |||
| control, | |||
| } = useFormContext<CreateProjectInputs>(); | |||
| return ( | |||
| @@ -55,14 +63,23 @@ const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({ | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <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> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -81,11 +81,15 @@ const ResourceMilestone: React.FC<Props> = ({ | |||
| ))} | |||
| </Select> | |||
| </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" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| {t("Reset")} | |||
| @@ -7,31 +7,65 @@ import Typography from "@mui/material/Typography"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import TransferList from "../TransferList"; | |||
| 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 RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| 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 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 { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
| import isNumber from "lodash/isNumber"; | |||
| interface Props { | |||
| allTasks: Task[]; | |||
| taskTemplates: TaskTemplate[]; | |||
| isActive: boolean; | |||
| } | |||
| const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => { | |||
| const TaskSetup: React.FC<Props> = ({ | |||
| allTasks: tasks, | |||
| taskTemplates, | |||
| isActive, | |||
| }) => { | |||
| 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(() => { | |||
| return tasks | |||
| .filter((t) => currentTasks[t.id]) | |||
| @@ -53,20 +87,24 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => { | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Task List Source")}</InputLabel> | |||
| <Select | |||
| <Select<"All" | number> | |||
| 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> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| <TransferList | |||
| allItems={items} | |||
| initiallySelectedItems={selectedItems} | |||
| selectedItems={selectedItems} | |||
| onChange={(selectedTasks) => { | |||
| const newTasks = selectedTasks.reduce<CreateProjectInputs["tasks"]>( | |||
| (acc, item) => { | |||
| @@ -85,7 +123,7 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => { | |||
| selectedItemsLabel={t("Project Task List")} | |||
| /> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| <Button variant="text" startIcon={<RestartAlt />} onClick={onReset}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> | |||
| @@ -10,10 +10,10 @@ const LoginPage = () => { | |||
| <Grid item sm sx={{ backgroundColor: 'neutral.900'}}> | |||
| </Grid> | |||
| <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%" }}> | |||
| <Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', svg: { maxHeight: 120 } }}> | |||
| <Logo /> | |||
| </Box> | |||
| <LoginForm /> | |||
| </Paper> | |||
| </Grid> | |||
| @@ -1,23 +1,24 @@ | |||
| "use client"; | |||
| import { ProjectResult } from "@/app/api/projects"; | |||
| import { ProjectCategory, ProjectResult } from "@/app/api/projects"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import EditNote from "@mui/icons-material/EditNote"; | |||
| import uniq from 'lodash/uniq'; | |||
| interface Props { | |||
| projects: ProjectResult[]; | |||
| projectCategories: ProjectCategory[]; | |||
| } | |||
| type SearchQuery = Partial<Omit<ProjectResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||
| const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => { | |||
| const { t } = useTranslation("projects"); | |||
| // If project searching is done on the server-side, then no need for this. | |||
| const [filteredProjects, setFilteredProjects] = useState(projects); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| @@ -28,22 +29,22 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||
| label: t("Client name"), | |||
| paramName: "client", | |||
| type: "select", | |||
| options: ["Client A", "Client B", "Client C"], | |||
| options: uniq(projects.map((project) => project.client)), | |||
| }, | |||
| { | |||
| label: t("Project category"), | |||
| paramName: "category", | |||
| type: "select", | |||
| options: ["Confirmed Project", "Project to be bidded"], | |||
| options: projectCategories.map((category) => category.label), | |||
| }, | |||
| { | |||
| label: t("Team"), | |||
| paramName: "team", | |||
| type: "select", | |||
| options: ["TW", "WY"], | |||
| options: uniq(projects.map((project) => project.team)), | |||
| }, | |||
| ], | |||
| [t], | |||
| [t, projectCategories, projects], | |||
| ); | |||
| 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 ProjectSearch from "./ProjectSearch"; | |||
| import ProjectSearchLoading from "./ProjectSearchLoading"; | |||
| @@ -8,9 +8,10 @@ interface SubComponents { | |||
| } | |||
| const ProjectSearchWrapper: React.FC & SubComponents = async () => { | |||
| const projectCategories = await fetchProjectCategories(); | |||
| const projects = await fetchProjects(); | |||
| return <ProjectSearch projects={projects} />; | |||
| return <ProjectSearch projects={projects} projectCategories={projectCategories} />; | |||
| }; | |||
| ProjectSearchWrapper.Loading = ProjectSearchLoading; | |||
| @@ -11,11 +11,11 @@ import { | |||
| ListSubheader, | |||
| MenuItem, | |||
| Select, | |||
| SelectProps, | |||
| SelectChangeEvent, | |||
| Stack, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import React, { useCallback } from "react"; | |||
| import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import uniqBy from "lodash/uniqBy"; | |||
| @@ -23,7 +23,7 @@ import groupBy from "lodash/groupBy"; | |||
| export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| allItems, | |||
| initiallySelectedItems, | |||
| selectedItems, | |||
| selectedItemsLabel, | |||
| allItemsLabel, | |||
| onChange, | |||
| @@ -39,33 +39,31 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| (a: number, b: number) => sortMap[a].index - sortMap[b].index, | |||
| [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( | |||
| () => () => { | |||
| if (selectedItems.length === allItems.length) { | |||
| setSelectedItems([]); | |||
| onChange([]); | |||
| } 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 groups: LabelGroup[] = uniqBy( | |||
| [ | |||
| @@ -85,7 +83,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| <InputLabel>{selectedItemsLabel}</InputLabel> | |||
| <Select | |||
| multiple | |||
| value={selectedItems} | |||
| value={selectedItems.map((item) => item.id)} | |||
| onChange={handleChange} | |||
| renderValue={(values) => { | |||
| return ( | |||
| @@ -153,7 +151,11 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| return ( | |||
| <MenuItem key={item.id} value={item.id} disableRipple> | |||
| <Checkbox | |||
| checked={selectedItems.includes(item.id)} | |||
| checked={Boolean( | |||
| selectedItems.find( | |||
| (selected) => selected.id === item.id, | |||
| ), | |||
| )} | |||
| disableRipple | |||
| /> | |||
| <ListItemText sx={{ whiteSpace: "normal" }}> | |||
| @@ -33,7 +33,7 @@ export interface LabelWithId { | |||
| export interface TransferListProps { | |||
| allItems: LabelWithId[]; | |||
| initiallySelectedItems: LabelWithId[]; | |||
| selectedItems: LabelWithId[]; | |||
| onChange: (selectedItems: LabelWithId[]) => void; | |||
| allItemsLabel: string; | |||
| selectedItemsLabel: string; | |||
| @@ -144,7 +144,7 @@ const ItemList: React.FC<ItemListProps> = ({ | |||
| const TransferList: React.FC<TransferListProps> = ({ | |||
| allItems, | |||
| initiallySelectedItems, | |||
| selectedItems, | |||
| allItemsLabel, | |||
| selectedItemsLabel, | |||
| onChange, | |||
| @@ -163,12 +163,15 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
| const [checkedList, setCheckedList] = 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 rightListChecked = intersection(checkedList, rightList); | |||
| @@ -196,23 +199,17 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
| ); | |||
| const handleCheckedRight = () => { | |||
| setRightList([...rightList, ...leftListChecked].sort(compareFn)); | |||
| onChange([...selectedItems, ...leftListChecked].sort(compareFn)); | |||
| setLeftList(differenceBy(leftList, leftListChecked, "id").sort(compareFn)); | |||
| setCheckedList(differenceBy(checkedList, leftListChecked, "id")); | |||
| }; | |||
| const handleCheckedLeft = () => { | |||
| setLeftList([...leftList, ...rightListChecked].sort(compareFn)); | |||
| setRightList( | |||
| differenceBy(rightList, rightListChecked, "id").sort(compareFn), | |||
| ); | |||
| onChange(differenceBy(rightList, rightListChecked, "id").sort(compareFn)); | |||
| setCheckedList(differenceBy(checkedList, rightListChecked, "id")); | |||
| }; | |||
| React.useEffect(() => { | |||
| onChange(rightList); | |||
| }, [onChange, rightList]); | |||
| return ( | |||
| <Stack spacing={2} direction="row" alignItems="center" position="relative"> | |||
| <ItemList | |||