feat: implement task group actions

- allow sorting specifc task groups
- duplicate task group
- delete all tasks in task group
This commit is contained in:
Jordan Knott
2020-09-10 18:15:06 -05:00
parent cdcccbfb4a
commit 0eb9c0db94
19 changed files with 1727 additions and 192 deletions

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/components/Lists';
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
export const ActionsList = styled.ul`
margin: 0;

View File

@ -8,6 +8,7 @@ import {
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useFindProjectQuery,
useSortTaskGroupMutation,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useCreateTaskMutation,
@ -21,6 +22,10 @@ import {
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery,
useDuplicateTaskGroupMutation,
DuplicateTaskGroupMutation,
DuplicateTaskGroupDocument,
useDeleteTaskGroupTasksMutation,
} from 'shared/generated/graphql';
import QuickCardEditor from 'shared/components/QuickCardEditor';
@ -33,10 +38,8 @@ import SimpleLists, {
TaskMeta,
TaskMetaMatch,
TaskMetaFilters,
TaskSorting,
TaskSortingType,
TaskSortingDirection,
} from 'shared/components/Lists';
import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting';
import produce from 'immer';
import MiniProfile from 'shared/components/MiniProfile';
import DueDateManager from 'shared/components/DueDateManager';
@ -44,6 +47,7 @@ import EmptyBoard from 'shared/components/EmptyBoard';
import NOOP from 'shared/utils/noop';
import LabelManagerEditor from 'Projects/Project/LabelManagerEditor';
import Chip from 'shared/components/Chip';
import { toast } from 'react-toastify';
import { useCurrentUser } from 'App/context';
import FilterStatus from './FilterStatus';
import FilterMeta from './FilterMeta';
@ -263,6 +267,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const [taskMetaFilters, setTaskMetaFilters] = useState(initTaskMetaFilters);
const [taskSorting, setTaskSorting] = useState(initTaskSorting);
const history = useHistory();
const [sortTaskGroup] = useSortTaskGroupMutation({
onCompleted: () => {
toast('List was sorted');
},
});
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
update: (client, deletedTaskGroupData) => {
updateApolloCache<FindProjectQuery>(
@ -315,6 +324,36 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const { loading, data } = useFindProjectQuery({
variables: { projectID },
});
const [deleteTaskGroupTasks] = useDeleteTaskGroupTasksMutation({
update: (client, resp) =>
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const idx = cache.findProject.taskGroups.findIndex(
t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks = [];
}
}),
{ projectID },
),
});
const [duplicateTaskGroup] = useDuplicateTaskGroupMutation({
update: (client, resp) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
}),
{ projectID },
);
},
});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [setTaskComplete] = useSetTaskCompleteMutation();
@ -624,15 +663,44 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => hidePopup()}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
<ListActions
taskGroupID={taskGroupID}
onDeleteTaskGroupTasks={() => {
deleteTaskGroupTasks({ variables: { taskGroupID } });
hidePopup();
}}
onSortTaskGroup={taskSort => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
if (taskGroup) {
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
.sort((a, b) => sortTasks(a, b, taskSort))
.reduce((prevTasks: Array<{ taskID: string; position: number }>, t, idx) => {
prevTasks.push({ taskID: t.id, position: (idx + 1) * 2048 });
return tasks;
}, []);
sortTaskGroup({ variables: { taskGroupID, tasks } });
hidePopup();
}}
/>
</Popup>,
}
}}
onDuplicateTaskGroup={newName => {
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID);
if (idx !== -1) {
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
const prevPos = taskGroups[idx].position;
const next = taskGroups[idx + 1];
let newPos = prevPos * 2;
if (next) {
newPos = (prevPos + next.position) / 2.0;
}
duplicateTaskGroup({ variables: { projectID, taskGroupID, name: newName, position: newPos } });
hidePopup();
}
}}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
hidePopup();
}}
/>,
);
}}
/>

View File

@ -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} />

View File

@ -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')} />;
};

View File

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

View File

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

View File

@ -275,13 +275,16 @@ export type Mutation = {
deleteTaskChecklist: DeleteTaskChecklistPayload;
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
deleteTaskGroup: DeleteTaskGroupPayload;
deleteTaskGroupTasks: DeleteTaskGroupTasksPayload;
deleteTeam: DeleteTeamPayload;
deleteTeamMember: DeleteTeamMemberPayload;
deleteUserAccount: DeleteUserAccountPayload;
duplicateTaskGroup: DuplicateTaskGroupPayload;
logoutUser: Scalars['Boolean'];
removeTaskLabel: Task;
setTaskChecklistItemComplete: TaskChecklistItem;
setTaskComplete: Task;
sortTaskGroup: SortTaskGroupPayload;
toggleTaskLabel: ToggleTaskLabelPayload;
unassignTask: Task;
updateProjectLabel: ProjectLabel;
@ -405,6 +408,11 @@ export type MutationDeleteTaskGroupArgs = {
};
export type MutationDeleteTaskGroupTasksArgs = {
input: DeleteTaskGroupTasks;
};
export type MutationDeleteTeamArgs = {
input: DeleteTeam;
};
@ -420,6 +428,11 @@ export type MutationDeleteUserAccountArgs = {
};
export type MutationDuplicateTaskGroupArgs = {
input: DuplicateTaskGroup;
};
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
@ -440,6 +453,11 @@ export type MutationSetTaskCompleteArgs = {
};
export type MutationSortTaskGroupArgs = {
input: SortTaskGroup;
};
export type MutationToggleTaskLabelArgs = {
input: ToggleTaskLabelInput;
};
@ -823,6 +841,44 @@ export type DeleteTaskChecklistPayload = {
taskChecklist: TaskChecklist;
};
export type DeleteTaskGroupTasks = {
taskGroupID: Scalars['UUID'];
};
export type DeleteTaskGroupTasksPayload = {
__typename?: 'DeleteTaskGroupTasksPayload';
taskGroupID: Scalars['UUID'];
tasks: Array<Scalars['UUID']>;
};
export type TaskPositionUpdate = {
taskID: Scalars['UUID'];
position: Scalars['Float'];
};
export type SortTaskGroupPayload = {
__typename?: 'SortTaskGroupPayload';
taskGroupID: Scalars['UUID'];
tasks: Array<Task>;
};
export type SortTaskGroup = {
taskGroupID: Scalars['UUID'];
tasks: Array<TaskPositionUpdate>;
};
export type DuplicateTaskGroup = {
projectID: Scalars['UUID'];
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
export type DuplicateTaskGroupPayload = {
__typename?: 'DuplicateTaskGroupPayload';
taskGroup: TaskGroup;
};
export type NewTaskGroupLocation = {
taskGroupID: Scalars['UUID'];
position: Scalars['Float'];
@ -1575,6 +1631,60 @@ export type UpdateTaskChecklistNameMutation = (
) }
);
export type DeleteTaskGroupTasksMutationVariables = {
taskGroupID: Scalars['UUID'];
};
export type DeleteTaskGroupTasksMutation = (
{ __typename?: 'Mutation' }
& { deleteTaskGroupTasks: (
{ __typename?: 'DeleteTaskGroupTasksPayload' }
& Pick<DeleteTaskGroupTasksPayload, 'tasks' | 'taskGroupID'>
) }
);
export type DuplicateTaskGroupMutationVariables = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
projectID: Scalars['UUID'];
};
export type DuplicateTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { duplicateTaskGroup: (
{ __typename?: 'DuplicateTaskGroupPayload' }
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& TaskFieldsFragment
)> }
) }
) }
);
export type SortTaskGroupMutationVariables = {
tasks: Array<TaskPositionUpdate>;
taskGroupID: Scalars['UUID'];
};
export type SortTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { sortTaskGroup: (
{ __typename?: 'SortTaskGroupPayload' }
& Pick<SortTaskGroupPayload, 'taskGroupID'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'position'>
)> }
) }
);
export type UpdateTaskGroupNameMutationVariables = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
@ -3292,6 +3402,118 @@ export function useUpdateTaskChecklistNameMutation(baseOptions?: ApolloReactHook
export type UpdateTaskChecklistNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistNameMutation>;
export type UpdateTaskChecklistNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistNameMutation>;
export type UpdateTaskChecklistNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistNameMutation, UpdateTaskChecklistNameMutationVariables>;
export const DeleteTaskGroupTasksDocument = gql`
mutation deleteTaskGroupTasks($taskGroupID: UUID!) {
deleteTaskGroupTasks(input: {taskGroupID: $taskGroupID}) {
tasks
taskGroupID
}
}
`;
export type DeleteTaskGroupTasksMutationFn = ApolloReactCommon.MutationFunction<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>;
/**
* __useDeleteTaskGroupTasksMutation__
*
* To run a mutation, you first call `useDeleteTaskGroupTasksMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteTaskGroupTasksMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteTaskGroupTasksMutation, { data, loading, error }] = useDeleteTaskGroupTasksMutation({
* variables: {
* taskGroupID: // value for 'taskGroupID'
* },
* });
*/
export function useDeleteTaskGroupTasksMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>(DeleteTaskGroupTasksDocument, baseOptions);
}
export type DeleteTaskGroupTasksMutationHookResult = ReturnType<typeof useDeleteTaskGroupTasksMutation>;
export type DeleteTaskGroupTasksMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupTasksMutation>;
export type DeleteTaskGroupTasksMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>;
export const DuplicateTaskGroupDocument = gql`
mutation duplicateTaskGroup($taskGroupID: UUID!, $name: String!, $position: Float!, $projectID: UUID!) {
duplicateTaskGroup(input: {projectID: $projectID, taskGroupID: $taskGroupID, name: $name, position: $position}) {
taskGroup {
id
name
position
tasks {
...TaskFields
}
}
}
}
${TaskFieldsFragmentDoc}`;
export type DuplicateTaskGroupMutationFn = ApolloReactCommon.MutationFunction<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>;
/**
* __useDuplicateTaskGroupMutation__
*
* To run a mutation, you first call `useDuplicateTaskGroupMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDuplicateTaskGroupMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [duplicateTaskGroupMutation, { data, loading, error }] = useDuplicateTaskGroupMutation({
* variables: {
* taskGroupID: // value for 'taskGroupID'
* name: // value for 'name'
* position: // value for 'position'
* projectID: // value for 'projectID'
* },
* });
*/
export function useDuplicateTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>) {
return ApolloReactHooks.useMutation<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>(DuplicateTaskGroupDocument, baseOptions);
}
export type DuplicateTaskGroupMutationHookResult = ReturnType<typeof useDuplicateTaskGroupMutation>;
export type DuplicateTaskGroupMutationResult = ApolloReactCommon.MutationResult<DuplicateTaskGroupMutation>;
export type DuplicateTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>;
export const SortTaskGroupDocument = gql`
mutation sortTaskGroup($tasks: [TaskPositionUpdate!]!, $taskGroupID: UUID!) {
sortTaskGroup(input: {taskGroupID: $taskGroupID, tasks: $tasks}) {
taskGroupID
tasks {
id
position
}
}
}
`;
export type SortTaskGroupMutationFn = ApolloReactCommon.MutationFunction<SortTaskGroupMutation, SortTaskGroupMutationVariables>;
/**
* __useSortTaskGroupMutation__
*
* To run a mutation, you first call `useSortTaskGroupMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSortTaskGroupMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [sortTaskGroupMutation, { data, loading, error }] = useSortTaskGroupMutation({
* variables: {
* tasks: // value for 'tasks'
* taskGroupID: // value for 'taskGroupID'
* },
* });
*/
export function useSortTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SortTaskGroupMutation, SortTaskGroupMutationVariables>) {
return ApolloReactHooks.useMutation<SortTaskGroupMutation, SortTaskGroupMutationVariables>(SortTaskGroupDocument, baseOptions);
}
export type SortTaskGroupMutationHookResult = ReturnType<typeof useSortTaskGroupMutation>;
export type SortTaskGroupMutationResult = ApolloReactCommon.MutationResult<SortTaskGroupMutation>;
export type SortTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<SortTaskGroupMutation, SortTaskGroupMutationVariables>;
export const UpdateTaskGroupNameDocument = gql`
mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) {
updateTaskGroupName(input: {taskGroupID: $taskGroupID, name: $name}) {

View File

@ -0,0 +1,10 @@
import gql from 'graphql-tag';
const DELETE_TASK_GROUP_TASKS_MUTATION = gql`
mutation deleteTaskGroupTasks($taskGroupID: UUID!) {
deleteTaskGroupTasks(input: { taskGroupID: $taskGroupID }) {
tasks
taskGroupID
}
}
`;

View File

@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task';
const DUPLICATE_TASK_GROUP_MUTATION = gql`
mutation duplicateTaskGroup($taskGroupID: UUID!, $name: String!, $position: Float!, $projectID: UUID!) {
duplicateTaskGroup(
input: {
projectID: $projectID
taskGroupID: $taskGroupID
name: $name
position: $position
}
) {
taskGroup {
id
name
position
tasks {
...TaskFields
}
}
}
${TASK_FRAGMENT}
}
`;

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
const SORT_TASK_GROUP_MUTATION = gql`
mutation sortTaskGroup($tasks: [TaskPositionUpdate!]!, $taskGroupID: UUID!) {
sortTaskGroup(input: { taskGroupID: $taskGroupID, tasks: $tasks }) {
taskGroupID
tasks {
id
position
}
}
}
`;

View File

@ -0,0 +1,132 @@
import moment from 'moment';
export enum TaskSortingType {
NONE,
COMPLETE,
DUE_DATE,
MEMBERS,
LABELS,
TASK_TITLE,
}
export enum TaskSortingDirection {
ASC,
DESC,
}
export type TaskSorting = {
type: TaskSortingType;
direction: TaskSortingDirection;
};
export function sortString(a: string, b: string) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
export 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.COMPLETE) {
if (a.complete && !b.complete) {
return -1;
}
if (b.complete && !a.complete) {
return 1;
}
return 0;
}
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;
}