List.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, {
  21. useMemo,
  22. useEffect,
  23. useState,
  24. ReactElement,
  25. useCallback,
  26. useRef,
  27. useImperativeHandle
  28. } from 'react'
  29. import classnames from 'classnames'
  30. import { connect } from 'react-redux'
  31. import { Row, Col, Tooltip, Popconfirm, Icon, Modal, Button } from 'antd'
  32. import saga from './sagas'
  33. import reducer from './reducer'
  34. import { compose } from 'redux'
  35. import { makeSelectLoginUser } from '../App/selectors'
  36. import {
  37. makeSelectProjects,
  38. makeSelectSearchProject,
  39. makeSelectStarUserList,
  40. makeSelectCollectProjects
  41. } from './selectors'
  42. import { ProjectActions } from './actions'
  43. import injectReducer from 'utils/injectReducer'
  44. import { createStructuredSelector } from 'reselect'
  45. import injectSaga from 'utils/injectSaga'
  46. import ProjectsForm from './component/ProjectForm'
  47. import reducerOrganization from '../Organizations/reducer'
  48. import { IOrganization } from '../Organizations/types'
  49. import sagaOrganization from '../Organizations/sagas'
  50. import { OrganizationActions } from '../Organizations/actions'
  51. import { makeSelectOrganizations } from '../Organizations/selectors'
  52. import { checkNameUniqueAction } from '../App/actions'
  53. import ComponentPermission from '../Account/components/checkMemberPermission'
  54. import Star from 'components/StarPanel/Star'
  55. import HistoryStack from '../Organizations/component/historyStack'
  56. import { RouteComponentWithParams } from 'utils/types'
  57. import {
  58. IProject,
  59. IProjectFormFieldProps,
  60. IProjectsProps,
  61. projectType,
  62. IProjectType,
  63. IToolbarProps,
  64. projectTypeSmall
  65. } from './types'
  66. import { FormComponentProps } from 'antd/lib/form/Form'
  67. import { uuid } from 'app/utils/util'
  68. import { useResize } from './hooks/useResize'
  69. import ProjectItem from './component/ProjectItem'
  70. const StarUserModal = Star.StarUser
  71. const historyStack = new HistoryStack()
  72. const styles = require('../Organizations/Project.less')
  73. function enhanceInput(props, ref) {
  74. const inputRef = useRef(null)
  75. useImperativeHandle(ref, () => ({}))
  76. return <input {...props} ref={inputRef} />
  77. }
  78. const EnhanceInput = React.forwardRef(enhanceInput)
  79. const Toolbar: React.FC<IToolbarProps> = React.memo(
  80. ({ pType, setPType, setKeywords, searchKeywords, showProForm }) => {
  81. const searchRef = useRef(null)
  82. const documentWidth = useResize()
  83. const checkoutType = useCallback(
  84. (type) => {
  85. return () => {
  86. if (setPType) {
  87. setPType(type)
  88. }
  89. }
  90. },
  91. [pType]
  92. )
  93. const menus = useMemo(() => {
  94. const types = ['all', 'join', 'create', 'favorite', 'history']
  95. return types.map((t: IProjectType) => {
  96. const classNames = classnames({
  97. [styles.selectMenu]: pType === t,
  98. [styles.menuitem]: true
  99. })
  100. return (
  101. <p key={t} className={classNames} onClick={checkoutType(t)}>
  102. {documentWidth <= 1200 ? projectTypeSmall[t] : projectType[t]}
  103. </p>
  104. )
  105. })
  106. }, [pType, documentWidth])
  107. const getKeywords = useCallback(
  108. (e) => {
  109. setKeywords(e.target.value)
  110. },
  111. [setKeywords]
  112. )
  113. const addPro = useCallback(
  114. (e) => {
  115. if (showProForm) {
  116. showProForm('add', {}, e)
  117. }
  118. },
  119. [showProForm]
  120. )
  121. return (
  122. <div className={styles.toolbar}>
  123. <div className={styles.menu}>{menus}</div>
  124. <div className={styles.searchs}>
  125. <EnhanceInput
  126. type='text'
  127. ref={searchRef}
  128. val={searchKeywords}
  129. onChange={getKeywords}
  130. placeholder='查找您的项目'
  131. />
  132. <span className={styles.searchButton}>
  133. <i className='iconfont icon-search' />
  134. </span>
  135. </div>
  136. <div className={styles.create}>
  137. <Button icon='plus' type='primary' shape='round' onClick={addPro}>
  138. {documentWidth < 860 ? '' : '创建'}
  139. </Button>
  140. </div>
  141. </div>
  142. )
  143. }
  144. )
  145. function stopPPG(e: React.MouseEvent<HTMLElement>) {
  146. if (e) {
  147. e.stopPropagation()
  148. }
  149. return
  150. }
  151. const Projects: React.FC<IProjectsProps & RouteComponentWithParams> = React.memo(
  152. ({
  153. projects,
  154. onLoadProjects,
  155. onLoadOrganizations,
  156. organizations,
  157. loginUser,
  158. onLoadCollectProjects,
  159. collectProjects,
  160. history,
  161. onAddProject,
  162. onCheckUniqueName,
  163. onTransferProject,
  164. onDeleteProject,
  165. onClickCollectProjects,
  166. starUserList,
  167. onStarProject,
  168. onGetProjectStarUser,
  169. onEditProject
  170. }) => {
  171. const [formKey, setFormKey] = useState(() => uuid(8, 16))
  172. const [projectType, setProjectType] = useState('all')
  173. const [formVisible, setFormVisible] = useState(false)
  174. const [formType, setFormType] = useState('add')
  175. const [currentPro, setCurrentPro] = useState({})
  176. const [modalLoading, setModalLoading] = useState(false)
  177. const [searchKeywords, setKeywords] = useState('')
  178. const [starModalVisble, setStarModalVisble] = useState(false)
  179. const onCloseStarModal = useCallback(() => {
  180. setStarModalVisble(false)
  181. }, [starModalVisble])
  182. const getStarProjectUserList = useCallback(
  183. (id) => () => {
  184. if (onGetProjectStarUser) {
  185. onGetProjectStarUser(id)
  186. }
  187. setStarModalVisble(true)
  188. },
  189. [setStarModalVisble, onGetProjectStarUser]
  190. )
  191. let proForm: FormComponentProps<IProjectFormFieldProps> = null
  192. useEffect(() => {
  193. window.localStorage.setItem('inDataService', '')
  194. }, [])
  195. useEffect(() => {
  196. if (onLoadProjects) {
  197. onLoadProjects()
  198. }
  199. if (onLoadOrganizations) {
  200. onLoadOrganizations()
  201. }
  202. if (onLoadCollectProjects) {
  203. onLoadCollectProjects()
  204. }
  205. }, ['nf'])
  206. useEffect(() => {
  207. if (projects) {
  208. historyStack.init(projects)
  209. }
  210. }, [projects])
  211. const loginUserId = useMemo(() => {
  212. return loginUser && loginUser.id
  213. }, [loginUser])
  214. const checkoutFormVisible = useCallback(() => {
  215. setFormVisible(true)
  216. }, [formVisible])
  217. const hideProForm = useCallback(() => {
  218. setFormVisible(false)
  219. }, [formVisible])
  220. const deletePro = useCallback(
  221. (proId: number, isFavorite: boolean) => {
  222. if (onDeleteProject) {
  223. onDeleteProject(proId, () => {
  224. if (isFavorite) {
  225. if (onLoadCollectProjects) {
  226. onLoadCollectProjects()
  227. }
  228. }
  229. if (onLoadProjects) {
  230. onLoadProjects()
  231. }
  232. })
  233. }
  234. },
  235. [formVisible]
  236. )
  237. const favoritePro = useCallback(
  238. (proId: number, isFavorite: boolean) => {
  239. if (onClickCollectProjects) {
  240. onClickCollectProjects(isFavorite, proId, () => {
  241. if (onLoadCollectProjects) {
  242. onLoadCollectProjects()
  243. }
  244. if (onLoadProjects) {
  245. onLoadProjects()
  246. }
  247. })
  248. }
  249. },
  250. [formVisible]
  251. )
  252. const showProForm = useCallback(
  253. (formType, project: IProject, e: React.MouseEvent<HTMLElement>) => {
  254. stopPPG(e)
  255. setFormVisible(true)
  256. setCurrentPro(project)
  257. setFormType(formType)
  258. },
  259. [formVisible, formType, currentPro]
  260. )
  261. const onModalOk = useCallback(() => {
  262. proForm.form.validateFieldsAndScroll(
  263. (err, values: IProjectFormFieldProps) => {
  264. if (!err) {
  265. setModalLoading(true)
  266. if (formType === 'add') {
  267. onAddProject(
  268. {
  269. ...values,
  270. visibility: values.visibility === 'true' ? true : false,
  271. pic: `${Math.ceil(Math.random() * 19)}`
  272. },
  273. () => {
  274. hideProForm()
  275. onLoadProjects()
  276. setModalLoading(false)
  277. const newFormKey = uuid(8, 16)
  278. setFormKey(newFormKey)
  279. }
  280. )
  281. } else {
  282. onEditProject(
  283. { ...values, ...{ orgId: Number(values.orgId) } },
  284. () => {
  285. hideProForm()
  286. onLoadProjects()
  287. setModalLoading(false)
  288. const newFormKey = uuid(8, 16)
  289. setFormKey(newFormKey)
  290. }
  291. )
  292. }
  293. }
  294. }
  295. )
  296. }, [formVisible, formType, setFormKey])
  297. const onTransfer = useCallback(() => {
  298. proForm.form.validateFieldsAndScroll((err, values) => {
  299. if (!err) {
  300. setModalLoading(true)
  301. const { id, orgId } = values
  302. onTransferProject(id, Number(orgId))
  303. hideProForm()
  304. const newFormKey = uuid(8, 16)
  305. setFormKey(newFormKey)
  306. }
  307. })
  308. }, [formVisible, setFormKey])
  309. const checkNameUnique = useCallback(
  310. (rule, value = '', callback) => {
  311. const fieldsValue = proForm.form.getFieldsValue()
  312. const { orgId, id } = fieldsValue
  313. onCheckUniqueName(
  314. 'project',
  315. {
  316. name: value,
  317. orgId,
  318. id
  319. },
  320. () => {
  321. callback()
  322. },
  323. (err) => {
  324. callback(err)
  325. }
  326. )
  327. },
  328. [formVisible]
  329. )
  330. const getProjectsBySearch = useMemo(() => {
  331. const { proIdList } = historyStack.getAll()
  332. function filterByKeyword(arr: IProject[]) {
  333. return (
  334. Array.isArray(arr) &&
  335. arr.filter(
  336. (pro: IProject) =>
  337. pro.name.toUpperCase().indexOf(searchKeywords.toUpperCase()) > -1
  338. )
  339. )
  340. }
  341. function filterByProjectType(arr: IProject[]) {
  342. if (Array.isArray(arr)) {
  343. switch (projectType) {
  344. case 'create':
  345. return arr.filter(
  346. (pro) => pro.createBy && pro.createBy.id === loginUserId
  347. )
  348. case 'join':
  349. return arr.filter(
  350. (pro) => pro.createBy && pro.createBy.id !== loginUserId
  351. )
  352. case 'favorite':
  353. return arr.filter((pro) => pro.isFavorites)
  354. case 'history':
  355. return proIdList.reduce((iteratee, pid) => {
  356. const pl = arr.find((pro) => pro.id === pid)
  357. if (pl) {
  358. iteratee.push(pl)
  359. }
  360. return iteratee
  361. }, [])
  362. case 'all':
  363. return arr
  364. default:
  365. return []
  366. }
  367. }
  368. }
  369. function pushForkTagProjects(arr: IProject[]) {
  370. const favoriteProjectsId =
  371. Array.isArray(collectProjects) &&
  372. collectProjects.length > 0 &&
  373. collectProjects.map((col) => col.id)
  374. return Array.isArray(arr)
  375. ? arr.map((pro) => {
  376. return favoriteProjectsId.includes &&
  377. favoriteProjectsId.includes(pro.id)
  378. ? { ...pro, isFavorites: true }
  379. : pro
  380. })
  381. : []
  382. }
  383. return compose(
  384. filterByProjectType,
  385. filterByKeyword,
  386. pushForkTagProjects
  387. )(projects)
  388. }, [projects, projectType, searchKeywords, loginUserId, collectProjects])
  389. const ProjectItems: ReactElement[] = useMemo(() => {
  390. const items = Array.isArray(projects)
  391. ? getProjectsBySearch.map((pro: IProject, index) => {
  392. const {
  393. pic,
  394. name,
  395. description,
  396. createBy,
  397. isStar,
  398. isFavorites,
  399. id,
  400. starNum,
  401. orgId
  402. } = pro
  403. const isMimePro = !!(createBy && createBy.id === loginUserId)
  404. const getTagType = (function(mime, favorite) {
  405. const tagType = []
  406. if (mime) {
  407. tagType.push({
  408. text: '创建',
  409. color: '#108EE9'
  410. })
  411. } else {
  412. tagType.push({
  413. text: '参与',
  414. color: '#FA8C15'
  415. })
  416. }
  417. return tagType
  418. })(isMimePro, isFavorites)
  419. const starProject = (id) => () => {
  420. onStarProject(id, () => {
  421. if (onLoadProjects) {
  422. onLoadProjects()
  423. }
  424. })
  425. }
  426. const toProject = () => {
  427. history.push(`/project/${id}`)
  428. saveHistory(pro)
  429. }
  430. const saveHistory = (pro) => historyStack.pushNode(pro)
  431. const currentOrganization: IOrganization = organizations.find(
  432. (org) => org.id === orgId
  433. )
  434. const CreateButton = ComponentPermission(
  435. currentOrganization,
  436. ''
  437. )(Icon)
  438. const isHistoryType = !!(projectType && projectType === 'history')
  439. const favoriteProject = (e: React.MouseEvent<HTMLElement>) => {
  440. const { id, isFavorites } = pro
  441. stopPPG(e)
  442. if (favoritePro) {
  443. favoritePro(id, isFavorites)
  444. }
  445. }
  446. const transferPro = (e: React.MouseEvent<HTMLElement>) => {
  447. stopPPG(e)
  448. if (showProForm) {
  449. showProForm('transfer', pro, e)
  450. }
  451. }
  452. const editPro = (e: React.MouseEvent<HTMLElement>) => {
  453. stopPPG(e)
  454. if (showProForm) {
  455. showProForm('edit', pro, e)
  456. }
  457. }
  458. const deleteProject = (e: React.MouseEvent<HTMLElement>) => {
  459. const { id, isFavorites } = pro
  460. if (deletePro) {
  461. deletePro(id, isFavorites)
  462. }
  463. stopPPG(e)
  464. }
  465. const { Favorite, Transfer, Edit, Delete } = (function() {
  466. const favoriteClassName = classnames({
  467. [styles.ft16]: true,
  468. [styles.mainColor]: isFavorites
  469. })
  470. const themeFavorite = isFavorites ? 'filled' : 'outlined'
  471. const Favorite = !isMimePro ? (
  472. <Tooltip title='收藏'>
  473. <Icon
  474. type='heart'
  475. theme={themeFavorite}
  476. className={favoriteClassName}
  477. onClick={favoriteProject}
  478. />
  479. </Tooltip>
  480. ) : (
  481. []
  482. )
  483. const Transfer = (
  484. <Tooltip title='移交'>
  485. <CreateButton
  486. type='swap'
  487. className={styles.ft16}
  488. onClick={transferPro}
  489. />
  490. </Tooltip>
  491. )
  492. const Edit = (
  493. <Tooltip title='编辑'>
  494. <CreateButton
  495. type='form'
  496. className={styles.ft16}
  497. onClick={editPro}
  498. />
  499. </Tooltip>
  500. )
  501. const Delete = (
  502. <Popconfirm
  503. title='确定删除?'
  504. placement='bottom'
  505. onCancel={stopPPG}
  506. onConfirm={deleteProject}
  507. >
  508. <Tooltip title='删除'>
  509. <CreateButton
  510. type='delete'
  511. className={styles.ft16}
  512. onClick={stopPPG}
  513. />
  514. </Tooltip>
  515. </Popconfirm>
  516. )
  517. return {
  518. Edit,
  519. Favorite,
  520. Transfer,
  521. Delete
  522. }
  523. })()
  524. return (
  525. <Col
  526. key={`pro${name}${uuid(8, 16)}orp${description}`}
  527. xxl={4}
  528. xl={6}
  529. lg={6}
  530. md={8}
  531. sm={12}
  532. xs={24}
  533. >
  534. <ProjectItem
  535. title={name}
  536. tags={getTagType}
  537. onClick={toProject}
  538. description={description}
  539. key={`projectItem${uuid}`}
  540. backgroundImg={`url(${require(`assets/images/bg${pic}.png`)})`}
  541. >
  542. <div className={styles.others}>
  543. {!isHistoryType ? Edit : ''}
  544. {Favorite}
  545. {!isHistoryType ? Transfer : ''}
  546. {!isHistoryType ? Delete : ''}
  547. </div>
  548. <div className={styles.stars}>
  549. <Star
  550. proId={id}
  551. starNum={starNum}
  552. isStar={isStar}
  553. unStar={starProject}
  554. userList={getStarProjectUserList}
  555. />
  556. </div>
  557. </ProjectItem>
  558. </Col>
  559. )
  560. })
  561. : []
  562. return items
  563. }, [
  564. getProjectsBySearch,
  565. projectType,
  566. projects,
  567. onLoadProjects,
  568. onStarProject,
  569. onGetProjectStarUser,
  570. loginUserId,
  571. organizations
  572. ])
  573. return (
  574. <div className={styles.wrapper}>
  575. <Toolbar
  576. pType={projectType}
  577. showProForm={showProForm}
  578. setPType={setProjectType}
  579. setKeywords={setKeywords}
  580. searchKeywords={searchKeywords}
  581. setFormVisible={checkoutFormVisible}
  582. />
  583. <div className={styles.content}>
  584. {projects ? (
  585. projects.length > 0 ? (
  586. <div className={styles.flex}>{ProjectItems}</div>
  587. ) : (
  588. <div className={styles.noprojects}>
  589. <p className={styles.desc}>无项目</p>
  590. </div>
  591. )
  592. ) : (
  593. ''
  594. )}
  595. </div>
  596. <Modal
  597. title={null}
  598. footer={null}
  599. visible={formVisible}
  600. onCancel={hideProForm}
  601. key={`modal${formKey}key`}
  602. >
  603. <ProjectsForm
  604. key={`form${formKey}key`}
  605. type={formType}
  606. onModalOk={onModalOk}
  607. onTransfer={onTransfer}
  608. currentPro={currentPro}
  609. modalLoading={modalLoading}
  610. organizations={organizations}
  611. onCheckUniqueName={checkNameUnique}
  612. wrappedComponentRef={(ref) => {
  613. proForm = ref
  614. }}
  615. />
  616. </Modal>
  617. <StarUserModal
  618. visible={starModalVisble}
  619. starUser={starUserList}
  620. closeUserListModal={onCloseStarModal}
  621. />
  622. </div>
  623. )
  624. }
  625. )
  626. const mapStateToProps = createStructuredSelector({
  627. projects: makeSelectProjects(),
  628. loginUser: makeSelectLoginUser(),
  629. starUserList: makeSelectStarUserList(),
  630. organizations: makeSelectOrganizations(),
  631. searchProject: makeSelectSearchProject(),
  632. collectProjects: makeSelectCollectProjects()
  633. })
  634. export function mapDispatchToProps(dispatch) {
  635. return {
  636. onLoadProjects: () => dispatch(ProjectActions.loadProjects()),
  637. onSearchProject: (param) => dispatch(ProjectActions.searchProject(param)),
  638. onLoadProjectDetail: (id) => dispatch(ProjectActions.loadProjectDetail(id)),
  639. onLoadCollectProjects: () => dispatch(ProjectActions.loadCollectProjects()),
  640. onLoadOrganizations: () =>
  641. dispatch(OrganizationActions.loadOrganizations()),
  642. onGetProjectStarUser: (id) =>
  643. dispatch(ProjectActions.getProjectStarUser(id)),
  644. onStarProject: (id, resolve) =>
  645. dispatch(ProjectActions.unStarProject(id, resolve)),
  646. onTransferProject: (id, orgId) =>
  647. dispatch(ProjectActions.transferProject(id, orgId)),
  648. onDeleteProject: (id, resolve) =>
  649. dispatch(ProjectActions.deleteProject(id, resolve)),
  650. onAddProject: (project, resolve) =>
  651. dispatch(ProjectActions.addProject(project, resolve)),
  652. onEditProject: (project, resolve) =>
  653. dispatch(ProjectActions.editProject(project, resolve)),
  654. onCheckUniqueName: (pathname, data, resolve, reject) =>
  655. dispatch(checkNameUniqueAction(pathname, data, resolve, reject)),
  656. onClickCollectProjects: (isFavorite, proId, result) =>
  657. dispatch(ProjectActions.clickCollectProjects(isFavorite, proId, result))
  658. }
  659. }
  660. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  661. const withReducer = injectReducer({ key: 'project', reducer })
  662. const withSaga = injectSaga({ key: 'project', saga })
  663. const withOrganizationReducer = injectReducer({
  664. key: 'organization',
  665. reducer: reducerOrganization
  666. })
  667. const withOrganizationSaga = injectSaga({
  668. key: 'organization',
  669. saga: sagaOrganization
  670. })
  671. export default compose(
  672. withReducer,
  673. withOrganizationReducer,
  674. withSaga,
  675. withOrganizationSaga,
  676. withConnect
  677. )(Projects)