feat: add task activity

This commit is contained in:
Jordan Knott 2020-12-18 20:34:35 -06:00
parent f732b211c9
commit f4ef7fec83
33 changed files with 6209 additions and 4648 deletions

View File

@ -21,4 +21,4 @@ windows:
- database:
root: ./
panes:
- pgcli postgres://taskcafe:taskcafe_test@localhost:5432/taskcafe
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe

View File

@ -182,7 +182,7 @@ const AdminRoute = () => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.invitedUsers = cache.invitedUsers.filter(
u => u.id !== response.data.deleteInvitedUserAccount.invitedUser.id,
u => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
);
}),
);
@ -192,7 +192,7 @@ const AdminRoute = () => {
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
draftCache.users = cache.users.filter(u => u.id !== response.data?.deleteUserAccount.userAccount.id);
}),
);
},
@ -203,7 +203,7 @@ const AdminRoute = () => {
query: UsersDocument,
});
const newData = produce(cacheData, (draftState: any) => {
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
draftState.users = [...draftState.users, { ...createData.data?.createUserAccount }];
});
client.writeQuery({

View File

@ -25,6 +25,11 @@ const MainContent = styled.div`
flex-grow: 1;
`;
type RefreshTokenResponse = {
accessToken: string;
setup?: null | { confirmToken: string };
};
const AuthorizedRoutes = () => {
const history = useHistory();
const [loading, setLoading] = useState(true);

View File

@ -167,7 +167,7 @@ const ProjectFinder = () => {
return <span>error</span>;
};
type ProjectPopupProps = {
history: History<History.PoorMansUnknown>;
history: any;
name: string;
projectID: string;
};
@ -182,7 +182,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
const newData = produce(cacheData, (draftState: any) => {
draftState.projects = draftState.projects.filter(
(project: any) => project.id !== deleteData.data.deleteProject.project.id,
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
);
});

View File

@ -46,10 +46,6 @@ const StyledContainer = styled(ToastContainer).attrs({
`;
const history = createBrowserHistory();
type RefreshTokenResponse = {
accessToken: string;
isInstalled: boolean;
};
const App = () => {
const [user, setUser] = useState<CurrentUserRaw | null>(null);

View File

@ -280,7 +280,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
);
}),
{ projectID },
@ -296,10 +296,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
if (idx !== -1) {
if (newTaskData.data) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
}
}
}),
{ projectID },
);
@ -313,7 +315,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (newTaskGroupData.data) {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
}
}),
{ projectID },
);
@ -332,7 +336,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
const idx = cache.findProject.taskGroups.findIndex(
t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks = [];
@ -348,7 +352,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (resp.data) {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
}
}),
{ projectID },
);
@ -364,21 +370,26 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (newTask.data) {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
if (previousTask) {
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...task },
{ ...previousTask },
];
}
}
}
}
}),
{ projectID },
);

View File

@ -138,10 +138,11 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
if (checklistID !== prevChecklistID) {
if (response.data) {
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
if (taskChecklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
if (item) {
@ -151,11 +152,12 @@ const Details: React.FC<DetailsProps> = ({
draftCache.findTask.checklists[newIdx].items.push({
...item,
position: checklistItem.position,
taskChecklistID: checklistID,
taskChecklistID: taskChecklistID,
});
}
}
}
}
}),
{ taskID },
);
@ -188,7 +190,7 @@ const Details: React.FC<DetailsProps> = ({
produce(cache, draftCache => {
const { checklists } = cache.findTask;
draftCache.findTask.checklists = checklists.filter(
c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id,
c => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
@ -212,8 +214,10 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
if (createData.data) {
const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({ ...item });
}
}),
{ taskID },
);
@ -227,6 +231,7 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
if (deleteData.data) {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
if (targetIdx > -1) {
@ -240,6 +245,7 @@ const Details: React.FC<DetailsProps> = ({
complete,
total,
};
}
}),
{ taskID },
);
@ -252,6 +258,7 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
if (newTaskItem.data) {
const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
@ -264,12 +271,13 @@ const Details: React.FC<DetailsProps> = ({
total,
};
}
}
}),
{ taskID },
);
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID }, fetchPolicy: 'cache-and-network' });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {

View File

@ -36,7 +36,9 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (newLabelData.data) {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
}
}),
{
projectID,
@ -53,7 +55,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data.deleteProjectLabel.id,
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
);
}),
{ projectID },

View File

@ -32,7 +32,6 @@ import {
FindProjectDocument,
FindProjectQuery,
} from 'shared/generated/graphql';
import produce from 'immer';
import UserContext, { useCurrentUser } from 'App/context';
import Input from 'shared/components/Input';
@ -423,14 +422,16 @@ const Project = () => {
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (resp.data) {
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
tg => tg.tasks.findIndex(t => t.id === resp.data.deleteTask.taskID) !== -1,
tg => tg.tasks.findIndex(t => t.id === resp.data?.deleteTask.taskID) !== -1,
);
if (taskGroupIdx !== -1) {
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
taskGroupIdx
].tasks.filter(t => t.id !== resp.data.deleteTask.taskID);
].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID);
}
}
}),
{ projectID },
@ -450,7 +451,7 @@ const Project = () => {
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.name = newName.data.updateProjectName.name;
draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
}),
{ projectID },
);
@ -464,6 +465,7 @@ const Project = () => {
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (response.data) {
draftCache.findProject.members = [
...cache.findProject.members,
...response.data.inviteProjectMembers.members,
@ -472,6 +474,7 @@ const Project = () => {
...cache.findProject.invitedMembers,
...response.data.inviteProjectMembers.invitedMembers,
];
}
}),
{ projectID },
);
@ -485,7 +488,7 @@ const Project = () => {
cache =>
produce(cache, draftCache => {
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
);
}),
{ projectID },
@ -500,7 +503,7 @@ const Project = () => {
cache =>
produce(cache, draftCache => {
draftCache.findProject.members = cache.findProject.members.filter(
m => m.id !== response.data.deleteProjectMember.member.id,
m => m.id !== response.data?.deleteProjectMember.member.id,
);
}),
{ projectID },

View File

@ -210,7 +210,9 @@ const Projects = () => {
update: (client, newProject) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
if (newProject.data) {
draftCache.projects.push({ ...newProject.data.createProject });
}
}),
);
},
@ -222,7 +224,9 @@ const Projects = () => {
update: (client, createData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
draftCache.teams.push({ ...createData.data.createTeam });
if (createData.data) {
draftCache.teams.push({ ...createData.data?.createTeam });
}
}),
);
},

View File

@ -430,11 +430,13 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
GetTeamDocument,
cache =>
produce(cache, draftCache => {
if (response.data) {
draftCache.findTeam.members.push({
...response.data.createTeamMember.teamMember,
member: { __typename: 'MemberList', projects: [], teams: [] },
owned: { __typename: 'OwnedList', projects: [], teams: [] },
});
}
}),
{ teamID },
);
@ -459,7 +461,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
cache =>
produce(cache, draftCache => {
draftCache.findTeam.members = cache.findTeam.members.filter(
member => member.id !== response.data.deleteTeamMember.userID,
member => member.id !== response.data?.deleteTeamMember.userID,
);
}),
{ teamID },

View File

@ -33,7 +33,7 @@ const Wrapper = styled.div`
`;
type TeamPopupProps = {
history: History<History.PoorMansUnknown>;
history: History<any>;
name: string;
teamID: string;
};
@ -44,9 +44,9 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
update: (client, deleteData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data.deleteTeam.team.id);
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data?.deleteTeam.team.id);
draftCache.projects = cache.projects.filter(
(project: any) => project.team.id !== deleteData.data.deleteTeam.team.id,
(project: any) => project.team.id !== deleteData.data?.deleteTeam.team.id,
);
}),
);

View File

@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onChange={(e: any) => {
setTeam(e.value);
}}
value={options.filter(d => d.value === team)}
value={options.find(d => d.value === team)}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}

View File

@ -0,0 +1,23 @@
import React from 'react';
import styled from 'styled-components';
import { TaskActivityData, ActivityType } from 'shared/generated/graphql';
type ActivityMessageProps = {
type: ActivityType;
data: Array<TaskActivityData>;
};
function getVariable(data: Array<TaskActivityData>, name: string) {
const target = data.find(d => d.name === name);
return target ? target.value : null;
}
const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
switch (type) {
case ActivityType.TaskAdded:
return <>`added this task to ${getVariable(data, 'TaskGroup')}`</>;
}
return <h1>hello</h1>;
};
export default ActivityMessage;

View File

@ -537,25 +537,26 @@ export const ActivityItem = styled.div`
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
display: flex;
`;
export const ActivityItemHeader = styled.div`
display: flex;
flex-direction: column;
padding-left: 8px;
`;
export const ActivityItemHeaderUser = styled(TaskAssignee)`
margin-right: 4px;
`;
export const ActivityItemHeaderUser = styled(TaskAssignee)``;
export const ActivityItemHeaderTitle = styled.div`
margin-left: 4px;
line-height: 18px;
display: flex;
align-items: center;
color: ${props => props.theme.colors.text.primary};
padding-bottom: 2px;
`;
export const ActivityItemHeaderTitleName = styled.span`
color: ${props => props.theme.colors.text.primary};
font-weight: 500;
padding-right: 2px;
`;
export const ActivityItemTimestamp = styled.span<{ margin: number }>`

View File

@ -19,8 +19,12 @@ import styled from 'styled-components';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import dayjs from 'dayjs';
import ActivityMessage from './ActivityMessage';
import Task from 'shared/icons/Task';
import {
ActivityItemHeader,
ActivityItemTimestamp,
ActivityItem,
TaskDetailLabel,
CommentContainer,
MetaDetailContent,
@ -67,9 +71,13 @@ import {
CommentInnerWrapper,
ActivitySection,
TaskDetailsEditor,
ActivityItemHeaderUser,
ActivityItemHeaderTitle,
ActivityItemHeaderTitleName,
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
import TaskAssignee from 'shared/components/TaskAssignee';
const ChecklistContainer = styled.div``;
@ -425,7 +433,36 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TabBarSection>
<TabBarItem>Activity</TabBarItem>
</TabBarSection>
<ActivitySection />
<ActivitySection>
{task.activity &&
task.activity.map(activity => (
<ActivityItem>
<ActivityItemHeaderUser
size={32}
member={{
id: activity.causedBy.id,
fullName: activity.causedBy.fullName,
profileIcon: activity.causedBy.profileIcon
? activity.causedBy.profileIcon
: {
url: null,
initials: activity.causedBy.fullName.charAt(0),
bgColor: '#fff',
},
}}
/>
<ActivityItemHeader>
<ActivityItemHeaderTitle>
<ActivityItemHeaderTitleName>{activity.causedBy.fullName}</ActivityItemHeaderTitleName>
<ActivityMessage type={activity.type} data={activity.data} />
</ActivityItemHeaderTitle>
<ActivityItemTimestamp margin={0}>
{dayjs(activity.createdAt).format('MMM D [at] h:mm A')}
</ActivityItemTimestamp>
</ActivityItemHeader>
</ActivityItem>
))}
</ActivitySection>
</InnerContentContainer>
<CommentContainer>
{me && (

View File

@ -168,6 +168,41 @@ export type TaskBadges = {
checklist?: Maybe<ChecklistBadge>;
};
export type CausedBy = {
__typename?: 'CausedBy';
id: Scalars['ID'];
fullName: Scalars['String'];
profileIcon?: Maybe<ProfileIcon>;
};
export type TaskActivityData = {
__typename?: 'TaskActivityData';
name: Scalars['String'];
value: Scalars['String'];
};
export enum ActivityType {
TaskAdded = 'TASK_ADDED',
TaskMoved = 'TASK_MOVED',
TaskMarkedComplete = 'TASK_MARKED_COMPLETE',
TaskMarkedIncomplete = 'TASK_MARKED_INCOMPLETE',
TaskDueDateChanged = 'TASK_DUE_DATE_CHANGED',
TaskDueDateAdded = 'TASK_DUE_DATE_ADDED',
TaskDueDateRemoved = 'TASK_DUE_DATE_REMOVED',
TaskChecklistChanged = 'TASK_CHECKLIST_CHANGED',
TaskChecklistAdded = 'TASK_CHECKLIST_ADDED',
TaskChecklistRemoved = 'TASK_CHECKLIST_REMOVED'
}
export type TaskActivity = {
__typename?: 'TaskActivity';
id: Scalars['ID'];
type: ActivityType;
data: Array<TaskActivityData>;
causedBy: CausedBy;
createdAt: Scalars['Time'];
};
export type Task = {
__typename?: 'Task';
id: Scalars['ID'];
@ -183,6 +218,7 @@ export type Task = {
labels: Array<TaskLabel>;
checklists: Array<TaskChecklist>;
badges: TaskBadges;
activity: Array<TaskActivity>;
};
export type Organization = {
@ -209,6 +245,11 @@ export type TaskChecklist = {
items: Array<TaskChecklistItem>;
};
export enum ShareStatus {
Invited = 'INVITED',
Joined = 'JOINED'
}
export enum RoleLevel {
Admin = 'ADMIN',
Member = 'MEMBER'
@ -1050,17 +1091,16 @@ export type DeleteInvitedUserAccountPayload = {
};
export type MemberSearchFilter = {
SearchFilter: Scalars['String'];
searchFilter: Scalars['String'];
projectID?: Maybe<Scalars['UUID']>;
};
export type MemberSearchResult = {
__typename?: 'MemberSearchResult';
similarity: Scalars['Int'];
user: UserAccount;
confirmed: Scalars['Boolean'];
invited: Scalars['Boolean'];
joined: Scalars['Boolean'];
id: Scalars['String'];
user?: Maybe<UserAccount>;
status: ShareStatus;
};
export type UpdateUserInfoPayload = {
@ -1344,7 +1384,21 @@ export type FindTaskQuery = (
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'>
), badges: (
), activity: Array<(
{ __typename?: 'TaskActivity' }
& Pick<TaskActivity, 'id' | 'type' | 'createdAt'>
& { causedBy: (
{ __typename?: 'CausedBy' }
& Pick<CausedBy, 'id' | 'fullName'>
& { profileIcon?: Maybe<(
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
)> }
), data: Array<(
{ __typename?: 'TaskActivityData' }
& Pick<TaskActivityData, 'name' | 'value'>
)> }
)>, badges: (
{ __typename?: 'TaskBadges' }
& { checklist?: Maybe<(
{ __typename?: 'ChecklistBadge' }
@ -2825,6 +2879,24 @@ export const FindTaskDocument = gql`
id
name
}
activity {
id
type
causedBy {
id
fullName
profileIcon {
initials
bgColor
url
}
}
createdAt
data {
name
value
}
}
badges {
checklist {
total

View File

@ -10,6 +10,24 @@ query findTask($taskID: UUID!) {
id
name
}
activity {
id
type
causedBy {
id
fullName
profileIcon {
initials
bgColor
url
}
}
createdAt
data {
name
value
}
}
badges {
checklist {
total

View File

@ -9,7 +9,7 @@ export function updateApolloCache<T>(
update: UpdateCacheFn<T>,
variables?: object,
) {
let queryArgs: DataProxy.Query<any>;
let queryArgs: DataProxy.Query<any, any>;
if (variables) {
queryArgs = {
query: document,

View File

@ -1,3 +1,10 @@
type ProjectLabel = {
id: string;
createdDate: string;
name?: string | null;
labelColor: LabelColor;
};
type ProfileIcon = {
url?: string | null;
initials?: string | null;
@ -56,6 +63,24 @@ type TaskBadges = {
checklist?: ChecklistBadge | null;
};
type TaskActivityData = {
name: string;
value: string;
};
type CausedBy = {
id: string;
fullName: string;
profileIcon?: null | ProfileIcon;
};
type TaskActivity = {
id: string;
type: any;
data: Array<TaskActivityData>;
causedBy: CausedBy;
createdAt: string;
};
type Task = {
id: string;
taskGroup: InnerTaskGroup;
@ -69,6 +94,7 @@ type Task = {
description?: string | null;
assigned?: Array<TaskUser>;
checklists?: Array<TaskChecklist> | null;
activity?: Array<TaskActivity> | null;
};
type Project = {
@ -89,10 +115,3 @@ type Team = {
name: string;
createdAt: string;
};
type ProjectLabel = {
id: string;
createdDate: string;
name?: string | null;
labelColor: LabelColor;
};

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ package db
import (
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
@ -103,6 +104,22 @@ type Task struct {
CompletedAt sql.NullTime `json:"completed_at"`
}
type TaskActivity struct {
TaskActivityID uuid.UUID `json:"task_activity_id"`
Active bool `json:"active"`
TaskID uuid.UUID `json:"task_id"`
CreatedAt time.Time `json:"created_at"`
CausedBy uuid.UUID `json:"caused_by"`
ActivityTypeID int32 `json:"activity_type_id"`
Data json.RawMessage `json:"data"`
}
type TaskActivityType struct {
TaskActivityTypeID int32 `json:"task_activity_type_id"`
Code string `json:"code"`
Template string `json:"template"`
}
type TaskAssigned struct {
TaskAssignedID uuid.UUID `json:"task_assigned_id"`
TaskID uuid.UUID `json:"task_id"`

View File

@ -23,6 +23,7 @@ type Querier interface {
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error)
CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error)
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error)
@ -55,6 +56,7 @@ type Querier interface {
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error)
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
GetAllOrganizations(ctx context.Context) ([]Organization, error)
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
@ -74,6 +76,7 @@ type Querier interface {
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error)
GetLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) (GetLastMoveForTaskIDRow, error)
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
@ -113,6 +116,7 @@ type Querier interface {
GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
GetTemplateForActivityID(ctx context.Context, taskActivityTypeID int32) (string, error)
GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
@ -120,6 +124,7 @@ type Querier interface {
HasActiveUser(ctx context.Context) (bool, error)
HasAnyUser(ctx context.Context) (bool, error)
SetFirstUserActive(ctx context.Context) (UserAccount, error)
SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error
SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)

View File

@ -0,0 +1,22 @@
-- name: CreateTaskActivity :one
INSERT INTO task_activity (task_id, caused_by, created_at, activity_type_id, data)
VALUES ($1, $2, $3, $4, $5) RETURNING *;
-- name: GetActivityForTaskID :many
SELECT * FROM task_activity WHERE task_id = $1 AND active = true;
-- name: GetTemplateForActivityID :one
SELECT template FROM task_activity_type WHERE task_activity_type_id = $1;
-- name: GetLastMoveForTaskID :one
SELECT active, created_at, data->>'CurTaskGroupID' AS cur_task_group_id, data->>'PrevTaskGroupID' AS prev_task_group_id FROM task_activity
WHERE task_id = $1 AND activity_type_id = 2 AND created_at >= NOW() - INTERVAL '5 minutes'
ORDER BY created_at DESC LIMIT 1;
-- name: SetInactiveLastMoveForTaskID :exec
UPDATE task_activity SET active = false WHERE task_activity_id = (
SELECT task_activity_id FROM task_activity AS ta
WHERE ta.activity_type_id = 2 AND ta.task_id = $1
AND ta.created_at >= NOW() - INTERVAL '5 minutes'
ORDER BY created_at DESC LIMIT 1
);

View File

@ -0,0 +1,131 @@
// Code generated by sqlc. DO NOT EDIT.
// source: task_activity.sql
package db
import (
"context"
"encoding/json"
"time"
"github.com/google/uuid"
)
const createTaskActivity = `-- name: CreateTaskActivity :one
INSERT INTO task_activity (task_id, caused_by, created_at, activity_type_id, data)
VALUES ($1, $2, $3, $4, $5) RETURNING task_activity_id, active, task_id, created_at, caused_by, activity_type_id, data
`
type CreateTaskActivityParams struct {
TaskID uuid.UUID `json:"task_id"`
CausedBy uuid.UUID `json:"caused_by"`
CreatedAt time.Time `json:"created_at"`
ActivityTypeID int32 `json:"activity_type_id"`
Data json.RawMessage `json:"data"`
}
func (q *Queries) CreateTaskActivity(ctx context.Context, arg CreateTaskActivityParams) (TaskActivity, error) {
row := q.db.QueryRowContext(ctx, createTaskActivity,
arg.TaskID,
arg.CausedBy,
arg.CreatedAt,
arg.ActivityTypeID,
arg.Data,
)
var i TaskActivity
err := row.Scan(
&i.TaskActivityID,
&i.Active,
&i.TaskID,
&i.CreatedAt,
&i.CausedBy,
&i.ActivityTypeID,
&i.Data,
)
return i, err
}
const getActivityForTaskID = `-- name: GetActivityForTaskID :many
SELECT task_activity_id, active, task_id, created_at, caused_by, activity_type_id, data FROM task_activity WHERE task_id = $1 AND active = true
`
func (q *Queries) GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error) {
rows, err := q.db.QueryContext(ctx, getActivityForTaskID, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskActivity
for rows.Next() {
var i TaskActivity
if err := rows.Scan(
&i.TaskActivityID,
&i.Active,
&i.TaskID,
&i.CreatedAt,
&i.CausedBy,
&i.ActivityTypeID,
&i.Data,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLastMoveForTaskID = `-- name: GetLastMoveForTaskID :one
SELECT active, created_at, data->>'CurTaskGroupID' AS cur_task_group_id, data->>'PrevTaskGroupID' AS prev_task_group_id FROM task_activity
WHERE task_id = $1 AND activity_type_id = 2 AND created_at >= NOW() - INTERVAL '5 minutes'
ORDER BY created_at DESC LIMIT 1
`
type GetLastMoveForTaskIDRow struct {
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
CurTaskGroupID interface{} `json:"cur_task_group_id"`
PrevTaskGroupID interface{} `json:"prev_task_group_id"`
}
func (q *Queries) GetLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) (GetLastMoveForTaskIDRow, error) {
row := q.db.QueryRowContext(ctx, getLastMoveForTaskID, taskID)
var i GetLastMoveForTaskIDRow
err := row.Scan(
&i.Active,
&i.CreatedAt,
&i.CurTaskGroupID,
&i.PrevTaskGroupID,
)
return i, err
}
const getTemplateForActivityID = `-- name: GetTemplateForActivityID :one
SELECT template FROM task_activity_type WHERE task_activity_type_id = $1
`
func (q *Queries) GetTemplateForActivityID(ctx context.Context, taskActivityTypeID int32) (string, error) {
row := q.db.QueryRowContext(ctx, getTemplateForActivityID, taskActivityTypeID)
var template string
err := row.Scan(&template)
return template, err
}
const setInactiveLastMoveForTaskID = `-- name: SetInactiveLastMoveForTaskID :exec
UPDATE task_activity SET active = false WHERE task_activity_id = (
SELECT task_activity_id FROM task_activity AS ta
WHERE ta.activity_type_id = 2 AND ta.task_id = $1
AND ta.created_at >= NOW() - INTERVAL '5 minutes'
ORDER BY created_at DESC LIMIT 1
)
`
func (q *Queries) SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, setInactiveLastMoveForTaskID, taskID)
return err
}

View File

@ -47,6 +47,7 @@ type ResolverRoot interface {
Query() QueryResolver
RefreshToken() RefreshTokenResolver
Task() TaskResolver
TaskActivity() TaskActivityResolver
TaskChecklist() TaskChecklistResolver
TaskChecklistItem() TaskChecklistItemResolver
TaskGroup() TaskGroupResolver
@ -60,6 +61,12 @@ type DirectiveRoot struct {
}
type ComplexityRoot struct {
CausedBy struct {
FullName func(childComplexity int) int
ID func(childComplexity int) int
ProfileIcon func(childComplexity int) int
}
ChecklistBadge struct {
Complete func(childComplexity int) int
Total func(childComplexity int) int
@ -346,6 +353,7 @@ type ComplexityRoot struct {
}
Task struct {
Activity func(childComplexity int) int
Assigned func(childComplexity int) int
Badges func(childComplexity int) int
Checklists func(childComplexity int) int
@ -361,6 +369,19 @@ type ComplexityRoot struct {
TaskGroup func(childComplexity int) int
}
TaskActivity struct {
CausedBy func(childComplexity int) int
CreatedAt func(childComplexity int) int
Data func(childComplexity int) int
ID func(childComplexity int) int
Type func(childComplexity int) int
}
TaskActivityData struct {
Name func(childComplexity int) int
Value func(childComplexity int) int
}
TaskBadges struct {
Checklist func(childComplexity int) int
}
@ -583,6 +604,13 @@ type TaskResolver interface {
Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error)
Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error)
Badges(ctx context.Context, obj *db.Task) (*TaskBadges, error)
Activity(ctx context.Context, obj *db.Task) ([]db.TaskActivity, error)
}
type TaskActivityResolver interface {
ID(ctx context.Context, obj *db.TaskActivity) (uuid.UUID, error)
Type(ctx context.Context, obj *db.TaskActivity) (ActivityType, error)
Data(ctx context.Context, obj *db.TaskActivity) ([]TaskActivityData, error)
CausedBy(ctx context.Context, obj *db.TaskActivity) (*CausedBy, error)
}
type TaskChecklistResolver interface {
ID(ctx context.Context, obj *db.TaskChecklist) (uuid.UUID, error)
@ -634,6 +662,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
_ = ec
switch typeName + "." + field {
case "CausedBy.fullName":
if e.complexity.CausedBy.FullName == nil {
break
}
return e.complexity.CausedBy.FullName(childComplexity), true
case "CausedBy.id":
if e.complexity.CausedBy.ID == nil {
break
}
return e.complexity.CausedBy.ID(childComplexity), true
case "CausedBy.profileIcon":
if e.complexity.CausedBy.ProfileIcon == nil {
break
}
return e.complexity.CausedBy.ProfileIcon(childComplexity), true
case "ChecklistBadge.complete":
if e.complexity.ChecklistBadge.Complete == nil {
break
@ -2126,6 +2175,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.SortTaskGroupPayload.Tasks(childComplexity), true
case "Task.activity":
if e.complexity.Task.Activity == nil {
break
}
return e.complexity.Task.Activity(childComplexity), true
case "Task.assigned":
if e.complexity.Task.Assigned == nil {
break
@ -2217,6 +2273,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.TaskGroup(childComplexity), true
case "TaskActivity.causedBy":
if e.complexity.TaskActivity.CausedBy == nil {
break
}
return e.complexity.TaskActivity.CausedBy(childComplexity), true
case "TaskActivity.createdAt":
if e.complexity.TaskActivity.CreatedAt == nil {
break
}
return e.complexity.TaskActivity.CreatedAt(childComplexity), true
case "TaskActivity.data":
if e.complexity.TaskActivity.Data == nil {
break
}
return e.complexity.TaskActivity.Data(childComplexity), true
case "TaskActivity.id":
if e.complexity.TaskActivity.ID == nil {
break
}
return e.complexity.TaskActivity.ID(childComplexity), true
case "TaskActivity.type":
if e.complexity.TaskActivity.Type == nil {
break
}
return e.complexity.TaskActivity.Type(childComplexity), true
case "TaskActivityData.name":
if e.complexity.TaskActivityData.Name == nil {
break
}
return e.complexity.TaskActivityData.Name(childComplexity), true
case "TaskActivityData.value":
if e.complexity.TaskActivityData.Value == nil {
break
}
return e.complexity.TaskActivityData.Value(childComplexity), true
case "TaskBadges.checklist":
if e.complexity.TaskBadges.Checklist == nil {
break
@ -2796,6 +2901,38 @@ type TaskBadges {
checklist: ChecklistBadge
}
type CausedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon
}
type TaskActivityData {
name: String!
value: String!
}
enum ActivityType {
TASK_ADDED
TASK_MOVED
TASK_MARKED_COMPLETE
TASK_MARKED_INCOMPLETE
TASK_DUE_DATE_CHANGED
TASK_DUE_DATE_ADDED
TASK_DUE_DATE_REMOVED
TASK_CHECKLIST_CHANGED
TASK_CHECKLIST_ADDED
TASK_CHECKLIST_REMOVED
}
type TaskActivity {
id: ID!
type: ActivityType!
data: [TaskActivityData!]!
causedBy: CausedBy!
createdAt: Time!
}
type Task {
id: ID!
taskGroup: TaskGroup!
@ -2810,6 +2947,7 @@ type Task {
labels: [TaskLabel!]!
checklists: [TaskChecklist!]!
badges: TaskBadges!
activity: [TaskActivity!]!
}
type Organization {
@ -4432,6 +4570,105 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
// region **************************** field.gotpl *****************************
func (ec *executionContext) _CausedBy_id(ctx context.Context, field graphql.CollectedField, obj *CausedBy) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CausedBy",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(uuid.UUID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
}
func (ec *executionContext) _CausedBy_fullName(ctx context.Context, field graphql.CollectedField, obj *CausedBy) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CausedBy",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.FullName, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _CausedBy_profileIcon(ctx context.Context, field graphql.CollectedField, obj *CausedBy) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CausedBy",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ProfileIcon, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*ProfileIcon)
fc.Result = res
return ec.marshalOProfileIcon2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProfileIcon(ctx, field.Selections, res)
}
func (ec *executionContext) _ChecklistBadge_complete(ctx context.Context, field graphql.CollectedField, obj *ChecklistBadge) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -12775,6 +13012,278 @@ func (ec *executionContext) _Task_badges(ctx context.Context, field graphql.Coll
return ec.marshalNTaskBadges2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskBadges(ctx, field.Selections, res)
}
func (ec *executionContext) _Task_activity(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().Activity(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]db.TaskActivity)
fc.Result = res
return ec.marshalNTaskActivity2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskActivityᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivity_id(ctx context.Context, field graphql.CollectedField, obj *db.TaskActivity) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivity",
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.TaskActivity().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(uuid.UUID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivity_type(ctx context.Context, field graphql.CollectedField, obj *db.TaskActivity) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivity",
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.TaskActivity().Type(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(ActivityType)
fc.Result = res
return ec.marshalNActivityType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActivityType(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivity_data(ctx context.Context, field graphql.CollectedField, obj *db.TaskActivity) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivity",
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.TaskActivity().Data(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]TaskActivityData)
fc.Result = res
return ec.marshalNTaskActivityData2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskActivityDataᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivity_causedBy(ctx context.Context, field graphql.CollectedField, obj *db.TaskActivity) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivity",
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.TaskActivity().CausedBy(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*CausedBy)
fc.Result = res
return ec.marshalNCausedBy2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCausedBy(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivity_createdAt(ctx context.Context, field graphql.CollectedField, obj *db.TaskActivity) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivity",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.CreatedAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivityData_name(ctx context.Context, field graphql.CollectedField, obj *TaskActivityData) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivityData",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Name, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskActivityData_value(ctx context.Context, field graphql.CollectedField, obj *TaskActivityData) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskActivityData",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Value, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskBadges_checklist(ctx context.Context, field graphql.CollectedField, obj *TaskBadges) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -17153,6 +17662,40 @@ func (ec *executionContext) unmarshalInputUpdateUserRole(ctx context.Context, ob
// region **************************** object.gotpl ****************************
var causedByImplementors = []string{"CausedBy"}
func (ec *executionContext) _CausedBy(ctx context.Context, sel ast.SelectionSet, obj *CausedBy) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, causedByImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CausedBy")
case "id":
out.Values[i] = ec._CausedBy_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "fullName":
out.Values[i] = ec._CausedBy_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "profileIcon":
out.Values[i] = ec._CausedBy_profileIcon(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var checklistBadgeImplementors = []string{"ChecklistBadge"}
func (ec *executionContext) _ChecklistBadge(ctx context.Context, sel ast.SelectionSet, obj *ChecklistBadge) graphql.Marshaler {
@ -19265,6 +19808,135 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj
}
return res
})
case "activity":
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_activity(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var taskActivityImplementors = []string{"TaskActivity"}
func (ec *executionContext) _TaskActivity(ctx context.Context, sel ast.SelectionSet, obj *db.TaskActivity) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, taskActivityImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("TaskActivity")
case "id":
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._TaskActivity_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "type":
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._TaskActivity_type(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "data":
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._TaskActivity_data(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "causedBy":
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._TaskActivity_causedBy(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "createdAt":
out.Values[i] = ec._TaskActivity_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var taskActivityDataImplementors = []string{"TaskActivityData"}
func (ec *executionContext) _TaskActivityData(ctx context.Context, sel ast.SelectionSet, obj *TaskActivityData) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, taskActivityDataImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("TaskActivityData")
case "name":
out.Values[i] = ec._TaskActivityData_name(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "value":
out.Values[i] = ec._TaskActivityData_value(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -20324,6 +20996,15 @@ func (ec *executionContext) marshalNActionType2githubᚗcomᚋjordanknottᚋtask
return v
}
func (ec *executionContext) unmarshalNActivityType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActivityType(ctx context.Context, v interface{}) (ActivityType, error) {
var res ActivityType
return res, res.UnmarshalGQL(v)
}
func (ec *executionContext) marshalNActivityType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActivityType(ctx context.Context, sel ast.SelectionSet, v ActivityType) graphql.Marshaler {
return v
}
func (ec *executionContext) unmarshalNActorType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActorType(ctx context.Context, v interface{}) (ActorType, error) {
var res ActorType
return res, res.UnmarshalGQL(v)
@ -20347,6 +21028,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res
}
func (ec *executionContext) marshalNCausedBy2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCausedBy(ctx context.Context, sel ast.SelectionSet, v CausedBy) graphql.Marshaler {
return ec._CausedBy(ctx, sel, &v)
}
func (ec *executionContext) marshalNCausedBy2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCausedBy(ctx context.Context, sel ast.SelectionSet, v *CausedBy) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CausedBy(ctx, sel, v)
}
func (ec *executionContext) unmarshalNCreateTaskChecklist2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateTaskChecklist(ctx context.Context, v interface{}) (CreateTaskChecklist, error) {
return ec.unmarshalInputCreateTaskChecklist(ctx, v)
}
@ -21530,6 +22225,88 @@ func (ec *executionContext) marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋtaskcaf
return ec._Task(ctx, sel, v)
}
func (ec *executionContext) marshalNTaskActivity2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskActivity(ctx context.Context, sel ast.SelectionSet, v db.TaskActivity) graphql.Marshaler {
return ec._TaskActivity(ctx, sel, &v)
}
func (ec *executionContext) marshalNTaskActivity2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskActivityᚄ(ctx context.Context, sel ast.SelectionSet, v []db.TaskActivity) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNTaskActivity2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskActivity(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNTaskActivityData2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskActivityData(ctx context.Context, sel ast.SelectionSet, v TaskActivityData) graphql.Marshaler {
return ec._TaskActivityData(ctx, sel, &v)
}
func (ec *executionContext) marshalNTaskActivityData2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskActivityDataᚄ(ctx context.Context, sel ast.SelectionSet, v []TaskActivityData) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNTaskActivityData2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskActivityData(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNTaskBadges2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐTaskBadges(ctx context.Context, sel ast.SelectionSet, v TaskBadges) graphql.Marshaler {
return ec._TaskBadges(ctx, sel, &v)
}
@ -22458,6 +23235,17 @@ func (ec *executionContext) marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknott
return ec._ChecklistBadge(ctx, sel, v)
}
func (ec *executionContext) marshalOProfileIcon2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProfileIcon(ctx context.Context, sel ast.SelectionSet, v ProfileIcon) graphql.Marshaler {
return ec._ProfileIcon(ctx, sel, &v)
}
func (ec *executionContext) marshalOProfileIcon2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProfileIcon(ctx context.Context, sel ast.SelectionSet, v *ProfileIcon) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._ProfileIcon(ctx, sel, v)
}
func (ec *executionContext) unmarshalOProjectsFilter2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐProjectsFilter(ctx context.Context, v interface{}) (ProjectsFilter, error) {
return ec.unmarshalInputProjectsFilter(ctx, v)
}

View File

@ -255,3 +255,15 @@ func GetActionType(actionType int32) ActionType {
panic("Not a valid entity type!")
}
}
type MemberType string
const (
MemberTypeInvited MemberType = "INVITED"
MemberTypeJoined MemberType = "JOINED"
)
type MasterEntry struct {
MemberType MemberType
ID uuid.UUID
}

View File

@ -41,3 +41,7 @@ func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*
return &MemberList{Teams: teams, Projects: projects}, nil
}
type ActivityData struct {
Data map[string]string
}

View File

@ -22,6 +22,12 @@ type AssignTaskInput struct {
UserID uuid.UUID `json:"userID"`
}
type CausedBy struct {
ID uuid.UUID `json:"id"`
FullName string `json:"fullName"`
ProfileIcon *ProfileIcon `json:"profileIcon"`
}
type ChecklistBadge struct {
Complete int `json:"complete"`
Total int `json:"total"`
@ -374,6 +380,11 @@ type SortTaskGroupPayload struct {
Tasks []db.Task `json:"tasks"`
}
type TaskActivityData struct {
Name string `json:"name"`
Value string `json:"value"`
}
type TaskBadges struct {
Checklist *ChecklistBadge `json:"checklist"`
}
@ -615,6 +626,63 @@ func (e ActionType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ActivityType string
const (
ActivityTypeTaskAdded ActivityType = "TASK_ADDED"
ActivityTypeTaskMoved ActivityType = "TASK_MOVED"
ActivityTypeTaskMarkedComplete ActivityType = "TASK_MARKED_COMPLETE"
ActivityTypeTaskMarkedIncomplete ActivityType = "TASK_MARKED_INCOMPLETE"
ActivityTypeTaskDueDateChanged ActivityType = "TASK_DUE_DATE_CHANGED"
ActivityTypeTaskDueDateAdded ActivityType = "TASK_DUE_DATE_ADDED"
ActivityTypeTaskDueDateRemoved ActivityType = "TASK_DUE_DATE_REMOVED"
ActivityTypeTaskChecklistChanged ActivityType = "TASK_CHECKLIST_CHANGED"
ActivityTypeTaskChecklistAdded ActivityType = "TASK_CHECKLIST_ADDED"
ActivityTypeTaskChecklistRemoved ActivityType = "TASK_CHECKLIST_REMOVED"
)
var AllActivityType = []ActivityType{
ActivityTypeTaskAdded,
ActivityTypeTaskMoved,
ActivityTypeTaskMarkedComplete,
ActivityTypeTaskMarkedIncomplete,
ActivityTypeTaskDueDateChanged,
ActivityTypeTaskDueDateAdded,
ActivityTypeTaskDueDateRemoved,
ActivityTypeTaskChecklistChanged,
ActivityTypeTaskChecklistAdded,
ActivityTypeTaskChecklistRemoved,
}
func (e ActivityType) IsValid() bool {
switch e {
case ActivityTypeTaskAdded, ActivityTypeTaskMoved, ActivityTypeTaskMarkedComplete, ActivityTypeTaskMarkedIncomplete, ActivityTypeTaskDueDateChanged, ActivityTypeTaskDueDateAdded, ActivityTypeTaskDueDateRemoved, ActivityTypeTaskChecklistChanged, ActivityTypeTaskChecklistAdded, ActivityTypeTaskChecklistRemoved:
return true
}
return false
}
func (e ActivityType) String() string {
return string(e)
}
func (e *ActivityType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ActivityType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ActivityType", str)
}
return nil
}
func (e ActivityType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ActorType string
const (

View File

@ -135,6 +135,38 @@ type TaskBadges {
checklist: ChecklistBadge
}
type CausedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon
}
type TaskActivityData {
name: String!
value: String!
}
enum ActivityType {
TASK_ADDED
TASK_MOVED
TASK_MARKED_COMPLETE
TASK_MARKED_INCOMPLETE
TASK_DUE_DATE_CHANGED
TASK_DUE_DATE_ADDED
TASK_DUE_DATE_REMOVED
TASK_CHECKLIST_CHANGED
TASK_CHECKLIST_ADDED
TASK_CHECKLIST_REMOVED
}
type TaskActivity {
id: ID!
type: ActivityType!
data: [TaskActivityData!]!
causedBy: CausedBy!
createdAt: Time!
}
type Task {
id: ID!
taskGroup: TaskGroup!
@ -149,6 +181,7 @@ type Task {
labels: [TaskLabel!]!
checklists: [TaskChecklist!]!
badges: TaskBadges!
activity: [TaskActivity!]!
}
type Organization {

View File

@ -7,7 +7,7 @@ import (
"context"
"crypto/tls"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
@ -17,12 +17,11 @@ import (
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/lithammer/fuzzysearch/fuzzy"
gomail "gopkg.in/mail.v2"
hermes "github.com/matcornic/hermes/v2"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
"golang.org/x/crypto/bcrypt"
gomail "gopkg.in/mail.v2"
)
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
@ -363,6 +362,26 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.T
createdAt := time.Now().UTC()
logger.New(ctx).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 {
logger.New(ctx).WithError(err).Error("issue while creating task")
return &db.Task{}, err
}
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
if err != nil {
logger.New(ctx).WithError(err).Error("issue while creating task")
return &db.Task{}, err
}
data := map[string]string{
"TaskGroup": taskGroup.Name,
}
d, err := json.Marshal(data)
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
TaskID: task.TaskID,
Data: d,
CreatedAt: createdAt,
ActivityTypeID: 1,
})
if err != nil {
logger.New(ctx).WithError(err).Error("issue while creating task")
return &db.Task{}, err
@ -387,12 +406,44 @@ func (r *mutationResolver) UpdateTaskDescription(ctx context.Context, input Upda
}
func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, error) {
userID, _ := GetUserID(ctx)
previousTask, err := r.Repository.GetTaskByID(ctx, input.TaskID)
if err != nil {
return &UpdateTaskLocationPayload{}, err
}
task, err := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{input.TaskID, input.TaskGroupID, input.Position})
task, _ := r.Repository.UpdateTaskLocation(ctx, db.UpdateTaskLocationParams{TaskID: input.TaskID, TaskGroupID: input.TaskGroupID, Position: input.Position})
if previousTask.TaskGroupID != input.TaskGroupID {
skipAndDelete := false
lastMove, err := r.Repository.GetLastMoveForTaskID(ctx, input.TaskID)
if err == nil {
if lastMove.Active && lastMove.PrevTaskGroupID == input.TaskGroupID.String() {
skipAndDelete = true
}
}
if skipAndDelete {
_ = r.Repository.SetInactiveLastMoveForTaskID(ctx, input.TaskID)
} else {
prevTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, previousTask.TaskGroupID)
curTaskGroup, _ := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
data := map[string]string{
"PrevTaskGroup": prevTaskGroup.Name,
"PrevTaskGroupID": prevTaskGroup.TaskGroupID.String(),
"CurTaskGroup": curTaskGroup.Name,
"CurTaskGroupID": curTaskGroup.TaskGroupID.String(),
}
createdAt := time.Now().UTC()
d, _ := json.Marshal(data)
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
TaskID: task.TaskID,
Data: d,
CausedBy: userID,
CreatedAt: createdAt,
ActivityTypeID: 2,
})
}
}
return &UpdateTaskLocationPayload{Task: &task, PreviousTaskGroupID: previousTask.TaskGroupID}, err
}
@ -1558,6 +1609,72 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
return &TaskBadges{Checklist: &ChecklistBadge{Total: total, Complete: complete}}, nil
}
func (r *taskResolver) Activity(ctx context.Context, obj *db.Task) ([]db.TaskActivity, error) {
activity, err := r.Repository.GetActivityForTaskID(ctx, obj.TaskID)
if err == sql.ErrNoRows {
return []db.TaskActivity{}, nil
}
return activity, err
}
func (r *taskActivityResolver) ID(ctx context.Context, obj *db.TaskActivity) (uuid.UUID, error) {
return obj.TaskActivityID, nil
}
func (r *taskActivityResolver) Type(ctx context.Context, obj *db.TaskActivity) (ActivityType, error) {
switch obj.ActivityTypeID {
case 1:
return ActivityTypeTaskAdded, nil
case 2:
return ActivityTypeTaskMoved, nil
case 3:
return ActivityTypeTaskMarkedComplete, nil
case 4:
return ActivityTypeTaskMarkedIncomplete, nil
case 5:
return ActivityTypeTaskDueDateChanged, nil
case 6:
return ActivityTypeTaskDueDateAdded, nil
case 7:
return ActivityTypeTaskDueDateRemoved, nil
case 8:
return ActivityTypeTaskChecklistChanged, nil
case 9:
return ActivityTypeTaskChecklistAdded, nil
case 10:
return ActivityTypeTaskChecklistRemoved, nil
default:
return ActivityTypeTaskAdded, errors.New("unknown type")
}
}
func (r *taskActivityResolver) Data(ctx context.Context, obj *db.TaskActivity) ([]TaskActivityData, error) {
var data map[string]string
_ = json.Unmarshal(obj.Data, &data)
activity := []TaskActivityData{}
for name, value := range data {
activity = append(activity, TaskActivityData{
Name: name,
Value: value,
})
}
return activity, nil
}
func (r *taskActivityResolver) CausedBy(ctx context.Context, obj *db.TaskActivity) (*CausedBy, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.CausedBy)
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
return &CausedBy{
ID: obj.CausedBy,
FullName: user.FullName,
ProfileIcon: profileIcon,
}, err
}
func (r *taskChecklistResolver) ID(ctx context.Context, obj *db.TaskChecklist) (uuid.UUID, error) {
return obj.TaskChecklistID, nil
}
@ -1619,6 +1736,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: obj.TeamID})
if err != nil {
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
@ -1634,7 +1752,6 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
return members, err
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
Username: user.Username, Owned: ownedList, Member: memberList, Role: &db.Role{Code: role.Code, Name: role.Name},
})
@ -1724,6 +1841,9 @@ func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenRes
// Task returns TaskResolver implementation.
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
// TaskActivity returns TaskActivityResolver implementation.
func (r *Resolver) TaskActivity() TaskActivityResolver { return &taskActivityResolver{r} }
// TaskChecklist returns TaskChecklistResolver implementation.
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
@ -1753,27 +1873,10 @@ type projectLabelResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type refreshTokenResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskActivityResolver struct{ *Resolver }
type taskChecklistResolver struct{ *Resolver }
type taskChecklistItemResolver struct{ *Resolver }
type taskGroupResolver struct{ *Resolver }
type taskLabelResolver struct{ *Resolver }
type teamResolver struct{ *Resolver }
type userAccountResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
type MemberType string
const (
MemberTypeInvited MemberType = "INVITED"
MemberTypeJoined MemberType = "JOINED"
)
type MasterEntry struct {
MemberType MemberType
ID uuid.UUID
}

View File

@ -135,6 +135,38 @@ type TaskBadges {
checklist: ChecklistBadge
}
type CausedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon
}
type TaskActivityData {
name: String!
value: String!
}
enum ActivityType {
TASK_ADDED
TASK_MOVED
TASK_MARKED_COMPLETE
TASK_MARKED_INCOMPLETE
TASK_DUE_DATE_CHANGED
TASK_DUE_DATE_ADDED
TASK_DUE_DATE_REMOVED
TASK_CHECKLIST_CHANGED
TASK_CHECKLIST_ADDED
TASK_CHECKLIST_REMOVED
}
type TaskActivity {
id: ID!
type: ActivityType!
data: [TaskActivityData!]!
causedBy: CausedBy!
createdAt: Time!
}
type Task {
id: ID!
taskGroup: TaskGroup!
@ -149,6 +181,7 @@ type Task {
labels: [TaskLabel!]!
checklists: [TaskChecklist!]!
badges: TaskBadges!
activity: [TaskActivity!]!
}
type Organization {

View File

@ -0,0 +1,28 @@
CREATE TABLE task_activity_type (
task_activity_type_id int PRIMARY KEY,
code text NOT NULL,
template text NOT NULL
);
INSERT INTO task_activity_type (task_activity_type_id, code, template) VALUES
(1, 'task_added_to_task_group', 'added this task to {{ index .Data "TaskGroup" }}'),
(2, 'task_moved_to_task_group', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
(3, 'task_mark_complete', 'marked this task complete'),
(4, 'task_mark_incomplete', 'marked this task incomplete'),
(5, 'task_due_date_changed', 'changed the due date to {{ index .Data "DueDate" }}'),
(6, 'task_due_date_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
(7, 'task_due_date_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
(8, 'task_checklist_changed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
(9, 'task_checklist_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'),
(10, 'task_checklist_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}');
CREATE TABLE task_activity (
task_activity_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
active boolean NOT NULL DEFAULT true,
task_id uuid NOT NULL REFERENCES task(task_id),
created_at timestamptz NOT NULL,
caused_by uuid NOT NULL,
activity_type_id int NOT NULL REFERENCES task_activity_type(task_activity_type_id),
data jsonb
);