1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-29 18:19: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,60 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useDeepCompareCallback, useForm } from '../../hooks';
import LabelColors from '../../constants/LabelColors';
import Editor from './Editor';
import styles from './AddStep.module.css';
const AddStep = React.memo(({ onCreate, onBack }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
color: LabelColors.KEYS[0],
}));
const editor = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
editor.current.selectNameField();
return;
}
onCreate(cleanData);
onBack();
}, [data, onCreate, onBack]);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.createLabel', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Editor ref={editor} data={data} onFieldChange={handleFieldChange} />
<Button positive content={t('action.createLabel')} className={styles.submitButton} />
</Form>
</Popup.Content>
</>
);
});
AddStep.propTypes = {
onCreate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
export default AddStep;

View file

@ -0,0 +1,3 @@
.submitButton {
margin-top: 12px;
}

View file

@ -0,0 +1,99 @@
import dequal from 'dequal';
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
import LabelColors from '../../constants/LabelColors';
import Editor from './Editor';
import DeleteStep from '../DeleteStep';
import styles from './EditStep.module.css';
const StepTypes = {
DELETE: 'DELETE',
};
const EditStep = React.memo(({
defaultData, onUpdate, onDelete, onBack,
}) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
color: LabelColors.KEYS[0],
...defaultData,
}));
const [step, openStep, handleBack] = useSteps();
const editor = useRef(null);
const handleSubmit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
editor.current.selectNameField();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
onBack();
}, [defaultData, data, onUpdate, onBack]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteLabel', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisLabel')}
buttonContent={t('action.deleteLabel')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editLabel', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Editor ref={editor} data={data} onFieldChange={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,
onBack: PropTypes.func.isRequired,
};
export default EditStep;

View file

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

View file

@ -0,0 +1,74 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Input } from '../../lib/custom-ui';
import LabelColors from '../../constants/LabelColors';
import styles from './Editor.module.css';
const Editor = React.forwardRef(({ data, onFieldChange }, ref) => {
const [t] = useTranslation();
const nameField = useRef(null);
const selectNameField = useCallback(() => {
nameField.current.select();
}, []);
useImperativeHandle(
ref,
() => ({
selectNameField,
}),
[selectNameField],
);
useEffect(() => {
selectNameField();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={onFieldChange}
/>
<div className={styles.text}>{t('common.color')}</div>
<div className={styles.colorButtons}>
{LabelColors.PAIRS.map(([name, hex]) => (
<Button
key={name}
type="button"
name="color"
value={name}
className={classNames(
styles.colorButton,
name === data.color && styles.colorButtonActive,
)}
style={{
background: hex,
}}
onClick={onFieldChange}
/>
))}
</div>
</>
);
});
Editor.propTypes = {
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onFieldChange: PropTypes.func.isRequired,
};
export default React.memo(Editor);

View file

@ -0,0 +1,49 @@
.colorButton {
float: left;
height: 40px;
margin: 4px !important;
padding: 0 !important;
position: relative;
width: 49.6px;
}
.colorButton:hover {
opacity: 0.9;
}
.colorButtonActive:before {
bottom: 3px;
color: #ffffff;
content: "Г";
font-size: 18px;
line-height: 36px;
position: absolute;
right: 6px;
text-align: center;
text-shadow: -1px 1px 0 rgba(0, 0, 0, 0.2);
top: 0;
transform: rotate(-135deg);
width: 36px;
}
.colorButtons {
margin: -4px;
padding-bottom: 16px;
}
.colorButtons:after {
content: "";
display: table;
clear: both;
}
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}

View file

@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Button } from 'semantic-ui-react';
import LabelColors from '../../constants/LabelColors';
import styles from './Item.module.css';
const Item = React.memo(({
name, color, isPersisted, isActive, onSelect, onDeselect, onEdit,
}) => {
const handleToggleClick = useCallback(() => {
if (isActive) {
onDeselect();
} else {
onSelect();
}
}, [isActive, onSelect, onDeselect]);
return (
<div className={styles.wrapper}>
<Button
fluid
content={name}
active={isActive}
disabled={!isPersisted}
className={classNames(styles.labelButton, isActive && styles.labelButtonActive)}
style={{
background: LabelColors.MAP[color],
}}
onClick={handleToggleClick}
/>
<Button
icon="pencil"
size="small"
disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
</div>
);
});
Item.propTypes = {
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
onDeselect: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
};
export default Item;

View file

@ -0,0 +1,51 @@
.editButton {
background: transparent !important;
box-shadow: none !important;
flex: 0 0 auto;
font-weight: normal !important;
margin: 0 !important;
padding: 8px 10px !important;
text-decoration: underline !important;
}
.editButton:hover {
background: #e9e9e9 !important;
}
.labelButton {
color: #fff !important;
flex: 1 1 auto;
font-size: 14px !important;
font-weight: bold !important;
overflow: hidden;
padding: 8px 32px 8px 10px !important;
position: relative;
text-align: left !important;
text-overflow: ellipsis;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2) !important;
}
.labelButton:hover {
opacity: 0.9;
}
.labelButtonActive:before {
bottom: 1px;
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;
}
.wrapper {
display: flex;
margin-bottom: 4px;
max-width: 280px;
white-space: nowrap;
}

View file

@ -0,0 +1,141 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import AddStep from './AddStep';
import EditStep from './EditStep';
import Item from './Item';
import styles from './LabelsStep.module.css';
const StepTypes = {
ADD: 'ADD',
EDIT: 'EDIT',
};
const LabelsStep = React.memo(
({
items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleAddClick = useCallback(() => {
openStep(StepTypes.ADD);
}, [openStep]);
const handleEdit = useCallback(
(id) => {
openStep(StepTypes.EDIT, {
id,
});
},
[openStep],
);
const handleSelect = useCallback(
(id) => {
onSelect(id);
},
[onSelect],
);
const handleDeselect = useCallback(
(id) => {
onDeselect(id);
},
[onDeselect],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
if (step) {
switch (step.type) {
case StepTypes.ADD:
return <AddStep onCreate={onCreate} onBack={handleBack} />;
case StepTypes.EDIT: {
const currentItem = items.find((item) => item.id === step.params.id);
if (currentItem) {
return (
<EditStep
defaultData={pick(currentItem, ['name', 'color'])}
onUpdate={(data) => handleUpdate(currentItem.id, data)}
onDelete={() => handleDelete(currentItem.id)}
onBack={handleBack}
/>
);
}
openStep(null);
break;
}
default:
}
}
return (
<>
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
<Popup.Content>
{items.map((item) => (
<Item
key={item.id}
name={item.name}
color={item.color}
isPersisted={item.isPersisted}
isActive={currentIds.includes(item.id)}
onSelect={() => handleSelect(item.id)}
onDeselect={() => handleDeselect(item.id)}
onEdit={() => handleEdit(item.id)}
/>
))}
<Button
fluid
content={t('action.createNewLabel')}
className={styles.addButton}
onClick={handleAddClick}
/>
</Popup.Content>
</>
);
},
);
LabelsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
items: PropTypes.array.isRequired,
currentIds: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
title: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onDeselect: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
LabelsStep.defaultProps = {
title: 'common.labels',
onBack: undefined,
};
export default LabelsStep;

View file

@ -0,0 +1,15 @@
.addButton {
background: transparent !important;
box-shadow: none !important;
color: #6b808c !important;
font-weight: normal !important;
margin-top: 8px !important;
padding: 6px 11px !important;
text-align: left !important;
text-decoration: underline !important;
transition: background 0.3s ease !important;
}
.addButton:hover {
background: #e9e9e9 !important;
}

View file

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