[Actual Combat] Eleven. Kanban page and task group page development (3) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (25)


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. Edit task function

Next, create a component for editing tasks:

If you are ready to call the editing task interface and obtain task details Hook, edit src\utils\task.ts:

...
import {
    
     useAddConfig, useEditConfig } from "./use-optimistic-options";

export const useEditTask = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    (params: Partial<Task>) =>
      client(`tasks/${
      
      params.id}`, {
    
    
        method: "PATCH",
        data: params,
      }),
    useEditConfig(queryKey)
  );
};

export const useTask = (id?: number) => {
    
    
  const client = useHttp();
  return useQuery<Task>(["task", id], () => client(`tasks/${
      
      id}`), {
    
    
    enabled: Boolean(id),
  });
};

EDIT src\screens\ViewBoard\utils.ts(added useTasksModal):

...
// import { useDebounce } from "utils";
import {
    
     useTask } from "utils/task";
...

export const useTasksSearchParams = () => {
    
    
  const [param] = useUrlQueryParam([
    "name",
    "typeId",
    "processorId",
    "tagId",
  ]);
  const projectId = useProjectIdInUrl();
  // const debouncedName = useDebounce(param.name)
  return useMemo(
    () => ({
    
    
      projectId,
      typeId: Number(param.typeId) || undefined,
      processorId: Number(param.processorId) || undefined,
      tagId: Number(param.tagId) || undefined,
      // name: debouncedName,
      name: param.name,
    }),
    // [projectId, param, debouncedName]
    [projectId, param]
  );
};

...

export const useTasksModal = () => {
    
    
  const [{
    
     editingTaskId }, setEditingTaskId] = useUrlQueryParam(['editingTaskId'])
  const {
    
     data: editingTask, isLoading } = useTask(Number(editingTaskId))
  const startEdit = useCallback((id: number) => {
    
    
    setEditingTaskId({
    
    editingTaskId: id})
  }, [setEditingTaskId])
  const close = useCallback(() => {
    
    
    setEditingTaskId({
    
    editingTaskId: ''})
  }, [setEditingTaskId])
  return {
    
    
    editingTaskId,
    editingTask,
    startEdit,
    close,
    isLoading
  }
}

The use of in the video useDebouncemakes the search start after the input is completely stopped, avoiding the waste of system resources caused by frequent searches during the input process, and affecting the user experience. After the blogger makes such changes, the Chinese input method cannot be used normally. . . Follow up

New component: src\screens\ViewBoard\components\taskModal.tsx:

import {
    
     useForm } from "antd/lib/form/Form"
import {
    
     useTasksModal, useTasksQueryKey } from "../utils"
import {
    
     useEditTask } from "utils/task"
import {
    
     useEffect } from "react"
import {
    
     Form, Input, Modal } from "antd"
import {
    
     UserSelect } from "components/user-select"
import {
    
     TaskTypeSelect } from "components/task-type-select"

const layout = {
    
    
  labelCol: {
    
    span: 8},
  wrapperCol: {
    
    span: 16}
}

export const TaskModal = () => {
    
    
  const [form] = useForm()
  const {
    
     editingTaskId, editingTask, close } = useTasksModal()
  const {
    
     mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())

  const onCancel = () => {
    
    
    close()
    form.resetFields()
  }

  const onOk = async () => {
    
    
    await editTask({
    
    ...editingTask, ...form.getFieldsValue()})
    close()
  }

  useEffect(() => {
    
    
    form.setFieldsValue(editingTask)
  }, [form, editingTask])

  return <Modal
    forceRender={
    
    true}
    onCancel={
    
    onCancel}
    onOk={
    
    onOk}
    okText={
    
    "确认"}
    cancelText={
    
    "取消"}
    confirmLoading={
    
    editLoading}
    title={
    
    "编辑任务"}
    open={
    
    !!editingTaskId}
  >
    <Form {
    
    ...layout} initialValues={
    
    editingTask} form={
    
    form}>
      <Form.Item
        label={
    
    "任务名"}
        name={
    
    "name"}
        rules={
    
    [{
    
     required: true, message: "请输入任务名" }]}
      >
        <Input />
      </Form.Item>
      <Form.Item label={
    
    "经办人"} name={
    
    "processorId"}>
        <UserSelect defaultOptionName={
    
    "经办人"} />
      </Form.Item>
      <Form.Item label={
    
    "类型"} name={
    
    "typeId"}>
        <TaskTypeSelect />
      </Form.Item>
    </Form>
  </Modal>
}

Note: Same Draweras , when Modalusing the useForm()extracted formbound in the component Form, you need to add forceRenderthe property, otherwise when the page is opened, if the binding is not available, an error will be reported . useForm' is not connected to any Form element. Forget…

EDIT: src\screens\ViewBoard\index.tsx(introduced TaskModal):

...
import {
    
     TaskModal } from "./components/taskModal";

export const ViewBoard = () => {
    
    
  ...

  return (
    <ViewContainer>
      ...
      <TaskModal/>
    </ViewContainer>
  );
};
...

EDIT: src\screens\ViewBoard\components\ViewboardCloumn.tsx(introduced useTasksModalso that clicking on a task card opens for TaskModalediting):

...
import {
    
     useTasksModal, useTasksSearchParams } from "../utils";
...

