Project managers, board members, auto-update after reconnection, refactoring
This commit is contained in:
110
client/src/components/Memberships/ActionsPopup.jsx
Executable file
110
client/src/components/Memberships/ActionsPopup.jsx
Executable file
@@ -0,0 +1,110 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
|
||||
import { useSteps } from '../../hooks';
|
||||
import User from '../User';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
import styles from './ActionsPopup.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(
|
||||
({
|
||||
user,
|
||||
leaveButtonContent,
|
||||
leaveConfirmationTitle,
|
||||
leaveConfirmationContent,
|
||||
leaveConfirmationButtonContent,
|
||||
deleteButtonContent,
|
||||
deleteConfirmationTitle,
|
||||
deleteConfirmationContent,
|
||||
deleteConfirmationButtonContent,
|
||||
canLeave,
|
||||
canDelete,
|
||||
onDelete,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
if (step && step.type === StepTypes.DELETE) {
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t(user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle, {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t(user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent)}
|
||||
buttonContent={t(
|
||||
user.isCurrent ? leaveConfirmationButtonContent : deleteConfirmationButtonContent,
|
||||
)}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.user}>
|
||||
<User name={user.name} avatarUrl={user.avatarUrl} size="large" />
|
||||
</span>
|
||||
<span className={styles.content}>
|
||||
<div className={styles.name}>{user.name}</div>
|
||||
<div className={styles.email}>{user.email}</div>
|
||||
{user.isCurrent
|
||||
? canLeave && (
|
||||
<Button
|
||||
content={t(leaveButtonContent)}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)
|
||||
: canDelete && (
|
||||
<Button
|
||||
content={t(deleteButtonContent)}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
leaveButtonContent: PropTypes.string,
|
||||
leaveConfirmationTitle: PropTypes.string,
|
||||
leaveConfirmationContent: PropTypes.string,
|
||||
leaveConfirmationButtonContent: PropTypes.string,
|
||||
deleteButtonContent: PropTypes.string,
|
||||
deleteConfirmationTitle: PropTypes.string,
|
||||
deleteConfirmationContent: PropTypes.string,
|
||||
deleteConfirmationButtonContent: PropTypes.string,
|
||||
canLeave: PropTypes.bool.isRequired,
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ActionsStep.defaultProps = {
|
||||
leaveButtonContent: 'action.leaveBoard',
|
||||
leaveConfirmationTitle: 'common.leaveBoard',
|
||||
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
|
||||
leaveConfirmationButtonContent: 'action.leaveBoard',
|
||||
deleteButtonContent: 'action.removeFromBoard',
|
||||
deleteConfirmationTitle: 'common.removeMember',
|
||||
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
|
||||
deleteConfirmationButtonContent: 'action.removeMember',
|
||||
};
|
||||
|
||||
export default withPopup(ActionsStep);
|
||||
42
client/src/components/Memberships/ActionsPopup.module.scss
Normal file
42
client/src/components/Memberships/ActionsPopup.module.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
:global(#app) {
|
||||
.content {
|
||||
display: inline-block;
|
||||
width: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
flex: 0 0 auto;
|
||||
font-weight: normal;
|
||||
margin: 0 0 0 -10px;
|
||||
padding: 11px 10px;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.email {
|
||||
color: #888888;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 0 4px 2px;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #212121;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
padding: 9px 28px 0 2px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
padding-right: 8px;
|
||||
padding-top: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
63
client/src/components/Memberships/AddPopup/AddPopup.jsx
Executable file
63
client/src/components/Memberships/AddPopup/AddPopup.jsx
Executable file
@@ -0,0 +1,63 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { withPopup } from '../../../lib/popup';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import UserItem from './UserItem';
|
||||
|
||||
import styles from './AddPopup.module.scss';
|
||||
|
||||
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
onCreate({
|
||||
userId: id,
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onCreate, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.menu}>
|
||||
{users.map((user) => (
|
||||
<UserItem
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
avatarUrl={user.avatarUrl}
|
||||
isActive={currentUserIds.includes(user.id)}
|
||||
onSelect={() => handleUserSelect(user.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AddStep.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
users: PropTypes.array.isRequired,
|
||||
currentUserIds: PropTypes.array.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
title: PropTypes.string,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AddStep.defaultProps = {
|
||||
title: 'common.addMember',
|
||||
};
|
||||
|
||||
export default withPopup(AddStep);
|
||||
@@ -0,0 +1,7 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
border: none;
|
||||
margin: -7px auto -5px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
31
client/src/components/Memberships/AddPopup/UserItem.jsx
Normal file
31
client/src/components/Memberships/AddPopup/UserItem.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import User from '../../User';
|
||||
|
||||
import styles from './UserItem.module.scss';
|
||||
|
||||
const UserItem = React.memo(({ name, avatarUrl, isActive, onSelect }) => (
|
||||
<button type="button" disabled={isActive} className={styles.menuItem} onClick={onSelect}>
|
||||
<span className={styles.user}>
|
||||
<User name={name} avatarUrl={avatarUrl} />
|
||||
</span>
|
||||
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
));
|
||||
|
||||
UserItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
avatarUrl: PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
UserItem.defaultProps = {
|
||||
avatarUrl: undefined,
|
||||
};
|
||||
|
||||
export default UserItem;
|
||||
@@ -0,0 +1,45 @@
|
||||
:global(#app) {
|
||||
.menuItem {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemText {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
position: relative;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.menuItemTextActive:before {
|
||||
bottom: 2px;
|
||||
color: #798d99;
|
||||
content: "Г";
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
text-align: center;
|
||||
transform: rotate(-135deg);
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
padding-right: 8px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
3
client/src/components/Memberships/AddPopup/index.js
Normal file
3
client/src/components/Memberships/AddPopup/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import AddPopup from './AddPopup';
|
||||
|
||||
export default AddPopup;
|
||||
107
client/src/components/Memberships/Memberships.jsx
Normal file
107
client/src/components/Memberships/Memberships.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
|
||||
import AddPopup from './AddPopup';
|
||||
import ActionsPopup from './ActionsPopup';
|
||||
import User from '../User';
|
||||
|
||||
import styles from './Memberships.module.scss';
|
||||
|
||||
const Memberships = React.memo(
|
||||
({
|
||||
items,
|
||||
allUsers,
|
||||
addTitle,
|
||||
leaveButtonContent,
|
||||
leaveConfirmationTitle,
|
||||
leaveConfirmationContent,
|
||||
leaveConfirmationButtonContent,
|
||||
deleteButtonContent,
|
||||
deleteConfirmationTitle,
|
||||
deleteConfirmationContent,
|
||||
deleteConfirmationButtonContent,
|
||||
canEdit,
|
||||
canLeaveIfLast,
|
||||
onCreate,
|
||||
onDelete,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<span className={styles.users}>
|
||||
{items.map((item) => (
|
||||
<span key={item.id} className={styles.user}>
|
||||
<ActionsPopup
|
||||
user={item.user}
|
||||
leaveButtonContent={leaveButtonContent}
|
||||
leaveConfirmationTitle={leaveConfirmationTitle}
|
||||
leaveConfirmationContent={leaveConfirmationContent}
|
||||
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
|
||||
deleteButtonContent={deleteButtonContent}
|
||||
deleteConfirmationTitle={deleteConfirmationTitle}
|
||||
deleteConfirmationContent={deleteConfirmationContent}
|
||||
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
|
||||
canLeave={items.length > 1 || canLeaveIfLast}
|
||||
canDelete={canEdit}
|
||||
onDelete={() => onDelete(item.id)}
|
||||
>
|
||||
<User
|
||||
name={item.user.name}
|
||||
avatarUrl={item.user.avatarUrl}
|
||||
size="large"
|
||||
isDisabled={!item.isPersisted}
|
||||
/>
|
||||
</ActionsPopup>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{canEdit && (
|
||||
<AddPopup
|
||||
users={allUsers}
|
||||
currentUserIds={items.map((item) => item.user.id)}
|
||||
title={addTitle}
|
||||
onCreate={onCreate}
|
||||
>
|
||||
<Button icon="add user" className={styles.addUser} />
|
||||
</AddPopup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Memberships.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
items: PropTypes.array.isRequired,
|
||||
allUsers: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
addTitle: PropTypes.string,
|
||||
leaveButtonContent: PropTypes.string,
|
||||
leaveConfirmationTitle: PropTypes.string,
|
||||
leaveConfirmationContent: PropTypes.string,
|
||||
leaveConfirmationButtonContent: PropTypes.string,
|
||||
deleteButtonContent: PropTypes.string,
|
||||
deleteConfirmationTitle: PropTypes.string,
|
||||
deleteConfirmationContent: PropTypes.string,
|
||||
deleteConfirmationButtonContent: PropTypes.string,
|
||||
canEdit: PropTypes.bool,
|
||||
canLeaveIfLast: PropTypes.bool,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Memberships.defaultProps = {
|
||||
addTitle: undefined,
|
||||
leaveButtonContent: undefined,
|
||||
leaveConfirmationTitle: undefined,
|
||||
leaveConfirmationContent: undefined,
|
||||
leaveConfirmationButtonContent: undefined,
|
||||
deleteButtonContent: undefined,
|
||||
deleteConfirmationTitle: undefined,
|
||||
deleteConfirmationContent: undefined,
|
||||
deleteConfirmationButtonContent: undefined,
|
||||
canEdit: true,
|
||||
canLeaveIfLast: true,
|
||||
};
|
||||
|
||||
export default Memberships;
|
||||
30
client/src/components/Memberships/Memberships.module.scss
Normal file
30
client/src/components/Memberships/Memberships.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
:global(#app) {
|
||||
.addUser {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
line-height: 36px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: all 0.1s ease 0s;
|
||||
vertical-align: top;
|
||||
width: 36px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
margin: 0 -4px 0 0;
|
||||
vertical-align: top;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.users {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
3
client/src/components/Memberships/index.js
Normal file
3
client/src/components/Memberships/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Memberships from './Memberships';
|
||||
|
||||
export default Memberships;
|
||||
Reference in New Issue
Block a user