From becffc9e9b18582526912e4ac3c9028d76c7c780 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Thu, 27 Aug 2020 18:57:23 -0500 Subject: [PATCH] feat: add task sorting & filtering adds filtering by task status (completion date, incomplete, completion) adds filtering by task metadata (task name, labels, members, due date) adds sorting by task name, labels, members, and due date --- frontend/.eslintrc.json | 1 + .../src/Projects/Project/Board/FilterMeta.tsx | 324 ++++++ .../Projects/Project/Board/FilterStatus.tsx | 149 +++ .../src/Projects/Project/Board/SortPopup.tsx | 80 ++ frontend/src/Projects/Project/Board/index.tsx | 226 +++- frontend/src/index.tsx | 8 + .../src/shared/components/Admin/index.tsx | 7 +- frontend/src/shared/components/Chip/index.tsx | 71 ++ .../ControlledInput/Input.stories.tsx | 2 +- .../shared/components/DropdownMenu/index.tsx | 4 +- .../shared/components/Input/Input.stories.tsx | 2 +- .../src/shared/components/Lists/index.tsx | 277 ++++- .../src/shared/components/Lists/metaFilter.ts | 132 +++ .../src/shared/components/Login/index.tsx | 2 +- .../src/shared/components/Register/index.tsx | 8 +- .../src/shared/components/Settings/index.tsx | 2 +- frontend/src/shared/generated/graphql.tsx | 4 +- frontend/src/shared/graphql/fragments/task.ts | 1 + frontend/src/shared/icons/Calendar.tsx | 12 + frontend/src/shared/icons/User.tsx | 19 +- frontend/src/shared/icons/index.ts | 2 + frontend/src/types.d.ts | 1 + frontend/tsconfig.json | 1 + internal/db/models.go | 1 + internal/db/query/task.sql | 2 +- internal/db/task.sql.go | 34 +- internal/graph/generated.go | 1035 +++++++++++++++-- internal/graph/schema.graphqls | 84 +- internal/graph/schema.resolvers.go | 10 +- internal/graph/schema/_models.gql | 1 + ...0051_add-completed_at-to-task-table.up.sql | 4 + 31 files changed, 2340 insertions(+), 166 deletions(-) create mode 100644 frontend/src/Projects/Project/Board/FilterMeta.tsx create mode 100644 frontend/src/Projects/Project/Board/FilterStatus.tsx create mode 100644 frontend/src/Projects/Project/Board/SortPopup.tsx create mode 100644 frontend/src/shared/components/Chip/index.tsx create mode 100644 frontend/src/shared/components/Lists/metaFilter.ts create mode 100644 frontend/src/shared/icons/Calendar.tsx create mode 100644 migrations/0051_add-completed_at-to-task-table.up.sql diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 6648837..edd54b0 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -30,6 +30,7 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], + "no-case-declarations": "off", "react/prop-types": 0, "react/jsx-props-no-spreading": "off", "no-param-reassign": "off", diff --git a/frontend/src/Projects/Project/Board/FilterMeta.tsx b/frontend/src/Projects/Project/Board/FilterMeta.tsx new file mode 100644 index 0000000..fb10af3 --- /dev/null +++ b/frontend/src/Projects/Project/Board/FilterMeta.tsx @@ -0,0 +1,324 @@ +import React, { useState, useEffect } from 'react'; +import styled, { css } from 'styled-components'; +import { Checkmark, User, Calendar, Tags, Clock } from 'shared/icons'; +import { TaskMetaFilters, TaskMeta, TaskMetaMatch, DueDateFilterType } from 'shared/components/Lists'; +import Input from 'shared/components/ControlledInput'; +import { Popup, usePopup } from 'shared/components/PopupMenu'; +import produce from 'immer'; +import moment from 'moment'; +import { mixin } from 'shared/utils/styles'; +import Member from 'shared/components/Member'; + +const FilterMember = styled(Member)` + margin: 2px 0; + &:hover { + cursor: pointer; + background: rgba(${props => props.theme.colors.primary}); + } +`; + +export const Labels = styled.ul` + list-style: none; + margin: 0 8px; + padding: 0; + margin-bottom: 8px; +`; + +export const Label = styled.li` + position: relative; +`; + +export const CardLabel = styled.span<{ active: boolean; color: string }>` + ${props => + props.active && + css` + margin-left: 4px; + box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)}; + border-radius: 3px; + `} + + cursor: pointer; + font-weight: 700; + margin: 0 0 4px; + min-height: 20px; + padding: 6px 12px; + position: relative; + transition: padding 85ms, margin 85ms, box-shadow 85ms; + background-color: ${props => props.color}; + color: #fff; + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-height: 31px; +`; + +export const ActionsList = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +`; + +export const ActionItem = styled.li` + position: relative; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + font-size: 14px; + &:hover { + background: rgb(115, 103, 240); + } +`; + +export const ActionTitle = styled.span` + margin-left: 20px; +`; + +const ActionItemSeparator = styled.li` + color: rgba(${props => props.theme.colors.text.primary}, 0.4); + font-size: 12px; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.75rem; + padding-bottom: 0.25rem; +`; + +const ActiveIcon = styled(Checkmark)` + position: absolute; + right: 4px; +`; + +const ItemIcon = styled.div` + position: absolute; +`; + +const TaskNameInput = styled(Input)` + margin: 0; +`; + +const ActionItemLine = styled.div` + height: 1px; + border-top: 1px solid #414561; + margin: 0.25rem !important; +`; + +type FilterMetaProps = { + filters: TaskMetaFilters; + userID: string; + labels: React.RefObject>; + members: React.RefObject>; + onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void; +}; + +const FilterMeta: React.FC = ({ filters, onChangeTaskMetaFilter, userID, labels, members }) => { + const [currentFilters, setFilters] = useState(filters); + const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : ''); + const [currentLabel, setCurrentLabel] = useState(''); + + const handleSetFilters = (f: TaskMetaFilters) => { + setFilters(f); + onChangeTaskMetaFilter(f); + }; + + const handleNameChange = (nFilter: string) => { + handleSetFilters( + produce(currentFilters, draftFilters => { + draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null; + }), + ); + setNameFilter(nFilter); + }; + + const { setTab } = usePopup(); + + const handleSetDueDate = (filterType: DueDateFilterType, label: string) => { + handleSetFilters( + produce(currentFilters, draftFilters => { + if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) { + draftFilters.dueDate = null; + } else { + draftFilters.dueDate = { + label, + type: filterType, + }; + } + }), + ); + }; + + return ( + <> + + + handleNameChange(e.currentTarget.value)} + value={nameFilter} + variant="alternate" + placeholder="Task name..." + /> + QUICK ADD + { + handleSetFilters( + produce(currentFilters, draftFilters => { + if (members.current) { + const member = members.current.find(m => m.id === userID); + const draftMember = draftFilters.members.find(m => m.id === userID); + if (member && !draftMember) { + draftFilters.members.push({ id: userID, username: member.username ? member.username : '' }); + } else { + draftFilters.members = draftFilters.members.filter(m => m.id !== userID); + } + } + }), + ); + }} + > + + + + Just my tasks + {currentFilters.members.find(m => m.id === userID) && } + + handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}> + + + + Due this week + {currentFilters.dueDate && currentFilters.dueDate.type === DueDateFilterType.THIS_WEEK && ( + + )} + + handleSetDueDate(DueDateFilterType.NEXT_WEEK, 'Due next week')}> + + + + Due next week + {currentFilters.dueDate && currentFilters.dueDate.type === DueDateFilterType.NEXT_WEEK && ( + + )} + + + setTab(1)}> + + + + By Label + + setTab(2)}> + + + + By Member + + setTab(3)}> + + + + By Due Date + + + + + + {labels.current && + labels.current + // .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase()))) + .map(label => ( + + ))} + + + + + {members.current && + members.current.map(member => ( + { + handleSetFilters( + produce(currentFilters, draftFilters => { + if (draftFilters.members.find(m => m.id === member.id)) { + draftFilters.members = draftFilters.members.filter(m => m.id !== member.id); + } else { + draftFilters.members.push({ id: member.id, username: member.username ?? '' }); + } + }), + ); + }} + /> + ))} + + + + + handleSetDueDate(DueDateFilterType.TODAY, 'Today')}> + Today + + handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}> + This week + + handleSetDueDate(DueDateFilterType.NEXT_WEEK, 'Due next week')}> + Next week + + handleSetDueDate(DueDateFilterType.OVERDUE, 'Overdue')}> + Overdue + + + handleSetDueDate(DueDateFilterType.TOMORROW, 'In the next day')}> + In the next day + + handleSetDueDate(DueDateFilterType.ONE_WEEK, 'In the next week')}> + In the next week + + handleSetDueDate(DueDateFilterType.TWO_WEEKS, 'In the next two weeks')}> + In the next two weeks + + handleSetDueDate(DueDateFilterType.THREE_WEEKS, 'In the next three weeks')}> + In the next three weeks + + handleSetDueDate(DueDateFilterType.NO_DUE_DATE, 'Has no due date')}> + Has no due date + + + + + ); +}; + +export default FilterMeta; diff --git a/frontend/src/Projects/Project/Board/FilterStatus.tsx b/frontend/src/Projects/Project/Board/FilterStatus.tsx new file mode 100644 index 0000000..4fd7b0f --- /dev/null +++ b/frontend/src/Projects/Project/Board/FilterStatus.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Checkmark } from 'shared/icons'; +import { TaskStatusFilter, TaskStatus, TaskSince } from 'shared/components/Lists'; + +export const ActionsList = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +`; + +export const ActionExtraMenuContainer = styled.div` + visibility: hidden; + position: absolute; + left: 100%; + top: -4px; + padding-left: 2px; + width: 100%; +`; + +export const ActionItem = styled.li` + position: relative; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + font-size: 14px; + &:hover { + background: rgb(115, 103, 240); + } + &:hover ${ActionExtraMenuContainer} { + visibility: visible; + } +`; + +export const ActionTitle = styled.span` + margin-left: 20px; +`; + +export const ActionExtraMenu = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + + padding: 5px; + padding-top: 8px; + border-radius: 5px; + box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1); + + color: #c2c6dc; + background: #262c49; + border: 1px solid rgba(0, 0, 0, 0.1); + border-color: #414561; +`; + +export const ActionExtraMenuItem = styled.li` + position: relative; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + font-size: 14px; + &:hover { + background: rgb(115, 103, 240); + } +`; +const ActionExtraMenuSeparator = styled.li` + color: rgba(${props => props.theme.colors.text.primary}, 0.4); + font-size: 12px; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +`; + +const ActiveIcon = styled(Checkmark)` + position: absolute; +`; + +type FilterStatusProps = { + filter: TaskStatusFilter; + onChangeTaskStatusFilter: (filter: TaskStatusFilter) => void; +}; + +const FilterStatus: React.FC = ({ filter, onChangeTaskStatusFilter }) => { + const [currentFilter, setFilter] = useState(filter); + const handleFilterChange = (f: TaskStatusFilter) => { + setFilter(f); + onChangeTaskStatusFilter(f); + }; + const handleCompleteClick = (s: TaskSince) => { + handleFilterChange({ status: TaskStatus.COMPLETE, since: s }); + }; + return ( + + handleFilterChange({ status: TaskStatus.INCOMPLETE, since: TaskSince.ALL })}> + {currentFilter.status === TaskStatus.INCOMPLETE && } + Incomplete Tasks + + + {currentFilter.status === TaskStatus.COMPLETE && } + Compelete Tasks + + + handleCompleteClick(TaskSince.ALL)}> + {currentFilter.since === TaskSince.ALL && } + All completed tasks + + Marked complete since + handleCompleteClick(TaskSince.TODAY)}> + {currentFilter.since === TaskSince.TODAY && } + Today + + handleCompleteClick(TaskSince.YESTERDAY)}> + {currentFilter.since === TaskSince.YESTERDAY && } + Yesterday + + handleCompleteClick(TaskSince.ONE_WEEK)}> + {currentFilter.since === TaskSince.ONE_WEEK && } + 1 week + + handleCompleteClick(TaskSince.TWO_WEEKS)}> + {currentFilter.since === TaskSince.TWO_WEEKS && } + 2 weeks + + handleCompleteClick(TaskSince.THREE_WEEKS)}> + {currentFilter.since === TaskSince.THREE_WEEKS && } + 3 weeks + + + + + handleFilterChange({ status: TaskStatus.ALL, since: TaskSince.ALL })}> + {currentFilter.status === TaskStatus.ALL && } + All Tasks + + + ); +}; + +export default FilterStatus; diff --git a/frontend/src/Projects/Project/Board/SortPopup.tsx b/frontend/src/Projects/Project/Board/SortPopup.tsx new file mode 100644 index 0000000..9407123 --- /dev/null +++ b/frontend/src/Projects/Project/Board/SortPopup.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/components/Lists'; + +export const ActionsList = styled.ul` + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +`; + +export const ActionItem = styled.li` + position: relative; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + cursor: pointer; + display: flex; + align-items: center; + font-size: 14px; + &:hover { + background: rgb(115, 103, 240); + } +`; + +export const ActionTitle = styled.span` + margin-left: 20px; +`; + +const ActionItemSeparator = styled.li` + color: rgba(${props => props.theme.colors.text.primary}, 0.4); + font-size: 12px; + padding-left: 4px; + padding-right: 4px; + padding-top: 0.75rem; + padding-bottom: 0.25rem; +`; + +type SortPopupProps = { + sorting: TaskSorting; + onChangeTaskSorting: (taskSorting: TaskSorting) => void; +}; + +const SortPopup: React.FC = ({ sorting, onChangeTaskSorting }) => { + const [currentSorting, setSorting] = useState(sorting); + const handleSetSorting = (s: TaskSorting) => { + setSorting(s); + onChangeTaskSorting(s); + }; + return ( + + handleSetSorting({ type: TaskSortingType.NONE, direction: TaskSortingDirection.ASC })}> + None + + handleSetSorting({ type: TaskSortingType.DUE_DATE, direction: TaskSortingDirection.ASC })} + > + Due date + + handleSetSorting({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })} + > + Members + + handleSetSorting({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })} + > + Labels + + handleSetSorting({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })} + > + Task title + + + ); +}; + +export default SortPopup; diff --git a/frontend/src/Projects/Project/Board/index.tsx b/frontend/src/Projects/Project/Board/index.tsx index e349b59..206ddb7 100644 --- a/frontend/src/Projects/Project/Board/index.tsx +++ b/frontend/src/Projects/Project/Board/index.tsx @@ -26,13 +26,85 @@ import { import QuickCardEditor from 'shared/components/QuickCardEditor'; import ListActions from 'shared/components/ListActions'; import MemberManager from 'shared/components/MemberManager'; -import SimpleLists from 'shared/components/Lists'; +import SimpleLists, { + TaskStatus, + TaskSince, + TaskStatusFilter, + TaskMeta, + TaskMetaMatch, + TaskMetaFilters, + TaskSorting, + TaskSortingType, + TaskSortingDirection, +} from 'shared/components/Lists'; import produce from 'immer'; import MiniProfile from 'shared/components/MiniProfile'; import DueDateManager from 'shared/components/DueDateManager'; 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 { useCurrentUser } from 'App/context'; +import FilterStatus from './FilterStatus'; +import FilterMeta from './FilterMeta'; +import SortPopup from './SortPopup'; + +type MetaFilterCloseFn = (meta: TaskMeta, key: string) => void; + +const renderTaskSortingLabel = (sorting: TaskSorting) => { + if (sorting.type === TaskSortingType.TASK_TITLE) { + return 'Sort: Card title'; + } + if (sorting.type === TaskSortingType.MEMBERS) { + return 'Sort: Members'; + } + if (sorting.type === TaskSortingType.DUE_DATE) { + return 'Sort: Due Date'; + } + if (sorting.type === TaskSortingType.LABELS) { + return 'Sort: Labels'; + } + return 'Sort'; +}; + +const renderMetaFilters = (filters: TaskMetaFilters, onClose: MetaFilterCloseFn) => { + const filterChips = []; + if (filters.taskName) { + filterChips.push( + onClose(TaskMeta.TITLE, 'task-name')} + />, + ); + } + + if (filters.dueDate) { + filterChips.push( + onClose(TaskMeta.DUE_DATE, 'due-date')} />, + ); + } + for (const memberFilter of filters.members) { + filterChips.push( + onClose(TaskMeta.MEMBER, memberFilter.id)} + />, + ); + } + for (const labelFilter of filters.labels) { + filterChips.push( + onClose(TaskMeta.LABEL, labelFilter.id)} + />, + ); + } + return filterChips; +}; const ProjectBar = styled.div` display: flex; @@ -47,7 +119,7 @@ const ProjectActions = styled.div` align-items: center; `; -const ProjectAction = styled.div<{ disabled?: boolean }>` +const ProjectActionWrapper = styled.div<{ disabled?: boolean }>` cursor: pointer; display: flex; align-items: center; @@ -74,6 +146,25 @@ const ProjectActionText = styled.span` padding-left: 4px; `; +type ProjectActionProps = { + onClick?: (target: React.RefObject) => void; + disabled?: boolean; +}; + +const ProjectAction: React.FC = ({ onClick, disabled = false, children }) => { + const $container = useRef(null); + const handleClick = () => { + if (onClick) { + onClick($container); + } + }; + return ( + + {children} + + ); +}; + interface QuickCardEditorState { isOpen: boolean; target: React.RefObject | null; @@ -99,18 +190,18 @@ export const BoardLoading = () => { <> - + All Tasks - - - Filter - - + Sort + + + Filter + @@ -132,16 +223,37 @@ export const BoardLoading = () => { ); }; +const initTaskStatusFilter: TaskStatusFilter = { + status: TaskStatus.ALL, + since: TaskSince.ALL, +}; + +const initTaskMetaFilters: TaskMetaFilters = { + match: TaskMetaMatch.MATCH_ANY, + dueDate: null, + taskName: null, + labels: [], + members: [], +}; + +const initTaskSorting: TaskSorting = { + type: TaskSortingType.NONE, + direction: TaskSortingDirection.ASC, +}; + const ProjectBoard: React.FC = ({ projectID, onCardLabelClick, cardLabelVariant }) => { const [assignTask] = useAssignTaskMutation(); const [unassignTask] = useUnassignTaskMutation(); - const $labelsRef = useRef(null); const match = useRouteMatch(); const labelsRef = useRef>([]); + const membersRef = useRef>([]); const { showPopup, hidePopup } = usePopup(); const taskLabelsRef = useRef>([]); const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({}); + const [taskStatusFilter, setTaskStatusFilter] = useState(initTaskStatusFilter); + const [taskMetaFilters, setTaskMetaFilters] = useState(initTaskMetaFilters); + const [taskSorting, setTaskSorting] = useState(initTaskSorting); const history = useHistory(); const [deleteTaskGroup] = useDeleteTaskGroupMutation({ update: (client, deletedTaskGroupData) => { @@ -225,6 +337,7 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick ); }, }); + const { user } = useCurrentUser(); const [deleteTask] = useDeleteTaskMutation(); const [toggleTaskLabel] = useToggleTaskLabelMutation({ onCompleted: newTaskLabel => { @@ -254,6 +367,7 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick id: `${Math.round(Math.random() * -1000000)}`, name, complete: false, + completedAt: null, taskGroup: { __typename: 'TaskGroup', id: taskGroup.id, @@ -290,8 +404,18 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick if (loading) { return ; } - if (data) { + const getTaskStatusFilterLabel = (filter: TaskStatusFilter) => { + if (filter.status === TaskStatus.COMPLETE) { + return 'Complete'; + } + if (filter.status === TaskStatus.INCOMPLETE) { + return 'Incomplete'; + } + return 'All Tasks'; + }; + if (data && user) { labelsRef.current = data.findProject.labels; + membersRef.current = data.findProject.members; const onQuickEditorOpen = ($target: React.RefObject, taskID: string, taskGroupID: string) => { const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID); const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null; @@ -315,23 +439,84 @@ const ProjectBoard: React.FC = ({ projectID, onCardLabelClick <> - + { + showPopup( + target, + + { + setTaskStatusFilter(filter); + hidePopup(); + }} + /> + , + 185, + ); + }} + > - All Tasks + {getTaskStatusFilterLabel(taskStatusFilter)} - + { + showPopup( + target, + + { + setTaskSorting(sorting); + }} + /> + , + 185, + ); + }} + > + + {renderTaskSortingLabel(taskSorting)} + + { + showPopup( + target, + { + setTaskMetaFilters(filter); + }} + userID={user?.id} + labels={labelsRef} + members={membersRef} + />, + 200, + ); + }} + > Filter - - - Sort - + {renderMetaFilters(taskMetaFilters, (meta, id) => { + setTaskMetaFilters( + produce(taskMetaFilters, draftFilters => { + if (meta === TaskMeta.MEMBER) { + draftFilters.members = draftFilters.members.filter(m => m.id !== id); + } else if (meta === TaskMeta.LABEL) { + draftFilters.labels = draftFilters.labels.filter(m => m.id !== id); + } else if (meta === TaskMeta.TITLE) { + draftFilters.taskName = null; + } else if (meta === TaskMeta.DUE_DATE) { + draftFilters.dueDate = null; + } + }), + ); + })} { + onClick={$labelsRef => { showPopup( $labelsRef, = ({ projectID, onCardLabelClick }); }} taskGroups={data.findProject.taskGroups} + taskStatusFilter={taskStatusFilter} + taskMetaFilters={taskMetaFilters} + taskSorting={taskSorting} onCreateTask={onCreateTask} onCreateTaskGroup={onCreateList} onCardMemberClick={($targetRef, _taskID, memberID) => { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 477d007..085471f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,6 +8,7 @@ import { HttpLink } from 'apollo-link-http'; import { onError } from 'apollo-link-error'; import { enableMapSet } from 'immer'; import { ApolloLink, Observable, fromPromise } from 'apollo-link'; +import moment from 'moment'; import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken'; import cache from './App/cache'; import App from './App'; @@ -15,6 +16,13 @@ import App from './App'; // https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8 enableMapSet(); +moment.updateLocale('en', { + week: { + dow: 1, // First day of week is Monday + doy: 7, // First week of year must contain 1 January (7 + 1 - 1) + }, +}); + let forward$; let isRefreshing = false; let pendingRequests: any = []; diff --git a/frontend/src/shared/components/Admin/index.tsx b/frontend/src/shared/components/Admin/index.tsx index 943e80b..13dbd07 100644 --- a/frontend/src/shared/components/Admin/index.tsx +++ b/frontend/src/shared/components/Admin/index.tsx @@ -430,6 +430,7 @@ const TabNavItem = styled.li` display: block; position: relative; `; + const TabNavItemButton = styled.button<{ active: boolean }>` cursor: pointer; display: flex; @@ -450,6 +451,10 @@ const TabNavItemButton = styled.button<{ active: boolean }>` fill: rgba(115, 103, 240); } `; +const TabItemUser = styled(User)<{ active: boolean }>` +fill: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')} +stroke: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')} +`; const TabNavItemSpan = styled.span` text-align: left; @@ -512,7 +517,7 @@ const NavItem: React.FC = ({ active, name, tab, onClick }) => { }} > - + {name} diff --git a/frontend/src/shared/components/Chip/index.tsx b/frontend/src/shared/components/Chip/index.tsx new file mode 100644 index 0000000..57c79aa --- /dev/null +++ b/frontend/src/shared/components/Chip/index.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { Cross } from 'shared/icons'; + +const LabelText = styled.span` + margin-left: 10px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(${props => props.theme.colors.text.primary}); +`; + +const Container = styled.div<{ color?: string }>` + margin: 0.75rem; + min-height: 26px; + min-width: 26px; + font-size: 0.8rem; + border-radius: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + ${props => + props.color + ? css` + background: ${props.color}; + & ${LabelText} { + color: rgba(${props.theme.colors.text.secondary}); + } + ` + : css` + background: rgba(${props.theme.colors.bg.primary}); + `} +`; + +const CloseButton = styled.button` + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin: 0 4px; + background: rgba(0, 0, 0, 0.15); + &:hover { + background: rgba(0, 0, 0, 0.25); + } +`; + +type ChipProps = { + label: string; + onClose?: () => void; + color?: string; +}; + +const Chip: React.FC = ({ label, onClose, color }) => { + return ( + + {label} + {onClose && ( + onClose()}> + + + )} + + ); +}; + +export default Chip; diff --git a/frontend/src/shared/components/ControlledInput/Input.stories.tsx b/frontend/src/shared/components/ControlledInput/Input.stories.tsx index e46da50..2ff7545 100644 --- a/frontend/src/shared/components/ControlledInput/Input.stories.tsx +++ b/frontend/src/shared/components/ControlledInput/Input.stories.tsx @@ -35,7 +35,7 @@ export const Default = () => { - } width="100%" placeholder="Placeholder" /> + } width="100%" placeholder="Placeholder" /> diff --git a/frontend/src/shared/components/DropdownMenu/index.tsx b/frontend/src/shared/components/DropdownMenu/index.tsx index 6a60fa4..f165695 100644 --- a/frontend/src/shared/components/DropdownMenu/index.tsx +++ b/frontend/src/shared/components/DropdownMenu/index.tsx @@ -18,7 +18,7 @@ const DropdownMenu: React.FC = ({ left, top, onLogout, onClos - + Profile @@ -54,7 +54,7 @@ const ProfileMenu: React.FC = ({ showAdminConsole, onAdminCons )} - + Profile diff --git a/frontend/src/shared/components/Input/Input.stories.tsx b/frontend/src/shared/components/Input/Input.stories.tsx index e46da50..2ff7545 100644 --- a/frontend/src/shared/components/Input/Input.stories.tsx +++ b/frontend/src/shared/components/Input/Input.stories.tsx @@ -35,7 +35,7 @@ export const Default = () => { - } width="100%" placeholder="Placeholder" /> + } width="100%" placeholder="Placeholder" /> diff --git a/frontend/src/shared/components/Lists/index.tsx b/frontend/src/shared/components/Lists/index.tsx index 18edb48..154974f 100644 --- a/frontend/src/shared/components/Lists/index.tsx +++ b/frontend/src/shared/components/Lists/index.tsx @@ -13,6 +13,249 @@ import { import moment from 'moment'; import { Container, BoardContainer, BoardWrapper } from './Styles'; +import shouldMetaFilter from './metaFilter'; + +export enum TaskMeta { + NONE, + TITLE, + MEMBER, + LABEL, + DUE_DATE, +} + +export enum TaskMetaMatch { + MATCH_ANY, + MATCH_ALL, +} + +export enum TaskStatus { + ALL, + COMPLETE, + INCOMPLETE, +} + +export enum TaskSince { + ALL, + TODAY, + YESTERDAY, + ONE_WEEK, + TWO_WEEKS, + THREE_WEEKS, +} + +export type TaskStatusFilter = { + status: TaskStatus; + since: TaskSince; +}; + +export interface TaskMetaFilterName { + meta: TaskMeta; + value?: string | moment.Moment | null; + id?: string | null; +} + +export type TaskNameMetaFilter = { + name: string; +}; + +export enum DueDateFilterType { + TODAY, + TOMORROW, + THIS_WEEK, + NEXT_WEEK, + ONE_WEEK, + TWO_WEEKS, + THREE_WEEKS, + OVERDUE, + NO_DUE_DATE, +} + +export type DueDateMetaFilter = { + type: DueDateFilterType; + label: string; +}; + +export type MemberMetaFilter = { + id: string; + username: string; +}; + +export type LabelMetaFilter = { + id: string; + name: string; + color: string; +}; + +export type TaskMetaFilters = { + match: TaskMetaMatch; + dueDate: DueDateMetaFilter | null; + taskName: TaskNameMetaFilter | null; + members: Array; + 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; + } + + if (filter.status === TaskStatus.INCOMPLETE && task.complete === false) { + return true; + } + if (filter.status === TaskStatus.COMPLETE && task.completedAt && task.complete === true) { + const completedAt = moment(task.completedAt); + const REFERENCE = moment(); // fixed just for testing, use moment(); + switch (filter.since) { + case TaskSince.TODAY: + const TODAY = REFERENCE.clone().startOf('day'); + return completedAt.isSame(TODAY, 'd'); + case TaskSince.YESTERDAY: + const YESTERDAY = REFERENCE.clone() + .subtract(1, 'days') + .startOf('day'); + return completedAt.isSameOrAfter(YESTERDAY, 'd'); + case TaskSince.ONE_WEEK: + const ONE_WEEK = REFERENCE.clone() + .subtract(7, 'days') + .startOf('day'); + return completedAt.isSameOrAfter(ONE_WEEK, 'd'); + case TaskSince.TWO_WEEKS: + const TWO_WEEKS = REFERENCE.clone() + .subtract(14, 'days') + .startOf('day'); + return completedAt.isSameOrAfter(TWO_WEEKS, 'd'); + case TaskSince.THREE_WEEKS: + const THREE_WEEKS = REFERENCE.clone() + .subtract(21, 'days') + .startOf('day'); + return completedAt.isSameOrAfter(THREE_WEEKS, 'd'); + default: + return true; + } + } + return false; +} interface SimpleProps { taskGroups: Array; @@ -28,8 +271,29 @@ interface SimpleProps { onCardMemberClick: OnCardMemberClick; onCardLabelClick: () => void; cardLabelVariant: CardLabelVariant; + taskStatusFilter?: TaskStatusFilter; + taskMetaFilters?: TaskMetaFilters; + taskSorting?: TaskSorting; } +const initTaskStatusFilter: TaskStatusFilter = { + status: TaskStatus.ALL, + since: TaskSince.ALL, +}; + +const initTaskMetaFilters: TaskMetaFilters = { + match: TaskMetaMatch.MATCH_ANY, + dueDate: null, + taskName: null, + labels: [], + members: [], +}; + +const initTaskSorting: TaskSorting = { + type: TaskSortingType.NONE, + direction: TaskSortingDirection.ASC, +}; + const SimpleLists: React.FC = ({ taskGroups, onTaskDrop, @@ -43,6 +307,9 @@ const SimpleLists: React.FC = ({ cardLabelVariant, onExtraMenuOpen, onCardMemberClick, + taskStatusFilter = initTaskStatusFilter, + taskMetaFilters = initTaskMetaFilters, + taskSorting = initTaskSorting, }) => { const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => { if (typeof destination === 'undefined') return; @@ -164,10 +431,18 @@ const SimpleLists: React.FC = ({ {taskGroup.tasks .slice() + .filter(t => shouldStatusFilter(t, taskStatusFilter)) + .filter(t => shouldMetaFilter(t, taskMetaFilters)) .sort((a: any, b: any) => a.position - b.position) + .sort((a: any, b: any) => sortTasks(a, b, taskSorting)) .map((task: Task, taskIndex: any) => { return ( - + {taskProvided => { return ( m.id === member.id) !== -1) { + isFiltered = ShouldFilter.VALID; + } + } + } + } + if (filters.labels.length !== 0) { + if (isFiltered === ShouldFilter.NO_FILTER) { + isFiltered = ShouldFilter.NOT_VALID; + } + for (const label of filters.labels) { + if (task.labels) { + if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) { + isFiltered = ShouldFilter.VALID; + } + } + } + } + if (isFiltered === ShouldFilter.NO_FILTER) { + return true; + } + if (isFiltered === ShouldFilter.VALID) { + return true; + } + return false; +} diff --git a/frontend/src/shared/components/Login/index.tsx b/frontend/src/shared/components/Login/index.tsx index e3c04ff..c339cd2 100644 --- a/frontend/src/shared/components/Login/index.tsx +++ b/frontend/src/shared/components/Login/index.tsx @@ -53,7 +53,7 @@ const Login = ({ onSubmit }: LoginProps) => { ref={register({ required: 'Username is required' })} /> - + {errors.username && {errors.username.message}} diff --git a/frontend/src/shared/components/Register/index.tsx b/frontend/src/shared/components/Register/index.tsx index d8107b4..5704028 100644 --- a/frontend/src/shared/components/Register/index.tsx +++ b/frontend/src/shared/components/Register/index.tsx @@ -55,7 +55,7 @@ const Register = ({ onSubmit }: RegisterProps) => { ref={register({ required: 'Full name is required' })} /> - + {errors.username && {errors.username.message}} @@ -68,7 +68,7 @@ const Register = ({ onSubmit }: RegisterProps) => { ref={register({ required: 'Username is required' })} /> - + {errors.username && {errors.username.message}} @@ -84,7 +84,7 @@ const Register = ({ onSubmit }: RegisterProps) => { })} /> - + {errors.email && {errors.email.message}} @@ -103,7 +103,7 @@ const Register = ({ onSubmit }: RegisterProps) => { })} /> - + {errors.initials && {errors.initials.message}} diff --git a/frontend/src/shared/components/Settings/index.tsx b/frontend/src/shared/components/Settings/index.tsx index 4433d0a..0c5af89 100644 --- a/frontend/src/shared/components/Settings/index.tsx +++ b/frontend/src/shared/components/Settings/index.tsx @@ -218,7 +218,7 @@ const NavItem: React.FC = ({ active, name, tab, onClick }) => { }} > - + {name} diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx index 54b83e1..aeb813b 100644 --- a/frontend/src/shared/generated/graphql.tsx +++ b/frontend/src/shared/generated/graphql.tsx @@ -162,6 +162,7 @@ export type Task = { description?: Maybe; dueDate?: Maybe; complete: Scalars['Boolean']; + completedAt?: Maybe; assigned: Array; labels: Array; checklists: Array; @@ -1189,7 +1190,7 @@ export type FindTaskQuery = ( export type TaskFieldsFragment = ( { __typename?: 'Task' } - & Pick + & Pick & { badges: ( { __typename?: 'TaskBadges' } & { checklist?: Maybe<( @@ -2013,6 +2014,7 @@ export const TaskFieldsFragmentDoc = gql` description dueDate complete + completedAt position badges { checklist { diff --git a/frontend/src/shared/graphql/fragments/task.ts b/frontend/src/shared/graphql/fragments/task.ts index 0cb2032..85f5b3f 100644 --- a/frontend/src/shared/graphql/fragments/task.ts +++ b/frontend/src/shared/graphql/fragments/task.ts @@ -7,6 +7,7 @@ const TASK_FRAGMENT = gql` description dueDate complete + completedAt position badges { checklist { diff --git a/frontend/src/shared/icons/Calendar.tsx b/frontend/src/shared/icons/Calendar.tsx new file mode 100644 index 0000000..1cbd229 --- /dev/null +++ b/frontend/src/shared/icons/Calendar.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Icon, { IconProps } from './Icon'; + +const Calender: React.FC = ({ width = '16px', height = '16px', className }) => { + return ( + + + + ); +}; + +export default Calender; diff --git a/frontend/src/shared/icons/User.tsx b/frontend/src/shared/icons/User.tsx index e03c659..510c4ec 100644 --- a/frontend/src/shared/icons/User.tsx +++ b/frontend/src/shared/icons/User.tsx @@ -1,21 +1,12 @@ import React from 'react'; +import Icon, { IconProps } from './Icon'; -type Props = { - size: number | string; - color: string; -}; - -const User = ({ size, color }: Props) => { +const User: React.FC = ({ width = '16px', height = '16px', className, onClick }) => { return ( - - - + + + ); }; -User.defaultProps = { - size: 16, - color: '#000', -}; - export default User; diff --git a/frontend/src/shared/icons/index.ts b/frontend/src/shared/icons/index.ts index 66839e8..1254f4a 100644 --- a/frontend/src/shared/icons/index.ts +++ b/frontend/src/shared/icons/index.ts @@ -1,5 +1,6 @@ import Cross from './Cross'; import Cog from './Cog'; +import Calendar from './Calendar'; import Sort from './Sort'; import Filter from './Filter'; import DoubleChevronUp from './DoubleChevronUp'; @@ -72,4 +73,5 @@ export { UserPlus, Crown, ToggleOn, + Calendar, }; diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 44bd43b..65c3340 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -64,6 +64,7 @@ type Task = { position: number; dueDate?: string; complete?: boolean; + completedAt?: string | null; labels: TaskLabel[]; description?: string | null; assigned?: Array; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 34a5c0a..a34906b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -12,6 +12,7 @@ "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, + "downlevelIteration": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, diff --git a/internal/db/models.go b/internal/db/models.go index 76bd415..bad118a 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -72,6 +72,7 @@ type Task struct { Description sql.NullString `json:"description"` DueDate sql.NullTime `json:"due_date"` Complete bool `json:"complete"` + CompletedAt sql.NullTime `json:"completed_at"` } type TaskAssigned struct { diff --git a/internal/db/query/task.sql b/internal/db/query/task.sql index 228ffcd..122584f 100644 --- a/internal/db/query/task.sql +++ b/internal/db/query/task.sql @@ -30,7 +30,7 @@ DELETE FROM task where task_group_id = $1; UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING *; -- name: SetTaskComplete :one -UPDATE task SET complete = $2 WHERE task_id = $1 RETURNING *; +UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING *; -- name: GetProjectIDForTask :one SELECT project_id FROM task diff --git a/internal/db/task.sql.go b/internal/db/task.sql.go index 0f7caec..85ab6e9 100644 --- a/internal/db/task.sql.go +++ b/internal/db/task.sql.go @@ -13,7 +13,7 @@ import ( const createTask = `-- name: CreateTask :one INSERT INTO task (task_group_id, created_at, name, position) - VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete + VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type CreateTaskParams struct { @@ -40,6 +40,7 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } @@ -66,7 +67,7 @@ func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid } const getAllTasks = `-- name: GetAllTasks :many -SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete FROM task +SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task ` func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) { @@ -87,6 +88,7 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) { &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ); err != nil { return nil, err } @@ -115,7 +117,7 @@ func (q *Queries) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uu } const getTaskByID = `-- name: GetTaskByID :one -SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete FROM task WHERE task_id = $1 +SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task WHERE task_id = $1 ` func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) { @@ -130,12 +132,13 @@ func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, erro &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many -SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete FROM task WHERE task_group_id = $1 +SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task WHERE task_group_id = $1 ` func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) { @@ -156,6 +159,7 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ); err != nil { return nil, err } @@ -171,16 +175,17 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U } const setTaskComplete = `-- name: SetTaskComplete :one -UPDATE task SET complete = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete +UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type SetTaskCompleteParams struct { - TaskID uuid.UUID `json:"task_id"` - Complete bool `json:"complete"` + TaskID uuid.UUID `json:"task_id"` + Complete bool `json:"complete"` + CompletedAt sql.NullTime `json:"completed_at"` } func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error) { - row := q.db.QueryRowContext(ctx, setTaskComplete, arg.TaskID, arg.Complete) + row := q.db.QueryRowContext(ctx, setTaskComplete, arg.TaskID, arg.Complete, arg.CompletedAt) var i Task err := row.Scan( &i.TaskID, @@ -191,12 +196,13 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } const updateTaskDescription = `-- name: UpdateTaskDescription :one -UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete +UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type UpdateTaskDescriptionParams struct { @@ -216,12 +222,13 @@ func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescr &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } const updateTaskDueDate = `-- name: UpdateTaskDueDate :one -UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete +UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type UpdateTaskDueDateParams struct { @@ -241,12 +248,13 @@ func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDatePa &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } const updateTaskLocation = `-- name: UpdateTaskLocation :one -UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete +UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type UpdateTaskLocationParams struct { @@ -267,12 +275,13 @@ func (q *Queries) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocation &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } const updateTaskName = `-- name: UpdateTaskName :one -UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete +UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at ` type UpdateTaskNameParams struct { @@ -292,6 +301,7 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) &i.Description, &i.DueDate, &i.Complete, + &i.CompletedAt, ) return i, err } diff --git a/internal/graph/generated.go b/internal/graph/generated.go index e654135..48806b7 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -275,6 +275,7 @@ type ComplexityRoot struct { Badges func(childComplexity int) int Checklists func(childComplexity int) int Complete func(childComplexity int) int + CompletedAt func(childComplexity int) int CreatedAt func(childComplexity int) int Description func(childComplexity int) int DueDate func(childComplexity int) int @@ -479,6 +480,7 @@ type TaskResolver interface { Description(ctx context.Context, obj *db.Task) (*string, error) DueDate(ctx context.Context, obj *db.Task) (*time.Time, error) + CompletedAt(ctx context.Context, obj *db.Task) (*time.Time, error) Assigned(ctx context.Context, obj *db.Task) ([]Member, error) Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error) Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error) @@ -1732,6 +1734,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Task.Complete(childComplexity), true + case "Task.completedAt": + if e.complexity.Task.CompletedAt == nil { + break + } + + return e.complexity.Task.CompletedAt(childComplexity), true + case "Task.createdAt": if e.complexity.Task.CreatedAt == nil { break @@ -2347,6 +2356,7 @@ type Task { description: String dueDate: Time complete: Boolean! + completedAt: Time assigned: [Member!]! labels: [TaskLabel!]! checklists: [TaskChecklist!]! @@ -2474,7 +2484,6 @@ type DeleteProjectPayload { project: Project! } - extend type Mutation { createProjectLabel(input: NewProjectLabel!): ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) @@ -2556,17 +2565,26 @@ type UpdateProjectMemberRolePayload { } extend type Mutation { - createTask(input: NewTask!): Task! - deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! + createTask(input: NewTask!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTask(input: DeleteTaskInput!): + DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - updateTaskDescription(input: UpdateTaskDescriptionInput!): Task! - updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload! - updateTaskName(input: UpdateTaskName!): Task! - setTaskComplete(input: SetTaskComplete!): Task! - updateTaskDueDate(input: UpdateTaskDueDate!): Task! + updateTaskDescription(input: UpdateTaskDescriptionInput!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskLocation(input: NewTaskLocation!): + UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskName(input: UpdateTaskName!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + setTaskComplete(input: SetTaskComplete!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskDueDate(input: UpdateTaskDueDate!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - assignTask(input: AssignTaskInput): Task! - unassignTask(input: UnassignTaskInput): Task! + assignTask(input: AssignTaskInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + unassignTask(input: UnassignTaskInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } input NewTask { @@ -2625,16 +2643,25 @@ input UpdateTaskName { } extend type Mutation { - createTaskChecklist(input: CreateTaskChecklist!): TaskChecklist! - deleteTaskChecklist(input: DeleteTaskChecklist!): DeleteTaskChecklistPayload! - updateTaskChecklistName(input: UpdateTaskChecklistName!): TaskChecklist! - createTaskChecklistItem(input: CreateTaskChecklistItem!): TaskChecklistItem! - updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem! - setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem! - deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload! + createTaskChecklist(input: CreateTaskChecklist!): + TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskChecklist(input: DeleteTaskChecklist!): + DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistName(input: UpdateTaskChecklistName!): + TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + createTaskChecklistItem(input: CreateTaskChecklistItem!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): + DeleteTaskChecklistItemPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): + UpdateTaskChecklistLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): + UpdateTaskChecklistItemLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload! - updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload! } input UpdateTaskChecklistItemLocation { @@ -2702,10 +2729,14 @@ type DeleteTaskChecklistPayload { } extend type Mutation { - createTaskGroup(input: NewTaskGroup!): TaskGroup! - updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! - updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup! - deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload! + createTaskGroup(input: NewTaskGroup!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskGroupLocation(input: NewTaskGroupLocation!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskGroupName(input: UpdateTaskGroupName!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskGroup(input: DeleteTaskGroupInput!): + DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } input NewTaskGroupLocation { @@ -2752,9 +2783,13 @@ type ToggleTaskLabelPayload { task: Task! } extend type Mutation { - addTaskLabel(input: AddTaskLabelInput): Task! - removeTaskLabel(input: RemoveTaskLabelInput): Task! - toggleTaskLabel(input: ToggleTaskLabelInput!): ToggleTaskLabelPayload! + addTaskLabel(input: AddTaskLabelInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + removeTaskLabel(input: RemoveTaskLabelInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + toggleTaskLabel(input: ToggleTaskLabelInput!): + ToggleTaskLabelPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + } extend type Mutation { @@ -2780,10 +2815,12 @@ type DeleteTeamPayload { } extend type Mutation { - createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) + createTeamMember(input: CreateTeamMember!): + CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) - deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) + deleteTeamMember(input: DeleteTeamMember!): + DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) } @@ -5986,8 +6023,40 @@ func (ec *executionContext) _Mutation_createTask(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTask(rctx, args["input"].(NewTask)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTask(rctx, args["input"].(NewTask)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6027,8 +6096,40 @@ func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTask(rctx, args["input"].(DeleteTaskInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTask(rctx, args["input"].(DeleteTaskInput)) + } + 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.(*DeleteTaskPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTaskPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6068,8 +6169,40 @@ func (ec *executionContext) _Mutation_updateTaskDescription(ctx context.Context, } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskDescription(rctx, args["input"].(UpdateTaskDescriptionInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskDescription(rctx, args["input"].(UpdateTaskDescriptionInput)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6109,8 +6242,40 @@ func (ec *executionContext) _Mutation_updateTaskLocation(ctx context.Context, fi } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskLocation(rctx, args["input"].(NewTaskLocation)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskLocation(rctx, args["input"].(NewTaskLocation)) + } + 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.(*UpdateTaskLocationPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateTaskLocationPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6150,8 +6315,40 @@ func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskName(rctx, args["input"].(UpdateTaskName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskName(rctx, args["input"].(UpdateTaskName)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6191,8 +6388,40 @@ func (ec *executionContext) _Mutation_setTaskComplete(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetTaskComplete(rctx, args["input"].(SetTaskComplete)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetTaskComplete(rctx, args["input"].(SetTaskComplete)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6232,8 +6461,40 @@ func (ec *executionContext) _Mutation_updateTaskDueDate(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskDueDate(rctx, args["input"].(UpdateTaskDueDate)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskDueDate(rctx, args["input"].(UpdateTaskDueDate)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6273,8 +6534,40 @@ func (ec *executionContext) _Mutation_assignTask(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().AssignTask(rctx, args["input"].(*AssignTaskInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AssignTask(rctx, args["input"].(*AssignTaskInput)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6314,8 +6607,40 @@ func (ec *executionContext) _Mutation_unassignTask(ctx context.Context, field gr } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UnassignTask(rctx, args["input"].(*UnassignTaskInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UnassignTask(rctx, args["input"].(*UnassignTaskInput)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6355,8 +6680,40 @@ func (ec *executionContext) _Mutation_createTaskChecklist(ctx context.Context, f } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTaskChecklist(rctx, args["input"].(CreateTaskChecklist)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTaskChecklist(rctx, args["input"].(CreateTaskChecklist)) + } + 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.(*db.TaskChecklist); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskChecklist`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6396,8 +6753,40 @@ func (ec *executionContext) _Mutation_deleteTaskChecklist(ctx context.Context, f } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTaskChecklist(rctx, args["input"].(DeleteTaskChecklist)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTaskChecklist(rctx, args["input"].(DeleteTaskChecklist)) + } + 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.(*DeleteTaskChecklistPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTaskChecklistPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6437,8 +6826,40 @@ func (ec *executionContext) _Mutation_updateTaskChecklistName(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskChecklistName(rctx, args["input"].(UpdateTaskChecklistName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskChecklistName(rctx, args["input"].(UpdateTaskChecklistName)) + } + 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.(*db.TaskChecklist); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskChecklist`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6478,8 +6899,40 @@ func (ec *executionContext) _Mutation_createTaskChecklistItem(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTaskChecklistItem(rctx, args["input"].(CreateTaskChecklistItem)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTaskChecklistItem(rctx, args["input"].(CreateTaskChecklistItem)) + } + 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.(*db.TaskChecklistItem); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskChecklistItem`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6519,8 +6972,40 @@ func (ec *executionContext) _Mutation_updateTaskChecklistItemName(ctx context.Co } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskChecklistItemName(rctx, args["input"].(UpdateTaskChecklistItemName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskChecklistItemName(rctx, args["input"].(UpdateTaskChecklistItemName)) + } + 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.(*db.TaskChecklistItem); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskChecklistItem`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6560,8 +7045,40 @@ func (ec *executionContext) _Mutation_setTaskChecklistItemComplete(ctx context.C } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetTaskChecklistItemComplete(rctx, args["input"].(SetTaskChecklistItemComplete)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetTaskChecklistItemComplete(rctx, args["input"].(SetTaskChecklistItemComplete)) + } + 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.(*db.TaskChecklistItem); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskChecklistItem`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6601,8 +7118,40 @@ func (ec *executionContext) _Mutation_deleteTaskChecklistItem(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTaskChecklistItem(rctx, args["input"].(DeleteTaskChecklistItem)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTaskChecklistItem(rctx, args["input"].(DeleteTaskChecklistItem)) + } + 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.(*DeleteTaskChecklistItemPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTaskChecklistItemPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6642,8 +7191,40 @@ func (ec *executionContext) _Mutation_updateTaskChecklistLocation(ctx context.Co } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskChecklistLocation(rctx, args["input"].(UpdateTaskChecklistLocation)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskChecklistLocation(rctx, args["input"].(UpdateTaskChecklistLocation)) + } + 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.(*UpdateTaskChecklistLocationPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateTaskChecklistLocationPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6683,8 +7264,40 @@ func (ec *executionContext) _Mutation_updateTaskChecklistItemLocation(ctx contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskChecklistItemLocation(rctx, args["input"].(UpdateTaskChecklistItemLocation)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskChecklistItemLocation(rctx, args["input"].(UpdateTaskChecklistItemLocation)) + } + 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.(*UpdateTaskChecklistItemLocationPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateTaskChecklistItemLocationPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6724,8 +7337,40 @@ func (ec *executionContext) _Mutation_createTaskGroup(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTaskGroup(rctx, args["input"].(NewTaskGroup)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTaskGroup(rctx, args["input"].(NewTaskGroup)) + } + 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.(*db.TaskGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6765,8 +7410,40 @@ func (ec *executionContext) _Mutation_updateTaskGroupLocation(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskGroupLocation(rctx, args["input"].(NewTaskGroupLocation)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskGroupLocation(rctx, args["input"].(NewTaskGroupLocation)) + } + 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.(*db.TaskGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6806,8 +7483,40 @@ func (ec *executionContext) _Mutation_updateTaskGroupName(ctx context.Context, f } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateTaskGroupName(rctx, args["input"].(UpdateTaskGroupName)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTaskGroupName(rctx, args["input"].(UpdateTaskGroupName)) + } + 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.(*db.TaskGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.TaskGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6847,8 +7556,40 @@ func (ec *executionContext) _Mutation_deleteTaskGroup(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteTaskGroup(rctx, args["input"].(DeleteTaskGroupInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteTaskGroup(rctx, args["input"].(DeleteTaskGroupInput)) + } + 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.(*DeleteTaskGroupPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.DeleteTaskGroupPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6888,8 +7629,40 @@ func (ec *executionContext) _Mutation_addTaskLabel(ctx context.Context, field gr } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().AddTaskLabel(rctx, args["input"].(*AddTaskLabelInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AddTaskLabel(rctx, args["input"].(*AddTaskLabelInput)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6929,8 +7702,40 @@ func (ec *executionContext) _Mutation_removeTaskLabel(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RemoveTaskLabel(rctx, args["input"].(*RemoveTaskLabelInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RemoveTaskLabel(rctx, args["input"].(*RemoveTaskLabelInput)) + } + 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.(*db.Task); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Task`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6970,8 +7775,40 @@ func (ec *executionContext) _Mutation_toggleTaskLabel(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ToggleTaskLabel(rctx, args["input"].(ToggleTaskLabelInput)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ToggleTaskLabel(rctx, args["input"].(ToggleTaskLabelInput)) + } + 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.(*ToggleTaskLabelPayload); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.ToggleTaskLabelPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -9413,6 +10250,37 @@ func (ec *executionContext) _Task_complete(ctx context.Context, field graphql.Co return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Task_completedAt(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Task", + Field: field, + Args: nil, + IsMethod: true, + } + + 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 ec.resolvers.Task().CompletedAt(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + func (ec *executionContext) _Task_assigned(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -15154,6 +16022,17 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } + case "completedAt": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Task_completedAt(ctx, field, obj) + return res + }) case "assigned": field := field out.Concurrently(i, func() (res graphql.Marshaler) { diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 83c5de9..873f881 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -129,6 +129,7 @@ type Task { description: String dueDate: Time complete: Boolean! + completedAt: Time assigned: [Member!]! labels: [TaskLabel!]! checklists: [TaskChecklist!]! @@ -256,7 +257,6 @@ type DeleteProjectPayload { project: Project! } - extend type Mutation { createProjectLabel(input: NewProjectLabel!): ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) @@ -338,17 +338,26 @@ type UpdateProjectMemberRolePayload { } extend type Mutation { - createTask(input: NewTask!): Task! - deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! + createTask(input: NewTask!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTask(input: DeleteTaskInput!): + DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - updateTaskDescription(input: UpdateTaskDescriptionInput!): Task! - updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload! - updateTaskName(input: UpdateTaskName!): Task! - setTaskComplete(input: SetTaskComplete!): Task! - updateTaskDueDate(input: UpdateTaskDueDate!): Task! + updateTaskDescription(input: UpdateTaskDescriptionInput!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskLocation(input: NewTaskLocation!): + UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskName(input: UpdateTaskName!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + setTaskComplete(input: SetTaskComplete!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskDueDate(input: UpdateTaskDueDate!): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - assignTask(input: AssignTaskInput): Task! - unassignTask(input: UnassignTaskInput): Task! + assignTask(input: AssignTaskInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + unassignTask(input: UnassignTaskInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } input NewTask { @@ -407,16 +416,25 @@ input UpdateTaskName { } extend type Mutation { - createTaskChecklist(input: CreateTaskChecklist!): TaskChecklist! - deleteTaskChecklist(input: DeleteTaskChecklist!): DeleteTaskChecklistPayload! - updateTaskChecklistName(input: UpdateTaskChecklistName!): TaskChecklist! - createTaskChecklistItem(input: CreateTaskChecklistItem!): TaskChecklistItem! - updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem! - setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem! - deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload! + createTaskChecklist(input: CreateTaskChecklist!): + TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskChecklist(input: DeleteTaskChecklist!): + DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistName(input: UpdateTaskChecklistName!): + TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + createTaskChecklistItem(input: CreateTaskChecklistItem!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): + TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): + DeleteTaskChecklistItemPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): + UpdateTaskChecklistLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): + UpdateTaskChecklistItemLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) - updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload! - updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload! } input UpdateTaskChecklistItemLocation { @@ -484,10 +502,14 @@ type DeleteTaskChecklistPayload { } extend type Mutation { - createTaskGroup(input: NewTaskGroup!): TaskGroup! - updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! - updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup! - deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload! + createTaskGroup(input: NewTaskGroup!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskGroupLocation(input: NewTaskGroupLocation!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + updateTaskGroupName(input: UpdateTaskGroupName!): + TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + deleteTaskGroup(input: DeleteTaskGroupInput!): + DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } input NewTaskGroupLocation { @@ -534,9 +556,13 @@ type ToggleTaskLabelPayload { task: Task! } extend type Mutation { - addTaskLabel(input: AddTaskLabelInput): Task! - removeTaskLabel(input: RemoveTaskLabelInput): Task! - toggleTaskLabel(input: ToggleTaskLabelInput!): ToggleTaskLabelPayload! + addTaskLabel(input: AddTaskLabelInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + removeTaskLabel(input: RemoveTaskLabelInput): + Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + toggleTaskLabel(input: ToggleTaskLabelInput!): + ToggleTaskLabelPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + } extend type Mutation { @@ -562,10 +588,12 @@ type DeleteTeamPayload { } extend type Mutation { - createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) + createTeamMember(input: CreateTeamMember!): + CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) - deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) + deleteTeamMember(input: DeleteTeamMember!): + DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 9116e5b..ff3b5dc 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -235,7 +235,8 @@ func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskN } func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskComplete) (*db.Task, error) { - task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete}) + completedAt := time.Now().UTC() + task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}}) if err != nil { return &db.Task{}, err } @@ -1041,6 +1042,13 @@ func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*time.Time, e return nil, nil } +func (r *taskResolver) CompletedAt(ctx context.Context, obj *db.Task) (*time.Time, error) { + if obj.CompletedAt.Valid { + return &obj.CompletedAt.Time, nil + } + return nil, nil +} + func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, error) { taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID) taskMembers := []Member{} diff --git a/internal/graph/schema/_models.gql b/internal/graph/schema/_models.gql index df59914..5373fe0 100644 --- a/internal/graph/schema/_models.gql +++ b/internal/graph/schema/_models.gql @@ -129,6 +129,7 @@ type Task { description: String dueDate: Time complete: Boolean! + completedAt: Time assigned: [Member!]! labels: [TaskLabel!]! checklists: [TaskChecklist!]! diff --git a/migrations/0051_add-completed_at-to-task-table.up.sql b/migrations/0051_add-completed_at-to-task-table.up.sql new file mode 100644 index 0000000..c416c4c --- /dev/null +++ b/migrations/0051_add-completed_at-to-task-table.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE task ADD COLUMN completed_at timestamptz; +UPDATE task as t1 SET completed_at = NOW() + FROM task as t2 + WHERE t1.task_id = t2.task_id AND t1.complete = true;