TaskList.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import React, { useEffect, useRef, useState } from "react";
  2. import { useIntl } from "react-intl";
  3. import { Button, Form, message, Space, Typography } from "antd";
  4. import type { ActionType, ProColumns } from "@ant-design/pro-components";
  5. import { EditableProTable, useRefFunction } from "@ant-design/pro-components";
  6. import type {
  7. IProject,
  8. IProjectData,
  9. IProjectResponse,
  10. ITaskData,
  11. ITaskListResponse,
  12. ITaskResponse,
  13. ITaskUpdateRequest,
  14. TTaskStatus,
  15. } from "../../api/task";
  16. import { get, post } from "../../request";
  17. import TaskEditDrawer from "./TaskEditDrawer";
  18. import { GroupIcon } from "../../assets/icon";
  19. import Options, { type IMenu } from "./Options";
  20. import Filter from "./Filter";
  21. import { Milestone } from "./TaskReader";
  22. import Assignees from "./Assignees";
  23. import TaskStatusButton from "./TaskStatusButton";
  24. import Executors from "./Executors";
  25. import Category from "./Category";
  26. import TaskListAdd from "./TaskListAdd";
  27. import { updateNode } from "./ProjectTask";
  28. import User from "../auth/User";
  29. const { Text } = Typography;
  30. export const treeToList = (tree: readonly ITaskData[]): ITaskData[] => {
  31. const output: ITaskData[] = [];
  32. const scan = (value: ITaskData) => {
  33. value.children?.forEach(scan);
  34. value.children = undefined;
  35. if (value.type !== "group") {
  36. output.push(value);
  37. }
  38. };
  39. tree.forEach(scan);
  40. return output;
  41. };
  42. function generateUUID() {
  43. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
  44. const r = (Math.random() * 16) | 0,
  45. v = c === "x" ? r : (r & 0x3) | 0x8;
  46. return v.toString(16);
  47. });
  48. }
  49. export interface IFilter {
  50. field:
  51. | "executor_id"
  52. | "owner_id"
  53. | "finished_at"
  54. | "assignees_id"
  55. | "participants_id"
  56. | "sign_up";
  57. operator:
  58. | "includes"
  59. | "not-includes"
  60. | "equals"
  61. | "not-equals"
  62. | "null"
  63. | "not-null"
  64. | null;
  65. value: string | string[] | null;
  66. }
  67. interface IParams {
  68. status?: string;
  69. orderby?: string;
  70. direction?: string;
  71. }
  72. interface IWidget {
  73. studioName?: string;
  74. projectId?: string;
  75. taskTree?: readonly ITaskData[];
  76. editable?: boolean;
  77. filters?: IFilter[];
  78. status?: TTaskStatus[];
  79. sortBy?: "order" | "created_at" | "updated_at" | "started_at" | "finished_at";
  80. groupBy?: "executor_id" | "owner_id" | "status" | "project_id";
  81. onChange?: (treeData: ITaskData[]) => void;
  82. }
  83. const TaskList = ({
  84. studioName,
  85. projectId,
  86. taskTree,
  87. editable = false,
  88. status,
  89. sortBy = "order",
  90. ___groupBy,
  91. filters,
  92. onChange,
  93. }: IWidget) => {
  94. const intl = useIntl();
  95. const [open, setOpen] = useState(false);
  96. const actionRef = useRef<ActionType | null>(null);
  97. const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
  98. const [dataSource, setDataSource] = useState<readonly ITaskData[]>([]);
  99. const [rawData, setRawData] = useState<readonly ITaskData[]>([]);
  100. const [form] = Form.useForm();
  101. const [selectedTask, setSelectedTask] = useState<string>();
  102. const [project, setProject] = useState<IProjectData>();
  103. const [currFilter, setCurrFilter] = useState(filters);
  104. console.info("render");
  105. const getChildren = (record: ITaskData, findIn: ITaskData[]): ITaskData[] => {
  106. const children = findIn
  107. .filter((item) => item.parent_id === record.id)
  108. .map((item) => {
  109. return { ...item, children: getChildren(item, findIn) };
  110. });
  111. return children;
  112. };
  113. useEffect(() => {
  114. if (!projectId) {
  115. return;
  116. }
  117. const url = `/v2/project/${projectId}`;
  118. console.info("api request", url);
  119. get<IProjectResponse>(url).then((json) => {
  120. if (json.ok) {
  121. setProject(json.data);
  122. } else {
  123. console.error(json.message);
  124. }
  125. });
  126. }, [projectId]);
  127. useEffect(() => {
  128. if (taskTree) {
  129. setDataSource(taskTree);
  130. }
  131. }, [taskTree]);
  132. const loopDataSourceFilter = (
  133. data: readonly ITaskData[],
  134. id: React.Key | undefined
  135. ): ITaskData[] => {
  136. return data
  137. .map((item) => {
  138. if (item.id !== id) {
  139. if (item.children) {
  140. const newChildren = loopDataSourceFilter(item.children, id);
  141. return {
  142. ...item,
  143. children: newChildren.length > 0 ? newChildren : undefined,
  144. };
  145. }
  146. return item;
  147. }
  148. return null;
  149. })
  150. .filter(Boolean) as ITaskData[];
  151. };
  152. const removeRow = useRefFunction((record: ITaskData) => {
  153. setDataSource(loopDataSourceFilter(dataSource, record.id));
  154. });
  155. const changeData = (data: ITaskData[]) => {
  156. /* console.debug("task change", data);
  157. const update = (item: ITaskData): ITaskData => {
  158. item.children = item.children?.map(update);
  159. const found = data.find((t) => t.id === item.id);
  160. if (found) {
  161. return { ...found, children: item.children };
  162. }
  163. return item;
  164. };
  165. const newData = dataSource.map(update);*/
  166. const origin = JSON.parse(JSON.stringify(dataSource));
  167. data.forEach((value) => {
  168. updateNode(origin, value);
  169. });
  170. console.debug("TaskList change", dataSource, origin);
  171. setRawData(treeToList(origin));
  172. setDataSource(origin);
  173. onChange && onChange(origin);
  174. };
  175. const columns: ProColumns<ITaskData>[] = [
  176. {
  177. title: intl.formatMessage({
  178. id: "forms.fields.title.label",
  179. }),
  180. dataIndex: "title",
  181. search: false,
  182. formItemProps: {
  183. rules: [
  184. {
  185. required: true,
  186. message: "此项为必填项",
  187. },
  188. ],
  189. },
  190. width: "30%",
  191. render(_dom, entity, _index, _action, _schema) {
  192. return (
  193. <Space>
  194. <Button
  195. type="link"
  196. onClick={() => {
  197. setSelectedTask(entity.id);
  198. setOpen(true);
  199. }}
  200. >
  201. {entity.title}
  202. </Button>
  203. {entity.type === "group" ? (
  204. <Text type="secondary">{entity.order}</Text>
  205. ) : (
  206. <>{entity.category ? <Category task={entity} /> : ""}</>
  207. )}
  208. <TaskStatusButton type="tag" task={entity} onChange={changeData} />
  209. <Milestone task={entity} />
  210. {entity.project ? (
  211. <Text type="secondary">
  212. {"< "}
  213. {entity.project?.title}
  214. </Text>
  215. ) : (
  216. <></>
  217. )}
  218. </Space>
  219. );
  220. },
  221. },
  222. {
  223. title: intl.formatMessage({
  224. id: "forms.fields.executor.label",
  225. }),
  226. key: "executor",
  227. dataIndex: "executor",
  228. search: false,
  229. readonly: true,
  230. render(_dom, entity, _index, _action, _schema) {
  231. return <Executors data={entity} all={rawData} />;
  232. },
  233. },
  234. {
  235. title: intl.formatMessage({
  236. id: "forms.fields.assignees.label",
  237. }),
  238. key: "assignees",
  239. dataIndex: "assignees",
  240. search: false,
  241. readonly: true,
  242. render(_dom, entity, _index, _action, _schema) {
  243. return <Assignees task={entity} onChange={changeData} />;
  244. },
  245. },
  246. {
  247. title: intl.formatMessage({
  248. id: "labels.task.prev.executors",
  249. }),
  250. key: "prev_executor",
  251. dataIndex: "executor",
  252. search: false,
  253. readonly: true,
  254. render(_dom, entity, _index, _action, _schema) {
  255. return (
  256. <div>
  257. {entity.pre_task?.map((item, id) => {
  258. return (
  259. <User
  260. {...item.executor}
  261. key={id}
  262. showName={entity.pre_task?.length === 1}
  263. />
  264. );
  265. })}
  266. </div>
  267. );
  268. },
  269. },
  270. {
  271. title: intl.formatMessage({
  272. id: "forms.fields.started-at.label",
  273. }),
  274. key: "state",
  275. dataIndex: "started_at",
  276. readonly: true,
  277. valueType: "date",
  278. sorter: true,
  279. search: false,
  280. },
  281. {
  282. title: intl.formatMessage({
  283. id: "forms.fields.finished-at.label",
  284. }),
  285. key: "state",
  286. dataIndex: "finished_at",
  287. readonly: true,
  288. valueType: "date",
  289. search: false,
  290. },
  291. {
  292. title: "状态",
  293. hideInTable: true,
  294. dataIndex: "status",
  295. valueType: "select",
  296. initialValue: status ? status.join("_") : "all",
  297. valueEnum: {
  298. all: { text: "全部任务" },
  299. done: { text: "已完成" },
  300. running_restarted: { text: "未完成" },
  301. running_restarted_published: { text: "待办" },
  302. published: { text: "未开始" },
  303. pending: { text: "未发布" },
  304. },
  305. },
  306. {
  307. title: "排序",
  308. hideInTable: true,
  309. dataIndex: "orderby",
  310. valueType: "select",
  311. initialValue: sortBy,
  312. valueEnum: {
  313. order: { text: "拖拽排序" },
  314. started_at: { text: "开始时间" },
  315. created_at: { text: "创建时间" },
  316. updated_at: { text: "更新时间" },
  317. finished_at: { text: "完成时间" },
  318. },
  319. },
  320. {
  321. title: "顺序",
  322. hideInTable: true,
  323. dataIndex: "direction",
  324. valueType: "select",
  325. initialValue: "asc",
  326. valueEnum: {
  327. desc: { text: "降序" },
  328. asc: { text: "升序" },
  329. },
  330. },
  331. editable
  332. ? {
  333. title: "操作",
  334. valueType: "option",
  335. width: 250,
  336. search: false,
  337. render: (_text, record, _, _action) => [
  338. <EditableProTable.RecordCreator
  339. key="copy"
  340. parentKey={record.id}
  341. record={{
  342. id: generateUUID(),
  343. parent_id: record.id,
  344. status: "pending",
  345. type: project?.type === "workflow" ? "workflow" : "instance",
  346. }}
  347. >
  348. <Button size="small" type="link">
  349. 插入子节点
  350. </Button>
  351. </EditableProTable.RecordCreator>,
  352. <Button
  353. type="link"
  354. danger
  355. size="small"
  356. key="delete"
  357. onClick={() => {
  358. removeRow(record);
  359. }}
  360. >
  361. 删除
  362. </Button>,
  363. ],
  364. }
  365. : { search: false },
  366. ];
  367. useEffect(() => {
  368. actionRef.current?.reload();
  369. }, [projectId]);
  370. const groupItems: IMenu[] = [
  371. {
  372. key: "none",
  373. label: "无分组",
  374. },
  375. {
  376. key: "project",
  377. label: "任务组",
  378. },
  379. {
  380. key: "title",
  381. label: "任务名称",
  382. },
  383. {
  384. key: "status",
  385. label: "状态",
  386. },
  387. {
  388. key: "creator",
  389. label: "创建人",
  390. },
  391. {
  392. key: "executor",
  393. label: "执行人",
  394. },
  395. {
  396. key: "started_at",
  397. label: "开始时间",
  398. },
  399. ];
  400. return (
  401. <>
  402. <EditableProTable<ITaskData, IParams>
  403. rowKey="id"
  404. scroll={{
  405. x: 960,
  406. }}
  407. search={{
  408. filterType: "light",
  409. }}
  410. options={{
  411. search: true,
  412. }}
  413. actionRef={actionRef}
  414. // 关闭默认的新建按钮
  415. recordCreatorProps={false}
  416. columns={columns}
  417. request={async (params = {}, _sorter, _filter) => {
  418. let url = `/v2/task?a=a`;
  419. if (projectId) {
  420. url += `&view=project&project_id=${projectId}`;
  421. } else {
  422. url += `&view=instance`;
  423. }
  424. if (currFilter) {
  425. url += `&`;
  426. url += currFilter
  427. .map((item) => {
  428. return item.field + "_" + item.operator + "=" + item.value;
  429. })
  430. .join("&");
  431. }
  432. url += params.status
  433. ? `&status=${params.status.replaceAll("_", ",")}`
  434. : "";
  435. url += params.orderby ? `&order=${params.orderby}` : "";
  436. url += params.direction ? `&dir=${params.direction}` : "";
  437. console.info("task list api request", url);
  438. const res = await get<ITaskListResponse>(url);
  439. console.info("task list api response", res);
  440. //setRawData(res.data.rows);
  441. const root = res.data.rows
  442. .filter((item) => item.parent_id === null)
  443. .map((item) => {
  444. return { ...item, children: getChildren(item, res.data.rows) };
  445. });
  446. return {
  447. data: root,
  448. total: res.data.count,
  449. success: res.ok,
  450. };
  451. }}
  452. value={dataSource}
  453. onChange={(value: readonly ITaskData[]) => {
  454. console.info("onChange");
  455. setRawData(treeToList(value));
  456. if (onChange) {
  457. onChange(JSON.parse(JSON.stringify(value)));
  458. } else {
  459. setDataSource(value);
  460. }
  461. }}
  462. editable={{
  463. form,
  464. editableKeys,
  465. onSave: async (_key, values) => {
  466. const data: ITaskUpdateRequest = {
  467. ...values,
  468. studio_name: studioName ?? "",
  469. project_id: projectId,
  470. };
  471. const url = `/v2/task`;
  472. console.info("task save api request", url, values);
  473. const res = await post<ITaskUpdateRequest, ITaskResponse>(
  474. url,
  475. data
  476. );
  477. onChange && onChange([res.data]);
  478. console.info("task save api response", res);
  479. },
  480. onChange: setEditableRowKeys,
  481. actionRender: (_row, _config, dom) => [dom.save, dom.cancel],
  482. }}
  483. toolBarRender={() => [
  484. <Options
  485. items={groupItems}
  486. initKey="none"
  487. icon={<GroupIcon />}
  488. onChange={(key: string) => {
  489. switch (key) {
  490. case "status":
  491. const statuses = new Map<string, number>();
  492. rawData.forEach((task) => {
  493. if (task.status) {
  494. if (statuses.has(task.status)) {
  495. statuses.set(
  496. task.status,
  497. statuses.get(task.status)! + 1
  498. );
  499. } else {
  500. statuses.set(task.status, 1);
  501. }
  502. }
  503. });
  504. const group: ITaskData[] = [];
  505. statuses.forEach((value, key) => {
  506. group.push({
  507. id: key,
  508. title: intl.formatMessage({
  509. id: `labels.task.status.${key}`,
  510. }),
  511. order: value,
  512. type: "group",
  513. is_milestone: false,
  514. });
  515. });
  516. const newGroup = group.map((item, _id) => {
  517. return {
  518. ...item,
  519. children: rawData.filter(
  520. (task) => task.status === item.id
  521. ),
  522. };
  523. });
  524. setDataSource(newGroup);
  525. break;
  526. case "project":
  527. const projectsId = new Map<string, number>();
  528. const projects = new Map<string, IProject>();
  529. rawData.forEach((task) => {
  530. if (task.project_id && task.project) {
  531. if (projectsId.has(task.project_id)) {
  532. projectsId.set(
  533. task.project_id,
  534. projectsId.get(task.project_id)! + 1
  535. );
  536. } else {
  537. projectsId.set(task.project_id, 1);
  538. projects.set(task.project_id, task.project);
  539. }
  540. }
  541. });
  542. const projectList: ITaskData[] = [];
  543. projectsId.forEach((value, key) => {
  544. const project = projects.get(key)!;
  545. projectList.push({
  546. id: project.id,
  547. title: `${project.title}`,
  548. type: "group",
  549. order: value,
  550. is_milestone: false,
  551. });
  552. });
  553. const newProject = projectList.map((item, _id) => {
  554. return {
  555. ...item,
  556. children: rawData.filter(
  557. (task) => task.project_id === item.id
  558. ),
  559. };
  560. });
  561. setDataSource(newProject);
  562. break;
  563. case "title":
  564. const titles = new Map<string, number>();
  565. rawData.forEach((task) => {
  566. if (task.title) {
  567. if (titles.has(task.title)) {
  568. titles.set(task.title, titles.get(task.title)! + 1);
  569. } else {
  570. titles.set(task.title, 1);
  571. }
  572. }
  573. });
  574. const titleGroups: ITaskData[] = [];
  575. titles.forEach((value, key) => {
  576. titleGroups.push({
  577. id: key,
  578. title: key,
  579. order: value,
  580. type: "group",
  581. is_milestone: false,
  582. });
  583. });
  584. const newTitleGroup = titleGroups.map((item, _id) => {
  585. return {
  586. ...item,
  587. children: rawData.filter(
  588. (task) => task.title === item.title
  589. ),
  590. };
  591. });
  592. setDataSource(newTitleGroup);
  593. break;
  594. default:
  595. break;
  596. }
  597. }}
  598. />,
  599. <Filter
  600. initValue={filters}
  601. onChange={(data) => {
  602. setCurrFilter(data);
  603. actionRef.current?.reload();
  604. }}
  605. />,
  606. <TaskListAdd
  607. studioName={studioName}
  608. projectId={projectId}
  609. project={project}
  610. readonly={!editable}
  611. onAddNew={() => {
  612. if (project) {
  613. actionRef.current?.addEditRecord?.({
  614. id: generateUUID(),
  615. title: "新建任务",
  616. type: project.type === "workflow" ? "workflow" : "instance",
  617. is_milestone: false,
  618. status: "pending",
  619. });
  620. }
  621. }}
  622. onWorkflow={() => {
  623. message.success("ok");
  624. actionRef.current?.reload();
  625. }}
  626. />,
  627. ]}
  628. />
  629. <TaskEditDrawer
  630. taskId={selectedTask}
  631. openDrawer={open}
  632. onClose={() => setOpen(false)}
  633. onChange={changeData}
  634. />
  635. </>
  636. );
  637. };
  638. export default TaskList;