feat: add comments badge to task card

This commit is contained in:
Jordan Knott 2021-10-25 15:14:24 -05:00
parent 3992e4c2de
commit 119a4b2868
18 changed files with 385 additions and 100 deletions

View File

@ -1,18 +1,27 @@
import styled, { css, keyframes } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons'; import { CheckCircle, CheckSquareOutline, Clock, Bubble } from 'shared/icons';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>` export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.secondary}, box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.secondary},
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.secondary, 0.07)}; inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
z-index: ${props => props.zIndex}; z-index: ${(props) => props.zIndex};
position: relative; position: relative;
`; `;
export const CommentsIcon = styled(Bubble)<{ color: 'success' | 'normal' }>`
${(props) =>
props.color === 'success' &&
css`
fill: ${props.theme.colors.success};
stroke: ${props.theme.colors.success};
`}
`;
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>` export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
${props => ${(props) =>
props.color === 'success' && props.color === 'success' &&
css` css`
fill: ${props.theme.colors.success}; fill: ${props.theme.colors.success};
@ -21,7 +30,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
`; `;
export const ClockIcon = styled(Clock)<{ color: string }>` export const ClockIcon = styled(Clock)<{ color: string }>`
fill: ${props => props.color}; fill: ${(props) => props.color};
`; `;
export const EditorTextarea = styled(TextareaAutosize)` export const EditorTextarea = styled(TextareaAutosize)`
@ -40,7 +49,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
padding: 0; padding: 0;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
&:focus { &:focus {
border: none; border: none;
outline: none; outline: none;
@ -54,6 +63,22 @@ export const ListCardBadges = styled.div`
margin-left: -2px; margin-left: -2px;
`; `;
export const CommentsBadge = styled.div`
color: #5e6c84;
display: flex;
align-items: center;
margin: 0 6px 4px 0;
font-size: 12px;
max-width: 100%;
min-height: 20px;
overflow: hidden;
position: relative;
padding: 2px;
text-decoration: none;
text-overflow: ellipsis;
vertical-align: top;
`;
export const ListCardBadge = styled.div` export const ListCardBadge = styled.div`
color: #5e6c84; color: #5e6c84;
display: flex; display: flex;
@ -76,7 +101,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>` export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
font-size: 12px; font-size: 12px;
${props => ${(props) =>
props.isPastDue && props.isPastDue &&
css` css`
padding-left: 4px; padding-left: 4px;
@ -91,7 +116,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
padding: 0 4px 0 6px; padding: 0 4px 0 6px;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
${props => props.color === 'success' && `color: ${props.theme.colors.success};`} ${(props) => props.color === 'success' && `color: ${props.theme.colors.success};`}
`; `;
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>` export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
@ -102,7 +127,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
cursor: pointer !important; cursor: pointer !important;
position: relative; position: relative;
background-color: ${props => background-color: ${(props) =>
props.isActive && !props.editable props.isActive && !props.editable
? mixin.darken(props.theme.colors.bg.secondary, 0.1) ? mixin.darken(props.theme.colors.bg.secondary, 0.1)
: `${props.theme.colors.bg.secondary}`}; : `${props.theme.colors.bg.secondary}`};
@ -119,7 +144,7 @@ export const ListCardDetails = styled.div<{ complete: boolean }>`
position: relative; position: relative;
z-index: 10; z-index: 10;
${props => props.complete && 'opacity: 0.6;'} ${(props) => props.complete && 'opacity: 0.6;'}
`; `;
const labelVariantExpandAnimation = keyframes` const labelVariantExpandAnimation = keyframes`
@ -157,7 +182,7 @@ export const ListCardLabelsWrapper = styled.div`
`; `;
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>` export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
${props => ${(props) =>
props.variant === 'small' props.variant === 'small'
? css` ? css`
height: 8px; height: 8px;
@ -183,14 +208,14 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
color: #fff; color: #fff;
display: flex; display: flex;
position: relative; position: relative;
background-color: ${props => props.color}; background-color: ${(props) => props.color};
`; `;
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>` export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
${props => ${(props) =>
props.toggleLabels && props.toggleLabels &&
props.toggleDirection === 'expand' && props.toggleDirection === 'expand' &&
css` css`
@ -201,7 +226,7 @@ export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirectio
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out; animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
} }
`} `}
${props => ${(props) =>
props.toggleLabels && props.toggleLabels &&
props.toggleDirection === 'shrink' && props.toggleDirection === 'shrink' &&
css` css`
@ -225,7 +250,7 @@ export const ListCardOperation = styled.span`
top: 2px; top: 2px;
z-index: 100; z-index: 100;
&:hover { &:hover {
background-color: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.25)}; background-color: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
} }
`; `;
@ -234,7 +259,7 @@ export const CardTitle = styled.div`
margin: 0 0 4px; margin: 0 0 4px;
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
display: block; display: block;
align-items: center; align-items: center;
`; `;
@ -251,7 +276,7 @@ export const CardMembers = styled.div`
`; `;
export const CompleteIcon = styled(CheckCircle)` export const CompleteIcon = styled(CheckCircle)`
fill: ${props => props.theme.colors.success}; fill: ${(props) => props.theme.colors.success};
margin-right: 4px; margin-right: 4px;
flex-shrink: 0; flex-shrink: 0;
margin-bottom: -2px; margin-bottom: -2px;

View File

@ -23,6 +23,8 @@ import {
CardTitle, CardTitle,
CardMembers, CardMembers,
CardTitleText, CardTitleText,
CommentsIcon,
CommentsBadge,
} from './Styles'; } from './Styles';
type DueDate = { type DueDate = {
@ -47,6 +49,7 @@ type Props = {
dueDate?: DueDate; dueDate?: DueDate;
checklists?: Checklist | null; checklists?: Checklist | null;
labels?: Array<ProjectLabel>; labels?: Array<ProjectLabel>;
comments?: { unread: boolean; total: number } | null;
watched?: boolean; watched?: boolean;
wrapperProps?: any; wrapperProps?: any;
members?: Array<TaskUser> | null; members?: Array<TaskUser> | null;
@ -72,6 +75,7 @@ const Card = React.forwardRef(
taskGroupID, taskGroupID,
complete, complete,
toggleLabels = false, toggleLabels = false,
comments,
toggleDirection = 'shrink', toggleDirection = 'shrink',
setToggleLabels, setToggleLabels,
onClick, onClick,
@ -138,7 +142,7 @@ const Card = React.forwardRef(
onMouseEnter={() => setActive(true)} onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)} onMouseLeave={() => setActive(false)}
ref={$cardRef} ref={$cardRef}
onClick={e => { onClick={(e) => {
if (onClick) { if (onClick) {
onClick(e); onClick(e);
} }
@ -151,7 +155,7 @@ const Card = React.forwardRef(
<ListCardInnerContainer ref={$innerCardRef}> <ListCardInnerContainer ref={$innerCardRef}>
{!isPublic && isActive && !editable && ( {!isPublic && isActive && !editable && (
<ListCardOperation <ListCardOperation
onClick={e => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (onContextMenu) { if (onContextMenu) {
onContextMenu($innerCardRef, taskID, taskGroupID); onContextMenu($innerCardRef, taskID, taskGroupID);
@ -167,7 +171,7 @@ const Card = React.forwardRef(
<ListCardLabels <ListCardLabels
toggleLabels={toggleLabels} toggleLabels={toggleLabels}
toggleDirection={toggleDirection} toggleDirection={toggleDirection}
onClick={e => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (onCardLabelClick) { if (onCardLabelClick) {
onCardLabelClick(); onCardLabelClick();
@ -177,7 +181,7 @@ const Card = React.forwardRef(
{labels {labels
.slice() .slice()
.sort((a, b) => a.labelColor.position - b.labelColor.position) .sort((a, b) => a.labelColor.position - b.labelColor.position)
.map(label => ( .map((label) => (
<ListCardLabel <ListCardLabel
onAnimationEnd={() => { onAnimationEnd={() => {
if (setToggleLabels) { if (setToggleLabels) {
@ -198,13 +202,13 @@ const Card = React.forwardRef(
<EditorContent> <EditorContent>
{complete && <CompleteIcon width={16} height={16} />} {complete && <CompleteIcon width={16} height={16} />}
<EditorTextarea <EditorTextarea
onChange={e => { onChange={(e) => {
setCardTitle(e.currentTarget.value); setCardTitle(e.currentTarget.value);
if (onCardTitleChange) { if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value); onCardTitleChange(e.currentTarget.value);
} }
}} }}
onClick={e => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -235,6 +239,12 @@ const Card = React.forwardRef(
<List width={8} height={8} /> <List width={8} height={8} />
</DescriptionBadge> </DescriptionBadge>
)} )}
{comments && (
<CommentsBadge>
<CommentsIcon color={comments.unread ? 'success' : 'normal'} width={8} height={8} />
<ListCardBadgeText color={comments.unread ? 'success' : 'normal'}>{comments.total}</ListCardBadgeText>
</CommentsBadge>
)}
{checklists && ( {checklists && (
<ListCardBadge> <ListCardBadge>
<ChecklistIcon <ChecklistIcon
@ -256,7 +266,7 @@ const Card = React.forwardRef(
size={28} size={28}
zIndex={members.length - idx} zIndex={members.length - idx}
member={member} member={member}
onMemberProfile={$target => { onMemberProfile={($target) => {
if (onCardMemberClick) { if (onCardMemberClick) {
onCardMemberClick($target, taskID, member.id); onCardMemberClick($target, taskID, member.id);
} }

View File

@ -111,24 +111,16 @@ function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
const TODAY = REFERENCE.clone().startOf('day'); const TODAY = REFERENCE.clone().startOf('day');
return completedAt.isSame(TODAY, 'd'); return completedAt.isSame(TODAY, 'd');
case TaskSince.YESTERDAY: case TaskSince.YESTERDAY:
const YESTERDAY = REFERENCE.clone() const YESTERDAY = REFERENCE.clone().subtract(1, 'day').startOf('day');
.subtract(1, 'day')
.startOf('day');
return completedAt.isSameOrAfter(YESTERDAY, 'd'); return completedAt.isSameOrAfter(YESTERDAY, 'd');
case TaskSince.ONE_WEEK: case TaskSince.ONE_WEEK:
const ONE_WEEK = REFERENCE.clone() const ONE_WEEK = REFERENCE.clone().subtract(7, 'day').startOf('day');
.subtract(7, 'day')
.startOf('day');
return completedAt.isSameOrAfter(ONE_WEEK, 'd'); return completedAt.isSameOrAfter(ONE_WEEK, 'd');
case TaskSince.TWO_WEEKS: case TaskSince.TWO_WEEKS:
const TWO_WEEKS = REFERENCE.clone() const TWO_WEEKS = REFERENCE.clone().subtract(14, 'day').startOf('day');
.subtract(14, 'day')
.startOf('day');
return completedAt.isSameOrAfter(TWO_WEEKS, 'd'); return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
case TaskSince.THREE_WEEKS: case TaskSince.THREE_WEEKS:
const THREE_WEEKS = REFERENCE.clone() const THREE_WEEKS = REFERENCE.clone().subtract(21, 'day').startOf('day');
.subtract(21, 'day')
.startOf('day');
return completedAt.isSameOrAfter(THREE_WEEKS, 'd'); return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
default: default:
return true; return true;
@ -203,14 +195,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
let beforeDropDraggables: Array<DraggableElement> | null = null; let beforeDropDraggables: Array<DraggableElement> | null = null;
if (isList) { if (isList) {
const droppedGroup = taskGroups.find(taskGroup => taskGroup.id === draggableId); const droppedGroup = taskGroups.find((taskGroup) => taskGroup.id === draggableId);
if (droppedGroup) { if (droppedGroup) {
droppedDraggable = { droppedDraggable = {
id: draggableId, id: draggableId,
position: droppedGroup.position, position: droppedGroup.position,
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
taskGroups.map(taskGroup => { taskGroups.map((taskGroup) => {
return { id: taskGroup.id, position: taskGroup.position }; return { id: taskGroup.id, position: taskGroup.position };
}), }),
); );
@ -234,13 +226,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
} }
} else { } else {
const curTaskGroup = taskGroups.findIndex( const curTaskGroup = taskGroups.findIndex(
taskGroup => taskGroup.tasks.findIndex(task => task.id === draggableId) !== -1, (taskGroup) => taskGroup.tasks.findIndex((task) => task.id === draggableId) !== -1,
); );
let targetTaskGroup = curTaskGroup; let targetTaskGroup = curTaskGroup;
if (!isSameList) { if (!isSameList) {
targetTaskGroup = taskGroups.findIndex(taskGroup => taskGroup.id === destination.droppableId); targetTaskGroup = taskGroups.findIndex((taskGroup) => taskGroup.id === destination.droppableId);
} }
const droppedTask = taskGroups[curTaskGroup].tasks.find(task => task.id === draggableId); const droppedTask = taskGroups[curTaskGroup].tasks.find((task) => task.id === draggableId);
if (droppedTask) { if (droppedTask) {
droppedDraggable = { droppedDraggable = {
@ -248,7 +240,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
position: droppedTask.position, position: droppedTask.position,
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
taskGroups[targetTaskGroup].tasks.map(task => { taskGroups[targetTaskGroup].tasks.map((task) => {
return { id: task.id, position: task.position }; return { id: task.id, position: task.position };
}), }),
); );
@ -286,7 +278,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
<BoardWrapper> <BoardWrapper>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root"> <Droppable direction="horizontal" type="column" droppableId="root">
{provided => ( {(provided) => (
<Container {...provided.droppableProps} ref={provided.innerRef}> <Container {...provided.droppableProps} ref={provided.innerRef}>
{taskGroups {taskGroups
.slice() .slice()
@ -294,14 +286,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
.map((taskGroup: TaskGroup, index: number) => { .map((taskGroup: TaskGroup, index: number) => {
return ( return (
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}> <Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
{columnDragProvided => ( {(columnDragProvided) => (
<Droppable type="tasks" droppableId={taskGroup.id}> <Droppable type="tasks" droppableId={taskGroup.id}>
{(columnDropProvided, snapshot) => ( {(columnDropProvided, snapshot) => (
<List <List
name={taskGroup.name} name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)} onOpenComposer={(id) => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id} isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)} onSaveName={(name) => onChangeTaskGroupName(taskGroup.id, name)}
isPublic={isPublic} isPublic={isPublic}
ref={columnDragProvided.innerRef} ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps} wrapperProps={columnDragProvided.draggableProps}
@ -314,8 +306,8 @@ const SimpleLists: React.FC<SimpleProps> = ({
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}> <ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{taskGroup.tasks {taskGroup.tasks
.slice() .slice()
.filter(t => shouldStatusFilter(t, taskStatusFilter)) .filter((t) => shouldStatusFilter(t, taskStatusFilter))
.filter(t => shouldMetaFilter(t, taskMetaFilters)) .filter((t) => shouldMetaFilter(t, taskMetaFilters))
.sort((a: any, b: any) => a.position - b.position) .sort((a: any, b: any) => a.position - b.position)
.sort((a: any, b: any) => sortTasks(a, b, taskSorting)) .sort((a: any, b: any) => sortTasks(a, b, taskSorting))
.map((task: Task, taskIndex: any) => { .map((task: Task, taskIndex: any) => {
@ -326,7 +318,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
index={taskIndex} index={taskIndex}
isDragDisabled={taskSorting.type !== TaskSortingType.NONE} isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
> >
{taskProvided => { {(taskProvided) => {
return ( return (
<Card <Card
toggleDirection={toggleDirection} toggleDirection={toggleDirection}
@ -352,7 +344,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
complete={task.complete ?? false} complete={task.complete ?? false}
taskGroupID={taskGroup.id} taskGroupID={taskGroup.id}
description="" description=""
labels={task.labels.map(label => label.projectLabel)} labels={task.labels.map((label) => label.projectLabel)}
dueDate={ dueDate={
task.dueDate task.dueDate
? { ? {
@ -367,6 +359,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
onTaskClick(task); onTaskClick(task);
}} }}
checklists={task.badges && task.badges.checklist} checklists={task.badges && task.badges.checklist}
comments={task.badges && task.badges.comments}
onCardMemberClick={onCardMemberClick} onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen} onContextMenu={onQuickEditorOpen}
/> />
@ -381,7 +374,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
onClose={() => { onClose={() => {
setCurrentComposer(''); setCurrentComposer('');
}} }}
onCreateCard={name => { onCreateCard={(name) => {
onCreateTask(taskGroup.id, name); onCreateTask(taskGroup.id, name);
}} }}
isOpen isOpen
@ -402,7 +395,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
</DragDropContext> </DragDropContext>
{!isPublic && ( {!isPublic && (
<AddList <AddList
onSave={listName => { onSave={(listName) => {
onCreateTaskGroup(listName); onCreateTaskGroup(listName);
}} }}
/> />

View File

@ -22,14 +22,14 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
position: relative; position: relative;
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: ${props => props.theme.borderRadius.alternate}; border-radius: ${(props) => props.theme.borderRadius.alternate};
display: flex; display: flex;
align-items: center; align-items: center;
background: transparent; background: transparent;
& span { & span {
margin-left: 4px; margin-left: 4px;
} }
${props => ${(props) =>
props.invert props.invert
? css` ? css`
background: ${props.theme.colors.success}; background: ${props.theme.colors.success};
@ -63,7 +63,7 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
color: ${props.theme.colors.success}; color: ${props.theme.colors.success};
} }
`} `}
${props => ${(props) =>
props.invert && props.invert &&
css` css`
opacity: 0.6; opacity: 0.6;
@ -89,7 +89,7 @@ export const SidebarTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 4px; padding-top: 4px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -110,12 +110,12 @@ export const skeletonKeyframes = keyframes`
export const SidebarButton = styled.div<{ $loading?: boolean }>` export const SidebarButton = styled.div<{ $loading?: boolean }>`
font-size: 14px; font-size: 14px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
border-radius: 6px; border-radius: 6px;
${props => ${(props) =>
props.$loading props.$loading
? css` ? css`
background: ${props.theme.colors.bg.primary}; background: ${props.theme.colors.bg.primary};
@ -183,7 +183,7 @@ export const TaskDetailsTitleWrapper = styled.div<{ $loading?: boolean }>`
margin: 8px 0 4px 0; margin: 8px 0 4px 0;
display: flex; display: flex;
border-radius: 6px; border-radius: 6px;
${props => props.$loading && `background: ${props.theme.colors.bg.primary};`} ${(props) => props.$loading && `background: ${props.theme.colors.bg.primary};`}
`; `;
export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>` export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>`
@ -201,7 +201,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>
&:disabled { &:disabled {
opacity: 1; opacity: 1;
} }
${props => ${(props) =>
props.$loading props.$loading
? css` ? css`
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor}); background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
@ -226,7 +226,7 @@ export const DueDateTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 8px; padding-top: 8px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -237,7 +237,7 @@ export const AssignedUsersSection = styled.div`
padding-right: 32px; padding-right: 32px;
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
border-bottom: 1px solid ${props => props.theme.colors.alternate}; border-bottom: 1px solid ${(props) => props.theme.colors.alternate};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
@ -255,10 +255,10 @@ export const AssignUserIcon = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&:hover { &:hover {
border: 1px solid ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)}; border: 1px solid ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)}; fill: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
`; `;
@ -273,17 +273,17 @@ export const AssignUsersButton = styled.div`
align-items: center; align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
&:hover { &:hover {
border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)}; border: 1px solid ${(props) => mixin.darken(props.theme.colors.alternate, 0.15)};
} }
&:hover ${AssignUserIcon} { &:hover ${AssignUserIcon} {
border: 1px solid ${props => props.theme.colors.alternate}; border: 1px solid ${(props) => props.theme.colors.alternate};
} }
`; `;
export const AssignUserLabel = styled.span` export const AssignUserLabel = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
line-height: 15px; line-height: 15px;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
`; `;
export const ExtraActionsSection = styled.div` export const ExtraActionsSection = styled.div`
@ -295,7 +295,7 @@ export const ExtraActionsSection = styled.div`
`; `;
export const ActionButtonsTitle = styled.h3` export const ActionButtonsTitle = styled.h3`
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -305,7 +305,7 @@ export const ActionButton = styled(Button)`
margin-top: 8px; margin-top: 8px;
margin-left: -10px; margin-left: -10px;
padding: 8px 16px; padding: 8px 16px;
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.5)}; background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
text-align: left; text-align: left;
transition: transform 0.2s ease; transition: transform 0.2s ease;
& span { & span {
@ -314,7 +314,7 @@ export const ActionButton = styled(Button)`
&:hover { &:hover {
box-shadow: none; box-shadow: none;
transform: translateX(4px); transform: translateX(4px);
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.75)}; background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
} }
`; `;
@ -333,10 +333,10 @@ export const HeaderActionIcon = styled.div`
cursor: pointer; cursor: pointer;
svg { svg {
fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)}; fill: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: ${props => mixin.rgba(props.theme.colors.primary, 0.75)}); fill: ${(props) => mixin.rgba(props.theme.colors.primary, 0.75)});
} }
`; `;
@ -393,7 +393,7 @@ export const MetaDetail = styled.div`
`; `;
export const MetaDetailTitle = styled.h3` export const MetaDetailTitle = styled.h3`
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -412,7 +412,7 @@ export const MetaDetailContent = styled.div`
`; `;
export const TaskDetailsAddLabel = styled.div` export const TaskDetailsAddLabel = styled.div`
border-radius: 3px; border-radius: 3px;
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)}; background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -427,7 +427,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 3px; border-radius: 3px;
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)}; background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -443,7 +443,7 @@ export const TaskDetailLabel = styled.div<{ color: string }>`
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
background-color: ${props => props.color}; background-color: ${(props) => props.color};
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -496,17 +496,22 @@ export const TabBarSection = styled.div`
margin-top: 2px; margin-top: 2px;
padding-left: 23px; padding-left: 23px;
display: flex; display: flex;
justify-content: space-between;
text-transform: uppercase; text-transform: uppercase;
min-height: 35px; min-height: 35px;
border-bottom: 1px solid #414561; border-bottom: 1px solid #414561;
`; `;
export const TabBarItem = styled.div` export const TabBarItem = styled.div`
box-shadow: inset 0 -2px ${props => props.theme.colors.primary}; box-shadow: inset 0 -2px ${(props) => props.theme.colors.primary};
padding: 12px 7px 14px 7px; padding: 12px 7px 14px 7px;
margin-bottom: -1px; margin-bottom: -1px;
margin-right: 36px; margin-right: 36px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`;
export const TabBarButton = styled(Button)`
padding: 6px 12px;
`; `;
export const CommentContainer = styled.div` export const CommentContainer = styled.div`
@ -542,13 +547,13 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
line-height: 28px; line-height: 28px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
background: #1f243e; background: #1f243e;
border: none; border: none;
transition: max-height 200ms, height 200ms, min-height 200ms; transition: max-height 200ms, height 200ms, min-height 200ms;
min-height: 36px; min-height: 36px;
max-height: 36px; max-height: 36px;
${props => ${(props) =>
props.$showCommentActions props.$showCommentActions
? css` ? css`
min-height: 80px; min-height: 80px;
@ -561,7 +566,7 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
`; `;
export const CommentEditorActions = styled.div<{ visible: boolean }>` export const CommentEditorActions = styled.div<{ visible: boolean }>`
display: ${props => (props.visible ? 'flex' : 'none')}; display: ${(props) => (props.visible ? 'flex' : 'none')};
align-items: center; align-items: center;
padding: 5px 5px 5px 9px; padding: 5px 5px 5px 9px;
border-top: 1px solid #414561; border-top: 1px solid #414561;
@ -594,7 +599,7 @@ export const ActivityItemCommentAction = styled.div`
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
svg { svg {
fill: ${props => props.theme.colors.text.primary} !important; fill: ${(props) => props.theme.colors.text.primary} !important;
} }
`; `;
@ -614,7 +619,7 @@ export const ActivityItemHeader = styled.div<{ editable?: boolean }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 8px; padding-left: 8px;
${props => props.editable && 'width: 100%;'} ${(props) => props.editable && 'width: 100%;'}
`; `;
export const ActivityItemHeaderUser = styled(TaskAssignee)` export const ActivityItemHeaderUser = styled(TaskAssignee)`
align-items: start; align-items: start;
@ -623,7 +628,7 @@ export const ActivityItemHeaderUser = styled(TaskAssignee)`
export const ActivityItemHeaderTitle = styled.div` export const ActivityItemHeaderTitle = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
padding-bottom: 2px; padding-bottom: 2px;
`; `;
@ -634,8 +639,8 @@ export const ActivityItemHeaderTitleName = styled.span`
export const ActivityItemTimestamp = styled.span<{ margin: number }>` export const ActivityItemTimestamp = styled.span<{ margin: number }>`
font-size: 12px; font-size: 12px;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.65)};
margin-left: ${props => props.margin}px; margin-left: ${(props) => props.margin}px;
`; `;
export const ActivityItemDetails = styled.div` export const ActivityItemDetails = styled.div`
@ -649,11 +654,11 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
border-radius: 3px; border-radius: 3px;
${mixin.boxShadowCard} ${mixin.boxShadowCard}
position: relative; position: relative;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
padding: 8px 12px; padding: 8px 12px;
margin: 4px 0; margin: 4px 0;
background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)}; background-color: ${(props) => mixin.darken(props.theme.colors.alternate, 0.1)};
${props => props.editable && 'width: 100%;'} ${(props) => props.editable && 'width: 100%;'}
& span { & span {
display: inline-flex; display: inline-flex;
@ -683,7 +688,7 @@ export const ActivityItemCommentActions = styled.div`
export const ActivityItemLog = styled.span` export const ActivityItemLog = styled.span`
margin-left: 2px; margin-left: 2px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
export const ViewRawButton = styled.button` export const ViewRawButton = styled.button`
@ -694,9 +699,9 @@ export const ViewRawButton = styled.button`
right: 4px; right: 4px;
bottom: -24px; bottom: -24px;
cursor: pointer; cursor: pointer;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.25)};
&:hover { &:hover {
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
} }
`; `;

