1 Commits

Author SHA1 Message Date
4a8d4a6ec3 feat: add update polling 2020-09-11 16:03:51 -05:00
25 changed files with 173 additions and 163 deletions

View File

@ -6,6 +6,14 @@
"@apollo/client": "^3.0.0-rc.8",
"@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.3",
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-brands-svg-icons": "^5.12.1",
"@fortawesome/free-regular-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/axios": "^0.14.0",
"@types/color": "^3.0.1",
"@types/date-fns": "^2.6.0",

View File

@ -323,6 +323,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const { loading, data } = useFindProjectQuery({
variables: { projectID },
pollInterval: 5000,
});
const [deleteTaskGroupTasks] = useDeleteTaskGroupTasksMutation({
update: (client, resp) =>

View File

@ -3,7 +3,7 @@ import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useRouteMatch, useHistory, Redirect } from 'react-router';
import {
useDeleteTaskChecklistMutation,
useUpdateTaskChecklistNameMutation,
@ -32,6 +32,7 @@ import Input from 'shared/components/Input';
import { useForm } from 'react-hook-form';
import updateApolloCache from 'shared/utils/cache';
import NOOP from 'shared/utils/noop';
import hasNotFoundError from 'shared/utils/error';
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
const total = checklists.reduce((prev: any, next: any) => {
@ -269,8 +270,8 @@ const Details: React.FC<DetailsProps> = ({
);
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [setTaskComplete] = useSetTaskCompleteMutation();
const { loading, data, refetch, error } = useFindTaskQuery({ variables: { taskID }, pollInterval: 5000 });
const [setTaskComplete, { error: setTaskCompleteError }] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
@ -289,6 +290,10 @@ const Details: React.FC<DetailsProps> = ({
refreshCache();
},
});
if (hasNotFoundError(error, setTaskCompleteError)) {
return <Redirect to={projectURL} />;
}
if (setTaskCompleteError && setTaskCompleteError)
if (loading) {
return null;
}
@ -346,7 +351,11 @@ const Details: React.FC<DetailsProps> = ({
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }).catch(r => {
if (hasNotFoundError(r)) {
history.push(projectURL);
}
});
}}
onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => {

View File

@ -234,7 +234,7 @@ type ShowNewProject = {
const Projects = () => {
const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only', pollInterval: 5000 });
useEffect(() => {
document.title = 'Taskcafé';
}, []);

View File

@ -154,7 +154,7 @@ type TeamProjectsProps = {
};
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const { loading, data } = useGetTeamQuery({ variables: { teamID }, pollInterval: 5000 });
if (loading) {
return <span>loading</span>;
}

View File

@ -1,7 +1,9 @@
import styled, { css, keyframes } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
import { CheckCircle, CheckSquareOutline } from 'shared/icons';
import { RefObject } from 'react';
import TaskAssignee from 'shared/components/TaskAssignee';
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
@ -18,9 +20,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
stroke: rgba(${props.theme.colors.success});
`}
`;
export const ClockIcon = styled(Clock)<{ color: string }>`
fill: ${props => props.color};
`;
export const ClockIcon = styled(FontAwesomeIcon)``;
export const EditorTextarea = styled(TextareaAutosize)`
overflow: hidden;

View File

@ -1,5 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { Pencil, Eye, List } from 'shared/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
import { faClock, faEye } from '@fortawesome/free-regular-svg-icons';
import {
EditorTextarea,
CardMember,
@ -153,7 +155,7 @@ const Card = React.forwardRef(
}
}}
>
<Pencil width={8} height={8} />
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
</ListCardOperation>
)}
<ListCardDetails complete={complete ?? false}>
@ -216,18 +218,18 @@ const Card = React.forwardRef(
<ListCardBadges>
{watched && (
<ListCardBadge>
<Eye width={8} height={8} />
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
</ListCardBadge>
)}
{dueDate && (
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} width={8} height={8} />
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
</DueDateCardBadge>
)}
{description && (
<DescriptionBadge>
<List width={8} height={8} />
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
</DescriptionBadge>
)}
{checklists && (

View File

@ -1,15 +1,15 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
export const CancelIconWrapper = styled.div`
export const CancelIcon = styled(FontAwesomeIcon)`
opacity: 0.8;
cursor: pointer;
font-size: 1.25em;
padding-left: 5px;
`;
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
padding-bottom: 8px;
display: ${props => (props.isOpen ? 'flex' : 'none')};

View File

@ -1,12 +1,12 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Cross } from 'shared/icons';
import {
CardComposerWrapper,
CancelIconWrapper,
CancelIcon,
AddCardButton,
ComposerControls,
ComposerControlsSaveSection,
@ -52,9 +52,7 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
>
Add Card
</AddCardButton>
<CancelIconWrapper onClick={() => onClose()}>
<Cross width={12} height={12} />
</CancelIconWrapper>
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
</ComposerControlsSaveSection>
<ComposerControlsActionsSection />
</ComposerControls>

View File

@ -585,30 +585,3 @@ export const ActivityItemLog = styled.span`
margin-left: 2px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const ViewRawButton = styled.button`
border-radius: 3px;
padding: 8px 12px;
display: flex;
position: absolute;
right: 4px;
bottom: -24px;
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary}, 0.25);
&:hover {
color: rgba(${props => props.theme.colors.text.primary});
}
`;
export const TaskDetailsEditor = styled(TextareaAutosize)`
min-height: 108px;
color: #c2c6dc;
background: #262c49;
border-radius: 3px;
line-height: 20px;
margin-left: 32px;
margin-right: 32px;
padding: 9px 8px 7px 8px;
outline: none;
border: none;
`;

View File

@ -30,7 +30,6 @@ import {
AssignUserLabel,
AssignUsersButton,
AssignedUsersSection,
ViewRawButton,
DueDateTitle,
Container,
LeftSidebar,
@ -66,7 +65,6 @@ import {
CommentProfile,
CommentInnerWrapper,
ActivitySection,
TaskDetailsEditor,
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
@ -155,7 +153,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
return true;
});
const [saveTimeout, setSaveTimeout] = useState<any>(null);
const [showRaw, setShowRaw] = useState(false);
const [showCommentActions, setShowCommentActions] = useState(false);
const taskDescriptionRef = useRef(task.description ?? '');
const $noMemberBtn = useRef<HTMLDivElement>(null);
@ -312,9 +309,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</HeaderContainer>
<InnerContentContainer>
<DescriptionContainer>
{showRaw ? (
<TaskDetailsEditor value={taskDescriptionRef.current} />
) : (
<EditorContainer
onClick={e => {
if (!editTaskDescription) {
@ -337,9 +331,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}}
/>
</EditorContainer>
)}
<ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
</DescriptionContainer>
<ChecklistSection>
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>

View File

@ -209,7 +209,8 @@ export enum ObjectType {
Org = 'ORG',
Team = 'TEAM',
Project = 'PROJECT',
Task = 'TASK'
Task = 'TASK',
TaskGroup = 'TASK_GROUP'
}
export type Query = {
@ -722,7 +723,7 @@ export type UpdateProjectMemberRolePayload = {
};
export type NewTask = {
taskGroupID: Scalars['String'];
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -1472,7 +1473,7 @@ export type UpdateProjectMemberRoleMutation = (
);
export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String'];
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -3044,7 +3045,7 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType<typeof useUpd
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
...TaskFields
}

View File

@ -2,7 +2,7 @@ import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task';
const CREATE_TASK_MUTATION = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
...TaskFields
}

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Eye: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 576 512">
<path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z" />
</Icon>
);
};
export default Eye;

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const List: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z" />
</Icon>
);
};
export default List;

View File

@ -1,7 +1,5 @@
import Cross from './Cross';
import Cog from './Cog';
import Eye from './Eye';
import List from './List';
import At from './At';
import Task from './Task';
import Smile from './Smile';
@ -87,6 +85,4 @@ export {
Clone,
Paperclip,
Share,
Eye,
List,
};

View File

@ -0,0 +1,13 @@
import { ApolloError } from '@apollo/client';
export default function hasNotFoundError(...errors: Array<ApolloError | undefined>) {
for (const error of errors) {
if (error && error.graphQLErrors.length !== 0) {
const notFound = error.graphQLErrors.find(e => e.extensions && e.extensions.code === '404');
if (notFound) {
return true;
}
}
}
return false;
}

View File

@ -2648,6 +2648,7 @@ enum ObjectType {
TEAM
PROJECT
TASK
TASK_GROUP
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
@ -2851,20 +2852,20 @@ type UpdateProjectMemberRolePayload {
extend type Mutation {
createTask(input: NewTask!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
deleteTask(input: DeleteTaskInput!):
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDescription(input: UpdateTaskDescriptionInput!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskLocation(input: NewTaskLocation!):
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskName(input: UpdateTaskName!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
setTaskComplete(input: SetTaskComplete!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDueDate(input: UpdateTaskDueDate!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
assignTask(input: AssignTaskInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
@ -2873,7 +2874,7 @@ extend type Mutation {
}
input NewTask {
taskGroupID: String!
taskGroupID: UUID!
name: String!
position: Float!
}
@ -6533,7 +6534,7 @@ func (ec *executionContext) _Mutation_createTask(ctx context.Context, field grap
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK_GROUP")
if err != nil {
return nil, err
}
@ -6606,7 +6607,7 @@ func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field grap
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -6679,7 +6680,7 @@ func (ec *executionContext) _Mutation_updateTaskDescription(ctx context.Context,
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -6752,7 +6753,7 @@ func (ec *executionContext) _Mutation_updateTaskLocation(ctx context.Context, fi
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -6825,7 +6826,7 @@ func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -6898,7 +6899,7 @@ func (ec *executionContext) _Mutation_setTaskComplete(ctx context.Context, field
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -6971,7 +6972,7 @@ func (ec *executionContext) _Mutation_updateTaskDueDate(ctx context.Context, fie
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "TASK")
if err != nil {
return nil, err
}
@ -15194,7 +15195,7 @@ func (ec *executionContext) unmarshalInputNewTask(ctx context.Context, obj inter
switch k {
case "taskGroupID":
var err error
it.TaskGroupID, err = ec.unmarshalNString2string(ctx, v)
it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}

View File

@ -19,6 +19,7 @@ import (
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// NewHandler returns a new graphql endpoint handler.
@ -51,6 +52,8 @@ func NewHandler(repo db.Repository) http.Handler {
fieldName = "TeamID"
case ObjectTypeTask:
fieldName = "TaskID"
case ObjectTypeTaskGroup:
fieldName = "TaskGroupID"
default:
fieldName = "ProjectID"
}
@ -68,6 +71,13 @@ func NewHandler(repo db.Repository) http.Handler {
if err != nil {
return nil, err
}
} else if typeArg == ObjectTypeTaskGroup {
log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching project ID using task group ID")
taskGroup, err := repo.GetTaskGroupByID(ctx, subjectID)
if err != nil {
return nil, err
}
subjectID = taskGroup.ProjectID
}
roles, err := GetProjectRoles(ctx, repo, subjectID)
if err != nil {
@ -186,3 +196,13 @@ func GetActionType(actionType int32) ActionType {
panic("Not a valid entity type!")
}
}
// NotFoundError creates a 404 gqlerror
func NotFoundError(message string) error {
return &gqlerror.Error{
Message: message,
Extensions: map[string]interface{}{
"code": "404",
},
}
}

View File

@ -229,7 +229,7 @@ type NewRefreshToken struct {
}
type NewTask struct {
TaskGroupID string `json:"taskGroupID"`
TaskGroupID uuid.UUID `json:"taskGroupID"`
Name string `json:"name"`
Position float64 `json:"position"`
}
@ -652,6 +652,7 @@ const (
ObjectTypeTeam ObjectType = "TEAM"
ObjectTypeProject ObjectType = "PROJECT"
ObjectTypeTask ObjectType = "TASK"
ObjectTypeTaskGroup ObjectType = "TASK_GROUP"
)
var AllObjectType = []ObjectType{
@ -659,11 +660,12 @@ var AllObjectType = []ObjectType{
ObjectTypeTeam,
ObjectTypeProject,
ObjectTypeTask,
ObjectTypeTaskGroup,
}
func (e ObjectType) IsValid() bool {
switch e {
case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask:
case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask, ObjectTypeTaskGroup:
return true
}
return false

View File

@ -174,6 +174,7 @@ enum ObjectType {
TEAM
PROJECT
TASK
TASK_GROUP
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
@ -377,20 +378,20 @@ type UpdateProjectMemberRolePayload {
extend type Mutation {
createTask(input: NewTask!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
deleteTask(input: DeleteTaskInput!):
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDescription(input: UpdateTaskDescriptionInput!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskLocation(input: NewTaskLocation!):
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskName(input: UpdateTaskName!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
setTaskComplete(input: SetTaskComplete!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDueDate(input: UpdateTaskDueDate!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
assignTask(input: AssignTaskInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
@ -399,7 +400,7 @@ extend type Mutation {
}
input NewTask {
taskGroupID: String!
taskGroupID: UUID!
name: String!
position: Float!
}

View File

@ -179,14 +179,9 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
}
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID)
if err != nil {
log.WithError(err).Error("issue while parsing task group ID")
return &db.Task{}, err
}
createdAt := time.Now().UTC()
log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": taskGroupID}).Info("creating task")
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{taskGroupID, createdAt, input.Name, input.Position})
log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
if err != nil {
log.WithError(err).Error("issue while creating task")
return &db.Task{}, err
@ -238,6 +233,9 @@ func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskCom
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 {
if err == sql.ErrNoRows {
return &db.Task{}, NotFoundError("task does not exist")
}
return &db.Task{}, err
}
return &task, nil
@ -1033,6 +1031,14 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
if err == sql.ErrNoRows {
return &db.Task{}, &gqlerror.Error{
Message: "Task does not exist",
Extensions: map[string]interface{}{
"code": "404",
},
}
}
return &task, err
}
@ -1240,6 +1246,9 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
taskMembers := []Member{}
if err != nil {
if err == sql.ErrNoRows {
return taskMembers, nil
}
return taskMembers, err
}
for _, taskMemberLink := range taskMemberLinks {
@ -1274,11 +1283,19 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
}
func (r *taskResolver) Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error) {
return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
labels, err := r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
if err != nil && err != sql.ErrNoRows {
return []db.TaskLabel{}, err
}
return labels, nil
}
func (r *taskResolver) Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error) {
return r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
checklists, err := r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
if err != nil && err != sql.ErrNoRows {
return []db.TaskChecklist{}, err
}
return checklists, nil
}
func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, error) {

View File

@ -14,6 +14,7 @@ enum ObjectType {
TEAM
PROJECT
TASK
TASK_GROUP
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION

View File

@ -1,19 +1,19 @@
extend type Mutation {
createTask(input: NewTask!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
deleteTask(input: DeleteTaskInput!):
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDescription(input: UpdateTaskDescriptionInput!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskLocation(input: NewTaskLocation!):
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskName(input: UpdateTaskName!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
setTaskComplete(input: SetTaskComplete!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
updateTaskDueDate(input: UpdateTaskDueDate!):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
assignTask(input: AssignTaskInput):
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
@ -22,7 +22,7 @@ extend type Mutation {
}
input NewTask {
taskGroupID: String!
taskGroupID: UUID!
name: String!
position: Float!
}