mirror of
https://github.com/plankanban/planka.git
synced 2025-08-03 12:35:26 +02:00
Initial commit
This commit is contained in:
commit
5ffef61fe7
613 changed files with 91659 additions and 0 deletions
60
client/src/components/Project/AddMembershipPopup/AddMembershipPopup.jsx
Executable file
60
client/src/components/Project/AddMembershipPopup/AddMembershipPopup.jsx
Executable file
|
@ -0,0 +1,60 @@
|
|||
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 './AddMembershipPopup.module.css';
|
||||
|
||||
const AddMembershipStep = React.memo(({
|
||||
users, currentUserIds, onCreate, onClose,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
onCreate({
|
||||
userId: id,
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onCreate, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.addMember', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.menu}>
|
||||
{users.map((user) => (
|
||||
<UserItem
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
avatar={user.avatar}
|
||||
isActive={currentUserIds.includes(user.id)}
|
||||
onSelect={() => handleUserSelect(user.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AddMembershipStep.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
users: PropTypes.array.isRequired,
|
||||
currentUserIds: PropTypes.array.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(AddMembershipStep);
|
|
@ -0,0 +1,5 @@
|
|||
.menu {
|
||||
border: none;
|
||||
margin: -7px auto -5px;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import User from '../../User';
|
||||
|
||||
import styles from './UserItem.module.css';
|
||||
|
||||
const UserItem = React.memo(({
|
||||
name, avatar, isActive, onSelect,
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isActive}
|
||||
className={classNames(styles.menuItem, isActive && styles.menuItemActive)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<span className={styles.user}>
|
||||
<User name={name} avatar={avatar} />
|
||||
</span>
|
||||
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
));
|
||||
|
||||
UserItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
UserItem.defaultProps = {
|
||||
avatar: undefined,
|
||||
};
|
||||
|
||||
export default UserItem;
|
|
@ -0,0 +1,51 @@
|
|||
.menuItem {
|
||||
border: none;
|
||||
display: block;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menuItemActive {
|
||||
background: transparent;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.menuItemActive: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;
|
||||
text-shadow: -1px 1px 0 rgba(0, 0, 0, 0.2);
|
||||
transform: rotate(-135deg);
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
padding-right: 8px;
|
||||
width: 40px;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import AddMembershipPopup from './AddMembershipPopup';
|
||||
|
||||
export default AddMembershipPopup;
|
65
client/src/components/Project/EditMembershipPopup.jsx
Executable file
65
client/src/components/Project/EditMembershipPopup.jsx
Executable file
|
@ -0,0 +1,65 @@
|
|||
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 './EditMembershipPopup.module.css';
|
||||
|
||||
const StepTypes = {
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const EditMembershipStep = React.memo(({ user, isEditable, 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('common.removeMember', {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t('common.areYouSureYouWantToRemoveThisMemberFromProject')}
|
||||
buttonContent={t('action.removeMember')}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.user}>
|
||||
<User name={user.name} avatar={user.avatar} size="large" />
|
||||
</span>
|
||||
<span className={styles.content}>
|
||||
<div className={styles.name}>{user.name}</div>
|
||||
<div className={styles.email}>{user.email}</div>
|
||||
{!user.isCurrent && isEditable && (
|
||||
<Button
|
||||
content={t('action.removeFromProject')}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
EditMembershipStep.propTypes = {
|
||||
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
isEditable: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(EditMembershipStep);
|
40
client/src/components/Project/EditMembershipPopup.module.css
Normal file
40
client/src/components/Project/EditMembershipPopup.module.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.content {
|
||||
display: inline-block;
|
||||
width: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
flex: 0 0 auto;
|
||||
font-weight: normal !important;
|
||||
margin: 0 0 0 -10px !important;
|
||||
padding: 11px 10px !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background: #e9e9e9 !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
108
client/src/components/Project/EditPopup.jsx
Executable file
108
client/src/components/Project/EditPopup.jsx
Executable file
|
@ -0,0 +1,108 @@
|
|||
import dequal from 'dequal';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
import styles from './EditPopup.module.css';
|
||||
|
||||
const StepTypes = {
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const EditStep = React.memo(({
|
||||
defaultData, onUpdate, onDelete, onClose,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange] = useForm(() => ({
|
||||
name: '',
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const nameField = useRef(null);
|
||||
|
||||
const handleSubmit = useDeepCompareCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
name: data.name.trim(),
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameField.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dequal(cleanData, defaultData)) {
|
||||
onUpdate(cleanData);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [defaultData, onUpdate, onClose, data]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
useEffect(() => {
|
||||
nameField.current.select();
|
||||
}, []);
|
||||
|
||||
if (step && step.type === StepTypes.DELETE) {
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t('common.deleteProject', {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t('common.areYouSureYouWantToDeleteThisProject')}
|
||||
buttonContent={t('action.deleteProject')}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.editProject', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
fluid
|
||||
ref={nameField}
|
||||
name="name"
|
||||
value={data.name}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<Button positive content={t('action.save')} />
|
||||
</Form>
|
||||
<Button
|
||||
content={t('action.delete')}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
EditStep.propTypes = {
|
||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPopup(EditStep);
|
10
client/src/components/Project/EditPopup.module.css
Normal file
10
client/src/components/Project/EditPopup.module.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.deleteButton {
|
||||
bottom: 12px;
|
||||
box-shadow: 0 1px 0 #cbcccc;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
94
client/src/components/Project/Project.jsx
Executable file
94
client/src/components/Project/Project.jsx
Executable file
|
@ -0,0 +1,94 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Grid } from 'semantic-ui-react';
|
||||
|
||||
import BoardsContainer from '../../containers/BoardsContainer';
|
||||
import EditPopup from './EditPopup';
|
||||
import AddMembershipPopup from './AddMembershipPopup';
|
||||
import EditMembershipPopup from './EditMembershipPopup';
|
||||
import User from '../User';
|
||||
|
||||
import styles from './Project.module.css';
|
||||
|
||||
const Project = React.memo(
|
||||
({
|
||||
name,
|
||||
memberships,
|
||||
allUsers,
|
||||
isEditable,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onMembershipCreate,
|
||||
onMembershipDelete,
|
||||
}) => {
|
||||
const handleMembershipDelete = useCallback(
|
||||
(id) => {
|
||||
onMembershipDelete(id);
|
||||
},
|
||||
[onMembershipDelete],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Grid className={styles.header}>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<EditPopup
|
||||
defaultData={{
|
||||
name,
|
||||
}}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<Button content={name} disabled={!isEditable} className={styles.name} />
|
||||
</EditPopup>
|
||||
<span className={styles.users}>
|
||||
{memberships.map((membership) => (
|
||||
<span key={membership.id} className={styles.user}>
|
||||
<EditMembershipPopup
|
||||
user={membership.user}
|
||||
isEditable={isEditable}
|
||||
onDelete={() => handleMembershipDelete(membership.id)}
|
||||
>
|
||||
<User
|
||||
name={membership.user.name}
|
||||
avatar={membership.user.avatar}
|
||||
size="large"
|
||||
isDisabled={!membership.isPersisted}
|
||||
/>
|
||||
</EditMembershipPopup>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{isEditable && (
|
||||
<AddMembershipPopup
|
||||
users={allUsers}
|
||||
currentUserIds={memberships.map((membership) => membership.user.id)}
|
||||
onCreate={onMembershipCreate}
|
||||
>
|
||||
<Button icon="add user" className={styles.addUser} />
|
||||
</AddMembershipPopup>
|
||||
)}
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
<BoardsContainer />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Project.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
memberships: PropTypes.array.isRequired,
|
||||
allUsers: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
isEditable: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onMembershipCreate: PropTypes.func.isRequired,
|
||||
onMembershipDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Project;
|
49
client/src/components/Project/Project.module.css
Normal file
49
client/src/components/Project/Project.module.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
.addUser {
|
||||
background: #2c3035 !important;
|
||||
border-radius: 50% !important;
|
||||
color: #fff !important;
|
||||
line-height: 36px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
transition: all 0.1s ease 0s !important;
|
||||
vertical-align: top !important;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.addUser:hover {
|
||||
background: #4b4f53;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 -1rem !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
background: transparent !important;
|
||||
color: #fff !important;
|
||||
font-size: 32px !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 36px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-block;
|
||||
margin: 0 -4px 0 0;
|
||||
vertical-align: top;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.users {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin: 0 20px;
|
||||
}
|
3
client/src/components/Project/index.js
Executable file
3
client/src/components/Project/index.js
Executable file
|
@ -0,0 +1,3 @@
|
|||
import Project from './Project';
|
||||
|
||||
export default Project;
|
Loading…
Add table
Add a link
Reference in a new issue