Merge pull request #475 from merico-dev/fix-config-ui-again

Fix config UI again
diff --git a/config-ui/next.config.js b/config-ui/next.config.js
index dc702b1..4c63f9f 100644
--- a/config-ui/next.config.js
+++ b/config-ui/next.config.js
@@ -1,5 +1,5 @@
 module.exports = {
-  pageExtensions: ['page.jsx'],
+  pageExtensions: ['page.jsx', 'js'],
 
   webpack: (config, { isServer }) => {
     if (!isServer) {
diff --git a/config-ui/pages/plugins/jira/MappingTag.jsx b/config-ui/pages/plugins/jira/MappingTag.jsx
index 2e3c0c7..083206a 100644
--- a/config-ui/pages/plugins/jira/MappingTag.jsx
+++ b/config-ui/pages/plugins/jira/MappingTag.jsx
@@ -19,7 +19,7 @@
             placeholder="Add Tags..."
             values={values || []}
             fill={true}
-            onChange={onChange}
+            onChange={value => onChange([...new Set(value)])}
             addOnPaste={true}
             rightElement={rightElement}
             onKeyDown={e => e.key === 'Enter' && e.preventDefault()}
diff --git a/config-ui/pages/plugins/jira/MappingTagStatus.jsx b/config-ui/pages/plugins/jira/MappingTagStatus.jsx
index 8c42b2a..59673ae 100644
--- a/config-ui/pages/plugins/jira/MappingTagStatus.jsx
+++ b/config-ui/pages/plugins/jira/MappingTagStatus.jsx
@@ -3,7 +3,7 @@
 const MappingTagStatus = ({reqValue, resValue, envName, clearBtnReq, clearBtnRes, onChangeReq, onChangeRes}) => {
   return <>
     <MappingTag
-      labelName="Requirement"
+      labelName="Rejected"
       labelIntent="danger"
       values={reqValue}
       helperText={envName}
diff --git a/config-ui/pages/plugins/jira/index.page.jsx b/config-ui/pages/plugins/jira/index.page.jsx
index 507b827..a2b7766 100644
--- a/config-ui/pages/plugins/jira/index.page.jsx
+++ b/config-ui/pages/plugins/jira/index.page.jsx
@@ -2,14 +2,12 @@
 import { useState, useEffect } from 'react'
 import styles from '../../../styles/Home.module.css'
 import {
-  Tooltip, Position, FormGroup, InputGroup, Button, Label, Icon, Classes, Tab, Tabs, Overlay, Dialog
+  Tooltip, Position, FormGroup, InputGroup, Button, Label, Icon, Classes, Dialog
 } from '@blueprintjs/core'
 import dotenv from 'dotenv'
 import path from 'path'
 import * as fs from 'fs/promises'
 import { existsSync } from 'fs'
-import { findStrBetween } from '../../../utils/findStrBetween'
-import { readAndSet } from '../../../utils/readAndSet'
 import Nav from '../../../components/Nav'
 import Sidebar from '../../../components/Sidebar'
 import Content from '../../../components/Content'
@@ -18,6 +16,25 @@
 import MappingTagStatus from './MappingTagStatus'
 import ClearButton from './ClearButton'
 
+function parseMapping(mappingString) {
+  const mapping = {}
+  if (!mappingString.trim()) {
+    return mapping
+  }
+  for (const item of mappingString.split(";")) {
+    let [standard, customs] = item.split(":")
+    standard = standard.trim()
+    mapping[standard] = mapping[standard] || []
+    if (!customs) {
+      continue
+    }
+    for (const custom of customs.split(",")) {
+      mapping[standard].push(custom.trim())
+    }
+  }
+  return mapping
+}
+
 export default function Home(props) {
   const { env } = props
 
@@ -30,22 +47,56 @@
   const [jiraBoardGitlabeProjects, setJiraBoardGitlabeProjects] = useState(env.JIRA_BOARD_GITLAB_PROJECTS)
 
   // Type mappings state
-  const [typeMappingBug, setTypeMappingBug] = useState([])
-  const [typeMappingIncident, setTypeMappingIncident] = useState([])
-  const [typeMappingRequirement, setTypeMappingRequirement] = useState([])
+  const defaultTypeMapping = parseMapping(env.JIRA_ISSUE_TYPE_MAPPING)
+  const [typeMappingBug, setTypeMappingBug] = useState(defaultTypeMapping.Bug || [])
+  const [typeMappingIncident, setTypeMappingIncident] = useState(defaultTypeMapping.Incident || [])
+  const [typeMappingRequirement, setTypeMappingRequirement] = useState(defaultTypeMapping.Requirement || [])
   const [typeMappingAll, setTypeMappingAll] = useState()
 
-  // Status mappings state
+  // status mapping
+  const defaultStatusMappings = []
+  for (const [key, value] of Object.entries(env)) {
+    const m = /^JIRA_ISSUE_([A-Z]+)_STATUS_MAPPING$/.exec(key)
+    if (!m) {
+      continue
+    }
+    const type = m[1]
+    defaultStatusMappings.push({
+      type,
+      key,
+      mapping: parseMapping(value)
+    })
+  }
+  const [statusMappings, setStatusMappings] = useState(defaultStatusMappings)
+  function setStatusMapping(key, values, status) {
+    setStatusMappings(statusMappings.map(mapping => {
+      if (mapping.key === key) {
+        mapping.mapping[status] = values
+      }
+      return mapping
+    }))
+  }
   const [customStatusOverlay, setCustomStatusOverlay] = useState(false)
-  const [statusTabId, setStatusTabId] = useState(0)
-  const [statusMappingReqBug, setStatusMappingReqBug] = useState([])
-  const [statusMappingResBug, setStatusMappingResBug] = useState([])
-  const [statusMappingReqIncident, setStatusMappingReqIncident] = useState([])
-  const [statusMappingResIncident, setStatusMappingResIncident] = useState([])
-  const [statusMappingReqStory, setStatusMappingReqStory] = useState([])
-  const [statusMappingResStory, setStatusMappingResStory] = useState([])
-  const [customStatus, setCustomStatus] = useState([])
   const [customStatusName, setCustomStatusName] = useState('')
+  function addStatusMapping(e) {
+    const type = customStatusName.trim().toUpperCase()
+    if (statusMappings.find(e => e.type === type)) {
+      return
+    }
+    setStatusMappings([
+      ...statusMappings,
+      {
+        type,
+        key: `JIRA_ISSUE_${type}_STATUS_MAPPING`,
+        mapping: {
+          Resolved: [],
+          Rejected: [],
+        }
+      }
+    ])
+    setCustomStatusOverlay(false)
+    e.preventDefault()
+  }
 
 
   function updateEnv(key, value) {
@@ -58,19 +109,14 @@
     updateEnv('JIRA_BASIC_AUTH_ENCODED', jiraBasicAuthEncoded)
     updateEnv('JIRA_ISSUE_EPIC_KEY_FIELD', jiraIssueEpicKeyField)
     updateEnv('JIRA_ISSUE_TYPE_MAPPING', typeMappingAll)
-    updateEnv('JIRA_ISSUE_BUG_STATUS_MAPPING', `Requirement:${statusMappingReqBug};Resolved:${statusMappingResBug};`)
-    updateEnv('JIRA_ISSUE_INCIDENT_STATUS_MAPPING', `Requirement:${statusMappingReqIncident};Resolved:${statusMappingResIncident};`)
-    updateEnv('JIRA_ISSUE_STORY_STATUS_MAPPING', `Requirement:${statusMappingReqStory};Resolved:${statusMappingResStory};`)
     updateEnv('JIRA_ISSUE_STORYPOINT_COEFFICIENT', jiraIssueStoryCoefficient)
     updateEnv('JIRA_ISSUE_STORYPOINT_FIELD', jiraIssueStoryPointField)
     updateEnv('JIRA_BOARD_GITLAB_PROJECTS', jiraBoardGitlabeProjects)
 
     // Save all custom status data
-    customStatus.map(status => {
-      const requirement = status.reqValue.toString()
-      const resolved = status.resValue.toString()
-      const name = `JIRA_ISSUE_${status.name.toUpperCase()}_STATUS_MAPPING`
-      updateEnv(name, `Requirement:${requirement};Resolved:${resolved};`)
+    statusMappings.map(mapping => {
+      const { Resolved, Rejected } = mapping.mapping
+      updateEnv(mapping.key, `Rejected:${Rejected ? Rejected.join(',') : ''};Resolved:${Resolved ? Resolved.join(',') : ''};`)
     })
 
     setAlertOpen(true)
@@ -84,68 +130,6 @@
     setTypeMappingAll(all)
   }, [typeMappingBug, typeMappingIncident, typeMappingRequirement])
 
-  useEffect(() => {
-    // Load type & status mappings
-    const envStr = [
-      env.JIRA_ISSUE_TYPE_MAPPING,
-      env.JIRA_ISSUE_BUG_STATUS_MAPPING,
-      env.JIRA_ISSUE_INCIDENT_STATUS_MAPPING,
-      env.JIRA_ISSUE_STORY_STATUS_MAPPING
-    ]
-    const fields = [
-      {
-        tagName: 'Bug:', tagLen: 4, isStatus: false, str: envStr[0],
-        fn1: (arr) => setTypeMappingBug(arr)
-      },
-      {
-        tagName: 'Incident:', tagLen: 9, isStatus: false, str: envStr[0],
-        fn1: (arr) => setTypeMappingIncident(arr)
-      },
-      {
-        tagName: 'Requirement:', tagLen: 12, isStatus: false, str: envStr[0],
-        fn1: (arr) => setTypeMappingRequirement(arr)
-      },
-      {
-        tagName: 'Bug:', tagLen: null, isStatus: true, str: envStr[1],
-        fn1: (arr) => setStatusMappingReqBug(arr),
-        fn2: (arr) => setStatusMappingResBug(arr)
-      },
-      {
-        tagName: 'Incident:', tagLen: null, isStatus: true, str: envStr[2],
-        fn1: (arr) => setStatusMappingReqIncident(arr),
-        fn2: (arr) => setStatusMappingResIncident(arr)
-      },
-      {
-        tagName: 'Story:', tagLen: null, isStatus: true, str: envStr[3],
-        fn1: (arr) => setStatusMappingReqStory(arr),
-        fn2: (arr) => setStatusMappingResStory(arr)
-      },
-    ]
-
-    fields.map(field => {
-      readAndSet(field.tagName, field.tagLen, field.isStatus, field.str, field.fn1, field.fn2)
-    })
-
-    //Load custom status mappings
-    for (const field in env) {
-      const bug = 'JIRA_ISSUE_BUG'
-      const incident = 'JIRA_ISSUE_INCIDENT'
-      const story = 'JIRA_ISSUE_STORY'
-      const isStatusMapping = field.includes('_STATUS_MAPPING')
-      const isNotDefault = (!field.includes(bug)) && (!field.includes(incident)) && (!field.includes(story))
-
-      if (isStatusMapping && isNotDefault) {
-        const strName = field.slice(11, -15)
-        const strValuesReq = findStrBetween(env[field], 'Requirement:', ';')
-        const strValuesRes = findStrBetween(env[field], 'Resolved:', ';')
-        const req = strValuesReq[0].slice(12, -1).split(',')
-        const res = strValuesRes[0].slice(9, -1).split(',')
-
-        setCustomStatus(customStatus => [...customStatus, {name: strName, reqValue: req || '', resValue: res || ''}])
-      }
-    }
-  }, [])
-
   return (
     <div className={styles.container}>
 
@@ -254,90 +238,39 @@
               <p className={styles.description}>Map your own issue statuses to Dev Lake's standard statuses for every issue type</p>
             </div>
 
-            <div className={styles.formContainer}>
-
-              <Tabs id="StatusMappings" onChange={(id) => setStatusTabId(id)} selectedTabId={statusTabId} className={styles.statusTabs}>
-                <Tab id={0} title="Bug" panel={
-                  <MappingTagStatus
-                    reqValue={statusMappingReqBug}
-                    resValue={statusMappingResBug}
-                    envName="JIRA_ISSUE_BUG_STATUS_MAPPING"
-                    clearBtnReq={<ClearButton onClick={() => setStatusMappingReqBug([])} />}
-                    clearBtnRes={<ClearButton onClick={() => setStatusMappingResBug([])} />}
-                    onChangeReq={(values) => setStatusMappingReqBug(values)}
-                    onChangeRes={(values) => setStatusMappingResBug(values)}
-                  />
-                } />
-                <Tab id={1} title="Incident" panel={
-                  <MappingTagStatus
-                    reqValue={statusMappingReqIncident}
-                    resValue={statusMappingResIncident}
-                    envName="JIRA_ISSUE_INCIDENT_STATUS_MAPPING"
-                    clearBtnReq={<ClearButton onClick={() => setStatusMappingReqIncident([])} />}
-                    clearBtnRes={<ClearButton onClick={() => setStatusMappingResIncident([])} />}
-                    onChangeReq={(values) => setStatusMappingReqIncident(values)}
-                    onChangeRes={(values) => setStatusMappingResIncident(values)}
-                  />
-                } />
-                <Tab id={2} title="Story" panel={
-                  <MappingTagStatus
-                    reqValue={statusMappingReqStory}
-                    resValue={statusMappingResStory}
-                    envName="JIRA_ISSUE_STORY_STATUS_MAPPING"
-                    clearBtnReq={<ClearButton onClick={() => setStatusMappingReqStory([])} />}
-                    clearBtnRes={<ClearButton onClick={() => setStatusMappingResStory([])} />}
-                    onChangeReq={(values) => setStatusMappingResStory(values)}
-                    onChangeRes={(values) => setStatusMappingResStory(values)}
-                  />
-                } />
-
-                {customStatus.length > 0 && customStatus.map((status, i) => {
-                  const statusAll = customStatus.filter(obj => obj.name != status.name)
-                  const mapObj = customStatus.find(obj => obj.name === status.name)
-
-                  return <Tab id={i + 3} key={i} title={status.name} panel={
-                    <MappingTagStatus
-                      reqValue={mapObj.reqValue.length > 0 ? mapObj.reqValue : []}
-                      resValue={mapObj.resValue.length > 0 ? mapObj.resValue : []}
-                      envName={`JIRA_ISSUE_${status.name.toUpperCase()}_STATUS_MAPPING`}
-                      clearBtnReq={<ClearButton onClick={() => {
-                        mapObj.reqValue = []
-                        setCustomStatus([...statusAll, mapObj])
-                      }} />}
-                      clearBtnRes={<ClearButton onClick={() => {
-                        mapObj.resValue = []
-                        setCustomStatus([...statusAll, mapObj])
-                      }} />}
-                      onChangeReq={(values) => {
-                        mapObj.reqValue = values
-                        setCustomStatus([...statusAll, mapObj])
-                      }}
-                      onChangeRes={(values) => {
-                        mapObj.resValue = values
-                        setCustomStatus([...statusAll, mapObj])
-                      }}
-                    />
-                  } />
-                })}
-
+            <div className={styles.formContainer} style={{height: 'auto', flexWrap: 'wrap'}}>
+                {statusMappings.length > 0 && statusMappings.map((statusMapping, i) =>
+                  <div
+                  key={statusMapping.key} style={{width: "100%"}}>
+                    <p>Mapping {statusMapping.type} </p>
+                    <div style={{marginLeft: "2em"}}>
+                      <MappingTagStatus
+                        reqValue={statusMapping.mapping.Rejected || []}
+                        resValue={statusMapping.mapping.Resolved || []}
+                        envName={statusMapping.key}
+                        clearBtnReq={<ClearButton onClick={() => setStatusMapping(statusMapping.key, [], 'Rejected')} />}
+                        clearBtnRes={<ClearButton onClick={() => setStatusMapping(statusMapping.key, [], 'Resolved')} />}
+                        onChangeReq={values => setStatusMapping(statusMapping.key, values, 'Rejected')}
+                        onChangeRes={values => setStatusMapping(statusMapping.key, values, 'Resolved')}
+                        style={{paddingLeft: '2em', boxSizing: 'border-box'}}
+                      />
+                    </div>
+                  </div>
+                )}
                 <Button icon="add" onClick={() => setCustomStatusOverlay(true)} className={styles.addNewStatusBtn}>Add New</Button>
 
                 <Dialog
                   style={{ width: '100%', maxWidth: "664px", height: "auto" }}
                   icon="diagram-tree"
                   onClose={() => setCustomStatusOverlay(false)}
-                  title="Add a New Custom Status"
+                  title="Add a New Status Mapping"
                   isOpen={customStatusOverlay}
                   onOpened={() => setCustomStatusName('')}
                   autoFocus={false}
                   className={styles.customStatusDialog}
                 >
                   <div className={Classes.DIALOG_BODY}>
-                  <form onSubmit={(e) => {
-                    e.preventDefault()
-                    setCustomStatus([...customStatus, {name: customStatusName, reqValue: '', resValue: ''}])
-                    setCustomStatusOverlay(false)
-                  }}>
+                  <form onSubmit={addStatusMapping}>
                     <FormGroup
                       className={styles.formGroup}
                       className={styles.customStatusFormGroup}
@@ -349,10 +282,7 @@
                         className={styles.customStatusInput}
                         autoFocus={true}
                       />
-                      <Button icon="add" onClick={() => {
-                          setCustomStatus([...customStatus, {name: customStatusName, reqValue: '', resValue: ''}])
-                          setCustomStatusOverlay(false)
-                        }}
+                      <Button icon="add" onClick={addStatusMapping}
                         className={styles.addNewStatusBtnDialog}
                         onSubmit={(e) => e.preventDefault()}>Add New</Button>
                     </FormGroup>
@@ -360,8 +290,6 @@
                   </div>
                 </Dialog>
 
-                <Tabs.Expander />
-              </Tabs>
             </div>
 
           <div className={styles.headlineContainer}>