-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revert "replace Manage Team modal with link to TAM invitation page (#327
- Loading branch information
Showing
4 changed files
with
283 additions
and
7 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
libs/tup-components/src/projects/users/UserList/AddUserModal.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* To prevent dynamic height adjustment */ | ||
.root :global(.modal-content) { | ||
height: 50vh; | ||
} | ||
|
||
/* To activate scrolling from SectionTableWrapper contentShouldScroll */ | ||
/* NOTE: I do not think this should be required in client CSS */ | ||
/* HELP: What other solutions exist? If not, then… | ||
should SectionTableWrapper apply this? */ | ||
.table-wrap { | ||
overflow-y: auto; | ||
} | ||
.body { | ||
display: grid; | ||
grid-template-rows: auto auto 1fr; | ||
} | ||
|
||
.add-remove-column { | ||
text-align: right; | ||
|
||
/* To "shrink-wrap" table cell */ | ||
/* CAVEAT: Requires table `table-layout: auto` (browser default) */ | ||
width: 1%; | ||
white-space: nowrap; | ||
} | ||
|
||
.success-icon { | ||
margin-right: 0.5rem; | ||
vertical-align: text-top; | ||
|
||
color: var(--global-color-success--normal); | ||
} |
27 changes: 27 additions & 0 deletions
27
libs/tup-components/src/projects/users/UserList/AddUserModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import AddUserModal from './AddUserModal'; | ||
import { testRender } from '@tacc/tup-testing'; | ||
import { screen, fireEvent, within } from '@testing-library/react'; | ||
|
||
describe('AddUserModal', () => { | ||
it('should display search results', async () => { | ||
testRender(<AddUserModal projectId={59184} />); | ||
const modalButton = screen.getByRole('button'); | ||
// open the modal | ||
fireEvent.click(modalButton); | ||
|
||
const searchButton = screen.getByText('Search'); | ||
fireEvent.click(searchButton); | ||
const rows = await screen.findAllByRole('row'); | ||
expect(rows.length).toBe(3); | ||
|
||
// A user in the project should display as added already | ||
const existingUserRow = rows[1]; | ||
const rowQuery = await within(existingUserRow).findByText(/Added/); | ||
expect(rowQuery).toBeDefined(); | ||
|
||
// A user who is not in the project should display a prompt. | ||
const newUserRow = rows[2]; | ||
const rowQuery2 = await within(newUserRow).findByText(/Add User/); | ||
expect(rowQuery2).toBeDefined(); | ||
}); | ||
}); |
222 changes: 222 additions & 0 deletions
222
libs/tup-components/src/projects/users/UserList/AddUserModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
import { | ||
Button, | ||
LoadingSpinner, | ||
Icon, | ||
SectionMessage, | ||
SectionTableWrapper, | ||
} from '@tacc/core-components'; | ||
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; | ||
import { Input } from 'reactstrap'; | ||
import React, { useState } from 'react'; | ||
import { | ||
useUserLookup, | ||
UserSearchResult, | ||
useProjectUsers, | ||
useAddProjectUser, | ||
useRemoveProjectUser, | ||
} from '@tacc/tup-hooks'; | ||
|
||
import styles from './AddUserModal.module.css'; | ||
import stylesUserList from './UserList.module.css'; | ||
|
||
type FieldValue = 'email' | 'username' | 'last_name'; | ||
|
||
const AddUserButton: React.FC<{ username: string; projectId: number }> = ({ | ||
username, | ||
projectId, | ||
}) => { | ||
const { mutate, isLoading } = useAddProjectUser(projectId); | ||
if (isLoading) return <LoadingSpinner placement="inline" />; | ||
return ( | ||
<Button type="link" onClick={() => mutate({ username })}> | ||
+ Add User | ||
</Button> | ||
); | ||
}; | ||
|
||
const RemoveUser: React.FC<{ username: string; projectId: number }> = ({ | ||
username, | ||
projectId, | ||
}) => { | ||
const { mutate, isLoading } = useRemoveProjectUser(projectId, username); | ||
if (isLoading) | ||
return ( | ||
<div> | ||
<LoadingSpinner placement="inline" /> | ||
</div> | ||
); | ||
return ( | ||
<> | ||
<Icon name="approved-reverse" className={styles['success-icon']}></Icon>{' '} | ||
Added | | ||
<Button type="link" onClick={() => mutate({})}> | ||
Remove | ||
</Button> | ||
</> | ||
); | ||
}; | ||
|
||
const UserSearchTable: React.FC<{ | ||
users: UserSearchResult[]; | ||
projectId: number; | ||
}> = ({ users, projectId }) => { | ||
const { data: projectUsers } = useProjectUsers(projectId); | ||
|
||
const userInProject = (username: string) => { | ||
return (projectUsers || []).some((user) => user.username === username); | ||
}; | ||
|
||
if (!users.length) | ||
return ( | ||
<SectionMessage type="warn"> | ||
No users matching your query could be found. | ||
</SectionMessage> | ||
); | ||
|
||
return ( | ||
<table className="o-fixed-header-table"> | ||
<thead> | ||
<tr> | ||
<th>Name</th> | ||
<th>Email</th> | ||
<th>Username</th> | ||
<th className={styles['add-remove-column']}></th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{users.map((user) => ( | ||
<tr key={user.username}> | ||
<td>{user.name}</td> | ||
<td>{user.email}</td> | ||
<td>{user.username}</td> | ||
<td className={styles['add-remove-column']}> | ||
{userInProject(user.username) ? ( | ||
<RemoveUser projectId={projectId} username={user.username} /> | ||
) : ( | ||
<AddUserButton username={user.username} projectId={projectId} /> | ||
)} | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
const AddUserModal: React.FC<{ projectId: number }> = ({ projectId }) => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
const toggle = () => { | ||
setIsOpen(!isOpen); | ||
setField('last_name'); | ||
setQuery(''); | ||
}; | ||
|
||
const [field, setField] = useState<FieldValue>('last_name'); | ||
const [query, setQuery] = useState(''); | ||
const { data, isFetching, refetch } = useUserLookup(projectId, query, field); | ||
|
||
const onSubmit = (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
refetch(); | ||
}; | ||
|
||
const closeBtn = ( | ||
<button className="close" onClick={toggle} type="button"> | ||
× | ||
</button> | ||
); | ||
|
||
return ( | ||
<> | ||
<Button onClick={() => toggle()}>+ Add Users</Button> | ||
<Modal | ||
isOpen={isOpen} | ||
toggle={toggle} | ||
size="lg" | ||
className={`${styles['root']} modal-dialog-centered`} | ||
> | ||
<ModalHeader toggle={toggle} close={closeBtn}> | ||
<span>Add Users</span> | ||
</ModalHeader> | ||
<ModalBody className={styles['body']}> | ||
<h3 style={{ marginBottom: '10px' }}>Search for User</h3> | ||
<form onSubmit={(e) => onSubmit(e)}> | ||
{/* Radio labels for selecting lastname/email/username for search */} | ||
<div className={stylesUserList['radio-group']}> | ||
<input | ||
name="adduser-field" | ||
id="adduser-radio-lastname" | ||
type="radio" | ||
value="last_name" | ||
onChange={(e) => setField(e.target.value as FieldValue)} | ||
checked={field === 'last_name'} | ||
/> | ||
<label htmlFor="adduser-radio-lastname">Last Name</label> | ||
|
||
<input | ||
name="adduser-field" | ||
id="adduser-radio-email" | ||
type="radio" | ||
value="email" | ||
onChange={(e) => setField(e.target.value as FieldValue)} | ||
checked={field === 'email'} | ||
/> | ||
<label htmlFor="adduser-radio-email">Email</label> | ||
|
||
<input | ||
name="adduser-field" | ||
id="adduser-radio-username" | ||
type="radio" | ||
value="username" | ||
onChange={(e) => setField(e.target.value as FieldValue)} | ||
checked={field === 'username'} | ||
/> | ||
<label htmlFor="adduser-radio-username">Username</label> | ||
</div> | ||
{/* Search bar input group */} | ||
<div className="input-group"> | ||
<div className="input-group-prepend"> | ||
<Button | ||
className={stylesUserList['search-button']} | ||
type="secondary" | ||
iconNameBefore="search" | ||
attr="submit" | ||
isLoading={isFetching} | ||
> | ||
Search | ||
</Button> | ||
</div> | ||
<Input | ||
style={{ fontSize: '1em' }} | ||
id="add-user" | ||
value={query} | ||
onChange={(e) => setQuery(e.target.value)} | ||
/> | ||
</div> | ||
<label | ||
className={stylesUserList['search-input-label']} | ||
htmlFor="add-user" | ||
> | ||
<i>Enter their exact name, email address, or username.</i> | ||
</label> | ||
</form> | ||
{/* Search result table */} | ||
{data && ( | ||
<SectionTableWrapper | ||
className={styles['table-wrap']} | ||
contentShouldScroll | ||
> | ||
<UserSearchTable | ||
users={data} | ||
projectId={projectId} | ||
></UserSearchTable> | ||
</SectionTableWrapper> | ||
)} | ||
</ModalBody> | ||
<ModalFooter /> | ||
</Modal> | ||
</> | ||
); | ||
}; | ||
|
||
export default AddUserModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters