diff --git a/frontend/src/Projects/Project/Board/SortPopup.tsx b/frontend/src/Projects/Project/Board/SortPopup.tsx index 9407123..efb6001 100644 --- a/frontend/src/Projects/Project/Board/SortPopup.tsx +++ b/frontend/src/Projects/Project/Board/SortPopup.tsx @@ -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; diff --git a/frontend/src/Projects/Project/Board/index.tsx b/frontend/src/Projects/Project/Board/index.tsx index 4bf375a..1e8ba8c 100644 --- a/frontend/src/Projects/Project/Board/index.tsx +++ b/frontend/src/Projects/Project/Board/index.tsx @@ -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 = ({ 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( @@ -315,6 +324,36 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick const { loading, data } = useFindProjectQuery({ variables: { projectID }, }); + const [deleteTaskGroupTasks] = useDeleteTaskGroupTasksMutation({ + update: (client, resp) => + updateApolloCache( + 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( + 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 = ({ projectID, onCardLabelClick onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => { showPopup( $targetRef, - hidePopup()}> - { - deleteTaskGroup({ variables: { taskGroupID: tgID } }); + { + 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(); - }} - /> - , + } + }} + 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(); + }} + />, ); }} /> diff --git a/frontend/src/shared/components/AddList/index.tsx b/frontend/src/shared/components/AddList/index.tsx index 7d1a4a0..3992fc9 100644 --- a/frontend/src/shared/components/AddList/index.tsx +++ b/frontend/src/shared/components/AddList/index.tsx @@ -16,11 +16,12 @@ import { } from './Styles'; type NameEditorProps = { + buttonLabel?: string; onSave: (listName: string) => void; onCancel: () => void; }; -const NameEditor: React.FC = ({ onSave, onCancel }) => { +export const NameEditor: React.FC = ({ onSave: handleSave, onCancel, buttonLabel = 'Save' }) => { const $editorRef = useRef(null); const [listName, setListName] = useState(''); useEffect(() => { @@ -28,6 +29,11 @@ const NameEditor: React.FC = ({ 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 = ({ onSave, onCancel }) => { } }} > - Save + {buttonLabel} onCancel()}> diff --git a/frontend/src/shared/components/ListActions/ListActions.stories.tsx b/frontend/src/shared/components/ListActions/ListActions.stories.tsx deleted file mode 100644 index 5fb8b62..0000000 --- a/frontend/src/shared/components/ListActions/ListActions.stories.tsx +++ /dev/null @@ -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 ; -}; diff --git a/frontend/src/shared/components/ListActions/index.tsx b/frontend/src/shared/components/ListActions/index.tsx index e41b5c7..1a3eeb9 100644 --- a/frontend/src/shared/components/ListActions/index.tsx +++ b/frontend/src/shared/components/ListActions/index.tsx @@ -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 = ({ taskGroupID, onArchiveTaskGroup }) => { + +const LabelManager: React.FC = ({ + taskGroupID, + onDeleteTaskGroupTasks, + onDuplicateTaskGroup, + onArchiveTaskGroup, + onSortTaskGroup, +}) => { + const { setTab } = usePopup(); return ( - - - - Add card... - - - Copy List... - - - Move card... - - - Watch - - - - - - Sort By... - - - - - - Move All Cards in This List... - - - Archive All Cards in This List... - - - - - onArchiveTaskGroup(taskGroupID)}> - Archive This List - - - + <> + + + + setTab(1)}> + Duplicate + + setTab(2)}> + Sort + + + + + onDeleteTaskGroupTasks()}> + Delete All Tasks + + + + + onArchiveTaskGroup(taskGroupID)}> + Delete + + + + + + + { + onDuplicateTaskGroup(listName); + }} + buttonLabel="Duplicate" + /> + + + + + + onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })} + > + Task title + + onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })} + > + Due date + + onSortTaskGroup({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })} + > + Complete + + onSortTaskGroup({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })} + > + Labels + + onSortTaskGroup({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })} + > + Members + + + + + ); }; export default LabelManager; diff --git a/frontend/src/shared/components/Lists/index.tsx b/frontend/src/shared/components/Lists/index.tsx index 154974f..0317519 100644 --- a/frontend/src/shared/components/Lists/index.tsx +++ b/frontend/src/shared/components/Lists/index.tsx @@ -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; }; -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; diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx index ef18598..0ff6c66 100644 --- a/frontend/src/shared/generated/graphql.tsx +++ b/frontend/src/shared/generated/graphql.tsx @@ -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; +}; + +export type TaskPositionUpdate = { + taskID: Scalars['UUID']; + position: Scalars['Float']; +}; + +export type SortTaskGroupPayload = { + __typename?: 'SortTaskGroupPayload'; + taskGroupID: Scalars['UUID']; + tasks: Array; +}; + +export type SortTaskGroup = { + taskGroupID: Scalars['UUID']; + tasks: Array; +}; + +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 + ) } +); + +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 + & { tasks: Array<( + { __typename?: 'Task' } + & TaskFieldsFragment + )> } + ) } + ) } +); + +export type SortTaskGroupMutationVariables = { + tasks: Array; + taskGroupID: Scalars['UUID']; +}; + + +export type SortTaskGroupMutation = ( + { __typename?: 'Mutation' } + & { sortTaskGroup: ( + { __typename?: 'SortTaskGroupPayload' } + & Pick + & { tasks: Array<( + { __typename?: 'Task' } + & Pick + )> } + ) } +); + export type UpdateTaskGroupNameMutationVariables = { taskGroupID: Scalars['UUID']; name: Scalars['String']; @@ -3292,6 +3402,118 @@ export function useUpdateTaskChecklistNameMutation(baseOptions?: ApolloReactHook export type UpdateTaskChecklistNameMutationHookResult = ReturnType; export type UpdateTaskChecklistNameMutationResult = ApolloReactCommon.MutationResult; export type UpdateTaskChecklistNameMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const DeleteTaskGroupTasksDocument = gql` + mutation deleteTaskGroupTasks($taskGroupID: UUID!) { + deleteTaskGroupTasks(input: {taskGroupID: $taskGroupID}) { + tasks + taskGroupID + } +} + `; +export type DeleteTaskGroupTasksMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __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) { + return ApolloReactHooks.useMutation(DeleteTaskGroupTasksDocument, baseOptions); + } +export type DeleteTaskGroupTasksMutationHookResult = ReturnType; +export type DeleteTaskGroupTasksMutationResult = ApolloReactCommon.MutationResult; +export type DeleteTaskGroupTasksMutationOptions = ApolloReactCommon.BaseMutationOptions; +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; + +/** + * __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) { + return ApolloReactHooks.useMutation(DuplicateTaskGroupDocument, baseOptions); + } +export type DuplicateTaskGroupMutationHookResult = ReturnType; +export type DuplicateTaskGroupMutationResult = ApolloReactCommon.MutationResult; +export type DuplicateTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions; +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; + +/** + * __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) { + return ApolloReactHooks.useMutation(SortTaskGroupDocument, baseOptions); + } +export type SortTaskGroupMutationHookResult = ReturnType; +export type SortTaskGroupMutationResult = ApolloReactCommon.MutationResult; +export type SortTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateTaskGroupNameDocument = gql` mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) { updateTaskGroupName(input: {taskGroupID: $taskGroupID, name: $name}) { diff --git a/frontend/src/shared/graphql/taskGroup/deleteTaskGroupTasks.ts b/frontend/src/shared/graphql/taskGroup/deleteTaskGroupTasks.ts new file mode 100644 index 0000000..137468b --- /dev/null +++ b/frontend/src/shared/graphql/taskGroup/deleteTaskGroupTasks.ts @@ -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 + } + } +`; diff --git a/frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts b/frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts new file mode 100644 index 0000000..3975b1b --- /dev/null +++ b/frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts @@ -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} +} +`; diff --git a/frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts b/frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts new file mode 100644 index 0000000..d73e7be --- /dev/null +++ b/frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts @@ -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 + } + } + } +`; diff --git a/frontend/src/shared/utils/sorting.ts b/frontend/src/shared/utils/sorting.ts new file mode 100644 index 0000000..f10ca4a --- /dev/null +++ b/frontend/src/shared/utils/sorting.ts @@ -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; +} diff --git a/internal/db/querier.go b/internal/db/querier.go index a2a81d9..322649b 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -19,6 +19,7 @@ type Querier interface { CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) + CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error) CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error) CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error) CreateTaskChecklistItem(ctx context.Context, arg CreateTaskChecklistItemParams) (TaskChecklistItem, error) @@ -111,6 +112,7 @@ type Querier interface { UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) + UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error) UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error) diff --git a/internal/db/query/task.sql b/internal/db/query/task.sql index 122584f..b8cf8d5 100644 --- a/internal/db/query/task.sql +++ b/internal/db/query/task.sql @@ -2,6 +2,10 @@ INSERT INTO task (task_group_id, created_at, name, position) VALUES($1, $2, $3, $4) RETURNING *; +-- name: CreateTaskAll :one +INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; + -- name: UpdateTaskDescription :one UPDATE task SET description = $2 WHERE task_id = $1 RETURNING *; @@ -17,6 +21,9 @@ SELECT * FROM task; -- name: UpdateTaskLocation :one UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING *; +-- name: UpdateTaskPosition :one +UPDATE task SET position = $2 WHERE task_id = $1 RETURNING *; + -- name: DeleteTaskByID :exec DELETE FROM task WHERE task_id = $1; diff --git a/internal/db/task.sql.go b/internal/db/task.sql.go index 85ab6e9..702b62a 100644 --- a/internal/db/task.sql.go +++ b/internal/db/task.sql.go @@ -45,6 +45,46 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e return i, err } +const createTaskAll = `-- name: CreateTaskAll :one +INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at +` + +type CreateTaskAllParams struct { + TaskGroupID uuid.UUID `json:"task_group_id"` + CreatedAt time.Time `json:"created_at"` + Name string `json:"name"` + Position float64 `json:"position"` + Description sql.NullString `json:"description"` + Complete bool `json:"complete"` + DueDate sql.NullTime `json:"due_date"` +} + +func (q *Queries) CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error) { + row := q.db.QueryRowContext(ctx, createTaskAll, + arg.TaskGroupID, + arg.CreatedAt, + arg.Name, + arg.Position, + arg.Description, + arg.Complete, + arg.DueDate, + ) + var i Task + err := row.Scan( + &i.TaskID, + &i.TaskGroupID, + &i.CreatedAt, + &i.Name, + &i.Position, + &i.Description, + &i.DueDate, + &i.Complete, + &i.CompletedAt, + ) + return i, err +} + const deleteTaskByID = `-- name: DeleteTaskByID :exec DELETE FROM task WHERE task_id = $1 ` @@ -305,3 +345,29 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) ) return i, err } + +const updateTaskPosition = `-- name: UpdateTaskPosition :one +UPDATE task SET position = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at +` + +type UpdateTaskPositionParams struct { + TaskID uuid.UUID `json:"task_id"` + Position float64 `json:"position"` +} + +func (q *Queries) UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error) { + row := q.db.QueryRowContext(ctx, updateTaskPosition, arg.TaskID, arg.Position) + var i Task + err := row.Scan( + &i.TaskID, + &i.TaskGroupID, + &i.CreatedAt, + &i.Name, + &i.Position, + &i.Description, + &i.DueDate, + &i.Complete, + &i.CompletedAt, + ) + return i, err +} diff --git a/internal/graph/generated.go b/internal/graph/generated.go index f1689d5..708fdb5 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -102,6 +102,11 @@ type ComplexityRoot struct { TaskGroup func(childComplexity int) int } + DeleteTaskGroupTasksPayload struct { + TaskGroupID func(childComplexity int) int + Tasks func(childComplexity int) int + } + DeleteTaskPayload struct { TaskID func(childComplexity int) int } @@ -123,6 +128,10 @@ type ComplexityRoot struct { UserAccount func(childComplexity int) int } + DuplicateTaskGroupPayload struct { + TaskGroup func(childComplexity int) int + } + LabelColor struct { ColorHex func(childComplexity int) int ID func(childComplexity int) int @@ -173,13 +182,16 @@ type ComplexityRoot struct { DeleteTaskChecklist func(childComplexity int, input DeleteTaskChecklist) int DeleteTaskChecklistItem func(childComplexity int, input DeleteTaskChecklistItem) int DeleteTaskGroup func(childComplexity int, input DeleteTaskGroupInput) int + DeleteTaskGroupTasks func(childComplexity int, input DeleteTaskGroupTasks) int DeleteTeam func(childComplexity int, input DeleteTeam) int DeleteTeamMember func(childComplexity int, input DeleteTeamMember) int DeleteUserAccount func(childComplexity int, input DeleteUserAccount) int + DuplicateTaskGroup func(childComplexity int, input DuplicateTaskGroup) int LogoutUser func(childComplexity int, input LogoutUser) int RemoveTaskLabel func(childComplexity int, input *RemoveTaskLabelInput) int SetTaskChecklistItemComplete func(childComplexity int, input SetTaskChecklistItemComplete) int SetTaskComplete func(childComplexity int, input SetTaskComplete) int + SortTaskGroup func(childComplexity int, input SortTaskGroup) int ToggleTaskLabel func(childComplexity int, input ToggleTaskLabelInput) int UnassignTask func(childComplexity int, input *UnassignTaskInput) int UpdateProjectLabel func(childComplexity int, input UpdateProjectLabel) int @@ -293,6 +305,11 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + SortTaskGroupPayload struct { + TaskGroupID func(childComplexity int) int + Tasks func(childComplexity int) int + } + Task struct { Assigned func(childComplexity int) int Badges func(childComplexity int) int @@ -447,6 +464,9 @@ type MutationResolver interface { UpdateTaskGroupLocation(ctx context.Context, input NewTaskGroupLocation) (*db.TaskGroup, error) UpdateTaskGroupName(ctx context.Context, input UpdateTaskGroupName) (*db.TaskGroup, error) DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error) + DuplicateTaskGroup(ctx context.Context, input DuplicateTaskGroup) (*DuplicateTaskGroupPayload, error) + SortTaskGroup(ctx context.Context, input SortTaskGroup) (*SortTaskGroupPayload, error) + DeleteTaskGroupTasks(ctx context.Context, input DeleteTaskGroupTasks) (*DeleteTaskGroupTasksPayload, error) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*db.Task, error) RemoveTaskLabel(ctx context.Context, input *RemoveTaskLabelInput) (*db.Task, error) ToggleTaskLabel(ctx context.Context, input ToggleTaskLabelInput) (*ToggleTaskLabelPayload, error) @@ -694,6 +714,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DeleteTaskGroupPayload.TaskGroup(childComplexity), true + case "DeleteTaskGroupTasksPayload.taskGroupID": + if e.complexity.DeleteTaskGroupTasksPayload.TaskGroupID == nil { + break + } + + return e.complexity.DeleteTaskGroupTasksPayload.TaskGroupID(childComplexity), true + + case "DeleteTaskGroupTasksPayload.tasks": + if e.complexity.DeleteTaskGroupTasksPayload.Tasks == nil { + break + } + + return e.complexity.DeleteTaskGroupTasksPayload.Tasks(childComplexity), true + case "DeleteTaskPayload.taskID": if e.complexity.DeleteTaskPayload.TaskID == nil { break @@ -757,6 +791,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DeleteUserAccountPayload.UserAccount(childComplexity), true + case "DuplicateTaskGroupPayload.taskGroup": + if e.complexity.DuplicateTaskGroupPayload.TaskGroup == nil { + break + } + + return e.complexity.DuplicateTaskGroupPayload.TaskGroup(childComplexity), true + case "LabelColor.colorHex": if e.complexity.LabelColor.ColorHex == nil { break @@ -1116,6 +1157,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteTaskGroup(childComplexity, args["input"].(DeleteTaskGroupInput)), true + case "Mutation.deleteTaskGroupTasks": + if e.complexity.Mutation.DeleteTaskGroupTasks == nil { + break + } + + args, err := ec.field_Mutation_deleteTaskGroupTasks_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteTaskGroupTasks(childComplexity, args["input"].(DeleteTaskGroupTasks)), true + case "Mutation.deleteTeam": if e.complexity.Mutation.DeleteTeam == nil { break @@ -1152,6 +1205,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteUserAccount(childComplexity, args["input"].(DeleteUserAccount)), true + case "Mutation.duplicateTaskGroup": + if e.complexity.Mutation.DuplicateTaskGroup == nil { + break + } + + args, err := ec.field_Mutation_duplicateTaskGroup_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DuplicateTaskGroup(childComplexity, args["input"].(DuplicateTaskGroup)), true + case "Mutation.logoutUser": if e.complexity.Mutation.LogoutUser == nil { break @@ -1200,6 +1265,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.SetTaskComplete(childComplexity, args["input"].(SetTaskComplete)), true + case "Mutation.sortTaskGroup": + if e.complexity.Mutation.SortTaskGroup == nil { + break + } + + args, err := ec.field_Mutation_sortTaskGroup_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SortTaskGroup(childComplexity, args["input"].(SortTaskGroup)), true + case "Mutation.toggleTaskLabel": if e.complexity.Mutation.ToggleTaskLabel == nil { break @@ -1829,6 +1906,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Role.Name(childComplexity), true + case "SortTaskGroupPayload.taskGroupID": + if e.complexity.SortTaskGroupPayload.TaskGroupID == nil { + break + } + + return e.complexity.SortTaskGroupPayload.TaskGroupID(childComplexity), true + + case "SortTaskGroupPayload.tasks": + if e.complexity.SortTaskGroupPayload.Tasks == nil { + break + } + + return e.complexity.SortTaskGroupPayload.Tasks(childComplexity), true + case "Task.assigned": if e.complexity.Task.Assigned == nil { break @@ -2897,6 +2988,47 @@ extend type Mutation { TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + duplicateTaskGroup(input: DuplicateTaskGroup!): + DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + sortTaskGroup(input: SortTaskGroup!): + SortTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskGroupTasks(input: DeleteTaskGroupTasks!): + DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) +} + +input DeleteTaskGroupTasks { + taskGroupID: UUID! +} + +type DeleteTaskGroupTasksPayload { + taskGroupID: UUID! + tasks: [UUID!]! +} + +input TaskPositionUpdate { + taskID: UUID! + position: Float! +} + +type SortTaskGroupPayload { + taskGroupID: UUID! + tasks: [Task!]! +} + +input SortTaskGroup { + taskGroupID: UUID! + tasks: [TaskPositionUpdate!]! +} + +input DuplicateTaskGroup { + projectID: UUID! + taskGroupID: UUID! + name: String! + position: Float! +} + +type DuplicateTaskGroupPayload { + taskGroup: TaskGroup! } input NewTaskGroupLocation { @@ -3369,6 +3501,20 @@ func (ec *executionContext) field_Mutation_deleteTaskChecklist_args(ctx context. return args, nil } +func (ec *executionContext) field_Mutation_deleteTaskGroupTasks_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 DeleteTaskGroupTasks + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNDeleteTaskGroupTasks2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupTasks(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteTaskGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3439,6 +3585,20 @@ func (ec *executionContext) field_Mutation_deleteUserAccount_args(ctx context.Co return args, nil } +func (ec *executionContext) field_Mutation_duplicateTaskGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 DuplicateTaskGroup + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNDuplicateTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDuplicateTaskGroup(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_logoutUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3495,6 +3655,20 @@ func (ec *executionContext) field_Mutation_setTaskComplete_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_sortTaskGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 SortTaskGroup + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNSortTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroup(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_toggleTaskLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4507,6 +4681,74 @@ func (ec *executionContext) _DeleteTaskGroupPayload_taskGroup(ctx context.Contex return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskGroup(ctx, field.Selections, res) } +func (ec *executionContext) _DeleteTaskGroupTasksPayload_taskGroupID(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskGroupTasksPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "DeleteTaskGroupTasksPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TaskGroupID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _DeleteTaskGroupTasksPayload_tasks(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskGroupTasksPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "DeleteTaskGroupTasksPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tasks, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]uuid.UUID) + fc.Result = res + return ec.marshalNUUID2ᚕgithubᚗcomᚋgoogleᚋuuidᚐUUIDᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _DeleteTaskPayload_taskID(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskPayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4813,6 +5055,40 @@ func (ec *executionContext) _DeleteUserAccountPayload_userAccount(ctx context.Co return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res) } +func (ec *executionContext) _DuplicateTaskGroupPayload_taskGroup(ctx context.Context, field graphql.CollectedField, obj *DuplicateTaskGroupPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "DuplicateTaskGroupPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TaskGroup, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*db.TaskGroup) + fc.Result = res + return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskGroup(ctx, field.Selections, res) +} + func (ec *executionContext) _LabelColor_id(ctx context.Context, field graphql.CollectedField, obj *db.LabelColor) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7766,6 +8042,225 @@ func (ec *executionContext) _Mutation_deleteTaskGroup(ctx context.Context, field return ec.marshalNDeleteTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupPayload(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_duplicateTaskGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_duplicateTaskGroup_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DuplicateTaskGroup(rctx, args["input"].(DuplicateTaskGroup)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DuplicateTaskGroupPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DuplicateTaskGroupPayload`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*DuplicateTaskGroupPayload) + fc.Result = res + return ec.marshalNDuplicateTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDuplicateTaskGroupPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_sortTaskGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_sortTaskGroup_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SortTaskGroup(rctx, args["input"].(SortTaskGroup)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*SortTaskGroupPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.SortTaskGroupPayload`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*SortTaskGroupPayload) + fc.Result = res + return ec.marshalNSortTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroupPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_deleteTaskGroupTasks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_deleteTaskGroupTasks_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTaskGroupTasks(rctx, args["input"].(DeleteTaskGroupTasks)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) + if err != nil { + return nil, err + } + level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT") + if err != nil { + return nil, err + } + typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*DeleteTaskGroupTasksPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTaskGroupTasksPayload`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*DeleteTaskGroupTasksPayload) + fc.Result = res + return ec.marshalNDeleteTaskGroupTasksPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupTasksPayload(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_addTaskLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10586,6 +11081,74 @@ func (ec *executionContext) _Role_name(ctx context.Context, field graphql.Collec return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _SortTaskGroupPayload_taskGroupID(ctx context.Context, field graphql.CollectedField, obj *SortTaskGroupPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "SortTaskGroupPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TaskGroupID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _SortTaskGroupPayload_tasks(ctx context.Context, field graphql.CollectedField, obj *SortTaskGroupPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "SortTaskGroupPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tasks, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]db.Task) + fc.Result = res + return ec.marshalNTask2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -14109,6 +14672,24 @@ func (ec *executionContext) unmarshalInputDeleteTaskGroupInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputDeleteTaskGroupTasks(ctx context.Context, obj interface{}) (DeleteTaskGroupTasks, error) { + var it DeleteTaskGroupTasks + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "taskGroupID": + var err error + it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteTaskInput(ctx context.Context, obj interface{}) (DeleteTaskInput, error) { var it DeleteTaskInput var asMap = obj.(map[string]interface{}) @@ -14199,6 +14780,42 @@ func (ec *executionContext) unmarshalInputDeleteUserAccount(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputDuplicateTaskGroup(ctx context.Context, obj interface{}) (DuplicateTaskGroup, error) { + var it DuplicateTaskGroup + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "projectID": + var err error + it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "taskGroupID": + var err error + it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "name": + var err error + it.Name, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "position": + var err error + it.Position, err = ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputFindProject(ctx context.Context, obj interface{}) (FindProject, error) { var it FindProject var asMap = obj.(map[string]interface{}) @@ -14637,6 +15254,54 @@ func (ec *executionContext) unmarshalInputSetTaskComplete(ctx context.Context, o return it, nil } +func (ec *executionContext) unmarshalInputSortTaskGroup(ctx context.Context, obj interface{}) (SortTaskGroup, error) { + var it SortTaskGroup + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "taskGroupID": + var err error + it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "tasks": + var err error + it.Tasks, err = ec.unmarshalNTaskPositionUpdate2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskPositionUpdateᚄ(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputTaskPositionUpdate(ctx context.Context, obj interface{}) (TaskPositionUpdate, error) { + var it TaskPositionUpdate + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "taskID": + var err error + it.TaskID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "position": + var err error + it.Position, err = ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputToggleTaskLabelInput(ctx context.Context, obj interface{}) (ToggleTaskLabelInput, error) { var it ToggleTaskLabelInput var asMap = obj.(map[string]interface{}) @@ -15367,6 +16032,38 @@ func (ec *executionContext) _DeleteTaskGroupPayload(ctx context.Context, sel ast return out } +var deleteTaskGroupTasksPayloadImplementors = []string{"DeleteTaskGroupTasksPayload"} + +func (ec *executionContext) _DeleteTaskGroupTasksPayload(ctx context.Context, sel ast.SelectionSet, obj *DeleteTaskGroupTasksPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteTaskGroupTasksPayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DeleteTaskGroupTasksPayload") + case "taskGroupID": + out.Values[i] = ec._DeleteTaskGroupTasksPayload_taskGroupID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "tasks": + out.Values[i] = ec._DeleteTaskGroupTasksPayload_tasks(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var deleteTaskPayloadImplementors = []string{"DeleteTaskPayload"} func (ec *executionContext) _DeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *DeleteTaskPayload) graphql.Marshaler { @@ -15500,6 +16197,33 @@ func (ec *executionContext) _DeleteUserAccountPayload(ctx context.Context, sel a return out } +var duplicateTaskGroupPayloadImplementors = []string{"DuplicateTaskGroupPayload"} + +func (ec *executionContext) _DuplicateTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, obj *DuplicateTaskGroupPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, duplicateTaskGroupPayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DuplicateTaskGroupPayload") + case "taskGroup": + out.Values[i] = ec._DuplicateTaskGroupPayload_taskGroup(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var labelColorImplementors = []string{"LabelColor"} func (ec *executionContext) _LabelColor(ctx context.Context, sel ast.SelectionSet, obj *db.LabelColor) graphql.Marshaler { @@ -15857,6 +16581,21 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "duplicateTaskGroup": + out.Values[i] = ec._Mutation_duplicateTaskGroup(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "sortTaskGroup": + out.Values[i] = ec._Mutation_sortTaskGroup(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "deleteTaskGroupTasks": + out.Values[i] = ec._Mutation_deleteTaskGroupTasks(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "addTaskLabel": out.Values[i] = ec._Mutation_addTaskLabel(ctx, field) if out.Values[i] == graphql.Null { @@ -16728,6 +17467,38 @@ func (ec *executionContext) _Role(ctx context.Context, sel ast.SelectionSet, obj return out } +var sortTaskGroupPayloadImplementors = []string{"SortTaskGroupPayload"} + +func (ec *executionContext) _SortTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, obj *SortTaskGroupPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, sortTaskGroupPayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SortTaskGroupPayload") + case "taskGroupID": + out.Values[i] = ec._SortTaskGroupPayload_taskGroupID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "tasks": + out.Values[i] = ec._SortTaskGroupPayload_tasks(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var taskImplementors = []string{"Task"} func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *db.Task) graphql.Marshaler { @@ -18064,6 +18835,24 @@ func (ec *executionContext) marshalNDeleteTaskGroupPayload2ᚖgithubᚗcomᚋjor return ec._DeleteTaskGroupPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteTaskGroupTasks2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupTasks(ctx context.Context, v interface{}) (DeleteTaskGroupTasks, error) { + return ec.unmarshalInputDeleteTaskGroupTasks(ctx, v) +} + +func (ec *executionContext) marshalNDeleteTaskGroupTasksPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupTasksPayload(ctx context.Context, sel ast.SelectionSet, v DeleteTaskGroupTasksPayload) graphql.Marshaler { + return ec._DeleteTaskGroupTasksPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteTaskGroupTasksPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskGroupTasksPayload(ctx context.Context, sel ast.SelectionSet, v *DeleteTaskGroupTasksPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._DeleteTaskGroupTasksPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDeleteTaskInput2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDeleteTaskInput(ctx context.Context, v interface{}) (DeleteTaskInput, error) { return ec.unmarshalInputDeleteTaskInput(ctx, v) } @@ -18136,6 +18925,24 @@ func (ec *executionContext) marshalNDeleteUserAccountPayload2ᚖgithubᚗcomᚋj return ec._DeleteUserAccountPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDuplicateTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDuplicateTaskGroup(ctx context.Context, v interface{}) (DuplicateTaskGroup, error) { + return ec.unmarshalInputDuplicateTaskGroup(ctx, v) +} + +func (ec *executionContext) marshalNDuplicateTaskGroupPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDuplicateTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v DuplicateTaskGroupPayload) graphql.Marshaler { + return ec._DuplicateTaskGroupPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDuplicateTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐDuplicateTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v *DuplicateTaskGroupPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._DuplicateTaskGroupPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNEntityType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐEntityType(ctx context.Context, v interface{}) (EntityType, error) { var res EntityType return res, res.UnmarshalGQL(v) @@ -18774,6 +19581,24 @@ func (ec *executionContext) unmarshalNSetTaskComplete2githubᚗcomᚋjordanknott return ec.unmarshalInputSetTaskComplete(ctx, v) } +func (ec *executionContext) unmarshalNSortTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroup(ctx context.Context, v interface{}) (SortTaskGroup, error) { + return ec.unmarshalInputSortTaskGroup(ctx, v) +} + +func (ec *executionContext) marshalNSortTaskGroupPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v SortTaskGroupPayload) graphql.Marshaler { + return ec._SortTaskGroupPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSortTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v *SortTaskGroupPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._SortTaskGroupPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } @@ -19047,6 +19872,30 @@ func (ec *executionContext) marshalNTaskLabel2ᚕgithubᚗcomᚋjordanknottᚋta return ret } +func (ec *executionContext) unmarshalNTaskPositionUpdate2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskPositionUpdate(ctx context.Context, v interface{}) (TaskPositionUpdate, error) { + return ec.unmarshalInputTaskPositionUpdate(ctx, v) +} + +func (ec *executionContext) unmarshalNTaskPositionUpdate2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskPositionUpdateᚄ(ctx context.Context, v interface{}) ([]TaskPositionUpdate, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]TaskPositionUpdate, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalNTaskPositionUpdate2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskPositionUpdate(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + func (ec *executionContext) marshalNTeam2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx context.Context, sel ast.SelectionSet, v db.Team) graphql.Marshaler { return ec._Team(ctx, sel, &v) } diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go index 5e12ef2..c7b42b1 100644 --- a/internal/graph/models_gen.go +++ b/internal/graph/models_gen.go @@ -111,6 +111,15 @@ type DeleteTaskGroupPayload struct { TaskGroup *db.TaskGroup `json:"taskGroup"` } +type DeleteTaskGroupTasks struct { + TaskGroupID uuid.UUID `json:"taskGroupID"` +} + +type DeleteTaskGroupTasksPayload struct { + TaskGroupID uuid.UUID `json:"taskGroupID"` + Tasks []uuid.UUID `json:"tasks"` +} + type DeleteTaskInput struct { TaskID string `json:"taskID"` } @@ -151,6 +160,17 @@ type DeleteUserAccountPayload struct { UserAccount *db.UserAccount `json:"userAccount"` } +type DuplicateTaskGroup struct { + ProjectID uuid.UUID `json:"projectID"` + TaskGroupID uuid.UUID `json:"taskGroupID"` + Name string `json:"name"` + Position float64 `json:"position"` +} + +type DuplicateTaskGroupPayload struct { + TaskGroup *db.TaskGroup `json:"taskGroup"` +} + type FindProject struct { ProjectID uuid.UUID `json:"projectID"` } @@ -296,10 +316,25 @@ type SetTaskComplete struct { Complete bool `json:"complete"` } +type SortTaskGroup struct { + TaskGroupID uuid.UUID `json:"taskGroupID"` + Tasks []TaskPositionUpdate `json:"tasks"` +} + +type SortTaskGroupPayload struct { + TaskGroupID uuid.UUID `json:"taskGroupID"` + Tasks []db.Task `json:"tasks"` +} + type TaskBadges struct { Checklist *ChecklistBadge `json:"checklist"` } +type TaskPositionUpdate struct { + TaskID uuid.UUID `json:"taskID"` + Position float64 `json:"position"` +} + type TeamRole struct { TeamID uuid.UUID `json:"teamID"` RoleCode RoleCode `json:"roleCode"` diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 5047388..82d96a8 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -547,6 +547,47 @@ extend type Mutation { TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + duplicateTaskGroup(input: DuplicateTaskGroup!): + DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + sortTaskGroup(input: SortTaskGroup!): + SortTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskGroupTasks(input: DeleteTaskGroupTasks!): + DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) +} + +input DeleteTaskGroupTasks { + taskGroupID: UUID! +} + +type DeleteTaskGroupTasksPayload { + taskGroupID: UUID! + tasks: [UUID!]! +} + +input TaskPositionUpdate { + taskID: UUID! + position: Float! +} + +type SortTaskGroupPayload { + taskGroupID: UUID! + tasks: [Task!]! +} + +input SortTaskGroup { + taskGroupID: UUID! + tasks: [TaskPositionUpdate!]! +} + +input DuplicateTaskGroup { + projectID: UUID! + taskGroupID: UUID! + name: String! + position: Float! +} + +type DuplicateTaskGroupPayload { + taskGroup: TaskGroup! } input NewTaskGroupLocation { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index cb2f2d3..36b6a54 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -438,6 +438,111 @@ func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTask return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil } +func (r *mutationResolver) DuplicateTaskGroup(ctx context.Context, input DuplicateTaskGroup) (*DuplicateTaskGroupPayload, error) { + createdAt := time.Now().UTC() + taskGroup, err := r.Repository.CreateTaskGroup(ctx, db.CreateTaskGroupParams{ProjectID: input.ProjectID, Position: input.Position, Name: input.Name, CreatedAt: createdAt}) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + + originalTasks, err := r.Repository.GetTasksForTaskGroupID(ctx, input.TaskGroupID) + if err != nil && err != sql.ErrNoRows { + return &DuplicateTaskGroupPayload{}, err + } + for _, originalTask := range originalTasks { + task, err := r.Repository.CreateTaskAll(ctx, db.CreateTaskAllParams{ + TaskGroupID: taskGroup.TaskGroupID, CreatedAt: createdAt, Name: originalTask.Name, Position: originalTask.Position, + Complete: originalTask.Complete, DueDate: originalTask.DueDate, Description: originalTask.Description}) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + members, err := r.Repository.GetAssignedMembersForTask(ctx, originalTask.TaskID) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + for _, member := range members { + _, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{ + TaskID: task.TaskID, UserID: member.UserID, AssignedDate: member.AssignedDate}) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + } + labels, err := r.Repository.GetTaskLabelsForTaskID(ctx, originalTask.TaskID) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + for _, label := range labels { + _, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{ + TaskID: task.TaskID, ProjectLabelID: label.ProjectLabelID, AssignedDate: label.AssignedDate}) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + } + checklists, err := r.Repository.GetTaskChecklistsForTask(ctx, originalTask.TaskID) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + for _, checklist := range checklists { + newChecklist, err := r.Repository.CreateTaskChecklist(ctx, db.CreateTaskChecklistParams{ + TaskID: task.TaskID, Name: checklist.Name, CreatedAt: createdAt, Position: checklist.Position}) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + checklistItems, err := r.Repository.GetTaskChecklistItemsForTaskChecklist(ctx, checklist.TaskChecklistID) + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + for _, checklistItem := range checklistItems { + item, err := r.Repository.CreateTaskChecklistItem(ctx, db.CreateTaskChecklistItemParams{ + TaskChecklistID: newChecklist.TaskChecklistID, + CreatedAt: createdAt, + Name: checklistItem.Name, + Position: checklist.Position, + }) + if checklistItem.Complete { + r.Repository.SetTaskChecklistItemComplete(ctx, db.SetTaskChecklistItemCompleteParams{TaskChecklistItemID: item.TaskChecklistItemID, Complete: true}) + } + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + } + + } + } + if err != nil { + return &DuplicateTaskGroupPayload{}, err + } + return &DuplicateTaskGroupPayload{TaskGroup: &taskGroup}, err +} + +func (r *mutationResolver) SortTaskGroup(ctx context.Context, input SortTaskGroup) (*SortTaskGroupPayload, error) { + tasks := []db.Task{} + for _, task := range input.Tasks { + t, err := r.Repository.UpdateTaskPosition(ctx, db.UpdateTaskPositionParams{TaskID: task.TaskID, Position: task.Position}) + if err != nil { + return &SortTaskGroupPayload{}, err + } + tasks = append(tasks, t) + } + return &SortTaskGroupPayload{Tasks: tasks, TaskGroupID: input.TaskGroupID}, nil +} + +func (r *mutationResolver) DeleteTaskGroupTasks(ctx context.Context, input DeleteTaskGroupTasks) (*DeleteTaskGroupTasksPayload, error) { + tasks, err := r.Repository.GetTasksForTaskGroupID(ctx, input.TaskGroupID) + if err != nil && err != sql.ErrNoRows { + return &DeleteTaskGroupTasksPayload{}, err + } + removedTasks := []uuid.UUID{} + for _, task := range tasks { + err = r.Repository.DeleteTaskByID(ctx, task.TaskID) + if err != nil { + return &DeleteTaskGroupTasksPayload{}, err + } + removedTasks = append(removedTasks, task.TaskID) + } + return &DeleteTaskGroupTasksPayload{TaskGroupID: input.TaskGroupID, Tasks: removedTasks}, nil +} + func (r *mutationResolver) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*db.Task, error) { assignedDate := time.Now().UTC() _, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{input.TaskID, input.ProjectLabelID, assignedDate}) diff --git a/internal/graph/schema/task_group.gql b/internal/graph/schema/task_group.gql index 56f0210..0ed8cb6 100644 --- a/internal/graph/schema/task_group.gql +++ b/internal/graph/schema/task_group.gql @@ -7,6 +7,47 @@ extend type Mutation { TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + duplicateTaskGroup(input: DuplicateTaskGroup!): + DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + sortTaskGroup(input: SortTaskGroup!): + SortTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskGroupTasks(input: DeleteTaskGroupTasks!): + DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) +} + +input DeleteTaskGroupTasks { + taskGroupID: UUID! +} + +type DeleteTaskGroupTasksPayload { + taskGroupID: UUID! + tasks: [UUID!]! +} + +input TaskPositionUpdate { + taskID: UUID! + position: Float! +} + +type SortTaskGroupPayload { + taskGroupID: UUID! + tasks: [Task!]! +} + +input SortTaskGroup { + taskGroupID: UUID! + tasks: [TaskPositionUpdate!]! +} + +input DuplicateTaskGroup { + projectID: UUID! + taskGroupID: UUID! + name: String! + position: Float! +} + +type DuplicateTaskGroupPayload { + taskGroup: TaskGroup! } input NewTaskGroupLocation {