View File

@ -79,6 +79,7 @@ import {
ActivityItemHeaderTitle, ActivityItemHeaderTitle,
ActivityItemHeaderTitleName, ActivityItemHeaderTitleName,
ActivityItemComment, ActivityItemComment,
TabBarButton,
} from './Styles'; } from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd'; import onDragEnd from './onDragEnd';

View File

@ -69,6 +69,12 @@ export type ChecklistBadge = {
total: Scalars['Int']; total: Scalars['Int'];
}; };
export type CommentsBadge = {
__typename?: 'CommentsBadge';
total: Scalars['Int'];
unread: Scalars['Boolean'];
};
export type CreateTaskChecklist = { export type CreateTaskChecklist = {
taskID: Scalars['UUID']; taskID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
@ -1027,6 +1033,7 @@ export type TaskActivityData = {
export type TaskBadges = { export type TaskBadges = {
__typename?: 'TaskBadges'; __typename?: 'TaskBadges';
checklist?: Maybe<ChecklistBadge>; checklist?: Maybe<ChecklistBadge>;
comments?: Maybe<CommentsBadge>;
}; };
export type TaskChecklist = { export type TaskChecklist = {
@ -1592,6 +1599,9 @@ export type TaskFieldsFragment = (
& { checklist?: Maybe<( & { checklist?: Maybe<(
{ __typename?: 'ChecklistBadge' } { __typename?: 'ChecklistBadge' }
& Pick<ChecklistBadge, 'complete' | 'total'> & Pick<ChecklistBadge, 'complete' | 'total'>
)>, comments?: Maybe<(
{ __typename?: 'CommentsBadge' }
& Pick<CommentsBadge, 'unread' | 'total'>
)> } )> }
), taskGroup: ( ), taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
@ -2693,6 +2703,10 @@ export const TaskFieldsFragmentDoc = gql`
complete complete
total total
} }
comments {
unread
total
}
} }
taskGroup { taskGroup {
id id

View File

@ -15,6 +15,10 @@ const TASK_FRAGMENT = gql`
complete complete
total total
} }
comments {
unread
total
}
} }
taskGroup { taskGroup {
id id

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Bubble: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 576 512">
<path d="M416 192c0-88.4-93.1-160-208-160S0 103.6 0 192c0 34.3 14.1 65.9 38 92-13.4 30.2-35.5 54.2-35.8 54.5-2.2 2.3-2.8 5.7-1.5 8.7S4.8 352 8 352c36.6 0 66.9-12.3 88.7-25 32.2 15.7 70.3 25 111.3 25 114.9 0 208-71.6 208-160zm122 220c23.9-26 38-57.7 38-92 0-66.9-53.5-124.2-129.3-148.1.9 6.6 1.3 13.3 1.3 20.1 0 105.9-107.7 192-240 192-10.8 0-21.3-.8-31.7-1.9C207.8 439.6 281.8 480 368 480c41 0 79.1-9.2 111.3-25 21.8 12.7 52.1 25 88.7 25 3.2 0 6.1-1.9 7.3-4.8 1.3-2.9.7-6.3-1.5-8.7-.3-.3-22.4-24.2-35.8-54.5z" />
</Icon>
);
};
export default Bubble;

View File

@ -1,6 +1,7 @@
import Cross from './Cross'; import Cross from './Cross';
import Cog from './Cog'; import Cog from './Cog';
import Cogs from './Cogs'; import Cogs from './Cogs';
import Bubble from './Bubble';
import ArrowDown from './ArrowDown'; import ArrowDown from './ArrowDown';
import CheckCircleOutline from './CheckCircleOutline'; import CheckCircleOutline from './CheckCircleOutline';
import Briefcase from './Briefcase'; import Briefcase from './Briefcase';
@ -110,5 +111,6 @@ export {
Briefcase, Briefcase,
DotCircle, DotCircle,
ChevronRight, ChevronRight,
Bubble,
Cogs, Cogs,
}; };

