mirror of
https://github.com/plankanban/planka.git
synced 2025-07-27 09:09:46 +02:00
parent
ad7fb51cfa
commit
2ee1166747
1557 changed files with 76832 additions and 47042 deletions
180
client/src/components/lists/List/ActionsStep.jsx
Executable file
180
client/src/components/lists/List/ActionsStep.jsx
Executable file
|
@ -0,0 +1,180 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useSteps } from '../../../hooks';
|
||||
import { ListTypes } from '../../../constants/Enums';
|
||||
import EditColorStep from './EditColorStep';
|
||||
import SortStep from './SortStep';
|
||||
import SelectListTypeStep from '../SelectListTypeStep';
|
||||
import ConfirmationStep from '../../common/ConfirmationStep';
|
||||
import ArchiveCardsStep from '../../cards/ArchiveCardsStep';
|
||||
|
||||
import styles from './ActionsStep.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
EDIT_TYPE: 'EDIT_TYPE',
|
||||
EDIT_COLOR: 'EDIT_COLOR',
|
||||
SORT: 'SORT',
|
||||
ARCHIVE_CARDS: 'ARCHIVE_CARDS',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const list = useSelector((state) => selectListById(state, listId));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleTypeSelect = useCallback(
|
||||
(type) => {
|
||||
dispatch(
|
||||
entryActions.updateList(listId, {
|
||||
type,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[listId, dispatch],
|
||||
);
|
||||
|
||||
const handleDeleteConfirm = useCallback(() => {
|
||||
dispatch(entryActions.deleteList(listId));
|
||||
}, [listId, dispatch]);
|
||||
|
||||
const handleEditNameClick = useCallback(() => {
|
||||
onNameEdit();
|
||||
onClose();
|
||||
}, [onNameEdit, onClose]);
|
||||
|
||||
const handleAddCardClick = useCallback(() => {
|
||||
onCardAdd();
|
||||
onClose();
|
||||
}, [onCardAdd, onClose]);
|
||||
|
||||
const handleEditTypeClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_TYPE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditColorClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_COLOR);
|
||||
}, [openStep]);
|
||||
|
||||
const handleSortClick = useCallback(() => {
|
||||
openStep(StepTypes.SORT);
|
||||
}, [openStep]);
|
||||
|
||||
const handleArchiveCardsClick = useCallback(() => {
|
||||
openStep(StepTypes.ARCHIVE_CARDS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.EDIT_TYPE:
|
||||
return (
|
||||
<SelectListTypeStep
|
||||
withButton
|
||||
defaultValue={list.type}
|
||||
title="common.editType"
|
||||
buttonContent="action.save"
|
||||
onSelect={handleTypeSelect}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_COLOR:
|
||||
return <EditColorStep listId={listId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.SORT:
|
||||
return <SortStep listId={listId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.ARCHIVE_CARDS:
|
||||
return <ArchiveCardsStep listId={listId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
title="common.deleteList"
|
||||
content="common.areYouSureYouWantToDeleteThisList"
|
||||
buttonContent="action.deleteList"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
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={handleEditTypeClick}>
|
||||
{t('action.editType', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditColorClick}>
|
||||
{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>
|
||||
{list.type === ListTypes.CLOSED && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleArchiveCardsClick}>
|
||||
{t('action.archiveCards', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onNameEdit: PropTypes.func.isRequired,
|
||||
onCardAdd: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActionsStep;
|
16
client/src/components/lists/List/ActionsStep.module.scss
Normal file
16
client/src/components/lists/List/ActionsStep.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: -7px -12px -5px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
94
client/src/components/lists/List/EditColorStep.jsx
Normal file
94
client/src/components/lists/List/EditColorStep.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import LIST_COLORS from '../../../constants/ListColors';
|
||||
|
||||
import styles from './EditColorStep.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const EditColorStep = React.memo(({ listId, onBack, onClose }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const defaultValue = useSelector((state) => selectListById(state, listId).color);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSelectClick = useCallback(
|
||||
(_, { value: color }) => {
|
||||
dispatch(
|
||||
entryActions.updateList(listId, {
|
||||
color,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[listId, dispatch],
|
||||
);
|
||||
|
||||
const handleClearClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateList(listId, {
|
||||
color: null,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [listId, onClose, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('common.editColor', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.colorButtons}>
|
||||
{LIST_COLORS.map((color) => (
|
||||
<Button
|
||||
key={color}
|
||||
type="button"
|
||||
value={color}
|
||||
className={classNames(
|
||||
styles.colorButton,
|
||||
color === defaultValue && styles.colorButtonActive,
|
||||
globalStyles[`background${upperFirst(camelCase(color))}`],
|
||||
)}
|
||||
onClick={handleSelectClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{defaultValue && (
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.removeColor')}
|
||||
className={styles.clearButton}
|
||||
onClick={handleClearClick}
|
||||
/>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
EditColorStep.propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EditColorStep;
|
60
client/src/components/lists/List/EditColorStep.module.scss
Normal file
60
client/src/components/lists/List/EditColorStep.module.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.clearButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
padding: 6px 11px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
}
|
94
client/src/components/lists/List/EditName.jsx
Executable file
94
client/src/components/lists/List/EditName.jsx
Executable file
|
@ -0,0 +1,94 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { TextArea } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useField, useNestedRef } from '../../../hooks';
|
||||
import { focusEnd } from '../../../utils/element-helpers';
|
||||
|
||||
import styles from './EditName.module.scss';
|
||||
|
||||
const EditName = React.memo(({ listId, onClose }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const defaultValue = useSelector((state) => selectListById(state, listId).name);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [value, handleFieldChange] = useField(defaultValue);
|
||||
|
||||
const [fieldRef, handleFieldRef] = useNestedRef();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanValue = value.trim();
|
||||
|
||||
if (cleanValue && cleanValue !== defaultValue) {
|
||||
dispatch(
|
||||
entryActions.updateList(listId, {
|
||||
name: cleanValue,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [listId, defaultValue, dispatch, value, onClose]);
|
||||
|
||||
const handleFieldClick = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleFieldKeyDown = useCallback(
|
||||
(event) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
submit();
|
||||
|
||||
break;
|
||||
case 'Escape':
|
||||
submit();
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[submit],
|
||||
);
|
||||
|
||||
const handleFieldBlur = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
useEffect(() => {
|
||||
focusEnd(fieldRef.current);
|
||||
}, [fieldRef]);
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
ref={handleFieldRef}
|
||||
as={TextareaAutosize}
|
||||
value={value}
|
||||
maxLength={128}
|
||||
spellCheck={false}
|
||||
className={styles.field}
|
||||
onClick={handleFieldClick}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EditName.propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EditName;
|
28
client/src/components/lists/List/EditName.module.scss
Normal file
28
client/src/components/lists/List/EditName.module.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.field {
|
||||
border: none;
|
||||
border-color: #bbb;
|
||||
border-radius: 3px;
|
||||
color: #17394d;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 4px 8px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
background: #fff;
|
||||
border-color: #5ba4cf;
|
||||
box-shadow: 0 0 0 1px #5ba4cf;
|
||||
}
|
||||
}
|
||||
}
|
241
client/src/components/lists/List/List.jsx
Executable file
241
client/src/components/lists/List/List.jsx
Executable file
|
@ -0,0 +1,241 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
import { useDidUpdate, useTransitioning } from '../../../lib/hooks';
|
||||
import { usePopup } from '../../../lib/popup';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import DroppableTypes from '../../../constants/DroppableTypes';
|
||||
import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums';
|
||||
import { ListTypeIcons } from '../../../constants/Icons';
|
||||
import EditName from './EditName';
|
||||
import ActionsStep from './ActionsStep';
|
||||
import DraggableCard from '../../cards/DraggableCard';
|
||||
import AddCard from '../../cards/AddCard';
|
||||
import ArchiveCardsStep from '../../cards/ArchiveCardsStep';
|
||||
import PlusMathIcon from '../../../assets/images/plus-math-icon.svg?react';
|
||||
|
||||
import styles from './List.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const List = React.memo(({ id, index }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const selectFilteredCardIdsByListId = useMemo(
|
||||
() => selectors.makeSelectFilteredCardIdsByListId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const isFavoritesActive = useSelector(selectors.selectIsFavoritesActiveForCurrentUser);
|
||||
const list = useSelector((state) => selectListById(state, id));
|
||||
const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id));
|
||||
|
||||
const { canEdit, canArchiveCards, canAddCard, canDropCard } = useSelector((state) => {
|
||||
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
|
||||
return {
|
||||
canEdit: isEditModeEnabled && isEditor,
|
||||
canArchiveCards: list.type === ListTypes.CLOSED && isEditor,
|
||||
canAddCard: isEditor,
|
||||
canDropCard: isEditor,
|
||||
};
|
||||
}, shallowEqual);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
|
||||
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
const cardsWrapperRef = useRef(null);
|
||||
|
||||
const handleCardCreate = useCallback(
|
||||
(data, autoOpen) => {
|
||||
dispatch(entryActions.createCard(id, data, autoOpen));
|
||||
},
|
||||
[id, dispatch],
|
||||
);
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
if (list.isPersisted && canEdit) {
|
||||
setIsEditNameOpened(true);
|
||||
}
|
||||
}, [list.isPersisted, canEdit]);
|
||||
|
||||
const handleAddCardClick = useCallback(() => {
|
||||
setIsAddCardOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleAddCardClose = useCallback(() => {
|
||||
setIsAddCardOpened(false);
|
||||
}, []);
|
||||
|
||||
const handleCardAdd = useCallback(() => {
|
||||
setIsAddCardOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleNameEdit = useCallback(() => {
|
||||
setIsEditNameOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleEditNameClose = useCallback(() => {
|
||||
setIsEditNameOpened(false);
|
||||
}, []);
|
||||
|
||||
const handleWrapperTransitionEnd = useTransitioning(
|
||||
wrapperRef,
|
||||
styles.outerWrapperTransitioning,
|
||||
[isFavoritesActive],
|
||||
);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (isAddCardOpened) {
|
||||
cardsWrapperRef.current.scrollTop = cardsWrapperRef.current.scrollHeight;
|
||||
}
|
||||
}, [cardIds, isAddCardOpened]);
|
||||
|
||||
const ActionsPopup = usePopup(ActionsStep);
|
||||
const ArchiveCardsPopup = usePopup(ArchiveCardsStep);
|
||||
|
||||
const cardsNode = (
|
||||
<Droppable
|
||||
droppableId={`list:${id}`}
|
||||
type={DroppableTypes.CARD}
|
||||
isDropDisabled={!list.isPersisted || !canDropCard}
|
||||
>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...droppableProps} ref={innerRef}>
|
||||
<div className={styles.cards}>
|
||||
{cardIds.map((cardId, cardIndex) => (
|
||||
<DraggableCard key={cardId} id={cardId} index={cardIndex} className={styles.card} />
|
||||
))}
|
||||
{placeholder}
|
||||
{canAddCard && (
|
||||
<AddCard
|
||||
isOpened={isAddCardOpened}
|
||||
className={styles.addCard}
|
||||
onCreate={handleCardCreate}
|
||||
onClose={handleAddCardClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
draggableId={`list:${id}`}
|
||||
index={index}
|
||||
isDragDisabled={!list.isPersisted || !canEdit || isEditNameOpened}
|
||||
>
|
||||
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||
<div
|
||||
{...draggableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
data-drag-scroller
|
||||
ref={innerRef}
|
||||
className={styles.innerWrapper}
|
||||
>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={classNames(
|
||||
styles.outerWrapper,
|
||||
isFavoritesActive && styles.outerWrapperWithFavorites,
|
||||
)}
|
||||
onTransitionEnd={handleWrapperTransitionEnd}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={classNames(styles.header, canEdit && styles.headerEditable)}
|
||||
onClick={handleHeaderClick}
|
||||
>
|
||||
{isEditNameOpened ? (
|
||||
<EditName listId={id} onClose={handleEditNameClose} />
|
||||
) : (
|
||||
<div className={styles.headerName}>
|
||||
{list.color && (
|
||||
<Icon
|
||||
name="circle"
|
||||
className={classNames(
|
||||
styles.headerNameColor,
|
||||
globalStyles[`color${upperFirst(camelCase(list.color))}`],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{list.name}
|
||||
</div>
|
||||
)}
|
||||
{list.type !== ListTypes.ACTIVE && (
|
||||
<Icon
|
||||
name={ListTypeIcons[list.type]}
|
||||
className={classNames(
|
||||
styles.headerIcon,
|
||||
list.isPersisted && (canEdit || canArchiveCards) && styles.headerIconHidable,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{list.isPersisted &&
|
||||
(canEdit ? (
|
||||
<ActionsPopup listId={id} onNameEdit={handleNameEdit} onCardAdd={handleCardAdd}>
|
||||
<Button className={styles.headerButton}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
</ActionsPopup>
|
||||
) : (
|
||||
canArchiveCards && (
|
||||
<ArchiveCardsPopup listId={id}>
|
||||
<Button className={styles.headerButton}>
|
||||
<Icon fitted name="archive" size="small" />
|
||||
</Button>
|
||||
</ArchiveCardsPopup>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
<div ref={cardsWrapperRef} className={styles.cardsInnerWrapper}>
|
||||
<div className={styles.cardsOuterWrapper}>{cardsNode}</div>
|
||||
</div>
|
||||
{!isAddCardOpened && canAddCard && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!list.isPersisted}
|
||||
className={styles.addCardButton}
|
||||
onClick={handleAddCardClick}
|
||||
>
|
||||
<PlusMathIcon className={styles.addCardButtonIcon} />
|
||||
<span className={styles.addCardButtonText}>
|
||||
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
|
||||
List.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default List;
|
172
client/src/components/lists/List/List.module.scss
Normal file
172
client/src/components/lists/List/List.module.scss
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.addCard {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.addCardButton {
|
||||
background: #dfe3e6;
|
||||
border: none;
|
||||
color: #6b808c;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
fill: #6b808c;
|
||||
flex: 0 0 auto;
|
||||
font-weight: normal;
|
||||
height: 36px;
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #c3cbd0;
|
||||
color: #17394d;
|
||||
fill: #17394d;
|
||||
}
|
||||
}
|
||||
|
||||
.addCardButtonIcon {
|
||||
height: 20px;
|
||||
padding: 0.64px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.addCardButtonText {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-left: 2px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.cardsInnerWrapper {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 290px;
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.cardsOuterWrapper {
|
||||
padding: 0 8px;
|
||||
white-space: normal;
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.header {
|
||||
outline: none;
|
||||
padding: 6px 36px 6px 8px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.headerButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.headerIconHidable {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.headerEditable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
color: #798d99;
|
||||
line-height: 32px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
width: 32px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.13);
|
||||
color: #516b7a;
|
||||
}
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
color: #798d99;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.headerName {
|
||||
color: #17394d;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
max-height: 256px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
padding: 4px 8px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.headerNameColor {
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.innerWrapper {
|
||||
margin-right: 8px;
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.outerWrapper {
|
||||
background: #dfe3e6;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 198px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.outerWrapperTransitioning {
|
||||
transition: max-height 0.2s ease;
|
||||
}
|
||||
|
||||
.outerWrapperWithFavorites {
|
||||
max-height: calc(100vh - 288px);
|
||||
}
|
||||
}
|
86
client/src/components/lists/List/SortStep.jsx
Normal file
86
client/src/components/lists/List/SortStep.jsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { ListSortFieldNames, SortOrders } from '../../../constants/Enums';
|
||||
|
||||
import styles from './ActionsStep.module.scss';
|
||||
|
||||
const Types = {
|
||||
ALPHABETICALLY: 'alphabetically',
|
||||
BY_DUE_DATE: 'byDueDate',
|
||||
OLDEST_FIRST: 'oldestFirst',
|
||||
NEWEST_FIRST: 'newestFirst',
|
||||
};
|
||||
|
||||
const DATA_BY_TYPE = {
|
||||
[Types.ALPHABETICALLY]: {
|
||||
fieldName: ListSortFieldNames.NAME,
|
||||
},
|
||||
[Types.BY_DUE_DATE]: {
|
||||
fieldName: ListSortFieldNames.DUE_DATE,
|
||||
},
|
||||
[Types.OLDEST_FIRST]: {
|
||||
fieldName: ListSortFieldNames.CREATED_AT,
|
||||
},
|
||||
[Types.NEWEST_FIRST]: {
|
||||
fieldName: ListSortFieldNames.CREATED_AT,
|
||||
order: SortOrders.DESC,
|
||||
},
|
||||
};
|
||||
|
||||
const SortStep = React.memo(({ listId, onBack, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSelectTypeClick = useCallback(
|
||||
(_, { value: type }) => {
|
||||
dispatch(entryActions.sortList(listId, DATA_BY_TYPE[type]));
|
||||
onClose();
|
||||
},
|
||||
[listId, onClose, dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('common.sortList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{[Types.ALPHABETICALLY, Types.BY_DUE_DATE, Types.OLDEST_FIRST, Types.NEWEST_FIRST].map(
|
||||
(type) => (
|
||||
<Menu.Item
|
||||
key={type}
|
||||
value={type}
|
||||
className={styles.menuItem}
|
||||
onClick={handleSelectTypeClick}
|
||||
>
|
||||
{t(`common.${type}`)}
|
||||
</Menu.Item>
|
||||
),
|
||||
)}
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SortStep.propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SortStep;
|
16
client/src/components/lists/List/SortStep.module.scss
Normal file
16
client/src/components/lists/List/SortStep.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: -7px -12px -5px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
8
client/src/components/lists/List/index.js
Executable file
8
client/src/components/lists/List/index.js
Executable file
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import List from './List';
|
||||
|
||||
export default List;
|
55
client/src/components/lists/ListsStep/Item.jsx
Executable file
55
client/src/components/lists/ListsStep/Item.jsx
Executable file
|
@ -0,0 +1,55 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import { ListTypes } from '../../../constants/Enums';
|
||||
import { ListTypeIcons } from '../../../constants/Icons';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const Item = React.memo(({ id, isActive, onSelect }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const list = useSelector((state) => selectListById(state, id));
|
||||
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSelectClick = useCallback(() => {
|
||||
if (!isActive && list.isPersisted) {
|
||||
onSelect(id);
|
||||
}
|
||||
}, [id, isActive, onSelect, list.isPersisted]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<span
|
||||
className={classNames(styles.name, isActive && styles.nameActive)}
|
||||
onClick={handleSelectClick}
|
||||
>
|
||||
{list.type !== ListTypes.ACTIVE && (
|
||||
<Icon name={ListTypeIcons[list.type]} className={styles.nameIcon} />
|
||||
)}
|
||||
{list.name || t(`common.${list.type}`)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Item;
|
54
client/src/components/lists/ListsStep/Item.module.scss
Normal file
54
client/src/components/lists/ListsStep/Item.module.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.name {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border-radius: 3px;
|
||||
color: #17394d;
|
||||
cursor: pointer;
|
||||
flex: 1 1 auto;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
padding: 8px 32px 8px 10px;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.nameActive {
|
||||
opacity: 0.45;
|
||||
|
||||
&:before {
|
||||
bottom: 1px;
|
||||
content: "Г";
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
text-align: center;
|
||||
transform: rotate(-135deg);
|
||||
width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.nameIcon {
|
||||
color: rgba(9, 30, 66, 0.24);
|
||||
font-size: 12px;
|
||||
margin: 0 8px 0 0;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
max-width: 280px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
80
client/src/components/lists/ListsStep/ListsStep.jsx
Executable file
80
client/src/components/lists/ListsStep/ListsStep.jsx
Executable file
|
@ -0,0 +1,80 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input, Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import { useField, useNestedRef } from '../../../hooks';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './ListsStep.module.scss';
|
||||
|
||||
const ListsStep = React.memo(({ currentId, onSelect }) => {
|
||||
const lists = useSelector(selectors.selectAvailableListsForCurrentBoard);
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredLists = useMemo(
|
||||
() =>
|
||||
lists.filter((list) =>
|
||||
(list.name ? list.name.toLowerCase() : list.type).includes(cleanSearch),
|
||||
),
|
||||
[lists, cleanSearch],
|
||||
);
|
||||
|
||||
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
useEffect(() => {
|
||||
searchFieldRef.current.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, [searchFieldRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.lists', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Input
|
||||
fluid
|
||||
ref={handleSearchFieldRef}
|
||||
value={search}
|
||||
placeholder={t('common.searchLists')}
|
||||
maxLength={128}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredLists.length > 0 && (
|
||||
<div className={styles.items}>
|
||||
{filteredLists.map((list) => (
|
||||
<Item
|
||||
key={list.id}
|
||||
id={list.id}
|
||||
isActive={list.id === currentId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ListsStep.propTypes = {
|
||||
currentId: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ListsStep;
|
32
client/src/components/lists/ListsStep/ListsStep.module.scss
Normal file
32
client/src/components/lists/ListsStep/ListsStep.module.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.items {
|
||||
margin-top: 8px;
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
border-left: 0.25em transparent solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
8
client/src/components/lists/ListsStep/index.js
Executable file
8
client/src/components/lists/ListsStep/index.js
Executable file
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import ListsStep from './ListsStep';
|
||||
|
||||
export default ListsStep;
|
|
@ -0,0 +1,99 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Icon, Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import { ListTypes } from '../../../constants/Enums';
|
||||
import { ListTypeIcons } from '../../../constants/Icons';
|
||||
|
||||
import styles from './SelectListTypeStep.module.scss';
|
||||
|
||||
const DESCRIPTION_BY_TYPE = {
|
||||
[ListTypes.ACTIVE]: 'common.cardsOnThisListAreReadyToBeWorkedOn',
|
||||
[ListTypes.CLOSED]: 'common.cardsOnThisListAreCompleteAndReadyToBeArchived',
|
||||
};
|
||||
|
||||
const SelectListTypeStep = React.memo(
|
||||
({ defaultValue, title, withButton, buttonContent, onSelect, onBack, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const handleSelectClick = useCallback(
|
||||
(_, { value: nextValue }) => {
|
||||
if (withButton) {
|
||||
setValue(nextValue);
|
||||
} else {
|
||||
if (nextValue !== defaultValue) {
|
||||
onSelect(nextValue);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[defaultValue, withButton, onSelect, onClose],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (value !== defaultValue) {
|
||||
onSelect(value);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [defaultValue, onSelect, onClose, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{[ListTypes.ACTIVE, ListTypes.CLOSED].map((type) => (
|
||||
<Menu.Item
|
||||
key={type}
|
||||
value={type}
|
||||
active={type === value}
|
||||
className={styles.menuItem}
|
||||
onClick={handleSelectClick}
|
||||
>
|
||||
<Icon name={ListTypeIcons[type]} className={styles.menuItemIcon} />
|
||||
<div className={styles.menuItemTitle}>{t(`common.${type}`)}</div>
|
||||
<p className={styles.menuItemDescription}>{t(DESCRIPTION_BY_TYPE[type])}</p>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
{withButton && <Button positive content={t(buttonContent)} />}
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SelectListTypeStep.propTypes = {
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
withButton: PropTypes.bool,
|
||||
buttonContent: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SelectListTypeStep.defaultProps = {
|
||||
title: 'common.selectType',
|
||||
withButton: false,
|
||||
buttonContent: 'action.selectType',
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
export default SelectListTypeStep;
|
|
@ -0,0 +1,28 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: 0 auto 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menuItem:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.menuItemDescription {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menuItemIcon {
|
||||
float: left;
|
||||
margin: 0 0.35714286em 0 0;
|
||||
}
|
||||
|
||||
.menuItemTitle {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
8
client/src/components/lists/SelectListTypeStep/index.js
Normal file
8
client/src/components/lists/SelectListTypeStep/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import SelectListTypeStep from './SelectListTypeStep';
|
||||
|
||||
export default SelectListTypeStep;
|
Loading…
Add table
Add a link
Reference in a new issue