import React, { useReducer } from 'react' import omitNil from 'omit-nil' import { robust } from 'memoize-fn' import { Button, Dropdown, Menu, Input, Modal, Drawer, Typography, ModalProps, Form, Row, Col, Avatar, Select, notification, Spin } from 'antd' import p from 'prefix-classname' import isHotKey from 'is-hotkey' import 'github-markdown-css/github-markdown.css' const cn = p() const c = p('jira_modal-import') const labelCol = { style: { width: 100 } } import './style.sass' import JiraApiBrowser from '../../shared/jira-api-browser' import parseHtmlTreeNode from '../../shared/parse-html-treenode' import treeNodeToMdast from '../../shared/tree-node-to-mdast' import JiraImportPreview from '../import-preview' import mdastToTreeNodes from '../../shared/mdast-to-tree-node' import { markdown2confluence } from '../../shared/markdown-to-confluence' import mdastStringify from '../../shared/mdast-stringify' import { JiraSuggest } from '../epic-link-suggest' import TreeNode from '../../shared/tree-node' import url from 'url' import treeNodesToConfluence from '../../shared/tree-node-to-confluence' import UserSuggest from '../user-suggest' const IssueLabel = (x) => ( {x.name} ) const JiraModalImport = React.forwardRef< {}, { token: string } & ModalProps >(({ token, ...props }, ref) => { React.useImperativeHandle(ref, () => ({}), []) const importPreviewRef = React.useRef(null) const [input, setInput] = React.useState('') const [issueTypes, setIssueTypes] = React.useState([]) const [loading, setLoading] = React.useState(false) const onImportInputKeydown = React.useCallback((evt) => { const isCollapsed = evt.target.selectionEnd === evt.target.selectionStart if (isCollapsed && isHotKey('tab', evt)) { document.execCommand('insertText', false, ' ') evt.preventDefault() } }, []) const jiraApi = React.useMemo( () => new JiraApiBrowser({ password: token }), [token] ) const [fields, setFields] = React.useState>([]) const queryFields = React.useCallback(() => { return jiraApi.queryFields() }, [jiraApi, JIRA.API.Projects.getCurrentProjectKey()]) React.useEffect(() => { if (props.visible && JIRA.API.Projects.getCurrentProjectKey()) { setLoading('加载 Issue 配置...') queryFields() .then((fields) => { setFields(fields) }) .finally(() => { setLoading(false) }) } else { setFields([]) } }, [props.visible, queryFields, setFields]) const [form] = Form.useForm() const [parseOptions, setParseOptions] = React.useState({ estimateRegExp: `\\s+\\(?(\\d+)'?\\s*$` // `(?(\\d+)'?\\s*$` }) const [sprintData, setSprintData] = React.useState({}) const [jiraComponents, setJiraComponents] = React.useState([]) const sprintFetcher = React.useCallback(async (val) => { const renderOption = (d: any) => { return { value: d.id, label: ( {d.name} ({d.stateKey}) ) } } const res = await jiraApi.querySuggestSprints({ query: val }) const { suggestions, allMatches } = res.data || {} return { 建议: suggestions.map(renderOption), 全部: allMatches.map(renderOption) } }, []) React.useEffect(() => { const fetch = async () => { if (JIRA?.API?.Projects?.getCurrentProjectKey()) { setLoading(true) const res = await jiraApi.queryProject(JIRA?.API?.Projects?.getCurrentProjectKey()) if (res.data?.issueTypes) { setIssueTypes(res.data?.issueTypes) } const components = await jiraApi.queryComponents().catch(() => []) setJiraComponents(components) setLoading(false) } } fetch() }, [setLoading, JIRA?.API?.Projects?.getCurrentProjectKey()]) const parseOpts = React.useMemo(() => { let estimateRegExp try { estimateRegExp = new RegExp(parseOptions.estimateRegExp) } catch (e) { console.error(e) } return { ...parseOptions, estimateRegExp } }, [parseOptions]) const hasKey = React.useCallback( (key) => { return fields.find((f) => f.id === key) }, [fields] ) return ( { const mdast = importPreviewRef.current?.mdast const nodes = mdastToTreeNodes(mdast) const getReqBody = (nodes: TreeNode[], type: 'issue' | 'subTask') => { const { issuePrefix, ...rawBody } = form.getFieldsValue() || {} const commonDescription = markdown2confluence(rawBody.description || '') return { ...rawBody, components: rawBody.components?.map((id) => ({ id })), tasksBody: nodes.map((node) => { const selfDescription = treeNodesToConfluence(node.children) return { ...node.data.params, summary: ( (type === 'issue' ? [node.value && issuePrefix ? issuePrefix : null, node.value].filter(Boolean).join('') : node.value) || '' ).replace(/\n/g, ' '), description: [ commonDescription, selfDescription && commonDescription && '\n======== 子任务如下 =========\n', selfDescription ] .filter(Boolean) .join('\n') } }) } } setLoading(true) const parentRes = await jiraApi.createIssues(getReqBody(nodes, 'issue'), { toastSuccess: false }) if (parentRes.data.issues && parentRes.data.issues.length) { const subTasks = [] parentRes.data.issues.forEach((issue, i) => { subTasks.push( ...nodes[i].children.map((node) => { node.data.params = { ...node.data.params, parent: { key: issue.key }, issuetype: '5', // 子任务不能存在以下属性 dod: undefined, sprint: undefined, epicLink: undefined } return node }) ) }) let issues = parentRes.data.issues if (issues.length === nodes.length) { const updateIssuesPromiseList = issues .map((issue) => issue.id) .map(async (id, i) => { if (nodes[i]?.data?.estimate) { return jiraApi.updateIssue(id, { estimate: nodes[i]?.data?.estimate }, { toast: false }) } return }) Promise.all(updateIssuesPromiseList).catch(console.error) } if (subTasks.length) { const subTasksRes = await jiraApi.createIssues(getReqBody(subTasks, 'subTask'), { toastSuccess: false }) issues = parentRes.data.issues.concat(subTasksRes.data.issues || []) } const string = issues.map((x) => x.key).join(',') notification.success({ duration: 0, message: '创建 Jira Issue 成功', description: ( {string} ) }) } setLoading(false) }} width={1200} >
公共配置
{!!fields.length && (
{hasKey('issuetype') && ( )} {hasKey('customfield_10004') && ( )} {hasKey('components') && ( )} {hasKey('customfield_10005') && ( { const res = await jiraApi.querySuggestEpics({ searchQuery: val }) return res.data.epicLists.reduce((acc, group) => { acc[group.listDescriptor] = group.epicNames.map((d) => { return { label: ( {d.name} - ({d.key}) ), value: d.key } }) return acc }, {}) }} /> )} {hasKey('priority') && ( )} {hasKey('labels') && ( { if (!input) { return [] } const res = await jiraApi.querySuggestLabels(input) if (res.data?.suggestions) { return { 建议: res.data?.suggestions.map((x) => ({ value: x.label, label: })) } } }} mode={'tags'} /> )} {hasKey('assignee') && ( )} {hasKey('reporter') && ( )} {hasKey('customfield_10506') && ( prevValues.issuetype !== nextValues.issuetype}> {(instance) => { return ( (!instance.getFieldsValue().issuetype || instance.getFieldsValue().issuetype === '10001') && ( ) ) }} )} {hasKey('description') && ( )}
)}
解析配置
setParseOptions(v)} labelCol={labelCol} >
Issue 录入 格式说明
{ setInput(event.target.value) }} placeholder={'输入 markdown 或者复制 石墨文档内容至此'} className={c('__import-left')} onPaste={(evt) => { const html = evt.clipboardData.getData('text/html') console.log('html', html) if (html) { const treeNode = parseHtmlTreeNode(html) // console.log('treeNode', treeNode); if (treeNode !== false) { const mdast = treeNodeToMdast(treeNode) const mdText = mdastStringify(mdast) console.log({ mdast, mdText, treeNode }) evt.preventDefault() document.execCommand('insertText', false, mdText) } } }} cols={12} />
) }) export default JiraModalImport