ps?: number;
q?: string;
selected?: string;
-}): Promise<{ paging: T.Paging; users: GroupUser[] }> {
+}): Promise<T.Paging & { users: GroupUser[] }> {
return getJSON('/api/user_groups/users', data).catch(throwGlobalError);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON, post, postJSON, RequestData } from '../helpers/request';
+import { getJSON, post, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
export function getCurrentUser(): Promise<T.CurrentUser> {
selected: boolean;
}
-export function getUserGroups(
- login: string,
- organization?: string,
- query?: string,
- selected?: string
-): Promise<{ paging: T.Paging; groups: UserGroup[] }> {
- const data: RequestData = { login };
- if (organization) {
- data.organization = organization;
- }
- if (query) {
- data.q = query;
- }
- if (selected) {
- data.selected = selected;
- }
+export function getUserGroups(data: {
+ login: string;
+ organization?: string;
+ p?: number;
+ ps?: number;
+ q?: string;
+ selected?: string;
+}): Promise<{ paging: T.Paging; groups: UserGroup[] }> {
return getJSON('/api/users/groups', data);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
import { find, without } from 'lodash';
-import Modal from '../../../components/controls/Modal';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
-import { ResetButtonLink } from '../../../components/ui/buttons';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
import {
- GroupUser,
- removeUserFromGroup,
addUserToGroup,
- getUsersInGroup
+ getUsersInGroup,
+ GroupUser,
+ removeUserFromGroup
} from '../../../api/user_groups';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Modal from '../../../components/controls/Modal';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import { ResetButtonLink } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
interface Props {
group: T.Group;
organization: string | undefined;
}
+export interface SearchParams {
+ name: string;
+ organization?: string;
+ page: number;
+ pageSize: number;
+ query?: string;
+ selected: string;
+}
+
interface State {
+ lastSearchParams: SearchParams;
+ listHasBeenTouched: boolean;
loading: boolean;
users: GroupUser[];
+ usersTotalCount?: number;
selectedUsers: string[];
}
+const PAGE_SIZE = 100;
+
export default class EditMembers extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { loading: true, users: [], selectedUsers: [] };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ lastSearchParams: {
+ name: props.group.name,
+ organization: props.organization,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ query: '',
+ selected: Filter.Selected
+ },
+ listHasBeenTouched: false,
+ loading: true,
+ users: [],
+ selectedUsers: []
+ };
+ }
componentDidMount() {
this.mounted = true;
- this.handleSearch('', Filter.Selected);
+ this.fetchUsers(this.state.lastSearchParams);
}
componentWillUnmount() {
this.mounted = false;
}
- handleSearch = (query: string, selected: Filter) => {
- return getUsersInGroup({
- name: this.props.group.name,
- organization: this.props.organization,
- ps: 100,
- q: query !== '' ? query : undefined,
- selected
+ fetchUsers = (searchParams: SearchParams, more?: boolean) =>
+ getUsersInGroup({
+ ...searchParams,
+ p: searchParams.page,
+ ps: searchParams.pageSize,
+ q: searchParams.query !== '' ? searchParams.query : undefined
}).then(
data => {
if (this.mounted) {
- this.setState({
- loading: false,
- users: data.users,
- selectedUsers: data.users.filter(user => user.selected).map(user => user.login)
+ this.setState(prevState => {
+ const users = more ? [...prevState.users, ...data.users] : data.users;
+ const newSelectedUsers = data.users
+ .filter(user => user.selected)
+ .map(user => user.login);
+ const selectedUsers = more
+ ? [...prevState.selectedUsers, ...newSelectedUsers]
+ : newSelectedUsers;
+
+ return {
+ lastSearchParams: searchParams,
+ listHasBeenTouched: false,
+ loading: false,
+ users,
+ usersTotalCount: data.total,
+ selectedUsers
+ };
});
}
},
}
}
);
- };
- handleSelect = (login: string) => {
- return addUserToGroup({
+ handleLoadMore = () =>
+ this.fetchUsers(
+ {
+ ...this.state.lastSearchParams,
+ page: this.state.lastSearchParams.page + 1
+ },
+ true
+ );
+
+ handleReload = () =>
+ this.fetchUsers({
+ ...this.state.lastSearchParams,
+ page: 1
+ });
+
+ handleSearch = (query: string, selected: Filter) =>
+ this.fetchUsers({
+ ...this.state.lastSearchParams,
+ page: 1,
+ query,
+ selected
+ });
+
+ handleSelect = (login: string) =>
+ addUserToGroup({
name: this.props.group.name,
login,
organization: this.props.organization
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
+ listHasBeenTouched: true,
selectedUsers: [...state.selectedUsers, login]
}));
}
});
- };
- handleUnselect = (login: string) => {
- return removeUserFromGroup({
+ handleUnselect = (login: string) =>
+ removeUserFromGroup({
name: this.props.group.name,
login,
organization: this.props.organization
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
+ listHasBeenTouched: true,
selectedUsers: without(state.selectedUsers, login)
}));
}
});
- };
renderElement = (login: string): React.ReactNode => {
const user = find(this.state.users, { login });
<DeferredSpinner loading={this.state.loading}>
<SelectList
elements={this.state.users.map(user => user.login)}
+ elementsTotalCount={this.state.usersTotalCount}
+ needReload={
+ this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
+ }
+ onLoadMore={this.handleLoadMore}
+ onReload={this.handleReload}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
+
click(wrapper.find('ResetButtonLink'));
expect(onEdit).toBeCalled();
expect(wrapper).toMatchSnapshot();
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-/* eslint-disable import/first, import/order */
-import * as React from 'react';
import { shallow } from 'enzyme';
-import EditMembersModal from '../EditMembersModal';
+import * as React from 'react';
+import EditMembersModal, { SearchParams } from '../EditMembersModal';
+import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { getUsersInGroup, addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups';
jest.mock('../../../../api/user_groups', () => ({
getUsersInGroup: jest.fn().mockResolvedValue({
- paging: { pageIndex: 0, pageSize: 10, total: 0 },
+ paging: { pageIndex: 1, pageSize: 10, total: 1 },
users: [
{
login: 'foo',
selected: true
}
]
- })
+ }),
+ addUserToGroup: jest.fn().mockResolvedValue({}),
+ removeUserFromGroup: jest.fn().mockResolvedValue({})
}));
-const getUsersInGroup = require('../../../../api/user_groups').getUsersInGroup as jest.Mock<any>;
-
-const group = { id: 1, name: 'foo', membersCount: 1 };
+beforeEach(() => {
+ jest.clearAllMocks();
+});
-it('should render modal', async () => {
- getUsersInGroup.mockClear();
+it('should render modal properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
- const wrapper = shallow(<EditMembersModal group={group} onClose={() => {}} organization="bar" />);
expect(wrapper).toMatchSnapshot();
+ expect(getUsersInGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1
+ })
+ );
+
+ wrapper.setState({ listHasBeenTouched: true });
+ expect(wrapper.find(SelectList).props().needReload).toBe(true);
+
+ wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
+ expect(wrapper.find(SelectList).props().needReload).toBe(false);
+});
+it('should handle reload properly', async () => {
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(getUsersInGroup).toHaveBeenCalledTimes(1);
- expect(wrapper).toMatchSnapshot();
+
+ wrapper.instance().handleReload();
+ expect(getUsersInGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
});
+
+it('should handle search reload properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSearch('foo', Filter.Selected);
+ expect(getUsersInGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1,
+ q: 'foo',
+ selected: Filter.Selected
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
+});
+
+it('should handle load more properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleLoadMore();
+ expect(getUsersInGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 2
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
+});
+
+it('should handle selection properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSelect('toto');
+ await waitAndUpdate(wrapper);
+ expect(addUserToGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ login: 'toto'
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(true);
+});
+
+it('should handle deselection properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleUnselect('tata');
+ await waitAndUpdate(wrapper);
+ expect(removeUserFromGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ login: 'tata'
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(true);
+});
+
+function shallowRender(props: Partial<EditMembersModal['props']> = {}) {
+ return shallow<EditMembersModal>(
+ <EditMembersModal
+ group={{ id: 1, name: 'foo', membersCount: 1 }}
+ onClose={jest.fn()}
+ organization={'bar'}
+ {...props}
+ />
+ );
+}
>
<SelectList
elements={Array []}
+ needReload={false}
+ onLoadMore={[Function]}
+ onReload={[Function]}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render modal 1`] = `
+exports[`should render modal properly 1`] = `
<Modal
contentLabel="users.update"
- onRequestClose={[Function]}
->
- <header
- className="modal-head"
- >
- <h2>
- users.update
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <DeferredSpinner
- loading={true}
- timeout={100}
- >
- <SelectList
- elements={Array []}
- onSearch={[Function]}
- onSelect={[Function]}
- onUnselect={[Function]}
- renderElement={[Function]}
- selectedElements={Array []}
- />
- </DeferredSpinner>
- </div>
- <footer
- className="modal-foot"
- >
- <ResetButtonLink
- onClick={[Function]}
- >
- Done
- </ResetButtonLink>
- </footer>
-</Modal>
-`;
-
-exports[`should render modal 2`] = `
-<Modal
- contentLabel="users.update"
- onRequestClose={[Function]}
+ onRequestClose={[MockFunction]}
>
<header
className="modal-head"
"foo",
]
}
+ needReload={false}
+ onLoadMore={[Function]}
+ onReload={[Function]}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
className="modal-foot"
>
<ResetButtonLink
- onClick={[Function]}
+ onClick={[MockFunction]}
>
Done
</ResetButtonLink>
loadUserGroups = () => {
this.setState({ loading: true });
- getUserGroups(this.props.member.login, this.props.organization.key).then(
+ getUserGroups({
+ login: this.props.member.login,
+ organization: this.props.organization.key
+ }).then(
response => {
if (this.mounted) {
this.setState({ loading: false, userGroups: keyBy(response.groups, 'name') });
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
import { find, without } from 'lodash';
+import * as React from 'react';
+import { getUserGroups, UserGroup } from '../../../api/users';
+import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
import Modal from '../../../components/controls/Modal';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import { translate } from '../../../helpers/l10n';
-import { getUserGroups, UserGroup } from '../../../api/users';
-import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
interface Props {
onClose: () => void;
user: T.User;
}
+export interface SearchParams {
+ login: string;
+ organization?: string;
+ page: number;
+ pageSize: number;
+ query?: string;
+ selected: string;
+}
+
interface State {
groups: UserGroup[];
+ groupsTotalCount?: number;
+ lastSearchParams: SearchParams;
+ listHasBeenTouched: boolean;
selectedGroups: string[];
}
-export default class GroupsForm extends React.PureComponent<Props> {
- container?: HTMLDivElement | null;
- state: State = { groups: [], selectedGroups: [] };
+const PAGE_SIZE = 100;
+
+export default class GroupsForm extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ groups: [],
+ lastSearchParams: {
+ login: props.user.login,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ query: '',
+ selected: Filter.Selected
+ },
+ listHasBeenTouched: false,
+ selectedGroups: []
+ };
+ }
componentDidMount() {
- this.handleSearch('', Filter.Selected);
+ this.mounted = true;
+ this.fetchUsers(this.state.lastSearchParams);
}
- handleSearch = (query: string, selected: Filter) => {
- return getUserGroups(this.props.user.login, undefined, query, selected).then(data => {
- this.setState({
- groups: data.groups,
- selectedGroups: data.groups.filter(group => group.selected).map(group => group.name)
- });
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchUsers = (searchParams: SearchParams, more?: boolean) =>
+ getUserGroups({
+ login: searchParams.login,
+ organization: searchParams.organization !== '' ? searchParams.organization : undefined,
+ p: searchParams.page,
+ ps: searchParams.pageSize,
+ q: searchParams.query !== '' ? searchParams.query : undefined,
+ selected: searchParams.selected
+ }).then(data => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const groups = more ? [...prevState.groups, ...data.groups] : data.groups;
+ const newSeletedGroups = data.groups.filter(gp => gp.selected).map(gp => gp.name);
+ const selectedGroups = more
+ ? [...prevState.selectedGroups, ...newSeletedGroups]
+ : newSeletedGroups;
+
+ return {
+ lastSearchParams: searchParams,
+ listHasBeenTouched: false,
+ groups,
+ groupsTotalCount: data.paging.total,
+ selectedGroups
+ };
+ });
+ }
});
- };
- handleSelect = (name: string) => {
- return addUserToGroup({
+ handleLoadMore = () =>
+ this.fetchUsers(
+ {
+ ...this.state.lastSearchParams,
+ page: this.state.lastSearchParams.page + 1
+ },
+ true
+ );
+
+ handleReload = () =>
+ this.fetchUsers({
+ ...this.state.lastSearchParams,
+ page: 1
+ });
+
+ handleSearch = (query: string, selected: Filter) =>
+ this.fetchUsers({
+ ...this.state.lastSearchParams,
+ page: 1,
+ query,
+ selected
+ });
+
+ handleSelect = (name: string) =>
+ addUserToGroup({
name,
login: this.props.user.login
}).then(() => {
- this.setState((state: State) => ({ selectedGroups: [...state.selectedGroups, name] }));
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ listHasBeenTouched: true,
+ selectedGroups: [...state.selectedGroups, name]
+ }));
+ }
});
- };
- handleUnselect = (name: string) => {
- return removeUserFromGroup({
+ handleUnselect = (name: string) =>
+ removeUserFromGroup({
name,
login: this.props.user.login
}).then(() => {
- this.setState((state: State) => ({
- selectedGroups: without(state.selectedGroups, name)
- }));
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ listHasBeenTouched: true,
+ selectedGroups: without(state.selectedGroups, name)
+ }));
+ }
});
- };
handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
<div className="modal-body">
<SelectList
elements={this.state.groups.map(group => group.name)}
+ elementsTotalCount={this.state.groupsTotalCount}
+ needReload={
+ this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
+ }
+ onLoadMore={this.handleLoadMore}
+ onReload={this.handleReload}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import GroupsForm, { SearchParams } from '../GroupsForm';
+import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { getUserGroups } from '../../../../api/users';
+import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups';
+import { mockUser } from '../../../../helpers/testMocks';
+
+jest.mock('../../../../api/users', () => ({
+ getUserGroups: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 10, total: 1 },
+ groups: [
+ {
+ id: 1001,
+ name: 'test1',
+ description: 'test1',
+ selected: true
+ },
+ {
+ id: 1002,
+ name: 'test2',
+ description: 'test2',
+ selected: true
+ },
+ {
+ id: 1003,
+ name: 'test3',
+ description: 'test3',
+ selected: false
+ }
+ ]
+ })
+}));
+
+jest.mock('../../../../api/user_groups', () => ({
+ addUserToGroup: jest.fn().mockResolvedValue({}),
+ removeUserFromGroup: jest.fn().mockResolvedValue({})
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper).toMatchSnapshot();
+ expect(getUserGroups).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1
+ })
+ );
+
+ wrapper.setState({ listHasBeenTouched: true });
+ expect(wrapper.find(SelectList).props().needReload).toBe(true);
+
+ wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
+ expect(wrapper.find(SelectList).props().needReload).toBe(false);
+});
+
+it('should handle reload properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleReload();
+ expect(getUserGroups).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
+});
+
+it('should handle search reload properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSearch('foo', Filter.Selected);
+ expect(getUserGroups).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 1,
+ q: 'foo',
+ selected: Filter.Selected
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
+});
+
+it('should handle load more properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleLoadMore();
+ expect(getUserGroups).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 2
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(false);
+});
+
+it('should handle selection properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSelect('toto');
+ await waitAndUpdate(wrapper);
+ expect(addUserToGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'toto'
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(true);
+});
+
+it('should handle deselection properly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleUnselect('tata');
+ await waitAndUpdate(wrapper);
+ expect(removeUserFromGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'tata'
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).toBe(true);
+});
+
+function shallowRender(props: Partial<GroupsForm['props']> = {}) {
+ return shallow<GroupsForm>(
+ <GroupsForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+ contentLabel="users.update_groups"
+ onRequestClose={[Function]}
+>
+ <div
+ className="modal-head"
+ >
+ <h2>
+ users.update_groups
+ </h2>
+ </div>
+ <div
+ className="modal-body"
+ >
+ <SelectList
+ elements={
+ Array [
+ "test1",
+ "test2",
+ "test3",
+ ]
+ }
+ elementsTotalCount={1}
+ needReload={false}
+ onLoadMore={[Function]}
+ onReload={[Function]}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "test1",
+ "test2",
+ ]
+ }
+ />
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <a
+ className="js-modal-close"
+ href="#"
+ onClick={[Function]}
+ >
+ Done
+ </a>
+ </footer>
+</Modal>
+`;