mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 13:19:44 +02:00
parent
ac09d9b6bd
commit
4821d407a7
19 changed files with 365 additions and 166 deletions
52
client/src/components/ColorPicker/ColorPicker.jsx
Normal file
52
client/src/components/ColorPicker/ColorPicker.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import classNames from 'classnames';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
import styles from './ColorPicker.module.scss';
|
||||
|
||||
const ColorPicker = React.memo(({ current, onChange, colors, allowDeletion }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div className={styles.colorButtons}>
|
||||
{colors.map((color) => (
|
||||
<Button
|
||||
key={color}
|
||||
type="button"
|
||||
name="color"
|
||||
value={color}
|
||||
className={classNames(
|
||||
styles.colorButton,
|
||||
color === current && styles.colorButtonActive,
|
||||
globalStyles[`background${upperFirst(camelCase(color))}`],
|
||||
)}
|
||||
onClick={onChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{current && allowDeletion && (
|
||||
<Button fluid value={undefined} onClick={onChange} content={t('action.removeColor')} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
current: PropTypes.string,
|
||||
colors: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
allowDeletion: PropTypes.bool,
|
||||
};
|
||||
|
||||
ColorPicker.defaultProps = {
|
||||
current: undefined,
|
||||
onChange: undefined,
|
||||
allowDeletion: false,
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
40
client/src/components/ColorPicker/ColorPicker.module.scss
Normal file
40
client/src/components/ColorPicker/ColorPicker.module.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
:global(#app) {
|
||||
.colorButton {
|
||||
float: left;
|
||||
height: 40px;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 49.6px;
|
||||
|
||||
&: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;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
3
client/src/components/ColorPicker/index.js
Normal file
3
client/src/components/ColorPicker/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ColorPicker from './ColorPicker';
|
||||
|
||||
export default ColorPicker;
|
|
@ -1,16 +1,12 @@
|
|||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useEffect, 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 ColorPicker from '../ColorPicker';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const Editor = React.memo(({ data, onFieldChange }) => {
|
||||
const [t] = useTranslation();
|
||||
|
@ -33,22 +29,7 @@ const Editor = React.memo(({ data, onFieldChange }) => {
|
|||
onChange={onFieldChange}
|
||||
/>
|
||||
<div className={styles.text}>{t('common.color')}</div>
|
||||
<div className={styles.colorButtons}>
|
||||
{LabelColors.map((color) => (
|
||||
<Button
|
||||
key={color}
|
||||
type="button"
|
||||
name="color"
|
||||
value={color}
|
||||
className={classNames(
|
||||
styles.colorButton,
|
||||
color === data.color && styles.colorButtonActive,
|
||||
globalStyles[`background${upperFirst(camelCase(color))}`],
|
||||
)}
|
||||
onClick={onFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ColorPicker colors={LabelColors} current={data.color} onChange={onFieldChange} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,43 +1,4 @@
|
|||
:global(#app) {
|
||||
.colorButton {
|
||||
float: left;
|
||||
height: 40px;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 49.6px;
|
||||
|
||||
&: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;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import ListColors from '../../constants/ListColors';
|
||||
import { useSteps } from '../../hooks';
|
||||
import ColorPicker from '../ColorPicker';
|
||||
import ListSortStep from '../ListSortStep';
|
||||
import DeleteStep from '../DeleteStep';
|
||||
|
||||
|
@ -13,93 +15,123 @@ import styles from './ActionsStep.module.scss';
|
|||
const StepTypes = {
|
||||
DELETE: 'DELETE',
|
||||
SORT: 'SORT',
|
||||
EDIT_COLOR: 'CHANGE_COLOR',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleEditNameClick = useCallback(() => {
|
||||
onNameEdit();
|
||||
onClose();
|
||||
}, [onNameEdit, onClose]);
|
||||
|
||||
const handleAddCardClick = useCallback(() => {
|
||||
onCardAdd();
|
||||
onClose();
|
||||
}, [onCardAdd, onClose]);
|
||||
|
||||
const handleSortClick = useCallback(() => {
|
||||
openStep(StepTypes.SORT);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleSortTypeSelect = useCallback(
|
||||
(type) => {
|
||||
onSort({
|
||||
type,
|
||||
});
|
||||
const ActionsStep = React.memo(
|
||||
({ onNameEdit, onCardAdd, onSort, onDelete, onClose, onColorEdit, color }) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleEditNameClick = useCallback(() => {
|
||||
onNameEdit();
|
||||
onClose();
|
||||
},
|
||||
[onSort, onClose],
|
||||
);
|
||||
}, [onNameEdit, onClose]);
|
||||
|
||||
if (step && step.type) {
|
||||
switch (step.type) {
|
||||
case StepTypes.SORT:
|
||||
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<DeleteStep
|
||||
title="common.deleteList"
|
||||
content="common.areYouSureYouWantToDeleteThisList"
|
||||
buttonContent="action.deleteList"
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
const handleAddCardClick = useCallback(() => {
|
||||
onCardAdd();
|
||||
onClose();
|
||||
}, [onCardAdd, onClose]);
|
||||
|
||||
const handleSortClick = useCallback(() => {
|
||||
openStep(StepTypes.SORT);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
const hanndleEditColorClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_COLOR);
|
||||
}, [openStep]);
|
||||
|
||||
const handleSortTypeSelect = useCallback(
|
||||
(type) => {
|
||||
onSort({
|
||||
type,
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onSort, onClose],
|
||||
);
|
||||
|
||||
if (step && step.type) {
|
||||
switch (step.type) {
|
||||
case StepTypes.SORT:
|
||||
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<DeleteStep
|
||||
title="common.deleteList"
|
||||
content="common.areYouSureYouWantToDeleteThisList"
|
||||
buttonContent="action.deleteList"
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_COLOR:
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={handleBack}>
|
||||
{t('action.editColor', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<ColorPicker
|
||||
colors={ListColors}
|
||||
current={color}
|
||||
allowDeletion
|
||||
onChange={(e) => onColorEdit(e.currentTarget.value)}
|
||||
/>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.listActions', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
|
||||
{t('action.editTitle', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
|
||||
{t('action.addCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
|
||||
{t('action.sortList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.listActions', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
|
||||
{t('action.editTitle', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={hanndleEditColorClick}>
|
||||
{t('action.editColor', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
|
||||
{t('action.addCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
|
||||
{t('action.sortList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
onNameEdit: PropTypes.func.isRequired,
|
||||
|
@ -107,6 +139,12 @@ ActionsStep.propTypes = {
|
|||
onDelete: PropTypes.func.isRequired,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onColorEdit: PropTypes.func.isRequired,
|
||||
color: PropTypes.string,
|
||||
};
|
||||
|
||||
ActionsStep.defaultProps = {
|
||||
color: undefined,
|
||||
};
|
||||
|
||||
export default ActionsStep;
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../lib/popup';
|
||||
|
||||
import DroppableTypes from '../../constants/DroppableTypes';
|
||||
import CardContainer from '../../containers/CardContainer';
|
||||
import NameEdit from './NameEdit';
|
||||
import CardAdd from './CardAdd';
|
||||
import NameEdit from './NameEdit';
|
||||
import ActionsStep from './ActionsStep';
|
||||
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
|
||||
|
||||
import styles from './List.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const List = React.memo(
|
||||
({
|
||||
id,
|
||||
index,
|
||||
name,
|
||||
color,
|
||||
isPersisted,
|
||||
cardIds,
|
||||
canEdit,
|
||||
|
@ -49,6 +53,15 @@ const List = React.memo(
|
|||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleColorEdit = useCallback(
|
||||
(newColor) => {
|
||||
onUpdate({
|
||||
color: newColor,
|
||||
});
|
||||
},
|
||||
[onUpdate],
|
||||
);
|
||||
|
||||
const handleAddCardClick = useCallback(() => {
|
||||
setIsAddCardOpened(true);
|
||||
}, []);
|
||||
|
@ -118,7 +131,18 @@ const List = React.memo(
|
|||
onClick={handleHeaderClick}
|
||||
>
|
||||
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
|
||||
<div className={styles.headerName}>{name}</div>
|
||||
<div className={styles.headerName}>
|
||||
{color && (
|
||||
<Icon
|
||||
name="circle"
|
||||
className={classNames(
|
||||
styles.headerNameColor,
|
||||
globalStyles[`color${upperFirst(camelCase(color))}`],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{name}
|
||||
</div>
|
||||
</NameEdit>
|
||||
{isPersisted && canEdit && (
|
||||
<ActionsPopup
|
||||
|
@ -126,6 +150,8 @@ const List = React.memo(
|
|||
onCardAdd={handleCardAdd}
|
||||
onDelete={onDelete}
|
||||
onSort={onSort}
|
||||
color={color}
|
||||
onColorEdit={handleColorEdit}
|
||||
>
|
||||
<Button className={classNames(styles.headerButton, styles.target)}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
|
@ -140,7 +166,7 @@ const List = React.memo(
|
|||
<button
|
||||
type="button"
|
||||
disabled={!isPersisted}
|
||||
className={classNames(styles.addCardButton)}
|
||||
className={styles.addCardButton}
|
||||
onClick={handleAddCardClick}
|
||||
>
|
||||
<PlusMathIcon className={styles.addCardButtonIcon} />
|
||||
|
@ -161,6 +187,7 @@ List.propTypes = {
|
|||
id: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
isPersisted: PropTypes.bool.isRequired,
|
||||
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
|
@ -170,4 +197,8 @@ List.propTypes = {
|
|||
onCardCreate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
List.defaultProps = {
|
||||
color: undefined,
|
||||
};
|
||||
|
||||
export default List;
|
||||
|
|
|
@ -127,6 +127,11 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.headerNameColor {
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.innerWrapper {
|
||||
margin-right: 8px;
|
||||
width: 272px;
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { dequal } from 'dequal';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Image } from 'semantic-ui-react';
|
||||
import { FilePicker } from '../../lib/custom-ui';
|
||||
|
||||
import ProjectBackgroundGradients from '../../constants/ProjectBackgroundGradients';
|
||||
import { ProjectBackgroundTypes } from '../../constants/Enums';
|
||||
import ColorPicker from '../ColorPicker';
|
||||
|
||||
import styles from './BackgroundPane.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const BackgroundPane = React.memo(
|
||||
({ item, imageCoverUrl, isImageUpdating, onUpdate, onImageUpdate, onImageDelete }) => {
|
||||
|
@ -68,23 +65,11 @@ const BackgroundPane = React.memo(
|
|||
return (
|
||||
<>
|
||||
<div className={styles.gradientButtons}>
|
||||
{ProjectBackgroundGradients.map((gradient) => (
|
||||
<Button
|
||||
key={gradient}
|
||||
type="button"
|
||||
name="gradient"
|
||||
value={gradient}
|
||||
className={classNames(
|
||||
styles.gradientButton,
|
||||
item &&
|
||||
item.type === ProjectBackgroundTypes.GRADIENT &&
|
||||
gradient === item.name &&
|
||||
styles.gradientButtonActive,
|
||||
globalStyles[`background${upperFirst(camelCase(gradient))}`],
|
||||
)}
|
||||
onClick={handleGradientClick}
|
||||
/>
|
||||
))}
|
||||
<ColorPicker
|
||||
colors={ProjectBackgroundGradients}
|
||||
current={item?.name}
|
||||
onChange={handleGradientClick}
|
||||
/>
|
||||
</div>
|
||||
{imageCoverUrl && (
|
||||
// TODO: wrap in button
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue