From 4821d407a7ce8344e74d1bbbd9c56028801d9b67 Mon Sep 17 00:00:00 2001
From: Lukas Corona <33840479+lukascorona@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:58:59 +0100
Subject: [PATCH] feat: Ability to set list color indicator (#1033)
Closes #840
---
.../components/ColorPicker/ColorPicker.jsx | 52 +++++
.../ColorPicker/ColorPicker.module.scss | 40 ++++
client/src/components/ColorPicker/index.js | 3 +
client/src/components/LabelsStep/Editor.jsx | 23 +-
.../components/LabelsStep/Editor.module.scss | 39 ----
client/src/components/List/ActionsStep.jsx | 198 +++++++++++-------
client/src/components/List/List.jsx | 37 +++-
client/src/components/List/List.module.scss | 5 +
.../ProjectSettingsModal/BackgroundPane.jsx | 27 +--
client/src/constants/ListColors.js | 12 ++
client/src/containers/ListContainer.js | 3 +-
client/src/locales/de-DE/core.js | 2 +
client/src/locales/en-GB/core.js | 2 +
client/src/locales/en-US/core.js | 2 +
client/src/models/List.js | 1 +
client/src/styles.module.scss | 42 ++++
server/api/controllers/lists/update.js | 7 +-
server/api/models/List.js | 19 ++
.../20250131202710_add_list_color.js | 17 ++
19 files changed, 365 insertions(+), 166 deletions(-)
create mode 100644 client/src/components/ColorPicker/ColorPicker.jsx
create mode 100644 client/src/components/ColorPicker/ColorPicker.module.scss
create mode 100644 client/src/components/ColorPicker/index.js
create mode 100644 client/src/constants/ListColors.js
create mode 100644 server/db/migrations/20250131202710_add_list_color.js
diff --git a/client/src/components/ColorPicker/ColorPicker.jsx b/client/src/components/ColorPicker/ColorPicker.jsx
new file mode 100644
index 00000000..58a12710
--- /dev/null
+++ b/client/src/components/ColorPicker/ColorPicker.jsx
@@ -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 (
+ <>
+
+ {colors.map((color) => (
+
+ ))}
+
+ {current && allowDeletion && (
+
+ )}
+ >
+ );
+});
+
+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;
diff --git a/client/src/components/ColorPicker/ColorPicker.module.scss b/client/src/components/ColorPicker/ColorPicker.module.scss
new file mode 100644
index 00000000..6901efbb
--- /dev/null
+++ b/client/src/components/ColorPicker/ColorPicker.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/client/src/components/ColorPicker/index.js b/client/src/components/ColorPicker/index.js
new file mode 100644
index 00000000..bd7d9357
--- /dev/null
+++ b/client/src/components/ColorPicker/index.js
@@ -0,0 +1,3 @@
+import ColorPicker from './ColorPicker';
+
+export default ColorPicker;
diff --git a/client/src/components/LabelsStep/Editor.jsx b/client/src/components/LabelsStep/Editor.jsx
index 23c0347a..e13e5608 100755
--- a/client/src/components/LabelsStep/Editor.jsx
+++ b/client/src/components/LabelsStep/Editor.jsx
@@ -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}
/>
{t('common.color')}
-
- {LabelColors.map((color) => (
-
- ))}
-
+
>
);
});
diff --git a/client/src/components/LabelsStep/Editor.module.scss b/client/src/components/LabelsStep/Editor.module.scss
index 31dbfd1d..8246167c 100644
--- a/client/src/components/LabelsStep/Editor.module.scss
+++ b/client/src/components/LabelsStep/Editor.module.scss
@@ -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;
}
diff --git a/client/src/components/List/ActionsStep.jsx b/client/src/components/List/ActionsStep.jsx
index bc04826c..4011361e 100755
--- a/client/src/components/List/ActionsStep.jsx
+++ b/client/src/components/List/ActionsStep.jsx
@@ -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 ;
- case StepTypes.DELETE:
- return (
-
- );
- 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 ;
+ case StepTypes.DELETE:
+ return (
+
+ );
+ case StepTypes.EDIT_COLOR:
+ return (
+ <>
+
+ {t('action.editColor', {
+ context: 'title',
+ })}
+
+
+ onColorEdit(e.currentTarget.value)}
+ />
+
+ >
+ );
+ default:
+ }
}
- }
- return (
- <>
-
- {t('common.listActions', {
- context: 'title',
- })}
-
-
-
-
- >
- );
-});
+ return (
+ <>
+
+ {t('common.listActions', {
+ context: 'title',
+ })}
+
+
+
+
+ >
+ );
+ },
+);
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;
diff --git a/client/src/components/List/List.jsx b/client/src/components/List/List.jsx
index e09a7a14..e313a4b6 100755
--- a/client/src/components/List/List.jsx
+++ b/client/src/components/List/List.jsx
@@ -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}
>
- {name}
+
+ {color && (
+
+ )}
+ {name}
+
{isPersisted && canEdit && (