1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Ability to set list color indicator (#1033)

Closes #840
This commit is contained in:
Lukas Corona 2025-02-25 15:58:59 +01:00 committed by GitHub
parent b0e9ed6162
commit 2732570607
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 365 additions and 166 deletions

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

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

View file

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

View file

@ -1,16 +1,12 @@
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Input } from '../../lib/custom-ui'; import { Input } from '../../lib/custom-ui';
import LabelColors from '../../constants/LabelColors'; import LabelColors from '../../constants/LabelColors';
import ColorPicker from '../ColorPicker';
import styles from './Editor.module.scss'; import styles from './Editor.module.scss';
import globalStyles from '../../styles.module.scss';
const Editor = React.memo(({ data, onFieldChange }) => { const Editor = React.memo(({ data, onFieldChange }) => {
const [t] = useTranslation(); const [t] = useTranslation();
@ -33,22 +29,7 @@ const Editor = React.memo(({ data, onFieldChange }) => {
onChange={onFieldChange} onChange={onFieldChange}
/> />
<div className={styles.text}>{t('common.color')}</div> <div className={styles.text}>{t('common.color')}</div>
<div className={styles.colorButtons}> <ColorPicker colors={LabelColors} current={data.color} onChange={onFieldChange} />
{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>
</> </>
); );
}); });

View file

@ -1,43 +1,4 @@
:global(#app) { :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 { .field {
margin-bottom: 8px; margin-bottom: 8px;
} }

View file