View File

@ -59,8 +59,13 @@ type ChecklistBadge = {
total: number; total: number;
}; };
type CommentsBadge = {
total: number;
unread: boolean;
};
type TaskBadges = { type TaskBadges = {
checklist?: ChecklistBadge | null; checklist?: ChecklistBadge | null;
comments?: CommentsBadge | null;
}; };
type TaskActivityData = { type TaskActivityData = {

View File

@ -74,6 +74,7 @@ type Querier interface {
GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error) GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error)
GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error)
GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error) GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error)
GetCommentCountForTask(ctx context.Context, taskID uuid.UUID) (int64, error)
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)

View File

@ -95,3 +95,6 @@ SELECT task.* FROM task_assigned
) )
) )
ORDER BY task.due_date DESC, task_group.project_id DESC; ORDER BY task.due_date DESC, task_group.project_id DESC;
-- name: GetCommentCountForTask :one
SELECT COUNT(*) FROM task_comment WHERE task_id = $1;

View File

@ -316,6 +316,17 @@ func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetA
return items, nil return items, nil
} }
const getCommentCountForTask = `-- name: GetCommentCountForTask :one
SELECT COUNT(*) FROM task_comment WHERE task_id = $1
`
func (q *Queries) GetCommentCountForTask(ctx context.Context, taskID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, getCommentCountForTask, taskID)
var count int64
err := row.Scan(&count)
return count, err
}
const getCommentsForTaskID = `-- name: GetCommentsForTaskID :many const getCommentsForTaskID = `-- name: GetCommentsForTaskID :many
SELECT task_comment_id, task_id, created_at, updated_at, created_by, pinned, message FROM task_comment WHERE task_id = $1 ORDER BY created_at SELECT task_comment_id, task_id, created_at, updated_at, created_by, pinned, message FROM task_comment WHERE task_id = $1 ORDER BY created_at
` `

