feat: projects redux storage and select
This commit is contained in:
@ -48,6 +48,7 @@
|
|||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/eslint-plugin-jsx-a11y": "^6",
|
"@types/eslint-plugin-jsx-a11y": "^6",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.11",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
|
|||||||
42
src/app/deals/components/Header/Header.tsx
Normal file
42
src/app/deals/components/Header/Header.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
|
import {
|
||||||
|
selectProject,
|
||||||
|
setProjects,
|
||||||
|
} from "@/lib/features/projects/projectsSlice";
|
||||||
|
import { RootState, useAppDispatch } from "@/lib/store";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const projectsState = useSelector(
|
||||||
|
(state: RootState) => state.projectsState
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mockProjects = [
|
||||||
|
{ id: 1, name: "Проект 1" },
|
||||||
|
{ id: 2, name: "Проект 2" },
|
||||||
|
{ id: 3, name: "Проект 3" },
|
||||||
|
];
|
||||||
|
dispatch(setProjects(mockProjects));
|
||||||
|
dispatch(selectProject(mockProjects[0]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={"flex-end"}
|
||||||
|
w={"100%"}>
|
||||||
|
<ProjectSelect
|
||||||
|
data={projectsState.projects}
|
||||||
|
value={projectsState.selectedProject}
|
||||||
|
onChange={value => value && dispatch(selectProject(value))}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import Boards from "@/app/deals/components/Boards/Boards";
|
import Boards from "@/app/deals/components/Boards/Boards";
|
||||||
|
import Header from "@/app/deals/components/Header/Header";
|
||||||
import PageBlock from "@/components/PageBlock/PageBlock";
|
import PageBlock from "@/components/PageBlock/PageBlock";
|
||||||
import PageContainer from "@/components/PageContainer/PageContainer";
|
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ export default function DealsPage() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageBlock>
|
<PageBlock>
|
||||||
|
<Header />
|
||||||
<Boards />
|
<Boards />
|
||||||
</PageBlock>
|
</PageBlock>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
110
src/components/selects/ObjectSelect/ObjectSelect.tsx
Normal file
110
src/components/selects/ObjectSelect/ObjectSelect.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { groupBy, omit } from "lodash";
|
||||||
|
import { Select, SelectProps } from "@mantine/core";
|
||||||
|
|
||||||
|
interface ObjectWithIdAndName {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectObjectType<T> = T;
|
||||||
|
|
||||||
|
type ControlledValueProps<T> = {
|
||||||
|
value: SelectObjectType<T>;
|
||||||
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
|
};
|
||||||
|
type CustomLabelAndKeyProps<T> = {
|
||||||
|
getLabelFn: (item: SelectObjectType<T>) => string;
|
||||||
|
getValueFn: (item: SelectObjectType<T>) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RestProps<T> = {
|
||||||
|
defaultValue?: SelectObjectType<T>;
|
||||||
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
|
data: SelectObjectType<T>[];
|
||||||
|
groupBy?: (item: SelectObjectType<T>) => string;
|
||||||
|
filterBy?: (item: SelectObjectType<T>) => boolean;
|
||||||
|
};
|
||||||
|
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||||
|
return item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||||
|
if (!item) return item;
|
||||||
|
return item.id.toString();
|
||||||
|
};
|
||||||
|
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||||
|
Partial<ControlledValueProps<T>>) &
|
||||||
|
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||||
|
(T extends ObjectWithIdAndName
|
||||||
|
? Partial<CustomLabelAndKeyProps<T>>
|
||||||
|
: CustomLabelAndKeyProps<T>);
|
||||||
|
|
||||||
|
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
|
||||||
|
const isControlled = "value" in props;
|
||||||
|
const haveGetValueFn = "getValueFn" in props;
|
||||||
|
const haveGetLabelFn = "getLabelFn" in props;
|
||||||
|
const [internalValue, setInternalValue] = useState<
|
||||||
|
SelectObjectType<T> | undefined
|
||||||
|
>(props.defaultValue);
|
||||||
|
|
||||||
|
const value = isControlled ? props.value : internalValue;
|
||||||
|
|
||||||
|
const getValueFn =
|
||||||
|
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||||
|
const getLabelFn =
|
||||||
|
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
const propsData = props.filterBy
|
||||||
|
? props.data.filter(props.filterBy)
|
||||||
|
: props.data;
|
||||||
|
if (props.groupBy) {
|
||||||
|
const groupedData = groupBy(propsData, props.groupBy);
|
||||||
|
return Object.entries(groupedData).map(([group, items]) => ({
|
||||||
|
group,
|
||||||
|
items: items.map(item => ({
|
||||||
|
label: getLabelFn(item),
|
||||||
|
value: getValueFn(item),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return propsData.map(item => ({
|
||||||
|
label: getLabelFn(item),
|
||||||
|
value: getValueFn(item),
|
||||||
|
}));
|
||||||
|
}, [props.data, props.groupBy]);
|
||||||
|
|
||||||
|
const handleOnChange = (event: string | null) => {
|
||||||
|
if (!event) return;
|
||||||
|
const object = props.data.find(item => event === getValueFn(item));
|
||||||
|
if (!object) return;
|
||||||
|
if (isControlled) {
|
||||||
|
props.onChange(object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInternalValue(object);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isControlled || !internalValue) return;
|
||||||
|
props.onChange(internalValue);
|
||||||
|
}, [internalValue]);
|
||||||
|
|
||||||
|
const restProps = omit(props, [
|
||||||
|
"filterBy",
|
||||||
|
"groupBy",
|
||||||
|
"getValueFn",
|
||||||
|
"getLabelFn",
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...restProps}
|
||||||
|
value={value && getValueFn(value)}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectSelect;
|
||||||
26
src/components/selects/ProjectSelect/ProjectSelect.tsx
Normal file
26
src/components/selects/ProjectSelect/ProjectSelect.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { ProjectSchema } from "@/types/ProjectSchema";
|
||||||
|
import ObjectSelect, { ObjectSelectProps } from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<ProjectSchema | null>,
|
||||||
|
"getLabelFn" | "getValueFn"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const ProjectSelect: FC<Props> = ({ data, ...props }) => {
|
||||||
|
const onClear = () => props.onChange(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
data={data}
|
||||||
|
searchable
|
||||||
|
placeholder={"Выберите проект"}
|
||||||
|
onClear={onClear}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectSelect;
|
||||||
29
src/lib/features/projects/projectsSlice.ts
Normal file
29
src/lib/features/projects/projectsSlice.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { ProjectSchema } from "@/types/ProjectSchema";
|
||||||
|
|
||||||
|
interface ProjectsState {
|
||||||
|
projects: ProjectSchema[];
|
||||||
|
selectedProject: ProjectSchema | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ProjectsState = {
|
||||||
|
projects: [],
|
||||||
|
selectedProject: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectsSlice = createSlice({
|
||||||
|
name: "projects",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setProjects: (state, action: PayloadAction<ProjectSchema[]>) => {
|
||||||
|
state.projects = action.payload;
|
||||||
|
},
|
||||||
|
selectProject: (state, action: PayloadAction<ProjectSchema>) => {
|
||||||
|
state.selectedProject = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setProjects, selectProject } = projectsSlice.actions;
|
||||||
|
|
||||||
|
export default projectsSlice.reducer;
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { combineReducers } from "@reduxjs/toolkit";
|
import { combineReducers } from "@reduxjs/toolkit";
|
||||||
import authReducer from "@/lib/features/auth/authSlice";
|
import authReducer from "@/lib/features/auth/authSlice";
|
||||||
|
import projectsReducer from "@/lib/features/projects/projectsSlice";
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
|
projectsState: projectsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|||||||
@ -13,8 +13,8 @@ export const myColor: MantineColorsTuple = [
|
|||||||
"#00718c",
|
"#00718c",
|
||||||
];
|
];
|
||||||
|
|
||||||
const radius = "lg";
|
const radius = "md";
|
||||||
const size = "lg";
|
const size = "md";
|
||||||
|
|
||||||
export const theme = createTheme({
|
export const theme = createTheme({
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
4
src/types/ProjectSchema.ts
Normal file
4
src/types/ProjectSchema.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type ProjectSchema = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@ -3904,6 +3904,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/lodash@npm:^4.17.20":
|
||||||
|
version: 4.17.20
|
||||||
|
resolution: "@types/lodash@npm:4.17.20"
|
||||||
|
checksum: 10c0/98cdd0faae22cbb8079a01a3bb65aa8f8c41143367486c1cbf5adc83f16c9272a2a5d2c1f541f61d0d73da543c16ee1d21cf2ef86cb93cd0cc0ac3bced6dd88f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/node@npm:*":
|
"@types/node@npm:*":
|
||||||
version: 24.1.0
|
version: 24.1.0
|
||||||
resolution: "@types/node@npm:24.1.0"
|
resolution: "@types/node@npm:24.1.0"
|
||||||
@ -5919,6 +5926,7 @@ __metadata:
|
|||||||
"@testing-library/user-event": "npm:^14.6.1"
|
"@testing-library/user-event": "npm:^14.6.1"
|
||||||
"@types/eslint-plugin-jsx-a11y": "npm:^6"
|
"@types/eslint-plugin-jsx-a11y": "npm:^6"
|
||||||
"@types/jest": "npm:^29.5.14"
|
"@types/jest": "npm:^29.5.14"
|
||||||
|
"@types/lodash": "npm:^4.17.20"
|
||||||
"@types/node": "npm:^22.13.11"
|
"@types/node": "npm:^22.13.11"
|
||||||
"@types/react": "npm:19.1.8"
|
"@types/react": "npm:19.1.8"
|
||||||
"@types/react-redux": "npm:^7.1.34"
|
"@types/react-redux": "npm:^7.1.34"
|
||||||
|
|||||||
Reference in New Issue
Block a user