1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-30 10:39:46 +02:00

Initial commit

This commit is contained in:
Maksim Eltyshev 2019-08-31 04:07:25 +05:00
commit 36fe34e8e1
583 changed files with 91539 additions and 0 deletions

View file

@ -0,0 +1,75 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import { useSteps } from '../../../hooks';
import DeleteStep from '../../DeleteStep';
import styles from './ActionsPopup.module.css';
const StepTypes = {
DELETE: 'DELETE',
};
const ActionsStep = React.memo(({ onNameEdit, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteTask', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisTask')}
buttonContent={t('action.deleteTask')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header>
{t('common.taskActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
{t('action.editDescription', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteTask', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View file

@ -0,0 +1,9 @@
.menu {
margin: -7px -12px -5px !important;
width: calc(100% + 24px) !important;
}
.menuItem {
margin: 0 !important;
padding-left: 14px !important;
}

View file

@ -0,0 +1,138 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import {
useClosableForm,
useDeepCompareCallback,
useDidUpdate,
useForm,
useToggle,
} from '../../../hooks';
import styles from './Add.module.css';
const DEFAULT_DATA = {
name: '',
};
const Add = React.forwardRef(({ children, onCreate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
const submit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.ref.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
selectNameField();
}, [onCreate, data, setData, selectNameField]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
},
[submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
nameField.current.ref.current.select();
}
}, [isOpened]);
useDidUpdate(() => {
nameField.current.ref.current.select();
}, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<TextArea
ref={nameField}
as={TextareaAutosize}
name="name"
value={data.name}
placeholder={t('common.enterTaskDescription')}
minRows={2}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.addTask')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
Add.propTypes = {
children: PropTypes.element.isRequired,
onCreate: PropTypes.func.isRequired,
};
export default React.memo(Add);

View file

@ -0,0 +1,23 @@
.controls {
clear: both !important;
margin-top: 6px !important;
}
.field {
background: #fff !important;
border: 1px solid rgba(9, 45, 66, 0.13) !important;
border-radius: 3px !important;
color: #17394d !important;
display: block !important;
line-height: 1.5 !important;
font-size: 14px !important;
margin-bottom: 4px !important;
overflow: hidden;
padding: 8px 12px !important;
resize: none !important;
}
.wrapper {
margin-top: 6px !important;
padding-bottom: 8px !important;
}

View file

@ -0,0 +1,116 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../../hooks';
import styles from './EditName.module.css';
const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [value, handleFieldChange, setValue] = useField(null);
const field = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
setValue(defaultValue);
}, [defaultValue, setValue]);
const close = useCallback(() => {
setIsOpened(false);
setValue(null);
}, [setValue]);
const submit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.ref.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
close();
}, [defaultValue, onUpdate, value, close]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
},
[submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
field.current.ref.current.select();
}
}, [isOpened]);
if (!isOpened) {
return children;
}
return (
<Form onSubmit={handleSubmit} className={styles.wrapper}>
<TextArea
ref={field}
as={TextareaAutosize}
value={value}
minRows={2}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
EditName.propTypes = {
children: PropTypes.element.isRequired,
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(EditName);

View file

@ -0,0 +1,26 @@
.controls {
clear: both;
margin-top: 6px !important;
}
.field {
background: #fff !important;
border: 1px solid rgba(9, 45, 66, 0.13) !important;
border-radius: 3px !important;
box-sizing: border-box !important;
color: #17394d !important;
display: block !important;
font-size: 14px !important;
line-height: 1.5 !important;
overflow: hidden !important;
padding: 8px 12px !important;
resize: none !important;
}
.field:focus {
outline: none;
}
.wrapper {
padding: 9px 32px 16px 40px;
}

View file

@ -0,0 +1,83 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Button, Checkbox, Icon } from 'semantic-ui-react';
import EditName from './EditName';
import ActionsPopup from './ActionsPopup';
import styles from './Item.module.css';
const Item = React.memo(({
name, isCompleted, isPersisted, onUpdate, onDelete,
}) => {
const editName = useRef(null);
const handleClick = useCallback(() => {
if (isPersisted) {
editName.current.open();
}
}, [isPersisted]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleToggleChange = useCallback(() => {
onUpdate({
isCompleted: !isCompleted,
});
}, [isCompleted, onUpdate]);
const handleNameEdit = useCallback(() => {
editName.current.open();
}, []);
return (
<div className={styles.wrapper}>
<span className={styles.checkboxWrapper}>
<Checkbox
checked={isCompleted}
disabled={!isPersisted}
className={styles.checkbox}
onChange={handleToggleChange}
/>
</span>
<EditName ref={editName} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.content}>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={styles.text} onClick={handleClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
</span>
</span>
{isPersisted && (
<ActionsPopup onNameEdit={handleNameEdit} onDelete={onDelete}>
<Button className={classNames(styles.button, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</div>
</EditName>
</div>
);
});
Item.propTypes = {
name: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default Item;

View file

@ -0,0 +1,73 @@
.button {
background: transparent !important;
box-shadow: none !important;
line-height: 28px !important;
margin: 0 !important;
min-height: auto !important;
opacity: 0;
padding: 0 !important;
position: absolute;
right: 2px;
top: 2px;
width: 28px;
}
.button:hover {
background-color: rgba(9, 45, 66, 0.13) !important;
}
.checkboxWrapper {
display: inline-block;
padding: 10px 15px 0px 8px;
position: absolute;
text-align: center;
top: 0;
left: 0;
vertical-align: top;
z-index: 2000;
line-height: 1;
height: 32px;
}
.content:hover {
background-color: rgba(9, 45, 66, 0.08);
}
.content:hover .target {
opacity: 1;
}
.task {
display: inline-block;
overflow: hidden;
overflow-wrap: break-word;
padding: 8px 0;
vertical-align: top;
width: 100%;
}
.taskCompleted {
text-decoration: line-through;
}
.text {
background: transparent;
border-radius: 3px;
color: #17394d;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 1.5;
min-height: 32px;
padding: 0 32px 0 40px;
width: 100%;
}
.wrapper {
border-radius: 3px;
margin-left: -40px;
min-height: 32px;
position: relative;
transition: all 0.14s ease-in;
width: calc(100% + 40px);
}

View file

@ -0,0 +1,72 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Progress } from 'semantic-ui-react';
import Item from './Item';
import Add from './Add';
import styles from './Tasks.module.css';
const Tasks = React.memo(({
items, onCreate, onUpdate, onDelete,
}) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const completedItems = items.filter((item) => item.isCompleted);
return (
<>
{items.length > 0 && (
<Progress
autoSuccess
value={completedItems.length}
total={items.length}
color="blue"
size="tiny"
className={styles.progress}
/>
)}
{items.map((item) => (
<Item
key={item.id}
name={item.name}
isCompleted={item.isCompleted}
isPersisted={item.isPersisted}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
<Add onCreate={onCreate}>
<button type="button" className={styles.taskButton}>
<span className={styles.taskButtonText}>
{items.length > 0 ? t('action.addAnotherTask') : t('action.addTask')}
</span>
</button>
</Add>
</>
);
});
Tasks.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default Tasks;

View file

@ -0,0 +1,30 @@
.progress {
margin: 0 0 16px !important;
}
.taskButton {
background: transparent;
border: none;
border-radius: 3px;
color: #6b808c;
cursor: pointer;
display: block;
margin-top: 6px;
min-height: 54px;
outline: none;
padding: 8px 12px;
position: relative;
text-align: left;
text-decoration: none;
width: 100%;
}
.taskButton:hover {
background-color: rgba(9, 45, 66, 0.13);
color: #092d42;
}
.taskButtonText {
position: absolute;
top: 12px;
}

View file

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