feat: implement task group actions
- allow sorting specifc task groups - duplicate task group - delete all tasks in task group
This commit is contained in:
@@ -16,11 +16,12 @@ import {
|
||||
} from './Styles';
|
||||
|
||||
type NameEditorProps = {
|
||||
buttonLabel?: string;
|
||||
onSave: (listName: string) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
||||
export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCancel, buttonLabel = 'Save' }) => {
|
||||
const $editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [listName, setListName] = useState('');
|
||||
useEffect(() => {
|
||||
@@ -28,6 +29,11 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
||||
$editorRef.current.focus();
|
||||
}
|
||||
});
|
||||
const onSave = (newName: string) => {
|
||||
if (newName.replace(/\s+/g, '') !== '') {
|
||||
handleSave(newName);
|
||||
}
|
||||
};
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -60,7 +66,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{buttonLabel}
|
||||
</AddListButton>
|
||||
<CancelAdd onClick={() => onCancel()}>
|
||||
<Cross width={16} height={16} />
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import ListActions from '.';
|
||||
|
||||
export default {
|
||||
component: ListActions,
|
||||
title: 'ListActions',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return <ListActions taskGroupID="1" onArchiveTaskGroup={action('on archive task group')} />;
|
||||
};
|
@@ -1,50 +1,100 @@
|
||||
import React from 'react';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { NameEditor } from 'shared/components/AddList';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import styled from 'styled-components';
|
||||
import { TaskSorting, TaskSortingDirection, TaskSortingType } from 'shared/utils/sorting';
|
||||
import { InnerContent, ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles';
|
||||
|
||||
const CopyWrapper = styled.div`
|
||||
margin: 0 12px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
taskGroupID: string;
|
||||
|
||||
onDuplicateTaskGroup: (newTaskGroupName: string) => void;
|
||||
onDeleteTaskGroupTasks: () => void;
|
||||
onArchiveTaskGroup: (taskGroupID: string) => void;
|
||||
onSortTaskGroup: (taskSorting: TaskSorting) => void;
|
||||
};
|
||||
const LabelManager: React.FC<Props> = ({ taskGroupID, onArchiveTaskGroup }) => {
|
||||
|
||||
const LabelManager: React.FC<Props> = ({
|
||||
taskGroupID,
|
||||
onDeleteTaskGroupTasks,
|
||||
onDuplicateTaskGroup,
|
||||
onArchiveTaskGroup,
|
||||
onSortTaskGroup,
|
||||
}) => {
|
||||
const { setTab } = usePopup();
|
||||
return (
|
||||
<InnerContent>
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Add card...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Copy List...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Move card...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Watch</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
<ListSeparator />
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Sort By...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
<ListSeparator />
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Move All Cards in This List...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper>
|
||||
<ListActionItem>Archive All Cards in This List...</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
<ListSeparator />
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
|
||||
<ListActionItem>Archive This List</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
</InnerContent>
|
||||
<>
|
||||
<Popup tab={0} title={null}>
|
||||
<InnerContent>
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper onClick={() => setTab(1)}>
|
||||
<ListActionItem>Duplicate</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper onClick={() => setTab(2)}>
|
||||
<ListActionItem>Sort</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
<ListSeparator />
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper onClick={() => onDeleteTaskGroupTasks()}>
|
||||
<ListActionItem>Delete All Tasks</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
<ListSeparator />
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
|
||||
<ListActionItem>Delete</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
</InnerContent>
|
||||
</Popup>
|
||||
<Popup tab={1} title="Copy list" onClose={NOOP}>
|
||||
<CopyWrapper>
|
||||
<NameEditor
|
||||
onCancel={NOOP}
|
||||
onSave={listName => {
|
||||
onDuplicateTaskGroup(listName);
|
||||
}}
|
||||
buttonLabel="Duplicate"
|
||||
/>
|
||||
</CopyWrapper>
|
||||
</Popup>
|
||||
<Popup tab={2} title="Sort list" onClose={NOOP}>
|
||||
<InnerContent>
|
||||
<ListActionsWrapper>
|
||||
<ListActionItemWrapper
|
||||
onClick={() => onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
||||
>
|
||||
<ListActionItem>Task title</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper
|
||||
onClick={() => onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
||||
>
|
||||
<ListActionItem>Due date</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper
|
||||
onClick={() => onSortTaskGroup({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
|
||||
>
|
||||
<ListActionItem>Complete</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper
|
||||
onClick={() => onSortTaskGroup({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })}
|
||||
>
|
||||
<ListActionItem>Labels</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
<ListActionItemWrapper
|
||||
onClick={() => onSortTaskGroup({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })}
|
||||
>
|
||||
<ListActionItem>Members</ListActionItem>
|
||||
</ListActionItemWrapper>
|
||||
</ListActionsWrapper>
|
||||
</InnerContent>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default LabelManager;
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
getAfterDropDraggableList,
|
||||
} from 'shared/utils/draggables';
|
||||
import moment from 'moment';
|
||||
import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting';
|
||||
|
||||
import { Container, BoardContainer, BoardWrapper } from './Styles';
|
||||
import shouldMetaFilter from './metaFilter';
|
||||
@@ -94,127 +95,6 @@ export type TaskMetaFilters = {
|
||||
labels: Array<LabelMetaFilter>;
|
||||
};
|
||||
|
||||
export enum TaskSortingType {
|
||||
NONE,
|
||||
DUE_DATE,
|
||||
MEMBERS,
|
||||
LABELS,
|
||||
TASK_TITLE,
|
||||
}
|
||||
|
||||
export enum TaskSortingDirection {
|
||||
ASC,
|
||||
DESC,
|
||||
}
|
||||
|
||||
export type TaskSorting = {
|
||||
type: TaskSortingType;
|
||||
direction: TaskSortingDirection;
|
||||
};
|
||||
|
||||
function sortString(a: string, b: string) {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
|
||||
if (taskSorting.type === TaskSortingType.TASK_TITLE) {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.DUE_DATE) {
|
||||
if (a.dueDate && !b.dueDate) {
|
||||
return -1;
|
||||
}
|
||||
if (b.dueDate && !a.dueDate) {
|
||||
return 1;
|
||||
}
|
||||
return moment(a.dueDate).diff(moment(b.dueDate));
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.LABELS) {
|
||||
// sorts non-empty labels by name, then by empty label color name
|
||||
let aLabels = [];
|
||||
let bLabels = [];
|
||||
let aLabelsEmpty = [];
|
||||
let bLabelsEmpty = [];
|
||||
if (a.labels) {
|
||||
for (const aLabel of a.labels) {
|
||||
if (aLabel.projectLabel.name && aLabel.projectLabel.name !== '') {
|
||||
aLabels.push(aLabel.projectLabel.name);
|
||||
} else {
|
||||
aLabelsEmpty.push(aLabel.projectLabel.labelColor.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (b.labels) {
|
||||
for (const bLabel of b.labels) {
|
||||
if (bLabel.projectLabel.name && bLabel.projectLabel.name !== '') {
|
||||
bLabels.push(bLabel.projectLabel.name);
|
||||
} else {
|
||||
bLabelsEmpty.push(bLabel.projectLabel.labelColor.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
aLabels = aLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
bLabels = bLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
aLabelsEmpty = aLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
bLabelsEmpty = bLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
if (aLabelsEmpty.length !== 0 || bLabelsEmpty.length !== 0) {
|
||||
if (aLabelsEmpty.length > bLabelsEmpty.length) {
|
||||
if (bLabels.length !== 0) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (aLabels.length < bLabels.length) {
|
||||
return 1;
|
||||
}
|
||||
if (aLabels.length > bLabels.length) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.MEMBERS) {
|
||||
let aMembers = [];
|
||||
let bMembers = [];
|
||||
if (a.assigned) {
|
||||
for (const aMember of a.assigned) {
|
||||
if (aMember.fullName) {
|
||||
aMembers.push(aMember.fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (b.assigned) {
|
||||
for (const bMember of b.assigned) {
|
||||
if (bMember.fullName) {
|
||||
bMembers.push(bMember.fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
aMembers = aMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||
bMembers = bMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||
if (aMembers.length < bMembers.length) {
|
||||
return 1;
|
||||
}
|
||||
if (aMembers.length > bMembers.length) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
||||
if (filter.status === TaskStatus.ALL) {
|
||||
return true;
|
||||
|
Reference in New Issue
Block a user