export const ViewboardColumn = ({
     
      viewboard }: {
     
      viewboard: Viewboard }) => {
    
    
  ...
  const {
    
     startEdit } = useTasksModal()
  return (
    <Container>
      ...
      <TasksContainer>
        {
    
    tasks?.map((task) => (
          <Card onClick={
    
    () => startEdit(task.id)} style={
    
    {
    
     marginBottom: "0.5rem", cursor: 'pointer' }} key={
    
    task.id}>
            ...
          </Card>
        ))}
        ...
      </TasksContainer>
    </Container>
  );
};
...

View the functions and effects, click on the task card to TaskModalappear, edit and confirm to see the modified task (using optimistic update, completely unfeeling):
insert image description here

8. Kanban and task deletion function

Next, implement a small function to highlight keywords in the search results

New src\screens\ViewBoard\components\mark.tsx:

export const Mark = ({
     
     name, keyword}: {
     
     name: string, keyword: string}) => {
    
    
  if(!keyword) {
    
    
    return <>{
    
    name}</>
  }
  const arr = name.split(keyword)
  return <>
    {
    
    
      arr.map((str, index) => <span key={
    
    index}>
        {
    
    str}
        {
    
    
          index === arr.length -1 ? null : <span style={
    
    {
    
     color: '#257AFD' }}>{
    
    keyword}</span>
        }
      </span>)
    }
  </>
}

EDIT src\screens\ViewBoard\components\ViewboardCloumn.tsx(introduced Taskand TaskCardextracted separately):

...
import {
    
     Task } from "types/Task";
import {
    
     Mark } from "./mark";

...

const TaskCard = ({
     
     task}: {
     
     task: Task}) => {
    
    
  const {
    
     startEdit } = useTasksModal();
  const {
    
     name: keyword } = useTasksSearchParams()
  return <Card
    onClick={
    
    () => startEdit(task.id)}
    style={
    
    {
    
     marginBottom: "0.5rem", cursor: "pointer" }}
    key={
    
    task.id}
  >
    <p>
      <Mark keyword={
    
    keyword} name={
    
    task.name}/>
    </p>
    <TaskTypeIcon id={
    
    task.id} />
  </Card>
}

export const ViewboardColumn = ({
     
      viewboard }: {
     
      viewboard: Viewboard }) => {
    
    
  const {
    
     data: allTasks } = useTasks(useTasksSearchParams());
  const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
  return (
    <Container>
      <h3>{
    
    viewboard.name}</h3>
      <TasksContainer>
        {
    
    tasks?.map((task) => <TaskCard task={
    
    task}/>)}
        <CreateTask kanbanId={
    
    viewboard.id} />
      </TasksContainer>
    </Container>
  );
};
...

View the effect:

insert image description here

Let's start developing the delete function

EDIT src\utils\viewboard.ts(created and exported useDeleteViewBoard):

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

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

...
import {
    
     Button, Card, Dropdown, MenuProps, Modal, Row } from "antd";
import {
    
     useDeleteViewBoard } from "utils/viewboard";

...

export const ViewboardColumn = ({
     
      viewboard }: {
     
      viewboard: Viewboard }) => {
    
    
  const {
    
     data: allTasks } = useTasks(useTasksSearchParams());
  const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
  return (
    <Container>
      <Row>
        <h3>{
    
    viewboard.name}</h3>
        <More viewboard={
    
    viewboard}/>
      </Row>
      <TasksContainer>
        {
    
    tasks?.map((task) => <TaskCard task={
    
    task}/>)}
        <CreateTask kanbanId={
    
    viewboard.id} />
      </TasksContainer>
    </Container>
  );
};

const More = ({
     
      viewboard }: {
     
      viewboard: Viewboard }) => {
    
    
  const {
    
    mutateAsync: deleteViewBoard} = useDeleteViewBoard(useViewBoardQueryKey())
  const startDelete = () => {
    
    
    Modal.confirm({
    
    
      okText: '确定',
      cancelText: '取消',
      title: '确定删除看板吗?',
      onOk() {
    
    
        deleteViewBoard(viewboard.id)
      }
    })
  }
  const items: MenuProps["items"] = [
    {
    
    
      key: 1,
      label: "删除",
      onClick: startDelete,
    },
  ];
  return <Dropdown menu={
    
    {
    
     items }}>
    <Button type="link" onClick={
    
    (e) => e.preventDefault()}>
      ...
    </Button>
  </Dropdown>
}
...

Test to delete the Kanban, the function is normal

The following is the delete task function

EDIT src\utils\task.ts(created and exported useDeleteTask):

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

EDIT src\screens\ViewBoard\components\taskModal.tsx:

...
import {
    
     useDeleteTask, useEditTask } from "utils/task";

export const TaskModal = () => {
    
    
  ...
  const {
    
     mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey());
  ...

  const startDelete = () => {
    
    
    close();
    Modal.confirm({
    
    
      okText: '确定',
      cancelText: '取消',
      title: '确定删除看板吗?',
      onOk() {
    
    
        deleteTask(Number(editingTaskId));
      }
    })
  }

  return (
    <Modal {
    
    ...}>
      <Form {
    
    ...}>
        ...
      </Form>
      <div style={
    
    {
    
     textAlign: 'right' }}>
        <Button style={
    
    {
    
    fontSize: '14px'}} size="small" onClick={
    
    startDelete}>删除</Button>
      </div>
    </Modal>
  );
};

Test to delete the task, the function is normal


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

Guess you like

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