Project.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import type { ActionType, ProColumns } from "@ant-design/pro-components";
  2. import { EditableProTable, useRefFunction } from "@ant-design/pro-components";
  3. import { Button, Dropdown, Form, Space, Typography } from "antd";
  4. import React, { useEffect, useRef, useState } from "react";
  5. import { useIntl } from "react-intl";
  6. import ProjectEditDrawer from "./ProjectEditDrawer";
  7. import type {
  8. IProjectData,
  9. IProjectListResponse,
  10. IProjectResponse,
  11. IProjectUpdateRequest,
  12. } from "../../api/task";
  13. import { get, post } from "../../request";
  14. import { TaskBuilderProjectsModal } from "./TaskBuilderProjects";
  15. const { Text } = Typography;
  16. function generateUUID() {
  17. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
  18. const r = (Math.random() * 16) | 0,
  19. v = c === "x" ? r : (r & 0x3) | 0x8;
  20. return v.toString(16);
  21. });
  22. }
  23. interface IWidget {
  24. studioName?: string;
  25. projectId?: string;
  26. onRowClick?: (data: IProjectData) => void;
  27. onSelect?: (id: string) => void;
  28. }
  29. const Project = ({ studioName, projectId, onRowClick, onSelect }: IWidget) => {
  30. const intl = useIntl();
  31. const [open, setOpen] = useState(false);
  32. const [editId, setEditId] = useState<string>();
  33. const [title, setTitle] = useState<React.ReactNode>();
  34. const [curr, setCurr] = useState<string>();
  35. const actionRef = useRef<ActionType | null>(null);
  36. const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
  37. const [dataSource, setDataSource] = useState<readonly IProjectData[]>([]);
  38. const [buildProjectsOpen, setBuildProjectsOpen] = useState(false);
  39. const [form] = Form.useForm();
  40. const ProjectTitle = ({ data }: { data?: IProjectData }) => (
  41. <Space>
  42. <Text strong>{data?.title}</Text>
  43. {data?.path?.reverse().map((item, id) => {
  44. return (
  45. <Text key={id} type="secondary">
  46. {" <"}
  47. <Button
  48. type="text"
  49. onClick={() => {
  50. if (onSelect) {
  51. onSelect(item.id);
  52. }
  53. }}
  54. >
  55. {item.title}
  56. </Button>
  57. </Text>
  58. );
  59. })}
  60. </Space>
  61. );
  62. const loopDataSourceFilter = (
  63. data: readonly IProjectData[],
  64. id: React.Key | undefined
  65. ): IProjectData[] => {
  66. return data
  67. .map((item) => {
  68. if (item.id !== id) {
  69. if (item.children) {
  70. const newChildren = loopDataSourceFilter(item.children, id);
  71. return {
  72. ...item,
  73. children: newChildren.length > 0 ? newChildren : undefined,
  74. };
  75. }
  76. return item;
  77. }
  78. return null;
  79. })
  80. .filter(Boolean) as IProjectData[];
  81. };
  82. const removeRow = useRefFunction((record: IProjectData) => {
  83. setDataSource(loopDataSourceFilter(dataSource, record.id));
  84. });
  85. const columns: ProColumns<IProjectData>[] = [
  86. {
  87. title: intl.formatMessage({
  88. id: "forms.fields.title.label",
  89. }),
  90. dataIndex: "title",
  91. formItemProps: {
  92. rules: [
  93. {
  94. required: true,
  95. message: "此项为必填项",
  96. },
  97. ],
  98. },
  99. width: "30%",
  100. render: (_dom, record, _, _action) => {
  101. return (
  102. <Button
  103. type="link"
  104. size="small"
  105. onClick={() => {
  106. if (onSelect) {
  107. onSelect(record.id);
  108. }
  109. }}
  110. >
  111. {record.title}
  112. </Button>
  113. );
  114. },
  115. },
  116. {
  117. title: intl.formatMessage({
  118. id: "forms.fields.milestone.label",
  119. }),
  120. key: "state",
  121. dataIndex: "state",
  122. readonly: true,
  123. },
  124. {
  125. title: intl.formatMessage({
  126. id: "forms.fields.status.label",
  127. }),
  128. key: "state",
  129. dataIndex: "state",
  130. readonly: true,
  131. },
  132. {
  133. title: "操作",
  134. valueType: "option",
  135. width: 250,
  136. render: (_text, record, _, _action) => [
  137. <Button
  138. size="small"
  139. type="link"
  140. key="editable"
  141. onClick={() => {
  142. setEditId(record.id);
  143. setOpen(true);
  144. }}
  145. >
  146. 编辑
  147. </Button>,
  148. record.type === "workflow" ? (
  149. <></>
  150. ) : (
  151. <EditableProTable.RecordCreator
  152. key="copy"
  153. parentKey={record.id}
  154. record={{
  155. id: generateUUID(),
  156. parent_id: record.id,
  157. }}
  158. >
  159. <Dropdown.Button
  160. size="small"
  161. type="link"
  162. menu={{
  163. items: [
  164. {
  165. key: "multi",
  166. label: "批量添加",
  167. },
  168. ],
  169. onClick: (e) => {
  170. switch (e.key) {
  171. case "multi":
  172. setCurr(record.id);
  173. setBuildProjectsOpen(true);
  174. break;
  175. default:
  176. break;
  177. }
  178. },
  179. }}
  180. >
  181. 插入子节点
  182. </Dropdown.Button>
  183. </EditableProTable.RecordCreator>
  184. ),
  185. <Button
  186. type="link"
  187. danger
  188. size="small"
  189. key="delete"
  190. onClick={() => {
  191. removeRow(record);
  192. }}
  193. >
  194. 删除
  195. </Button>,
  196. ],
  197. },
  198. ];
  199. const getChildren = (
  200. record: IProjectData,
  201. findIn: IProjectData[]
  202. ): IProjectData[] | undefined => {
  203. const children = findIn
  204. .filter((item) => item.parent?.id === record.id)
  205. .map((item) => {
  206. return { ...item, children: getChildren(item, findIn) };
  207. });
  208. if (children.length > 0) {
  209. return children;
  210. }
  211. return undefined;
  212. };
  213. useEffect(() => {
  214. actionRef.current?.reload();
  215. }, [projectId]);
  216. return (
  217. <>
  218. <TaskBuilderProjectsModal
  219. studioName={studioName}
  220. parentId={curr}
  221. open={buildProjectsOpen}
  222. onClose={() => setBuildProjectsOpen(false)}
  223. onDone={() => actionRef.current?.reload()}
  224. />
  225. <EditableProTable<IProjectData>
  226. onRow={(record) => ({
  227. onClick: () => {
  228. if (onRowClick) {
  229. onRowClick(record);
  230. }
  231. },
  232. })}
  233. rowKey="id"
  234. scroll={{
  235. x: 960,
  236. }}
  237. actionRef={actionRef}
  238. headerTitle={title}
  239. maxLength={5}
  240. search={false}
  241. // 关闭默认的新建按钮
  242. recordCreatorProps={false}
  243. columns={columns}
  244. request={async () => {
  245. const url = `/v2/project?view=project-tree&project_id=${projectId}`;
  246. console.info("api request", url);
  247. const res = await get<IProjectListResponse>(url);
  248. console.info("project api response", res);
  249. const root = res.data.rows
  250. .filter((item) => item.id === projectId)
  251. .map((item) => {
  252. return { ...item, children: getChildren(item, res.data.rows) };
  253. });
  254. return {
  255. data: root,
  256. total: res.data.count,
  257. success: res.ok,
  258. };
  259. }}
  260. value={dataSource}
  261. onChange={(value: readonly IProjectData[]) => {
  262. const root = value.find((item) => item.id === projectId);
  263. setTitle(ProjectTitle({ data: root }));
  264. setDataSource(value);
  265. }}
  266. editable={{
  267. form,
  268. editableKeys,
  269. onSave: async (_key, values) => {
  270. const data: IProjectUpdateRequest = {
  271. ...values,
  272. studio_name: studioName ?? "",
  273. };
  274. const url = `/v2/project`;
  275. console.info("save api request", url, data);
  276. const res = await post<IProjectUpdateRequest, IProjectResponse>(
  277. url,
  278. data
  279. );
  280. console.info("save api response", res);
  281. },
  282. onChange: setEditableRowKeys,
  283. actionRender: (_row, _config, dom) => [dom.save, dom.cancel],
  284. }}
  285. />
  286. <ProjectEditDrawer
  287. studioName={studioName}
  288. projectId={editId}
  289. openDrawer={open}
  290. onClose={() => setOpen(false)}
  291. />
  292. </>
  293. );
  294. };
  295. export default Project;