[Practice] Eleven. Kanban page and task group page development (5) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (27)


Source of learning content: React + React Hook + TS Best Practice - MOOC


Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):

item Version
react & react-dom ^18.2.0
react-router & react-router-dom ^6.11.2
antd ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
husky ^8.0.3
lint-staged ^13.1.2
prettier 2.8.4
json-server 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
qs ^6.11.0
dayjs ^1.11.7
react-helmet ^6.1.0
@types/react-helmet ^6.1.6
react-query ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/react & @emotion/styled ^11.10.6

The specific configuration, operation and content will be different, and the "pit" will also be different. . .


1. Project launch: project initialization and configuration

2. React and Hook application: implement the project list

3. TS Application: JS God Assist - Strong Type

4. JWT, user authentication and asynchronous request


5. CSS is actually very simple - add styles with CSS-in-JS


6. User experience optimization - loading and error state handling



7. Hook, routing, and URL state management



8. User selector and item editing function


9. In-depth React state management and Redux mechanism





10. Use react-query to get data and manage cache


11. Kanban page and task group page development

1~3

4~6

7&8

9&10

11. Sort Optimistic Updates

Create a new one src\utils\reorder.ts(this part of the video does not go into details...you can dig deeper if you use it in the project):

/**
 * 在本地对排序进行乐观更新
 * @param fromId 要排序的项目的id
 * @param type 'before' | 'after'
 * @param referenceId 参照id
 * @param list 要排序的列表, 比如tasks, kanbans
 */
export const reorder = ({
    
    
  fromId,
  type,
  referenceId,
  list,
}: {
    
    
  list: {
    
     id: number }[];
  fromId: number;
  type: "after" | "before";
  referenceId: number;
}) => {
    
    
  const copiedList = [...list];
  // 找到fromId对应项目的下标
  const movingItemIndex = copiedList.findIndex((item) => item.id === fromId);
  if (!referenceId) {
    
    
    return insertAfter([...copiedList], movingItemIndex, copiedList.length - 1);
  }
  const targetIndex = copiedList.findIndex((item) => item.id === referenceId);
  const insert = type === "after" ? insertAfter : insertBefore;
  return insert([...copiedList], movingItemIndex, targetIndex);
};

/**
 * 在list中,把from放在to的前边
 * @param list
 * @param from
 * @param to
 */
const insertBefore = (list: unknown[], from: number, to: number) => {
    
    
  const toItem = list[to];
  const removedItem = list.splice(from, 1)[0];
  const toIndex = list.indexOf(toItem);
  list.splice(toIndex, 0, removedItem);
  return list;
};

/**
 * 在list中,把from放在to的后面
 * @param list
 * @param from
 * @param to
 */
const insertAfter = (list: unknown[], from: number, to: number) => {
    
    
  const toItem = list[to];
  const removedItem = list.splice(from, 1)[0];
  const toIndex = list.indexOf(toItem);
  list.splice(toIndex + 1, 0, removedItem);
  return list;
};

EDIT src\utils\use-optimistic-options.ts(called in the previously written config to complete optimistic updates):

...
export const useReorderViewboardConfig = (queryKey: QueryKey) =>
  useConfig(queryKey, (target, old) => reorder({
    
     list: old, ...target }));

export const useReorderTaskConfig = (queryKey: QueryKey) =>
  useConfig(queryKey, (target, old) => {
    
    
    const orderedList = reorder({
    
     list: old, ...target }) as Task[];
    return orderedList.map((item) =>
      item.id === target.fromId
        ? {
    
     ...item, kanbanId: target.toKanbanId }
        : item
    );
  });

Since the sorting of tasks may be cross-panel, it will be more complicated

After checking the effect, I found that after dragging to other panels, if the original panel is empty, I cannot drag it back. . . So you need to DropChildadd a minimum height to

edit src\screens\ViewBoard\components\ViewboardCloumn.tsx:

...
export const ViewboardColumn = React.forwardRef<...>((...) => {
    
    
  ...
  return (
    <Container {
    
    ...props} ref={
    
    ref}>
      ...
      <TasksContainer>
        <Drop {
    
    ...}>
          <DropChild style={
    
    {
    
    minHeight: '5px'}}>
            ...
          </DropChild>
        </Drop>
        <CreateTask kanbanId={
    
    viewboard.id} />
      </TasksContainer>
    </Container>
  );
});
...

So far, drag and drop is done!

12. Task group page (on)

After the Kanban page is developed, the task group page is next

New src\types\TaskGroup.ts:

export interface TaskGroup {
    
    
  id: number;
  name: string;
  projectId: number;
  // 开始时间
  start: number;
  // 结束时间
  end: number;
}

New src\utils\taskGroup.ts(similar to Kanban Viewboard( kanban), can be copied and modified):

import {
    
     cleanObject } from "utils";
import {
    
     useHttp } from "./http";
import {
    
     TaskGroup } from "types/TaskGroup";
import {
    
     QueryKey, useMutation, useQuery } from "react-query";
import {
    
    
  useAddConfig,
  useDeleteConfig,
} from "./use-optimistic-options";

export const useTaskGroups = (param?: Partial<TaskGroup>) => {
    
    
  const client = useHttp();

  return useQuery<TaskGroup[]>(["taskgroups", param], () =>
    client("epics", {
    
     data: cleanObject(param || {
    
    }) })
  );
};

