/*! * 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, useContext, 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, useToggle, useTransitioning } from '../../../lib/hooks'; import { Tooltip } from '../../../lib/custom-ui'; import { usePopup } from '../../../lib/popup'; import selectors from '../../../selectors'; import entryActions from '../../../entry-actions'; import { BoardShortcutsContext } from '../../../contexts'; 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 AddCardPositions = { TOP: 'top', BOTTOM: 'bottom', }; const INDEX_BY_ADD_CARD_POSITION = { [AddCardPositions.TOP]: 0, }; const List = React.memo(({ id, index }) => { const selectListById = useMemo(() => selectors.makeSelectListById(), []); const selectFilteredCardIdsByListId = useMemo( () => selectors.makeSelectFilteredCardIdsByListId(), [], ); const clipboard = useSelector(selectors.selectClipboard); const isFavoritesActive = useSelector(selectors.selectIsFavoritesActiveForCurrentUser); const list = useSelector((state) => selectListById(state, id)); const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id)); const { canEdit, canArchiveCards, canAddCard, canPasteCard, 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, canPasteCard: isEditor, canDropCard: isEditor, }; }, shallowEqual, ); const dispatch = useDispatch(); const [t] = useTranslation(); const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [addCardPosition, setAddCardPosition] = useState(null); const [scrollBottomState, scrollBottom] = useToggle(); const [handleListMouseEnter, handleListMouseLeave] = useContext(BoardShortcutsContext); const wrapperRef = useRef(null); const cardsWrapperRef = useRef(null); const handleCardCreate = useCallback( (data, autoOpen) => { dispatch( entryActions.createCard(id, data, INDEX_BY_ADD_CARD_POSITION[addCardPosition], autoOpen), ); }, [id, dispatch, addCardPosition], ); const handlePasteCardClick = useCallback(() => { dispatch(entryActions.pasteCard(id)); scrollBottom(); }, [id, dispatch, scrollBottom]); const handleMouseEnter = useCallback(() => { handleListMouseEnter(id, () => { scrollBottom(); }); }, [id, scrollBottom, handleListMouseEnter]); const handleHeaderClick = useCallback(() => { if (list.isPersisted && canEdit) { setIsEditNameOpened(true); } }, [list.isPersisted, canEdit]); const handleAddCardClick = useCallback(() => { setAddCardPosition(AddCardPositions.BOTTOM); }, []); const handleAddCardClose = useCallback(() => { setAddCardPosition(null); }, []); const handleCardAdd = useCallback(() => { setAddCardPosition(AddCardPositions.TOP); }, []); const handleNameEdit = useCallback(() => { setIsEditNameOpened(true); }, []); const handleEditNameClose = useCallback(() => { setIsEditNameOpened(false); }, []); const handleWrapperTransitionEnd = useTransitioning( wrapperRef, styles.outerWrapperTransitioning, [isFavoritesActive], ); useDidUpdate(() => { if (!addCardPosition) { return; } cardsWrapperRef.current.scrollTop = addCardPosition === AddCardPositions.TOP ? 0 : cardsWrapperRef.current.scrollHeight; }, [cardIds, addCardPosition]); useDidUpdate(() => { cardsWrapperRef.current.scrollTop = cardsWrapperRef.current.scrollHeight; }, [scrollBottomState]); const ActionsPopup = usePopup(ActionsStep); const ArchiveCardsPopup = usePopup(ArchiveCardsStep); const addCardNode = canAddCard && ( ); const cardsNode = ( {({ innerRef, droppableProps, placeholder }) => ( // eslint-disable-next-line react/jsx-props-no-spreading
{addCardPosition === AddCardPositions.TOP && addCardNode} {cardIds.map((cardId, cardIndex) => ( ))} {placeholder} {addCardPosition === AddCardPositions.BOTTOM && addCardNode}
)}
); return ( {({ innerRef, draggableProps, dragHandleProps }) => (
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
{isEditNameOpened ? ( ) : (
{list.color && ( )} {list.name}
)} {list.type !== ListTypes.ACTIVE && ( )} {list.isPersisted && (canEdit ? ( ) : ( canArchiveCards && ( ) ))}
{cardsNode}
{!addCardPosition && canAddCard && (
{clipboard && canPasteCard && ( )}
)}
)}
); }); List.propTypes = { id: PropTypes.string.isRequired, index: PropTypes.number.isRequired, }; export default List;