feat(client): add board component
This commit is contained in:
parent
05e2cf3b8d
commit
8da98d8c2b
@ -28,6 +28,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^21.6.0",
|
"i18next": "^21.6.0",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.0.12",
|
||||||
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-i18next": "^11.15.1",
|
"react-i18next": "^11.15.1",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
|
295
packages/client/src/board/Board.tsx
Normal file
295
packages/client/src/board/Board.tsx
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import React, { forwardRef, useState } from 'react';
|
||||||
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
import Column from './Column';
|
||||||
|
import ColumnAdder from './ColumnAdder';
|
||||||
|
import DefaultCard from './DefaultCard';
|
||||||
|
import DefaultColumnHeader from './DefaultColumnHeader';
|
||||||
|
import * as helpers from './helpers';
|
||||||
|
import { addCard, addColumn, changeColumn, moveCard, moveColumn, removeCard, removeColumn } from './helpers';
|
||||||
|
import {
|
||||||
|
getCard,
|
||||||
|
getCoordinates,
|
||||||
|
isAColumnMove,
|
||||||
|
isMovingACardToAnotherPosition,
|
||||||
|
isMovingAColumnToAnotherPosition
|
||||||
|
} from './services';
|
||||||
|
import { partialRight, when } from './utils';
|
||||||
|
import withDroppable from './withDroppable';
|
||||||
|
|
||||||
|
const Columns = forwardRef((props, ref: any) => <div ref={ref} style={{ whiteSpace: 'nowrap' }} {...props} />);
|
||||||
|
|
||||||
|
const DroppableBoard = withDroppable(Columns);
|
||||||
|
|
||||||
|
const Board: any = (props) => {
|
||||||
|
return props.initialBoard ? <UncontrolledBoard {...props} /> : <ControlledBoard {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(helpers).forEach((key) => {
|
||||||
|
Board[key] = helpers[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
function UncontrolledBoard({
|
||||||
|
initialBoard,
|
||||||
|
onCardDragEnd,
|
||||||
|
onColumnDragEnd,
|
||||||
|
allowAddColumn,
|
||||||
|
cardAdderPosition,
|
||||||
|
renderCardAdder,
|
||||||
|
renderColumnAdder,
|
||||||
|
onNewColumnConfirm,
|
||||||
|
onColumnRemove,
|
||||||
|
renderColumnHeader,
|
||||||
|
allowRemoveColumn,
|
||||||
|
allowRenameColumn,
|
||||||
|
onColumnRename,
|
||||||
|
onCardNew,
|
||||||
|
renderCard,
|
||||||
|
allowRemoveCard,
|
||||||
|
onCardRemove,
|
||||||
|
onColumnNew,
|
||||||
|
disableCardDrag,
|
||||||
|
disableColumnDrag,
|
||||||
|
allowAddCard,
|
||||||
|
onNewCardConfirm,
|
||||||
|
}) {
|
||||||
|
const [board, setBoard] = useState(initialBoard);
|
||||||
|
const handleOnCardDragEnd = partialRight(handleOnDragEnd, { moveCallback: moveCard, notifyCallback: onCardDragEnd });
|
||||||
|
const handleOnColumnDragEnd = partialRight(handleOnDragEnd, {
|
||||||
|
moveCallback: moveColumn,
|
||||||
|
notifyCallback: onColumnDragEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleOnDragEnd({ source, destination, subject }, { moveCallback, notifyCallback }) {
|
||||||
|
const reorderedBoard = moveCallback(board, source, destination);
|
||||||
|
when(notifyCallback)((callback) => callback(reorderedBoard, subject, source, destination));
|
||||||
|
setBoard(reorderedBoard);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleColumnAdd(newColumn) {
|
||||||
|
const column = renderColumnAdder ? newColumn : await onNewColumnConfirm(newColumn);
|
||||||
|
const boardWithNewColumn = addColumn(board, column);
|
||||||
|
onColumnNew(boardWithNewColumn, column);
|
||||||
|
setBoard(boardWithNewColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColumnRemove(column) {
|
||||||
|
const filteredBoard = removeColumn(board, column);
|
||||||
|
onColumnRemove(filteredBoard, column);
|
||||||
|
setBoard(filteredBoard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColumnRename(column, title) {
|
||||||
|
const boardWithRenamedColumn = changeColumn(board, column, { title });
|
||||||
|
onColumnRename(boardWithRenamedColumn, { ...column, title });
|
||||||
|
setBoard(boardWithRenamedColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardAdd(column, card, options = {}) {
|
||||||
|
const boardWithNewCard = addCard(board, column, card, options);
|
||||||
|
|
||||||
|
onCardNew(
|
||||||
|
boardWithNewCard,
|
||||||
|
boardWithNewCard.columns.find(({ id }) => id === column.id),
|
||||||
|
card,
|
||||||
|
);
|
||||||
|
setBoard(boardWithNewCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDraftCardAdd(column, card, options = {}) {
|
||||||
|
const newCard = await onNewCardConfirm(card);
|
||||||
|
handleCardAdd(column, newCard, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardRemove(column, card) {
|
||||||
|
const boardWithoutCard = removeCard(board, column, card);
|
||||||
|
onCardRemove(
|
||||||
|
boardWithoutCard,
|
||||||
|
boardWithoutCard.columns.find(({ id }) => id === column.id),
|
||||||
|
card,
|
||||||
|
);
|
||||||
|
setBoard(boardWithoutCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoardContainer
|
||||||
|
cardAdderPosition={cardAdderPosition}
|
||||||
|
onCardDragEnd={handleOnCardDragEnd}
|
||||||
|
onColumnDragEnd={handleOnColumnDragEnd}
|
||||||
|
renderCardAdder={renderCardAdder}
|
||||||
|
renderColumnAdder={() => {
|
||||||
|
if (!allowAddColumn) return null;
|
||||||
|
if (renderColumnAdder) return renderColumnAdder({ addColumn: handleColumnAdd });
|
||||||
|
if (!onNewColumnConfirm) return null;
|
||||||
|
return <ColumnAdder onConfirm={(title) => handleColumnAdd({ title, cards: [] })} />;
|
||||||
|
}}
|
||||||
|
{...(renderColumnHeader && {
|
||||||
|
renderColumnHeader: (column) =>
|
||||||
|
renderColumnHeader(column, {
|
||||||
|
removeColumn: handleColumnRemove.bind(null, column),
|
||||||
|
renameColumn: handleColumnRename.bind(null, column),
|
||||||
|
addCard: handleCardAdd.bind(null, column),
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
renderCard={(column, card, dragging) => {
|
||||||
|
if (renderCard) return renderCard(card, { removeCard: handleCardRemove.bind(null, column, card), dragging });
|
||||||
|
return (
|
||||||
|
<DefaultCard
|
||||||
|
dragging={dragging}
|
||||||
|
allowRemoveCard={allowRemoveCard}
|
||||||
|
onCardRemove={(card) => handleCardRemove(column, card)}
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
</DefaultCard>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
allowRemoveColumn={allowRemoveColumn}
|
||||||
|
onColumnRemove={handleColumnRemove}
|
||||||
|
allowRenameColumn={allowRenameColumn}
|
||||||
|
onColumnRename={handleColumnRename}
|
||||||
|
disableColumnDrag={disableColumnDrag}
|
||||||
|
disableCardDrag={disableCardDrag}
|
||||||
|
onCardNew={(column, card) => handleDraftCardAdd(column, card, allowAddCard)}
|
||||||
|
allowAddCard={allowAddCard && onNewCardConfirm}
|
||||||
|
>
|
||||||
|
{board}
|
||||||
|
</BoardContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlledBoard({
|
||||||
|
children: board,
|
||||||
|
onCardDragEnd,
|
||||||
|
onColumnDragEnd,
|
||||||
|
allowAddColumn,
|
||||||
|
renderCardAdder,
|
||||||
|
renderColumnAdder,
|
||||||
|
onNewColumnConfirm,
|
||||||
|
onColumnRemove,
|
||||||
|
renderColumnHeader,
|
||||||
|
allowRemoveColumn,
|
||||||
|
allowRenameColumn,
|
||||||
|
onColumnRename,
|
||||||
|
renderCard,
|
||||||
|
allowAddCard,
|
||||||
|
allowRemoveCard,
|
||||||
|
onCardRemove,
|
||||||
|
disableCardDrag,
|
||||||
|
disableColumnDrag,
|
||||||
|
cardAdderPosition,
|
||||||
|
}) {
|
||||||
|
const handleOnCardDragEnd = partialRight(handleOnDragEnd, { notifyCallback: onCardDragEnd });
|
||||||
|
const handleOnColumnDragEnd = partialRight(handleOnDragEnd, { notifyCallback: onColumnDragEnd });
|
||||||
|
|
||||||
|
function handleOnDragEnd({ source, destination, subject }, { notifyCallback }) {
|
||||||
|
when(notifyCallback)((callback) => callback(subject, source, destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoardContainer
|
||||||
|
cardAdderPosition={cardAdderPosition}
|
||||||
|
onCardDragEnd={handleOnCardDragEnd}
|
||||||
|
onColumnDragEnd={handleOnColumnDragEnd}
|
||||||
|
renderCardAdder={renderCardAdder}
|
||||||
|
renderColumnAdder={() => {
|
||||||
|
if (!allowAddColumn) return null;
|
||||||
|
if (renderColumnAdder) return renderColumnAdder();
|
||||||
|
if (!onNewColumnConfirm) return null;
|
||||||
|
return <ColumnAdder onConfirm={(title) => onNewColumnConfirm({ title, cards: [] })} />;
|
||||||
|
}}
|
||||||
|
{...(renderColumnHeader && { renderColumnHeader: renderColumnHeader })}
|
||||||
|
renderCard={(column, card, dragging) => {
|
||||||
|
if (renderCard) return renderCard(card, { column, dragging });
|
||||||
|
return (
|
||||||
|
<DefaultCard
|
||||||
|
dragging={dragging}
|
||||||
|
allowRemoveCard={allowRemoveCard}
|
||||||
|
onCardRemove={(card) => onCardRemove(card, column)}
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
</DefaultCard>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
allowAddCard={allowAddCard}
|
||||||
|
allowRemoveColumn={allowRemoveColumn}
|
||||||
|
onColumnRemove={onColumnRemove}
|
||||||
|
allowRenameColumn={allowRenameColumn}
|
||||||
|
onColumnRename={onColumnRename}
|
||||||
|
disableColumnDrag={disableColumnDrag}
|
||||||
|
disableCardDrag={disableCardDrag}
|
||||||
|
>
|
||||||
|
{board}
|
||||||
|
</BoardContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoardContainer(props) {
|
||||||
|
const {
|
||||||
|
children: board,
|
||||||
|
renderCard,
|
||||||
|
disableColumnDrag,
|
||||||
|
disableCardDrag,
|
||||||
|
cardAdderPosition,
|
||||||
|
renderCardAdder,
|
||||||
|
renderColumnHeader,
|
||||||
|
renderColumnAdder,
|
||||||
|
allowRemoveColumn,
|
||||||
|
onColumnRemove,
|
||||||
|
allowRenameColumn,
|
||||||
|
onColumnRename,
|
||||||
|
onColumnDragEnd,
|
||||||
|
onCardDragEnd,
|
||||||
|
onCardNew,
|
||||||
|
allowAddCard,
|
||||||
|
} = props;
|
||||||
|
function handleOnDragEnd(event) {
|
||||||
|
const coordinates = getCoordinates(event, board);
|
||||||
|
if (!coordinates.source) return;
|
||||||
|
|
||||||
|
isAColumnMove(event.type)
|
||||||
|
? isMovingAColumnToAnotherPosition(coordinates) &&
|
||||||
|
onColumnDragEnd({ ...coordinates, subject: board.columns[coordinates.source.fromPosition] })
|
||||||
|
: isMovingACardToAnotherPosition(coordinates) &&
|
||||||
|
onCardDragEnd({ ...coordinates, subject: getCard(board, coordinates.source) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
|
<div style={{ overflowY: 'hidden', display: 'flex', alignItems: 'flex-start' }} className="react-kanban-board">
|
||||||
|
<DroppableBoard droppableId="board-droppable" direction="horizontal" type="BOARD">
|
||||||
|
{board.columns.map((column, index) => (
|
||||||
|
<Column
|
||||||
|
key={column.id}
|
||||||
|
index={index}
|
||||||
|
renderCard={renderCard}
|
||||||
|
renderCardAdder={renderCardAdder}
|
||||||
|
renderColumnHeader={(column) =>
|
||||||
|
renderColumnHeader ? (
|
||||||
|
renderColumnHeader(column)
|
||||||
|
) : (
|
||||||
|
<DefaultColumnHeader
|
||||||
|
allowRemoveColumn={allowRemoveColumn}
|
||||||
|
onColumnRemove={onColumnRemove}
|
||||||
|
allowRenameColumn={allowRenameColumn}
|
||||||
|
onColumnRename={onColumnRename}
|
||||||
|
>
|
||||||
|
{column}
|
||||||
|
</DefaultColumnHeader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cardAdderPosition={cardAdderPosition}
|
||||||
|
disableColumnDrag={disableColumnDrag}
|
||||||
|
disableCardDrag={disableCardDrag}
|
||||||
|
onCardNew={onCardNew}
|
||||||
|
allowAddCard={allowAddCard}
|
||||||
|
>
|
||||||
|
{column}
|
||||||
|
</Column>
|
||||||
|
))}
|
||||||
|
</DroppableBoard>
|
||||||
|
{renderColumnAdder()}
|
||||||
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Board;
|
23
packages/client/src/board/Card.tsx
Normal file
23
packages/client/src/board/Card.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
function Card({ children, index, renderCard, disableCardDrag }) {
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={String(children.id)} index={index} isDragDisabled={disableCardDrag}>
|
||||||
|
{(provided, { isDragging }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
data-testid={`card-${children.id}`}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'inline-block', whiteSpace: 'normal' }}>{renderCard(isDragging)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
24
packages/client/src/board/CardAdder.tsx
Normal file
24
packages/client/src/board/CardAdder.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import CardForm from './CardForm';
|
||||||
|
|
||||||
|
function CardAdder({ column, onConfirm }) {
|
||||||
|
function confirmCard(card) {
|
||||||
|
onConfirm(column, card)
|
||||||
|
setAddingCard(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [addingCard, setAddingCard] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{addingCard ? (
|
||||||
|
<CardForm onConfirm={confirmCard} onCancel={() => setAddingCard(false)} />
|
||||||
|
) : (
|
||||||
|
<button className='react-kanban-card-adder-button' onClick={() => setAddingCard(!addingCard)}>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default CardAdder;
|
44
packages/client/src/board/CardForm.tsx
Normal file
44
packages/client/src/board/CardForm.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { when } from './utils';
|
||||||
|
|
||||||
|
function CardForm({ onConfirm, onCancel }) {
|
||||||
|
const inputCardTitle = useRef<any>();
|
||||||
|
const inputCardDescription = useRef<any>();
|
||||||
|
|
||||||
|
function addCard(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
when(inputCardTitle.current.value)((value) => {
|
||||||
|
onConfirm({ title: value, description: inputCardDescription.current.value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="react-kanban-card-adder-form">
|
||||||
|
<form onSubmit={addCard}>
|
||||||
|
<input
|
||||||
|
className="react-kanban-card-adder-form__title"
|
||||||
|
name="title"
|
||||||
|
autoFocus
|
||||||
|
defaultValue="Title"
|
||||||
|
ref={inputCardTitle}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="react-kanban-card-adder-form__description"
|
||||||
|
name="description"
|
||||||
|
defaultValue="Description"
|
||||||
|
ref={inputCardDescription}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '5px' }}>
|
||||||
|
<button className="react-kanban-card-adder-form__button" type="submit">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
<button className="react-kanban-card-adder-form__button" type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardForm;
|
73
packages/client/src/board/Column.tsx
Normal file
73
packages/client/src/board/Column.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
import Card from './Card';
|
||||||
|
import CardAdder from './CardAdder';
|
||||||
|
import { pickPropOut } from './utils';
|
||||||
|
import withDroppable from './withDroppable';
|
||||||
|
|
||||||
|
const ColumnEmptyPlaceholder = forwardRef((props, ref: any) => (
|
||||||
|
<div ref={ref} style={{ minHeight: 'inherit', height: 'inherit' }} {...props} />
|
||||||
|
));
|
||||||
|
|
||||||
|
const DroppableColumn = withDroppable(ColumnEmptyPlaceholder);
|
||||||
|
|
||||||
|
function Column({
|
||||||
|
children,
|
||||||
|
index: columnIndex,
|
||||||
|
renderCard,
|
||||||
|
renderCardAdder = ({ column, onConfirm }) => <CardAdder column={column} onConfirm={onConfirm} />,
|
||||||
|
renderColumnHeader,
|
||||||
|
disableColumnDrag,
|
||||||
|
disableCardDrag,
|
||||||
|
onCardNew,
|
||||||
|
allowAddCard,
|
||||||
|
cardAdderPosition = 'top',
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={`column-draggable-${children.id}`} index={columnIndex} isDragDisabled={disableColumnDrag}>
|
||||||
|
{(columnProvided) => {
|
||||||
|
const draggablePropsWithoutStyle = pickPropOut(columnProvided.draggableProps, 'style');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={columnProvided.innerRef}
|
||||||
|
{...draggablePropsWithoutStyle}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
minHeight: '28px',
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
...columnProvided.draggableProps.style,
|
||||||
|
}}
|
||||||
|
className="react-kanban-column"
|
||||||
|
data-testid={`column-${children.id}`}
|
||||||
|
>
|
||||||
|
<div {...columnProvided.dragHandleProps}>{renderColumnHeader(children)}</div>
|
||||||
|
{cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })}
|
||||||
|
<DroppableColumn droppableId={String(children.id)}>
|
||||||
|
{children.cards.length ? (
|
||||||
|
children.cards.map((card, index) => (
|
||||||
|
<Card
|
||||||
|
key={card.id}
|
||||||
|
index={index}
|
||||||
|
renderCard={(dragging) => renderCard(children, card, dragging)}
|
||||||
|
disableCardDrag={disableCardDrag}
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="react-kanban-card-skeleton" />
|
||||||
|
)}
|
||||||
|
</DroppableColumn>
|
||||||
|
{cardAdderPosition === 'bottom' &&
|
||||||
|
allowAddCard &&
|
||||||
|
renderCardAdder({ column: children, onConfirm: onCardNew })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Column;
|
26
packages/client/src/board/ColumnAdder.tsx
Normal file
26
packages/client/src/board/ColumnAdder.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import ColumnForm from './ColumnForm';
|
||||||
|
|
||||||
|
function ColumnAdder({ onConfirm }) {
|
||||||
|
const [isAddingColumn, setAddingColumn] = useState(false);
|
||||||
|
|
||||||
|
function confirmColumn(title) {
|
||||||
|
onConfirm(title);
|
||||||
|
setAddingColumn(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAddingColumn ? (
|
||||||
|
<ColumnForm onConfirm={confirmColumn} onCancel={() => setAddingColumn(false)} />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: '230px' }}
|
||||||
|
className="react-kanban-column-adder-button"
|
||||||
|
onClick={() => setAddingColumn(true)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
➕
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnAdder;
|
27
packages/client/src/board/ColumnForm.tsx
Normal file
27
packages/client/src/board/ColumnForm.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { createRef } from 'react';
|
||||||
|
import { when } from './utils';
|
||||||
|
|
||||||
|
function ColumnForm({ onConfirm, onCancel }) {
|
||||||
|
// FIXME use hook
|
||||||
|
const inputColumnTitle = createRef<any>()
|
||||||
|
|
||||||
|
function addColumn(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
when(inputColumnTitle.current.value)(onConfirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='react-kanban-column' style={{ minWidth: '230px' }}>
|
||||||
|
<form style={{ display: 'flex', justifyContent: 'space-between' }} onSubmit={addColumn}>
|
||||||
|
<input type='text' ref={inputColumnTitle} autoFocus />
|
||||||
|
<button type='submit'>Add</button>
|
||||||
|
<button type='button' onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnForm
|
21
packages/client/src/board/DefaultCard.tsx
Normal file
21
packages/client/src/board/DefaultCard.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function DefaultCard({ children: card, dragging, allowRemoveCard, onCardRemove }) {
|
||||||
|
return (
|
||||||
|
<div className={`react-kanban-card ${dragging ? 'react-kanban-card--dragging' : ''}`}>
|
||||||
|
<span>
|
||||||
|
<div className="react-kanban-card__title">
|
||||||
|
<span>{card.title}</span>
|
||||||
|
{allowRemoveCard && (
|
||||||
|
<span style={{ cursor: 'pointer' }} onClick={() => onCardRemove(card)}>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<div className="react-kanban-card__description">{card.description}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DefaultCard;
|
72
packages/client/src/board/DefaultColumnHeader.tsx
Normal file
72
packages/client/src/board/DefaultColumnHeader.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
function ColumnTitle({ allowRenameColumn, onClick, children: title }) {
|
||||||
|
return allowRenameColumn ? (
|
||||||
|
<span style={{ cursor: 'pointer' }} onClick={onClick}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{title}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRenameMode(state) {
|
||||||
|
const [renameMode, setRenameMode] = useState(state)
|
||||||
|
|
||||||
|
function toggleRenameMode() {
|
||||||
|
setRenameMode(!renameMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [renameMode, toggleRenameMode]
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultColumnHeader({ children: column, allowRemoveColumn, onColumnRemove, allowRenameColumn, onColumnRename }) {
|
||||||
|
const [renameMode, toggleRenameMode] = useRenameMode(false)
|
||||||
|
const [titleInput, setTitleInput] = useState('')
|
||||||
|
|
||||||
|
function handleRenameColumn(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
onColumnRename(column, titleInput)
|
||||||
|
toggleRenameMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRenameMode() {
|
||||||
|
setTitleInput(column.title)
|
||||||
|
toggleRenameMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='react-kanban-column-header'>
|
||||||
|
{renameMode ? (
|
||||||
|
<form onSubmit={handleRenameColumn}>
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={titleInput}
|
||||||
|
onChange={({ target: { value } }) => setTitleInput(value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button className='react-kanban-column-header__button' type='submit'>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button className='react-kanban-column-header__button' type='button' onClick={handleRenameMode}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ColumnTitle allowRenameColumn={allowRenameColumn} onClick={handleRenameMode}>
|
||||||
|
{column.title}
|
||||||
|
</ColumnTitle>
|
||||||
|
{allowRemoveColumn && <span onClick={() => onColumnRemove(column)}>×</span>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DefaultColumnHeader;
|
128
packages/client/src/board/Kanban.tsx
Normal file
128
packages/client/src/board/Kanban.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import '@asseinfo/react-kanban/dist/styles.css';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import Board from './Board';
|
||||||
|
import { addCard, moveCard, removeCard } from './helpers';
|
||||||
|
|
||||||
|
const board = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Backlog',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Card title 1',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Card title 2',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Card title 3',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Doing',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'Card title 9',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Q&A',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'Card title 10',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Card title 11',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Production',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Card title 12',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
title: 'Card title 13',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function UncontrolledBoard() {
|
||||||
|
const [controlledBoard, setBoard] = useState(board);
|
||||||
|
function handleCardMove(card, source, destination) {
|
||||||
|
const updatedBoard = moveCard(controlledBoard, source, destination);
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}
|
||||||
|
function handleCardRemove(card, column) {
|
||||||
|
const updatedBoard = removeCard(controlledBoard, column, card);
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Board
|
||||||
|
disableColumnDrag
|
||||||
|
allowRemoveCard
|
||||||
|
allowAddCard={{ on: 'bottom' }}
|
||||||
|
onLaneRemove={console.log}
|
||||||
|
onCardRemove={handleCardRemove}
|
||||||
|
onCardDragEnd={handleCardMove}
|
||||||
|
onLaneRename={console.log}
|
||||||
|
cardAdderPosition={'bottom'}
|
||||||
|
onNewCardConfirm={(draftCard) => draftCard}
|
||||||
|
onCardNew={console.log}
|
||||||
|
renderCardAdder={({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedBoard = addCard(controlledBoard, column, {
|
||||||
|
id: new Date().getTime(),
|
||||||
|
title: 'Card title ' + new Date().getTime(),
|
||||||
|
description: 'Card content',
|
||||||
|
});
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加卡片
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{controlledBoard}
|
||||||
|
</Board>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Kanban() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UncontrolledBoard />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
119
packages/client/src/board/demos/demo2.tsx
Normal file
119
packages/client/src/board/demos/demo2.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { Board } from '@nocobase/client';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import '../style.less';
|
||||||
|
|
||||||
|
const board = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Backlog',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Card title 1',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Card title 2',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Card title 3',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Doing',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'Card title 9',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Q&A',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'Card title 10',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Card title 11',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Production',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Card title 12',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
title: 'Card title 13',
|
||||||
|
description: 'Card content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [controlledBoard, setBoard] = useState(board);
|
||||||
|
function handleCardMove(card, source, destination) {
|
||||||
|
const updatedBoard = Board.moveCard(controlledBoard, source, destination);
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}
|
||||||
|
function handleCardRemove(card, column) {
|
||||||
|
const updatedBoard = Board.removeCard(controlledBoard, column, card);
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Board
|
||||||
|
disableColumnDrag
|
||||||
|
allowRemoveCard
|
||||||
|
allowAddCard={{ on: 'bottom' }}
|
||||||
|
onLaneRemove={console.log}
|
||||||
|
onCardRemove={handleCardRemove}
|
||||||
|
onCardDragEnd={handleCardMove}
|
||||||
|
onLaneRename={console.log}
|
||||||
|
cardAdderPosition={'bottom'}
|
||||||
|
onNewCardConfirm={(draftCard) => draftCard}
|
||||||
|
onCardNew={console.log}
|
||||||
|
renderCardAdder={({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedBoard = Board.addCard(controlledBoard, column, {
|
||||||
|
id: new Date().getTime(),
|
||||||
|
title: 'Card title ' + new Date().getTime(),
|
||||||
|
description: 'Card content',
|
||||||
|
});
|
||||||
|
setBoard(updatedBoard);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加卡片
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{controlledBoard}
|
||||||
|
</Board>
|
||||||
|
);
|
||||||
|
}
|
91
packages/client/src/board/helpers.ts
Executable file
91
packages/client/src/board/helpers.ts
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
addInArrayAtPosition,
|
||||||
|
changeElementOfPositionInArray,
|
||||||
|
removeFromArrayAtPosition,
|
||||||
|
replaceElementOfArray
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function reorderCardsOnColumn(column, reorderCards) {
|
||||||
|
return { ...column, cards: reorderCards(column.cards) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveColumn(board, { fromPosition }, { toPosition }) {
|
||||||
|
return { ...board, columns: changeElementOfPositionInArray(board.columns, fromPosition, toPosition) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCard(board, { fromPosition, fromColumnId }, { toPosition, toColumnId }) {
|
||||||
|
const sourceColumn = board.columns.find((column) => column.id === fromColumnId);
|
||||||
|
const destinationColumn = board.columns.find((column) => column.id === toColumnId);
|
||||||
|
|
||||||
|
const reorderColumnsOnBoard = (reorderColumnsMapper) => ({
|
||||||
|
...board,
|
||||||
|
columns: board.columns.map(reorderColumnsMapper),
|
||||||
|
});
|
||||||
|
const reorderCardsOnSourceColumn = reorderCardsOnColumn.bind(null, sourceColumn);
|
||||||
|
const reorderCardsOnDestinationColumn = reorderCardsOnColumn.bind(null, destinationColumn);
|
||||||
|
|
||||||
|
if (sourceColumn.id === destinationColumn.id) {
|
||||||
|
const reorderedCardsOnColumn = reorderCardsOnSourceColumn((cards) => {
|
||||||
|
return changeElementOfPositionInArray(cards, fromPosition, toPosition);
|
||||||
|
});
|
||||||
|
return reorderColumnsOnBoard((column) => (column.id === sourceColumn.id ? reorderedCardsOnColumn : column));
|
||||||
|
} else {
|
||||||
|
const reorderedCardsOnSourceColumn = reorderCardsOnSourceColumn((cards) => {
|
||||||
|
return removeFromArrayAtPosition(cards, fromPosition);
|
||||||
|
});
|
||||||
|
const reorderedCardsOnDestinationColumn = reorderCardsOnDestinationColumn((cards) => {
|
||||||
|
return addInArrayAtPosition(cards, sourceColumn.cards[fromPosition], toPosition);
|
||||||
|
});
|
||||||
|
return reorderColumnsOnBoard((column) => {
|
||||||
|
if (column.id === sourceColumn.id) return reorderedCardsOnSourceColumn;
|
||||||
|
if (column.id === destinationColumn.id) return reorderedCardsOnDestinationColumn;
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addColumn(board, column) {
|
||||||
|
return { ...board, columns: addInArrayAtPosition(board.columns, column, board.columns.length) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeColumn(board, column) {
|
||||||
|
return { ...board, columns: board.columns.filter(({ id }) => id !== column.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeColumn(board, column, newColumn) {
|
||||||
|
const changedColumns = replaceElementOfArray(board.columns)({
|
||||||
|
when: ({ id }) => id === column.id,
|
||||||
|
for: (value) => ({ ...value, ...newColumn }),
|
||||||
|
});
|
||||||
|
return { ...board, columns: changedColumns };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCard(board, inColumn, card, { on }: any = {}) {
|
||||||
|
const columnToAdd = board.columns.find(({ id }) => id === inColumn.id);
|
||||||
|
const cards = addInArrayAtPosition(columnToAdd.cards, card, on === 'top' ? 0 : columnToAdd.cards.length);
|
||||||
|
const columns = replaceElementOfArray(board.columns)({
|
||||||
|
when: ({ id }) => inColumn.id === id,
|
||||||
|
for: (value) => ({ ...value, cards }),
|
||||||
|
});
|
||||||
|
return { ...board, columns };
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCard(board, fromColumn, card) {
|
||||||
|
const columnToRemove = board.columns.find(({ id }) => id === fromColumn.id);
|
||||||
|
const filteredCards = columnToRemove.cards.filter(({ id }) => card.id !== id);
|
||||||
|
const columnWithoutCard = { ...columnToRemove, cards: filteredCards };
|
||||||
|
const filteredColumns = board.columns.map((column) => (fromColumn.id === column.id ? columnWithoutCard : column));
|
||||||
|
return { ...board, columns: filteredColumns };
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeCard(board, cardId, newCard) {
|
||||||
|
const changedCards = (cards) =>
|
||||||
|
replaceElementOfArray(cards)({
|
||||||
|
when: ({ id }) => id === cardId,
|
||||||
|
for: (card) => ({ ...card, ...newCard }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...board, columns: board.columns.map((column) => ({ ...column, cards: changedCards(column.cards) })) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { moveColumn, moveCard, addColumn, removeColumn, changeColumn, addCard, removeCard, changeCard };
|
14
packages/client/src/board/index.md
Normal file
14
packages/client/src/board/index.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /client
|
||||||
|
---
|
||||||
|
|
||||||
|
# Board
|
||||||
|
|
||||||
|
基于 [@asseinfo/react-kanban](https://github.com/asseinfo/react-kanban) 做了一些改进和 BUG 修复,简化了目录结构,并改为了 TypeScript 版本。
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx" />
|
2
packages/client/src/board/index.ts
Normal file
2
packages/client/src/board/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as Board } from './Board';
|
||||||
|
|
41
packages/client/src/board/services.ts
Normal file
41
packages/client/src/board/services.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
function getCoordinates(event, board) {
|
||||||
|
if (event.destination === null) return {}
|
||||||
|
|
||||||
|
const columnSource = { fromPosition: event.source.index }
|
||||||
|
const columnDestination = { toPosition: event.destination.index }
|
||||||
|
|
||||||
|
if (isAColumnMove(event.type)) {
|
||||||
|
return { source: columnSource, destination: columnDestination }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: { ...columnSource, fromColumnId: getColumn(board, event.source.droppableId).id },
|
||||||
|
destination: { ...columnDestination, toColumnId: getColumn(board, event.destination.droppableId).id },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAColumnMove(type) {
|
||||||
|
return type === 'BOARD'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCard(board, sourceCoordinate) {
|
||||||
|
const column = board.columns.find((column) => column.id === sourceCoordinate.fromColumnId)
|
||||||
|
return column.cards[sourceCoordinate.fromPosition]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumn(board, droppableId) {
|
||||||
|
return board.columns.find(({ id }) => String(id) === droppableId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMovingAColumnToAnotherPosition(coordinates) {
|
||||||
|
return coordinates.source.fromPosition !== coordinates.destination.toPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMovingACardToAnotherPosition(coordinates) {
|
||||||
|
return !(
|
||||||
|
coordinates.source.fromPosition === coordinates.destination.toPosition &&
|
||||||
|
coordinates.source.fromColumnId === coordinates.destination.toColumnId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getCard, getCoordinates, isAColumnMove, isMovingAColumnToAnotherPosition, isMovingACardToAnotherPosition }
|
147
packages/client/src/board/style.less
Normal file
147
packages/client/src/board/style.less
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
.react-kanban-board {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 250px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-skeleton {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card--dragging {
|
||||||
|
box-shadow: 2px 2px grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card__description {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card__title {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-column {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #eee;
|
||||||
|
margin: 5px;
|
||||||
|
input {
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-adder-form {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 250px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
input {
|
||||||
|
border: 0px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-adder-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-adder-form__title {
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-adder-form__description {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-card-adder-form__button {
|
||||||
|
background-color: #eee;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
width: 45%;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
&:hover {
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-column-header {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
input {
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-column-header__button {
|
||||||
|
color: #333333;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #cccccc;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-kanban-column-adder-button {
|
||||||
|
border: 2px dashed #eee;
|
||||||
|
height: 132px;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
56
packages/client/src/board/utils.ts
Executable file
56
packages/client/src/board/utils.ts
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
function compose(...fns) {
|
||||||
|
return (arg) => fns.reduce((acc, fn) => fn(acc), arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function partialRight(fn, ...args) {
|
||||||
|
return (...leftArgs) => fn(...leftArgs, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInArrayAtPosition(array, element, position) {
|
||||||
|
const arrayCopy = [...array]
|
||||||
|
arrayCopy.splice(position, 0, element)
|
||||||
|
return arrayCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromArrayAtPosition(array, position) {
|
||||||
|
return array.reduce((acc, value, idx) => (idx === position ? acc : [...acc, value]), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeElementOfPositionInArray(array, from, to) {
|
||||||
|
const removeFromArrayAtPositionFrom = partialRight(removeFromArrayAtPosition, from)
|
||||||
|
const addInArrayAtPositionTo = partialRight(addInArrayAtPosition, array[from], to)
|
||||||
|
|
||||||
|
return compose(removeFromArrayAtPositionFrom, addInArrayAtPositionTo)(array)
|
||||||
|
}
|
||||||
|
|
||||||
|
function identity(value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function when(value, predicate = identity) {
|
||||||
|
return function callback(callback) {
|
||||||
|
if (predicate(value)) return callback(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceElementOfArray(array) {
|
||||||
|
return function (options) {
|
||||||
|
return array.map((element) => (options.when(element) ? options.for(element) : element))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPropOut(object, prop) {
|
||||||
|
return Object.keys(object).reduce((obj, key) => {
|
||||||
|
return key === prop ? obj : { ...obj, [key]: object[key] }
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
addInArrayAtPosition,
|
||||||
|
removeFromArrayAtPosition,
|
||||||
|
changeElementOfPositionInArray,
|
||||||
|
when,
|
||||||
|
replaceElementOfArray,
|
||||||
|
partialRight,
|
||||||
|
pickPropOut,
|
||||||
|
}
|
19
packages/client/src/board/withDroppable.tsx
Normal file
19
packages/client/src/board/withDroppable.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Droppable } from 'react-beautiful-dnd'
|
||||||
|
|
||||||
|
function withDroppable(Component) {
|
||||||
|
return function WrapperComponent({ children, ...droppableProps }) {
|
||||||
|
return (
|
||||||
|
<Droppable {...droppableProps}>
|
||||||
|
{(provided) => (
|
||||||
|
<Component ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
{children}
|
||||||
|
{provided.placeholder}
|
||||||
|
</Component>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withDroppable
|
@ -6,6 +6,7 @@ export * from './antd-config-provider';
|
|||||||
export * from './api-client';
|
export * from './api-client';
|
||||||
export * from './application';
|
export * from './application';
|
||||||
export * from './async-data-provider';
|
export * from './async-data-provider';
|
||||||
|
export * from './board';
|
||||||
export * from './collection-manager';
|
export * from './collection-manager';
|
||||||
export * from './current-user';
|
export * from './current-user';
|
||||||
export * from './document-title';
|
export * from './document-title';
|
||||||
|
85
yarn.lock
85
yarn.lock
@ -1006,6 +1006,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
|
||||||
|
version "7.16.7"
|
||||||
|
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
||||||
|
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.16.0", "@babel/template@^7.3.3":
|
"@babel/template@^7.16.0", "@babel/template@^7.3.3":
|
||||||
version "7.16.0"
|
version "7.16.0"
|
||||||
resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
|
resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
|
||||||
@ -2857,6 +2864,14 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724"
|
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724"
|
||||||
integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==
|
integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==
|
||||||
|
|
||||||
|
"@types/hoist-non-react-statics@^3.3.0":
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||||
|
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
"@types/http-assert@*":
|
"@types/http-assert@*":
|
||||||
version "1.5.3"
|
version "1.5.3"
|
||||||
resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
|
resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
|
||||||
@ -3060,6 +3075,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-redux@^7.1.20":
|
||||||
|
version "7.1.22"
|
||||||
|
resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz#0eab76a37ef477cc4b53665aeaf29cb60631b72a"
|
||||||
|
integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/hoist-non-react-statics" "^3.3.0"
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
redux "^4.0.0"
|
||||||
|
|
||||||
"@types/react-router-config@5.0.2":
|
"@types/react-router-config@5.0.2":
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.2.tgz#4d3b52e71ed363a1976a12321e67b09a99ad6d10"
|
resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.2.tgz#4d3b52e71ed363a1976a12321e67b09a99ad6d10"
|
||||||
@ -5459,6 +5484,13 @@ css-blank-pseudo@^0.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.5"
|
postcss "^7.0.5"
|
||||||
|
|
||||||
|
css-box-model@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||||
|
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||||
|
dependencies:
|
||||||
|
tiny-invariant "^1.0.6"
|
||||||
|
|
||||||
css-has-pseudo@^0.10.0:
|
css-has-pseudo@^0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee"
|
resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee"
|
||||||
@ -7607,7 +7639,7 @@ hmac-drbg@^1.0.1:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2:
|
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
@ -9869,6 +9901,11 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
|
memoize-one@^5.1.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||||
|
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
|
|
||||||
memoize-one@^6.0.0:
|
memoize-one@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||||
@ -12270,6 +12307,11 @@ quick-lru@^4.0.1:
|
|||||||
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
||||||
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
||||||
|
|
||||||
|
raf-schd@^4.0.2:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||||
|
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||||
|
|
||||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||||
@ -12721,6 +12763,19 @@ react-attr-converter@^0.3.1:
|
|||||||
resolved "https://registry.npmjs.org/react-attr-converter/-/react-attr-converter-0.3.1.tgz#4a2abf6d907b7ddae4d862dfec80e489ce41ad6e"
|
resolved "https://registry.npmjs.org/react-attr-converter/-/react-attr-converter-0.3.1.tgz#4a2abf6d907b7ddae4d862dfec80e489ce41ad6e"
|
||||||
integrity sha512-dSxo2Mn6Zx4HajeCeQNLefwEO4kNtV/0E682R1+ZTyFRPqxDa5zYb5qM/ocqw9Bxr/kFQO0IUiqdV7wdHw+Cdg==
|
integrity sha512-dSxo2Mn6Zx4HajeCeQNLefwEO4kNtV/0E682R1+ZTyFRPqxDa5zYb5qM/ocqw9Bxr/kFQO0IUiqdV7wdHw+Cdg==
|
||||||
|
|
||||||
|
react-beautiful-dnd@^13.1.0:
|
||||||
|
version "13.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
|
||||||
|
integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.9.2"
|
||||||
|
css-box-model "^1.2.0"
|
||||||
|
memoize-one "^5.1.1"
|
||||||
|
raf-schd "^4.0.2"
|
||||||
|
react-redux "^7.2.0"
|
||||||
|
redux "^4.0.4"
|
||||||
|
use-memo-one "^1.1.1"
|
||||||
|
|
||||||
react-docgen-typescript-dumi-tmp@^1.22.1-0:
|
react-docgen-typescript-dumi-tmp@^1.22.1-0:
|
||||||
version "1.22.1-0"
|
version "1.22.1-0"
|
||||||
resolved "https://registry.npmjs.org/react-docgen-typescript-dumi-tmp/-/react-docgen-typescript-dumi-tmp-1.22.1-0.tgz#6f452de05c5c114a6e1dd60b34930afaa7ae39a0"
|
resolved "https://registry.npmjs.org/react-docgen-typescript-dumi-tmp/-/react-docgen-typescript-dumi-tmp-1.22.1-0.tgz#6f452de05c5c114a6e1dd60b34930afaa7ae39a0"
|
||||||
@ -12782,7 +12837,7 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-i
|
|||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^17.0.1:
|
react-is@^17.0.1, react-is@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
@ -12802,6 +12857,18 @@ react-modal@^3.11.1:
|
|||||||
react-lifecycles-compat "^3.0.0"
|
react-lifecycles-compat "^3.0.0"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
|
react-redux@^7.2.0:
|
||||||
|
version "7.2.6"
|
||||||
|
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa"
|
||||||
|
integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.15.4"
|
||||||
|
"@types/react-redux" "^7.1.20"
|
||||||
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^17.0.2"
|
||||||
|
|
||||||
react-refresh@0.10.0:
|
react-refresh@0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
|
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
|
||||||
@ -13084,6 +13151,13 @@ redent@^3.0.0:
|
|||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
|
redux@^4.0.0, redux@^4.0.4:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
|
||||||
|
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.9.2"
|
||||||
|
|
||||||
regenerate-unicode-properties@^9.0.0:
|
regenerate-unicode-properties@^9.0.0:
|
||||||
version "9.0.0"
|
version "9.0.0"
|
||||||
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
|
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
|
||||||
@ -14572,7 +14646,7 @@ timers-browserify@^2.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
setimmediate "^1.0.4"
|
setimmediate "^1.0.4"
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
||||||
@ -15227,6 +15301,11 @@ use-deep-compare-effect@^1.8.1:
|
|||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
dequal "^2.0.2"
|
dequal "^2.0.2"
|
||||||
|
|
||||||
|
use-memo-one@^1.1.1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||||
|
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
||||||
|
|
||||||
use-subscription@1.5.1:
|
use-subscription@1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
resolved "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
||||||
|
Loading…
Reference in New Issue
Block a user