View File

@ -73,6 +73,11 @@ type ComplexityRoot struct {
Total func(childComplexity int) int Total func(childComplexity int) int
} }
CommentsBadge struct {
Total func(childComplexity int) int
Unread func(childComplexity int) int
}
CreateTaskCommentPayload struct { CreateTaskCommentPayload struct {
Comment func(childComplexity int) int Comment func(childComplexity int) int
TaskID func(childComplexity int) int TaskID func(childComplexity int) int
@ -419,6 +424,7 @@ type ComplexityRoot struct {
TaskBadges struct { TaskBadges struct {
Checklist func(childComplexity int) int Checklist func(childComplexity int) int
Comments func(childComplexity int) int
} }
TaskChecklist struct { TaskChecklist struct {
@ -768,6 +774,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ChecklistBadge.Total(childComplexity), true return e.complexity.ChecklistBadge.Total(childComplexity), true
case "CommentsBadge.total":
if e.complexity.CommentsBadge.Total == nil {
break
}
return e.complexity.CommentsBadge.Total(childComplexity), true
case "CommentsBadge.unread":
if e.complexity.CommentsBadge.Unread == nil {
break
}
return e.complexity.CommentsBadge.Unread(childComplexity), true
case "CreateTaskCommentPayload.comment": case "CreateTaskCommentPayload.comment":
if e.complexity.CreateTaskCommentPayload.Comment == nil { if e.complexity.CreateTaskCommentPayload.Comment == nil {
break break
@ -2553,6 +2573,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskBadges.Checklist(childComplexity), true return e.complexity.TaskBadges.Checklist(childComplexity), true
case "TaskBadges.comments":
if e.complexity.TaskBadges.Comments == nil {
break
}
return e.complexity.TaskBadges.Comments(childComplexity), true
case "TaskChecklist.id": case "TaskChecklist.id":
if e.complexity.TaskChecklist.ID == nil { if e.complexity.TaskChecklist.ID == nil {
break break
@ -3212,8 +3239,14 @@ type ChecklistBadge {
total: Int! total: Int!
} }
type CommentsBadge {
total: Int!
unread: Boolean!
}
type TaskBadges { type TaskBadges {
checklist: ChecklistBadge checklist: ChecklistBadge
comments: CommentsBadge
} }
type CausedBy { type CausedBy {
@ -5280,6 +5313,76 @@ func (ec *executionContext) _ChecklistBadge_total(ctx context.Context, field gra
return ec.marshalNInt2int(ctx, field.Selections, res) return ec.marshalNInt2int(ctx, field.Selections, res)
} }
func (ec *executionContext) _CommentsBadge_total(ctx context.Context, field graphql.CollectedField, obj *CommentsBadge) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommentsBadge",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: 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.Total, 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.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _CommentsBadge_unread(ctx context.Context, field graphql.CollectedField, obj *CommentsBadge) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommentsBadge",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: 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.Unread, 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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _CreateTaskCommentPayload_taskID(ctx context.Context, field graphql.CollectedField, obj *CreateTaskCommentPayload) (ret graphql.Marshaler) { func (ec *executionContext) _CreateTaskCommentPayload_taskID(ctx context.Context, field graphql.CollectedField, obj *CreateTaskCommentPayload) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -14831,6 +14934,38 @@ func (ec *executionContext) _TaskBadges_checklist(ctx context.Context, field gra
return ec.marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐChecklistBadge(ctx, field.Selections, res) return ec.marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐChecklistBadge(ctx, field.Selections, res)
} }
func (ec *executionContext) _TaskBadges_comments(ctx context.Context, field graphql.CollectedField, obj *TaskBadges) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "TaskBadges",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: 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.Comments, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*CommentsBadge)
fc.Result = res
return ec.marshalOCommentsBadge2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCommentsBadge(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskChecklist_id(ctx context.Context, field graphql.CollectedField, obj *db.TaskChecklist) (ret graphql.Marshaler) { func (ec *executionContext) _TaskChecklist_id(ctx context.Context, field graphql.CollectedField, obj *db.TaskChecklist) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -20124,6 +20259,38 @@ func (ec *executionContext) _ChecklistBadge(ctx context.Context, sel ast.Selecti
return out return out
} }
var commentsBadgeImplementors = []string{"CommentsBadge"}
func (ec *executionContext) _CommentsBadge(ctx context.Context, sel ast.SelectionSet, obj *CommentsBadge) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, commentsBadgeImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CommentsBadge")
case "total":
out.Values[i] = ec._CommentsBadge_total(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "unread":
out.Values[i] = ec._CommentsBadge_unread(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var createTaskCommentPayloadImplementors = []string{"CreateTaskCommentPayload"} var createTaskCommentPayloadImplementors = []string{"CreateTaskCommentPayload"}
func (ec *executionContext) _CreateTaskCommentPayload(ctx context.Context, sel ast.SelectionSet, obj *CreateTaskCommentPayload) graphql.Marshaler { func (ec *executionContext) _CreateTaskCommentPayload(ctx context.Context, sel ast.SelectionSet, obj *CreateTaskCommentPayload) graphql.Marshaler {
@ -22580,6 +22747,8 @@ func (ec *executionContext) _TaskBadges(ctx context.Context, sel ast.SelectionSe
out.Values[i] = graphql.MarshalString("TaskBadges") out.Values[i] = graphql.MarshalString("TaskBadges")
case "checklist": case "checklist":
out.Values[i] = ec._TaskBadges_checklist(ctx, field, obj) out.Values[i] = ec._TaskBadges_checklist(ctx, field, obj)
case "comments":
out.Values[i] = ec._TaskBadges_comments(ctx, field, obj)
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -26306,6 +26475,13 @@ func (ec *executionContext) marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknott
return ec._ChecklistBadge(ctx, sel, v) return ec._ChecklistBadge(ctx, sel, v)
} }
func (ec *executionContext) marshalOCommentsBadge2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCommentsBadge(ctx context.Context, sel ast.SelectionSet, v *CommentsBadge) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._CommentsBadge(ctx, sel, v)
}
func (ec *executionContext) unmarshalOCreateTaskComment2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateTaskComment(ctx context.Context, v interface{}) (*CreateTaskComment, error) { func (ec *executionContext) unmarshalOCreateTaskComment2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateTaskComment(ctx context.Context, v interface{}) (*CreateTaskComment, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil

View File

@ -33,6 +33,11 @@ type ChecklistBadge struct {
Total int `json:"total"` Total int `json:"total"`
} }
type CommentsBadge struct {
Total int `json:"total"`
Unread bool `json:"unread"`
}
type CreateTaskChecklist struct { type CreateTaskChecklist struct {
TaskID uuid.UUID `json:"taskID"` TaskID uuid.UUID `json:"taskID"`
Name string `json:"name"` Name string `json:"name"`
@ -431,6 +436,7 @@ type TaskActivityData struct {
type TaskBadges struct { type TaskBadges struct {
Checklist *ChecklistBadge `json:"checklist"` Checklist *ChecklistBadge `json:"checklist"`
Comments *CommentsBadge `json:"comments"`
} }
type TaskPositionUpdate struct { type TaskPositionUpdate struct {

View File

@ -138,8 +138,14 @@ type ChecklistBadge {
total: Int! total: Int!
} }
type CommentsBadge {
total: Int!
unread: Boolean!
}
type TaskBadges { type TaskBadges {
checklist: ChecklistBadge checklist: ChecklistBadge
comments: CommentsBadge
} }
type CausedBy { type CausedBy {
@ -994,4 +1000,3 @@ type DeleteUserAccountPayload {
ok: Boolean! ok: Boolean!
userAccount: UserAccount! userAccount: UserAccount!
} }

View File

@ -1721,8 +1721,9 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
if err != nil { if err != nil {
return &TaskBadges{}, err return &TaskBadges{}, err
} }
if len(checklists) == 0 { comments, err := r.Repository.GetCommentCountForTask(ctx, obj.TaskID)
return &TaskBadges{Checklist: nil}, err if err != nil {
return &TaskBadges{}, err
} }
complete := 0 complete := 0
total := 0 total := 0
@ -1738,10 +1739,15 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
} }
} }
} }
if complete == 0 && total == 0 { var taskChecklist *ChecklistBadge
return &TaskBadges{Checklist: nil}, nil if total != 0 {
taskChecklist = &ChecklistBadge{Total: total, Complete: complete}
} }
return &TaskBadges{Checklist: &ChecklistBadge{Total: total, Complete: complete}}, nil var taskComments *CommentsBadge
if comments != 0 {
taskComments = &CommentsBadge{Total: int(comments), Unread: false}
}
return &TaskBadges{Checklist: taskChecklist, Comments: taskComments}, nil
} }
func (r *taskResolver) Activity(ctx context.Context, obj *db.Task) ([]db.TaskActivity, error) { func (r *taskResolver) Activity(ctx context.Context, obj *db.Task) ([]db.TaskActivity, error) {

View File

@ -138,8 +138,14 @@ type ChecklistBadge {
total: Int! total: Int!
} }
type CommentsBadge {
total: Int!
unread: Boolean!
}
type TaskBadges { type TaskBadges {
checklist: ChecklistBadge checklist: ChecklistBadge
comments: CommentsBadge
} }
type CausedBy { type CausedBy {