export const useAddTaskGroup = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    (params: Partial<TaskGroup>) =>
      client(`epics`, {
    
    
        method: "POST",
        data: params,
      }),
    useAddConfig(queryKey)
  );
};

export const useDeleteTaskGroup = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    (id?: number) =>
      client(`epics/${
      
      id}`, {
    
    
        method: "DELETE",
      }),
    useDeleteConfig(queryKey)
  );
};

new src\screens\TaskGroup\utils.ts:

import {
    
     useProjectIdInUrl } from "screens/ViewBoard/utils";

export const useTaskGroupSearchParams = () => ({
    
    
  projectId: useProjectIdInUrl(),
});

export const useTaskGroupsQueryKey = () => [
  "taskgroups",
  useTaskGroupSearchParams(),
];

Modification src\types\Task.ts(the attribute field needs to be consistent with the actual data...):

export interface Task {
    
    
  id: number;
  name: string;
  projectId: number;
  processorId: number; // 经办人
  epicId: number; // 任务组(原 taskGroupId)
  kanbanId: number;
  typeId: number; // bug or task
  note: string;
}

Edit src\screens\TaskGroup\index.tsx(created when creating a new route before, part of the page layout is the same as the Kanban, you can take it src\screens\ViewBoard\index.tsx):

import {
    
     Row, ViewContainer } from "components/lib";
import {
    
     useProjectInUrl } from "screens/ViewBoard/utils";
import {
    
     useTaskGroups } from "utils/taskGroup";
import {
    
     useTaskGroupSearchParams, useTaskGroupsQueryKey } from "./utils";
import {
    
     Button, List, Modal } from "antd";
import dayjs from "dayjs";
import {
    
     useTasks } from "utils/task";
import {
    
     Link } from "react-router-dom";
import {
    
     TaskGroup } from "types/TaskGroup";
import {
    
     useState } from "react";

export const TaskGroupIndex = () => {
    
    
  const {
    
     data: currentProject } = useProjectInUrl();
  const {
    
     data: taskGroups } = useTaskGroups(useTaskGroupSearchParams());
  const {
    
     data: tasks } = useTasks({
    
     projectId: currentProject?.id });

  return (
    <ViewContainer>
      <Row between={
    
    true}>
        <h1>{
    
    currentProject?.name}任务组</h1>
        <Button onClick={
    
    () => setEpicCreateOpen(true)} type={
    
    "link"}>
          创建任务组
        </Button>
      </Row>
      <List
        style={
    
    {
    
     overflow: "scroll" }}
        dataSource={
    
    taskGroups}
        itemLayout={
    
    "vertical"}
        renderItem={
    
    (taskGroup) => (
          <List.Item>
            <List.Item.Meta
              title={
    
    
                <Row between={
    
    true}>
                  <span>{
    
    taskGroup.name}</span>
                  <Button onClick={
    
    () => {
    
    }} type={
    
    "link"}>
                    删除
                  </Button>
                </Row>
              }
              description={
    
    
                <div>
                  <div>开始时间:{
    
    dayjs(taskGroup.start).format("YYYY-MM-DD")}</div>
                  <div>结束时间:{
    
    dayjs(taskGroup.end).format("YYYY-MM-DD")}</div>
                </div>
              }
            />
            <div>
              {
    
    tasks
                ?.filter((task) => task.epicId === taskGroup.id)
                .map((task) => (
                  <Link
                    to={
    
    `/projects/${
      
      currentProject?.id}/viewboard?editingTaskId=${
      
      task.id}`}
                    key={
    
    task.id}
                  >
                    {
    
    task.name}
                  </Link>
                ))}
            </div>
          </List.Item>
        )}
      />
    </ViewContainer>
  );
};

Check the page effect, click on the corresponding task to jump to the Kanban and open the task editing window

EDIT src\screens\TaskGroup\index.tsx(add delete task group function):

...
import {
    
     useDeleteTaskGroup, useTaskGroups } from "utils/taskGroup";

export const TaskGroupIndex = () => {
    
    
  ...
  const {
    
     mutate: deleteTaskGroup } = useDeleteTaskGroup(useTaskGroupsQueryKey());

  const confirmDeleteEpic = (taskGroup: TaskGroup) => {
    
    
    Modal.confirm({
    
    
      title: `确定删除项目组:${
      
      taskGroup.name}`,
      content: "点击确定删除",
      okText: "确定",
      onOk() {
    
    
        deleteTaskGroup(taskGroup.id);
      },
    });
  };

  return (
    <ViewContainer>
      <Row between={
    
    true}>...</Row>
      <List
        style={
    
    {
    
     overflow: "scroll" }}
        dataSource={
    
    taskGroups}
        itemLayout={
    
    "vertical"}
        renderItem={
    
    (taskGroup) => (
          <List.Item>
            <List.Item.Meta
              title={
    
    
                <Row between={
    
    true}>
                  <span>{
    
    taskGroup.name}</span>
                  <Button onClick={
    
    () => confirmDeleteEpic(taskGroup)} type={
    
    "link"}
                  >
                    删除
                  </Button>
                </Row>
              }
              description={
    
    ...}
            />
            <div>...</div>
          </List.Item>
        )}
      />
    </ViewContainer>
  );
};

View the page, you can delete the task group normally (it is recommended to try the function after the function is created, you know...)


Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/132529315