List.tsx 21 KB

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