ProjectMemberModal.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import { observer } from 'mobx-react'
  17. import styled from 'styled-components'
  18. import type { Field as FieldType } from '../../../types/Field'
  19. import type { User } from '../../../types/User'
  20. import type { Project, Role } from '../../../types/Project'
  21. import Button from '../../atoms/Button'
  22. import Modal from '../../molecules/Modal'
  23. import Field, { Asterisk } from '../../molecules/EndpointField'
  24. import ToggleButtonBar from '../../atoms/ToggleButtonBar'
  25. import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
  26. import StyleProps from '../../styleUtils/StyleProps'
  27. import Palette from '../../styleUtils/Palette'
  28. import userImage from './images/user.svg'
  29. import KeyboardManager from '../../../utils/KeyboardManager'
  30. const Wrapper = styled.div`
  31. padding: 48px 32px 32px 32px;
  32. display: flex;
  33. flex-direction: column;
  34. `
  35. const Image = styled.div`
  36. width: 96px;
  37. height: 96px;
  38. background: url('${userImage}') center no-repeat;
  39. margin: 0 auto;
  40. `
  41. const ToggleButtonBarStyled = styled(ToggleButtonBar)`
  42. margin-top: 48px;
  43. `
  44. const Form = styled.div`
  45. display: flex;
  46. justify-content: space-between;
  47. flex-wrap: wrap;
  48. margin-top: 32px;
  49. overflow: auto;
  50. > div {
  51. margin-top: 16px;
  52. }
  53. `
  54. const FieldStyled = styled(Field)`
  55. ${StyleProps.exactWidth('224px')}
  56. `
  57. const FormField = styled.div``
  58. const FormLabel = styled.div`
  59. font-size: 10px;
  60. font-weight: ${StyleProps.fontWeights.medium};
  61. color: ${Palette.grayscale[3]};
  62. text-transform: uppercase;
  63. margin-bottom: 2px;
  64. display: flex;
  65. align-items: center;
  66. `
  67. const Buttons = styled.div`
  68. margin-top: 32px;
  69. display: flex;
  70. justify-content: space-between;
  71. `
  72. type Props = {
  73. loading: boolean,
  74. users: User[],
  75. projects: Project[],
  76. onRequestClose: () => void,
  77. onAddClick: (user: User, isNew: boolean, roles: Role[]) => void,
  78. roles: Role[],
  79. }
  80. type State = {
  81. isNew: boolean,
  82. selectedUser: ?User,
  83. username: string,
  84. description: string,
  85. email: string,
  86. projectId: string,
  87. password: string,
  88. confirmPassword: string,
  89. enabled: boolean,
  90. highlightFieldNames: string[],
  91. selectedRolesExisting: string[],
  92. selectedRolesNew: string[],
  93. }
  94. @observer
  95. class ProjectMemberModal extends React.Component<Props, State> {
  96. state = {
  97. isNew: false,
  98. selectedUser: null,
  99. username: '',
  100. description: '',
  101. email: '',
  102. projectId: '',
  103. password: '',
  104. confirmPassword: '',
  105. enabled: true,
  106. highlightFieldNames: [],
  107. selectedRolesExisting: [],
  108. selectedRolesNew: [],
  109. }
  110. componentDidMount() {
  111. KeyboardManager.onEnter('projectMemberModal', () => {
  112. this.handleAddClick()
  113. })
  114. }
  115. componentWillUnmount() {
  116. KeyboardManager.removeKeyDown('projectMemberModal')
  117. }
  118. handleAddClick() {
  119. if (this.highlightFields()) {
  120. return
  121. }
  122. let user: User
  123. let roles = []
  124. if (this.state.isNew) {
  125. user = {
  126. id: '',
  127. project: { id: '', name: '' },
  128. project_id: this.state.projectId,
  129. email: this.state.email,
  130. name: this.state.username,
  131. description: this.state.description,
  132. password: this.state.password,
  133. enabled: this.state.enabled,
  134. }
  135. roles = this.state.selectedRolesNew
  136. } else if (this.state.selectedUser) {
  137. user = this.state.selectedUser
  138. roles = this.state.selectedRolesExisting
  139. } else {
  140. return
  141. }
  142. roles = roles.map(id => this.props.roles.find(r => r.id === id) || { id: 'undefined', name: '' })
  143. this.props.onAddClick(user, this.state.isNew, roles)
  144. }
  145. highlightFields(): boolean {
  146. const highlightFieldNames = []
  147. if (!this.state.isNew) {
  148. if (!this.state.selectedUser) {
  149. highlightFieldNames.push('selectedUser')
  150. }
  151. if (this.state.selectedRolesExisting.length === 0) {
  152. highlightFieldNames.push('rolesExisting')
  153. }
  154. if (highlightFieldNames.length > 0) {
  155. this.setState({ highlightFieldNames })
  156. return true
  157. }
  158. this.setState({ highlightFieldNames: [] })
  159. return false
  160. }
  161. if (!this.state.username) {
  162. highlightFieldNames.push('username')
  163. }
  164. if (!this.state.password) {
  165. highlightFieldNames.push('password')
  166. }
  167. if (this.state.password && this.state.password !== this.state.confirmPassword) {
  168. highlightFieldNames.push('confirm_password')
  169. }
  170. if (this.state.selectedRolesNew.length === 0) {
  171. highlightFieldNames.push('rolesNew')
  172. }
  173. if (highlightFieldNames.length > 0) {
  174. this.setState({ highlightFieldNames })
  175. return true
  176. }
  177. this.setState({ highlightFieldNames: [] })
  178. return false
  179. }
  180. renderToggleButton() {
  181. const items = [{
  182. value: 'existing',
  183. label: 'Existing',
  184. }, {
  185. value: 'new',
  186. label: 'New',
  187. }]
  188. return (
  189. <ToggleButtonBarStyled
  190. items={items}
  191. selectedValue={this.state.isNew ? 'new' : 'existing'}
  192. onChange={item => { this.setState({ isNew: item.value === 'new' }) }}
  193. />
  194. )
  195. }
  196. renderRolesField() {
  197. let selectedRoles = this.state.isNew ? this.state.selectedRolesNew : this.state.selectedRolesExisting
  198. let setSelectedRoles = (roles: string[]) => {
  199. if (this.state.isNew) {
  200. this.setState({ selectedRolesNew: roles })
  201. } else {
  202. this.setState({ selectedRolesExisting: roles })
  203. }
  204. }
  205. let highlighFieldName = this.state.isNew ? 'rolesNew' : 'rolesExisting'
  206. return (
  207. <Field
  208. name="role(s)"
  209. type="array"
  210. onChange={roleId => {
  211. if (selectedRoles.find(id => id === roleId)) {
  212. setSelectedRoles(selectedRoles.filter(r => r !== roleId))
  213. } else {
  214. setSelectedRoles([...selectedRoles, roleId])
  215. }
  216. }}
  217. selectedItems={selectedRoles}
  218. value={null}
  219. large
  220. disabled={this.props.loading}
  221. items={this.props.roles.filter(r => r.name !== 'key-manager:service-admin').map(r => { return { label: r.name, value: r.id } })}
  222. required
  223. highlight={Boolean(this.state.highlightFieldNames.find(n => n === highlighFieldName))}
  224. noSelectionMessage="Choose role(s)"
  225. noItemsMessage="No available roles"
  226. />
  227. )
  228. }
  229. renderField(field: FieldType, value: any, onChange: (value: any) => void) {
  230. return (
  231. <FieldStyled
  232. key={field.name}
  233. name={field.name}
  234. type={field.type || 'string'}
  235. value={value}
  236. onChange={onChange}
  237. large
  238. disabled={this.props.loading}
  239. enum={field.enum}
  240. password={field.name === 'password' || field.name === 'confirm_password'}
  241. // $FlowIssue
  242. required={field.required}
  243. highlight={Boolean(this.state.highlightFieldNames.find(n => n === field.name))}
  244. noSelectionMessage="Choose a project"
  245. noItemsMessage="No available members"
  246. />
  247. )
  248. }
  249. renderNewForm() {
  250. const userProjects = this.props.projects.map(p => { return { label: p.name, value: p.id } })
  251. const fields = [
  252. this.renderField(
  253. { name: 'username', required: true },
  254. this.state.username,
  255. username => { this.setState({ username }) }
  256. ),
  257. this.renderField(
  258. { name: 'description' },
  259. this.state.description,
  260. description => { this.setState({ description }) }
  261. ),
  262. this.renderField(
  263. {
  264. name: 'Primary Project',
  265. // $FlowIssue
  266. enum: [{ label: 'Choose a project', value: null }].concat(userProjects),
  267. },
  268. this.state.projectId,
  269. projectId => { this.setState({ projectId }) },
  270. ),
  271. this.renderRolesField(),
  272. this.renderField(
  273. { name: 'password', required: true },
  274. this.state.password,
  275. password => { this.setState({ password }) }
  276. ),
  277. this.renderField(
  278. { name: 'confirm_password', required: true },
  279. this.state.confirmPassword,
  280. confirmPassword => { this.setState({ confirmPassword }) }
  281. ),
  282. this.renderField(
  283. { name: 'Email' },
  284. this.state.email,
  285. email => { this.setState({ email }) }
  286. ),
  287. this.renderField(
  288. { name: 'Enabled', type: 'boolean' },
  289. this.state.enabled,
  290. enabled => { this.setState({ enabled }) }
  291. ),
  292. ]
  293. return (
  294. <Form>
  295. {fields}
  296. </Form>
  297. )
  298. }
  299. renderExistingForm() {
  300. const users = this.props.users.map(u => { return { label: u.name, value: u.id } })
  301. return (
  302. <Form style={{ marginBottom: '80px' }}>
  303. <FormField>
  304. <FormLabel>
  305. Username
  306. <Asterisk marginLeft="8px" />
  307. </FormLabel>
  308. <AutocompleteDropdown
  309. items={users}
  310. disabled={this.props.loading}
  311. selectedItem={this.state.selectedUser ? this.state.selectedUser.id : ''}
  312. highlight={Boolean(this.state.highlightFieldNames.find(n => n === 'selectedUser'))}
  313. onChange={item => {
  314. this.setState({ selectedUser: this.props.users.find(u => u.id === item.value) })
  315. }}
  316. />
  317. </FormField>
  318. {this.renderRolesField()}
  319. </Form>
  320. )
  321. }
  322. renderForm() {
  323. if (this.state.isNew) {
  324. return this.renderNewForm()
  325. }
  326. return this.renderExistingForm()
  327. }
  328. render() {
  329. return (
  330. <Modal
  331. isOpen
  332. title="Add Project Member"
  333. onRequestClose={this.props.onRequestClose}
  334. >
  335. <Wrapper>
  336. <Image />
  337. {this.renderToggleButton()}
  338. {this.renderForm()}
  339. <Buttons>
  340. <Button
  341. secondary
  342. large
  343. onClick={this.props.onRequestClose}
  344. >Cancel</Button>
  345. <Button
  346. large
  347. disabled={this.props.loading}
  348. onClick={() => { this.handleAddClick() }}
  349. data-test-id="projectModal-addButton"
  350. >Add Member</Button>
  351. </Buttons>
  352. </Wrapper>
  353. </Modal>
  354. )
  355. }
  356. }
  357. export default ProjectMemberModal