1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-03 12:35:26 +02:00

Initial commit

This commit is contained in:
Maksim Eltyshev 2019-08-31 04:07:25 +05:00
commit 5ffef61fe7
613 changed files with 91659 additions and 0 deletions

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

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

@ -0,0 +1,10 @@
.deleteButton {
bottom: 12px;
box-shadow: 0 1px 0 #cbcccc;
position: absolute;
right: 9px;
}
.field {
margin-bottom: 8px;
}

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

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

View file

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