Project managers, board members, auto-update after reconnection, refactoring

This commit is contained in:
Maksim Eltyshev
2021-06-24 01:05:22 +05:00
parent d6cb1f6683
commit b39119ace4
478 changed files with 21226 additions and 19495 deletions

View 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);

View 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;
}
}

View 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);

View File

@@ -0,0 +1,7 @@
:global(#app) {
.menu {
border: none;
margin: -7px auto -5px;
width: 100%;
}
}

View 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;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
import AddPopup from './AddPopup';
export default AddPopup;

View 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;

View 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;
}
}

View File

@@ -0,0 +1,3 @@
import Memberships from './Memberships';
export default Memberships;