@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react'; import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui'; import { Popup } from '../../lib/custom-ui';
import ListColors from '../../constants/ListColors';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import ColorPicker from '../ColorPicker';
import ListSortStep from '../ListSortStep'; import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
@ -13,93 +15,123 @@ import styles from './ActionsStep.module.scss';
const StepTypes = { const StepTypes = {
DELETE: 'DELETE', DELETE: 'DELETE',
SORT: 'SORT', SORT: 'SORT',
EDIT_COLOR: 'CHANGE_COLOR',
}; };
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => { const ActionsStep = React.memo(
const [t] = useTranslation(); ({ onNameEdit, onCardAdd, onSort, onDelete, onClose, onColorEdit, color }) => {
const [step, openStep, handleBack] = useSteps(); 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 handleEditNameClick = useCallback(() => {
onNameEdit();
onClose(); onClose();
}, }, [onNameEdit, onClose]);
[onSort, onClose],
);
if (step && step.type) { const handleAddCardClick = useCallback(() => {
switch (step.type) { onCardAdd();
case StepTypes.SORT: onClose();
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />; }, [onCardAdd, onClose]);
case StepTypes.DELETE:
return ( const handleSortClick = useCallback(() => {
<DeleteStep openStep(StepTypes.SORT);
title="common.deleteList" }, [openStep]);
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList" const handleDeleteClick = useCallback(() => {
onConfirm={onDelete} openStep(StepTypes.DELETE);
onBack={handleBack} }, [openStep]);
/>
); const hanndleEditColorClick = useCallback(() => {
default: 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 ( return (
<> <>
<Popup.Header> <Popup.Header>
{t('common.listActions', { {t('common.listActions', {
context: 'title', context: 'title',
})} })}
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<Menu secondary vertical className={styles.menu}> <Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}> <Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', { {t('action.editTitle', {
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}> <Menu.Item className={styles.menuItem} onClick={hanndleEditColorClick}>
{t('action.addCard', { {t('action.editColor', {
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}> <Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
{t('action.sortList', { {t('action.addCard', {
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> <Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.deleteList', { {t('action.sortList', {
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
</Menu> <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
</Popup.Content> {t('action.deleteList', {
</> context: 'title',
); })}
}); </Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = { ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired, onNameEdit: PropTypes.func.isRequired,
@ -107,6 +139,12 @@ ActionsStep.propTypes = {
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired, onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onColorEdit: PropTypes.func.isRequired,
color: PropTypes.string,
};
ActionsStep.defaultProps = {
color: undefined,
}; };
export default ActionsStep; export default ActionsStep;

View file

@ -1,25 +1,29 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import camelCase from 'lodash/camelCase';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import upperFirst from 'lodash/upperFirst';
import { Draggable, Droppable } from 'react-beautiful-dnd'; import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react'; import { Button, Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import DroppableTypes from '../../constants/DroppableTypes'; import DroppableTypes from '../../constants/DroppableTypes';
import CardContainer from '../../containers/CardContainer'; import CardContainer from '../../containers/CardContainer';
import NameEdit from './NameEdit';
import CardAdd from './CardAdd'; import CardAdd from './CardAdd';
import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep'; import ActionsStep from './ActionsStep';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg'; import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './List.module.scss'; import styles from './List.module.scss';
import globalStyles from '../../styles.module.scss';
const List = React.memo( const List = React.memo(
({ ({
id, id,
index, index,
name, name,
color,
isPersisted, isPersisted,
cardIds, cardIds,
canEdit, canEdit,
@ -49,6 +53,15 @@ const List = React.memo(
[onUpdate], [onUpdate],
); );
const handleColorEdit = useCallback(
(newColor) => {
onUpdate({
color: newColor,
});
},
[onUpdate],
);
const handleAddCardClick = useCallback(() => { const handleAddCardClick = useCallback(() => {
setIsAddCardOpened(true); setIsAddCardOpened(true);
}, []); }, []);
@ -118,7 +131,18 @@ const List = React.memo(
onClick={handleHeaderClick} onClick={handleHeaderClick}
> >
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}> <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> </NameEdit>
{isPersisted && canEdit && ( {isPersisted && canEdit && (
<ActionsPopup <ActionsPopup
@ -126,6 +150,8 @@ const List = React.memo(
onCardAdd={handleCardAdd} onCardAdd={handleCardAdd}
onDelete={onDelete} onDelete={onDelete}
onSort={onSort} onSort={onSort}
color={color}
onColorEdit={handleColorEdit}
> >
<Button className={classNames(styles.headerButton, styles.target)}> <Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />
@ -140,7 +166,7 @@ const List = React.memo(
<button <button
type="button" type="button"
disabled={!isPersisted} disabled={!isPersisted}
className={classNames(styles.addCardButton)} className={styles.addCardButton}
onClick={handleAddCardClick} onClick={handleAddCardClick}
> >
<PlusMathIcon className={styles.addCardButtonIcon} /> <PlusMathIcon className={styles.addCardButtonIcon} />
@ -161,6 +187,7 @@ List.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
color: PropTypes.string,
isPersisted: PropTypes.bool.isRequired, isPersisted: PropTypes.bool.isRequired,
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
@ -170,4 +197,8 @@ List.propTypes = {
onCardCreate: PropTypes.func.isRequired, onCardCreate: PropTypes.func.isRequired,
}; };
List.defaultProps = {
color: undefined,
};
export default List; export default List;

View file

@ -127,6 +127,11 @@
word-break: break-word; word-break: break-word;
} }
.headerNameColor {
line-height: 1;
margin-right: 6px;
}
.innerWrapper { .innerWrapper {
margin-right: 8px; margin-right: 8px;
width: 272px; width: 272px;

View file

@ -1,18 +1,15 @@
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Image } from 'semantic-ui-react'; import { Button, Image } from 'semantic-ui-react';
import { FilePicker } from '../../lib/custom-ui'; import { FilePicker } from '../../lib/custom-ui';
import ProjectBackgroundGradients from '../../constants/ProjectBackgroundGradients'; import ProjectBackgroundGradients from '../../constants/ProjectBackgroundGradients';
import { ProjectBackgroundTypes } from '../../constants/Enums'; import { ProjectBackgroundTypes } from '../../constants/Enums';
import ColorPicker from '../ColorPicker';
import styles from './BackgroundPane.module.scss'; import styles from './BackgroundPane.module.scss';
import globalStyles from '../../styles.module.scss';
const BackgroundPane = React.memo( const BackgroundPane = React.memo(
({ item, imageCoverUrl, isImageUpdating, onUpdate, onImageUpdate, onImageDelete }) => { ({ item, imageCoverUrl, isImageUpdating, onUpdate, onImageUpdate, onImageDelete }) => {
@ -68,23 +65,11 @@ const BackgroundPane = React.memo(
return ( return (
<> <>
<div className={styles.gradientButtons}> <div className={styles.gradientButtons}>
{ProjectBackgroundGradients.map((gradient) => ( <ColorPicker
<Button colors={ProjectBackgroundGradients}
key={gradient} current={item?.name}
type="button" onChange={handleGradientClick}
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}
/>
))}
</div> </div>
{imageCoverUrl && ( {imageCoverUrl && (
// TODO: wrap in button // TODO: wrap in button

View file

@ -0,0 +1,12 @@
export default [
'berry-red',
'pumpkin-orange',
'lagoon-blue',
'pink-tulip',
'light-mud',
'orange-peel',
'bright-moss',
'antique-blue',
'dark-granite',
'lagune-blue',
];

View file

@ -11,7 +11,7 @@ const makeMapStateToProps = () => {
const selectCardIdsByListId = selectors.makeSelectCardIdsByListId(); const selectCardIdsByListId = selectors.makeSelectCardIdsByListId();
return (state, { id, index }) => { return (state, { id, index }) => {
const { name, isPersisted } = selectListById(state, id); const { name, color, isPersisted } = selectListById(state, id);
const cardIds = selectCardIdsByListId(state, id); const cardIds = selectCardIdsByListId(state, id);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@ -22,6 +22,7 @@ const makeMapStateToProps = () => {
id, id,
index, index,
name, name,
color,
isPersisted, isPersisted,
cardIds, cardIds,
canEdit: isCurrentUserEditor, canEdit: isCurrentUserEditor,

View file

@ -196,6 +196,7 @@ export default {
duplicate: 'Kopieren', duplicate: 'Kopieren',
duplicateCard_title: 'Karte kopieren', duplicateCard_title: 'Karte kopieren',
edit: 'Bearbeiten', edit: 'Bearbeiten',
editColor_title: 'Farbe ändern',
editDueDate_title: 'Fälligkeitsdatum bearbeiten', editDueDate_title: 'Fälligkeitsdatum bearbeiten',
editDescription_title: 'Beschreibung ändern', editDescription_title: 'Beschreibung ändern',
editEmail_title: 'E-Mail-Adresse bearbeiten', editEmail_title: 'E-Mail-Adresse bearbeiten',
@ -212,6 +213,7 @@ export default {
moveCard_title: 'Karte bewegen', moveCard_title: 'Karte bewegen',
remove: 'Löschen', remove: 'Löschen',
removeBackground: 'Hintergrund löschen', removeBackground: 'Hintergrund löschen',
removeColor: 'Farbe löschen',
removeCover_title: 'Vorschau löschen', removeCover_title: 'Vorschau löschen',
removeFromBoard: 'Vom Board entfernen', removeFromBoard: 'Vom Board entfernen',
removeFromProject: 'Vom Projekt entfernen', removeFromProject: 'Vom Projekt entfernen',

View file

@ -214,6 +214,7 @@ export default {
duplicate: 'Duplicate', duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card', duplicateCard_title: 'Duplicate Card',
edit: 'Edit', edit: 'Edit',
editColor_title: 'Edit Color',
editDueDate_title: 'Edit Due Date', editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description', editDescription_title: 'Edit Description',
editEmail_title: 'Edit E-mail', editEmail_title: 'Edit E-mail',
@ -233,6 +234,7 @@ export default {
moveCard_title: 'Move Card', moveCard_title: 'Move Card',
remove: 'Remove', remove: 'Remove',
removeBackground: 'Remove background', removeBackground: 'Remove background',
removeColor: 'Remove color',
removeCover_title: 'Remove Cover', removeCover_title: 'Remove Cover',
removeFromBoard: 'Remove from board', removeFromBoard: 'Remove from board',
removeFromProject: 'Remove from project', removeFromProject: 'Remove from project',

View file

@ -210,6 +210,7 @@ export default {
duplicate: 'Duplicate', duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card', duplicateCard_title: 'Duplicate Card',
edit: 'Edit', edit: 'Edit',
editColor_title: 'Edit Color',
editDueDate_title: 'Edit Due Date', editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description', editDescription_title: 'Edit Description',
editEmail_title: 'Edit E-mail', editEmail_title: 'Edit E-mail',
@ -229,6 +230,7 @@ export default {
moveCard_title: 'Move Card', moveCard_title: 'Move Card',
remove: 'Remove', remove: 'Remove',
removeBackground: 'Remove background', removeBackground: 'Remove background',
removeColor: 'Remove color',
removeCover_title: 'Remove Cover', removeCover_title: 'Remove Cover',
removeFromBoard: 'Remove from board', removeFromBoard: 'Remove from board',
removeFromProject: 'Remove from project', removeFromProject: 'Remove from project',

View file

@ -11,6 +11,7 @@ export default class extends BaseModel {
id: attr(), id: attr(),
position: attr(), position: attr(),
name: attr(), name: attr(),
color: attr(),
boardId: fk({ boardId: fk({
to: 'Board', to: 'Board',
as: 'board', as: 'board',

View file

@ -384,4 +384,46 @@
.backgroundRedCurtain { .backgroundRedCurtain {
background: radial-gradient(circle 371px at 2.9% 14.3%, rgba(255, 0, 102, 1) 0%, rgba(80, 5, 35, 1) 100.7%) !important; background: radial-gradient(circle 371px at 2.9% 14.3%, rgba(255, 0, 102, 1) 0%, rgba(80, 5, 35, 1) 100.7%) !important;
} }
/* Colors */
.colorBerryRed {
color: #e04556 !important;
}
.colorPumpkinOrange {
color: #f0982d !important;
}
.colorLagoonBlue {
color: #109dc0 !important;
}
.colorPinkTulip {
color: #f97394 !important;
}
.colorLightMud {
color: #c7a57b !important;
}
.colorOrangePeel {
color: #fab623 !important;
}
.colorBrightMoss {
color: #a5c261 !important;
}
.colorAntiqueBlue {
color: #6c99bb !important;
}
.colorDarkGranite {
color: #8b8680 !important;
}
.colorLaguneBlue {
color: #00b4b1 !important;
}
} }

View file

@ -21,6 +21,11 @@ module.exports = {
type: 'string', type: 'string',
isNotEmptyString: true, isNotEmptyString: true,
}, },
color: {
type: 'string',
isIn: List.COLORS,
allowNull: true,
},
}, },
exits: { exits: {
@ -55,7 +60,7 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS; throw Errors.NOT_ENOUGH_RIGHTS;
} }
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name', 'color']);
list = await sails.helpers.lists.updateOne.with({ list = await sails.helpers.lists.updateOne.with({
values, values,

View file

@ -12,8 +12,22 @@ const SortTypes = {
CREATED_AT_DESC: 'createdAt_desc', CREATED_AT_DESC: 'createdAt_desc',
}; };
const COLORS = [
'berry-red',
'pumpkin-orange',
'lagoon-blue',
'pink-tulip',
'light-mud',
'orange-peel',
'bright-moss',
'antique-blue',
'dark-granite',
'lagune-blue',
];
module.exports = { module.exports = {
SortTypes, SortTypes,
COLORS,
attributes: { attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
@ -28,6 +42,11 @@ module.exports = {
type: 'string', type: 'string',
required: true, required: true,
}, },
color: {
type: 'string',
isIn: COLORS,
allowNull: true,
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗

View file

@ -0,0 +1,17 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = (knex) =>
knex.schema.alterTable('list', (table) => {
table.text('color');
});
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = (knex) =>
knex.schema.alterTable('list', (table) => {
table.dropColumn('color');
});