Browse Source

看板

master
YuJian920 3 years ago
parent
commit
00e26073f8
  1. BIN
      src/assets/bug.png
  2. BIN
      src/assets/task.png
  3. 9
      src/components/ProjectModal.tsx
  4. 10
      src/components/TaskTypeSelect.tsx
  5. 10
      src/context/auth-context.tsx
  6. 26
      src/hook/useKanban.ts
  7. 2
      src/hook/useProjectModal.ts
  8. 14
      src/hook/useProjects.ts
  9. 44
      src/hook/useTask.ts
  10. 48
      src/pages/Kanban/KanbanColumn/index.tsx
  11. 45
      src/pages/Kanban/SearchPanel/index.tsx
  12. 31
      src/pages/Kanban/index.tsx
  13. 32
      src/pages/Kanban/style.ts
  14. 7
      src/pages/Project/Kanban/index.tsx
  15. 5
      src/pages/Project/index.tsx
  16. 18
      src/pages/Project/style.ts
  17. 61
      src/pages/ProjectList/List/index.tsx
  18. 22
      src/type/index.ts
  19. 2
      src/utils/request.ts

BIN
src/assets/bug.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
src/assets/task.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

9
src/components/ProjectModal.tsx

@ -20,6 +20,11 @@ const ProjectModal = () => { @@ -20,6 +20,11 @@ const ProjectModal = () => {
});
};
const closeModal = () => {
form.resetFields();
close();
};
const title = editeingProject ? "编辑项目" : "创建项目";
useEffect(() => {
@ -29,7 +34,7 @@ const ProjectModal = () => { @@ -29,7 +34,7 @@ const ProjectModal = () => {
return (
<Drawer
forceRender={true}
onClose={close}
onClose={closeModal}
visible={projectModalOpen}
width="100%"
>
@ -65,7 +70,7 @@ const ProjectModal = () => { @@ -65,7 +70,7 @@ const ProjectModal = () => {
<Form.Item label="负责人" name="personId">
<UserSelect defaultOptionName="负责人" />
</Form.Item>
<Form.Item style={{ textAlign: 'right' }}>
<Form.Item style={{ textAlign: "right" }}>
<Button
loading={mutateLoading}
type="primary"

10
src/components/TaskTypeSelect.tsx

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import React from "react";
import { useTasksTypes } from "../hook/useTask";
import IdSelect from "./IdSelect";
const TaskTypeSelect = (props: React.ComponentProps<typeof IdSelect>) => {
const { data: taskTypes } = useTasksTypes();
return <IdSelect options={taskTypes || []} {...props} />;
};
export default React.memo(TaskTypeSelect);

10
src/context/auth-context.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import React, { useState, useContext, ReactNode } from "react";
import React, { useContext, ReactNode } from "react";
import { useQueryClient } from "react-query";
import { FullPageLoading } from "../components/lib";
import useAsync from "../hook/useAsync";
import useMount from "../hook/useMount";
@ -32,6 +33,7 @@ const bootstrapUser = async () => { @@ -32,6 +33,7 @@ const bootstrapUser = async () => {
};
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const queryClient = useQueryClient();
const {
data: user,
isLoading,
@ -42,7 +44,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { @@ -42,7 +44,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const login = (form: AuthForm) => auth.login(form).then(setUser);
const register = (form: AuthForm) => auth.register(form).then(setUser);
const logout = () => auth.logout().then(() => setUser(null));
const logout = () =>
auth.logout().then(() => {
setUser(null);
queryClient.clear();
});
useMount(() => {
run(bootstrapUser());

26
src/hook/useKanban.ts

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
import { useQuery } from "react-query";
import { useLocation } from "react-router";
import { Kanban } from "../type";
import { useProject } from "./useProjects";
import useRequest from "./useRequest";
export const useKanbans = (param?: Partial<Kanban>) => {
const request = useRequest();
return useQuery<Kanban[], Error>(["kanbans", param], () =>
request("/kanbans", { data: param })
);
};
export const useProjectIdInUrl = () => {
const { pathname } = useLocation();
const id = pathname.match(/projects\/(\d+)/)?.[1];
return Number(id);
};
export const useProjectInUrl = () => useProject(useProjectIdInUrl());
export const useKanbanSearchParams = () => ({ projectId: useProjectIdInUrl() });
export const useKanbansQueryKey = () => ["kanbans", useKanbanSearchParams()];

2
src/hook/useProjectModal.ts

@ -21,7 +21,7 @@ const useProjectModal = () => { @@ -21,7 +21,7 @@ const useProjectModal = () => {
setEditingProjectId({ editeingProjectId: id });
return {
projectModalOpen: projectCreate === "true" || Boolean(editeingProject),
projectModalOpen: projectCreate === "true" || Boolean(editeingProjectId),
open,
close,
startEdit,

14
src/hook/useProjects.ts

@ -31,15 +31,25 @@ const useAddProject = () => { @@ -31,15 +31,25 @@ const useAddProject = () => {
);
};
const useDeleteProject = () => {
const request = useRequest();
const queryClient = useQueryClient();
return useMutation(
(id: number) => request(`/projects/${id}`, { method: "DELETE" }),
{ onSuccess: () => queryClient.invalidateQueries("projects") }
);
};
const useProject = (id?: number) => {
const request = useRequest();
return (
useQuery<Project>(
["project", { id }],
() => request(`/projects/${id}`, {}),
() => request(`/projects/${id}`),
{ enabled: !!id }
)
);
};
export { useProjects, useEditProject, useAddProject, useProject };
export { useProjects, useEditProject, useAddProject, useProject, useDeleteProject };

44
src/hook/useTask.ts

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
import { useMemo } from "react";
import { useQuery } from "react-query";
import { Task, TaskType } from "../type";
import { useProjectIdInUrl } from "./useKanban";
import useRequest from "./useRequest";
import useUrlQueryParams from "./useUrlQueryParams";
export const useTasks = (param?: Partial<Task>) => {
const request = useRequest();
return useQuery<Task[]>(["tasks", param], () =>
request("/tasks", { data: param })
);
};
export const useTasksTypes = (param?: Partial<Task>) => {
const request = useRequest();
return useQuery<TaskType[]>(["taskTypes", param], () =>
request("/taskTypes")
);
};
export const useTasksSearchParmas = () => {
const [param, setParam] = useUrlQueryParams([
"name",
"typeId",
"processorId",
"tagId",
]);
const projectId = useProjectIdInUrl();
return useMemo(
() => ({
projectId,
typeId: Number(param.typeId) || undefined,
processorId: Number(param.processorId) || undefined,
tagId: Number(param.tagId) || undefined,
name: param.name,
}),
[projectId, param]
);
};
export const useTasksQueryKey = () => ["tasks", useTasksSearchParmas()];

48
src/pages/Kanban/KanbanColumn/index.tsx

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
import { Card } from "antd";
import React from "react";
import {
useTasks,
useTasksSearchParmas,
useTasksTypes,
} from "../../../hook/useTask";
import { Kanban } from "../../../type";
import { Container, TasksContainer } from "../style";
const TaskTypeIcon = ({ id }: { id: number }) => {
const { data: taskTypes } = useTasksTypes();
const name = taskTypes?.find((taskType) => taskType.id === id)?.name;
if (!name) return null;
else
return (
<img
src={
name === "task"
? require("../../../assets/task.png")
: require("../../../assets/bug.png")
}
alt="icon"
/>
);
};
const KanbanColumn = ({ kanban }: { kanban: Kanban }) => {
const { data: allTasks } = useTasks(useTasksSearchParmas());
const tasks = allTasks?.filter((task) => task.kanbanId === kanban.id);
return (
<Container>
<h3>{kanban.name}</h3>
<TasksContainer>
{tasks?.map((task) => (
<Card style={{ marginBottom: "0.5rem" }} key={task.id}>
<div>{task.name}</div>
<TaskTypeIcon id={task.typeId} />
</Card>
))}
</TasksContainer>
</Container>
);
};
export default React.memo(KanbanColumn);

45
src/pages/Kanban/SearchPanel/index.tsx

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
import { Button, Input } from "antd";
import React from "react";
import { Row } from "../../../components/lib";
import TaskTypeSelect from "../../../components/TaskTypeSelect";
import UserSelect from "../../../components/UserSelect";
import useSetUrlSearchParam from "../../../hook/useSetUrlSearchParam";
import { useTasksSearchParmas } from "../../../hook/useTask";
const SearchPanel = () => {
const searchParmas = useTasksSearchParmas();
const setSearchParams = useSetUrlSearchParam();
const reset = () => {
setSearchParams({
typeId: undefined,
processorId: undefined,
tagId: undefined,
name: undefined,
});
};
return (
<Row marginBottom={4} gap={true}>
<Input
style={{ width: "20rem" }}
placeholder="任务名"
value={searchParmas.name}
onChange={(evt) => setSearchParams({ name: evt.target.value })}
/>
<UserSelect
defaultOptionName="经办人"
value={searchParmas.processorId}
onChange={(value) => setSearchParams({ processorId: value })}
/>
<TaskTypeSelect
defaultOptionName="类型"
value={searchParmas.typeId}
onChange={(value) => setSearchParams({ typeId: value })}
/>
<Button onClick={reset}></Button>
</Row>
);
};
export default React.memo(SearchPanel);

31
src/pages/Kanban/index.tsx

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
import React from "react";
import useDocumentTitle from "../../hook/useDocumentTitle";
import {
useKanbans,
useKanbanSearchParams,
useProjectInUrl,
} from "../../hook/useKanban";
import KanbanColumn from "./KanbanColumn";
import SearchPanel from "./SearchPanel";
import { ScreenContainer, ColumnsContainer } from "./style";
const Kanban = () => {
useDocumentTitle("看板列表");
const { data: currentProject } = useProjectInUrl();
const { data: kanbans } = useKanbans(useKanbanSearchParams());
return (
<ScreenContainer>
<h1>{currentProject?.name}</h1>
<SearchPanel />
<ColumnsContainer>
{kanbans?.map((kanban) => (
<KanbanColumn kanban={kanban} key={kanban.id} />
))}
</ColumnsContainer>
</ScreenContainer>
);
};
export default Kanban;

32
src/pages/Kanban/style.ts

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
import styled from "@emotion/styled";
export const ScreenContainer = styled.div`
padding: 3.2rem;
width: 100%;
display: flex;
flex-direction: column;
`;
export const ColumnsContainer = styled.div`
display: flex;
overflow: hidden;
margin-right: 2rem;
`;
export const Container = styled.div`
min-width: 27rem;
border-radius: 6px;
background-color: rgb(244, 245, 247);
display: flex;
flex-direction: column;
padding: 0.7rem 0.7rem 1rem;
margin-right: 1.5rem;
`;
export const TasksContainer = styled.div`
overflow: scroll;
flex: 1;
::-webkit-scrollbar {
display: none;
}
`;

7
src/pages/Project/Kanban/index.tsx

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
import React from "react";
const Kanban = () => {
return <div>Kanban</div>;
};
export default Kanban;

5
src/pages/Project/index.tsx

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
import React from "react";
import { Routes, Route, Navigate } from "react-router";
import { Link } from "react-router-dom";
import Kanban from "./Kanban";
import Kanban from "../Kanban";
import Task from "./Task";
import { Aside, Main, Container } from "./style"
const Project = () => {
return (
@ -15,7 +16,7 @@ const Project = () => { @@ -15,7 +16,7 @@ const Project = () => {
<Route path="/task" element={<Task />} />
<Route
path="*"
element={<Navigate to={window.location.pathname + "/kanban"} />}
element={<Navigate to={window.location.pathname + "/kanban"} replace={true} />}
/>
</Routes>
</div>

18
src/pages/Project/style.ts

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
import styled from "@emotion/styled";
export const Aside = styled.aside`
background-color: rgb(244, 245, 247);
display: flex;
`;
export const Main = styled.div`
box-shadow: -5px 0 5px -5px rgba(0, 0, 0, 0.1);
display: flex;
overflow: hidden;
`;
export const Container = styled.div`
display: grid;
grid-template-columns: 16rem 1fr;
width: 100%;
`;

61
src/pages/ProjectList/List/index.tsx

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
import React from "react";
import { Link } from "react-router-dom";
import { Button, Dropdown, Menu, Table, TableProps } from "antd";
import { Button, Dropdown, Menu, Modal, Table, TableProps } from "antd";
import dayjs from "dayjs";
import { Project, User } from "../../../type";
import Pin from "../../../components/Pin";
import { useEditProject } from "../../../hook/useProjects";
import { useDeleteProject, useEditProject } from "../../../hook/useProjects";
import useProjectModal from "../../../hook/useProjectModal";
interface ListProps extends TableProps<Project> {
users: User[];
@ -12,9 +12,8 @@ interface ListProps extends TableProps<Project> { @@ -12,9 +12,8 @@ interface ListProps extends TableProps<Project> {
const ProjectList = ({ users, ...props }: ListProps) => {
const { mutate } = useEditProject();
const { startEdit } = useProjectModal()
const pinProject = (id: number) => (pin: boolean) => mutate({ id, pin });
const editProject = (id: number) => startEdit(id);
return (
<Table
@ -67,20 +66,7 @@ const ProjectList = ({ users, ...props }: ListProps) => { @@ -67,20 +66,7 @@ const ProjectList = ({ users, ...props }: ListProps) => {
},
{
render: (value, project) => {
return (
<Dropdown
overlay={
<Menu>
<Menu.Item onClick={() => editProject(project.id)} key="edit"></Menu.Item>
<Menu.Item onClick={() => {}} key="delete"></Menu.Item>
</Menu>
}
>
<Button type="link" style={{ padding: 0 }}>
...
</Button>
</Dropdown>
);
return <More project={project} />;
},
},
]}
@ -90,4 +76,43 @@ const ProjectList = ({ users, ...props }: ListProps) => { @@ -90,4 +76,43 @@ const ProjectList = ({ users, ...props }: ListProps) => {
);
};
const More = ({ project }: { project: Project }) => {
const { startEdit } = useProjectModal();
const editProject = (id: number) => startEdit(id);
const { mutateAsync: deleteProject } = useDeleteProject();
const confimDeleteProject = (id: number) => {
Modal.confirm({
title: "确认删除?",
content: "删除后不可恢复",
okText: "确认",
onOk: async () => {
await deleteProject(id);
},
});
};
return (
<Dropdown
overlay={
<Menu>
<Menu.Item onClick={() => editProject(project.id)} key="edit">
</Menu.Item>
<Menu.Item
onClick={() => confimDeleteProject(project.id)}
key="delete"
>
</Menu.Item>
</Menu>
}
>
<Button type="link" style={{ padding: 0 }}>
...
</Button>
</Dropdown>
);
};
export default React.memo(ProjectList);

22
src/type/index.ts

@ -15,3 +15,25 @@ export interface Project { @@ -15,3 +15,25 @@ export interface Project {
organization: string;
created: number;
}
export interface Kanban {
id: number;
name: string;
projectId: number;
}
export interface Task {
id: number;
name: string;
processorId: number;
projectId: number;
epicId: number;
kanbanId: number;
typeId: number;
note: string;
}
export interface TaskType {
id: number;
name: string;
}

2
src/utils/request.ts

@ -11,7 +11,7 @@ interface Config extends RequestInit { @@ -11,7 +11,7 @@ interface Config extends RequestInit {
export const request = async (
endpoint: string,
{ data, token, headers, ...customConfig }: Config
{ data, token, headers, ...customConfig }: Config = {}
) => {
const config = {
method: "GET",

Loading…
Cancel
Save