feat: add comments badge to task card
This commit is contained in:
parent
3992e4c2de
commit
119a4b2868
@ -1,18 +1,27 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
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';
|
||||
|
||||
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||
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)};
|
||||
z-index: ${props => props.zIndex};
|
||||
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)};
|
||||
z-index: ${(props) => props.zIndex};
|
||||
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' }>`
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.color === 'success' &&
|
||||
css`
|
||||
fill: ${props.theme.colors.success};
|
||||
@ -21,7 +30,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
|
||||
`;
|
||||
|
||||
export const ClockIcon = styled(Clock)<{ color: string }>`
|
||||
fill: ${props => props.color};
|
||||
fill: ${(props) => props.color};
|
||||
`;
|
||||
|
||||
export const EditorTextarea = styled(TextareaAutosize)`
|
||||
@ -40,7 +49,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
@ -54,6 +63,22 @@ export const ListCardBadges = styled.div`
|
||||
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`
|
||||
color: #5e6c84;
|
||||
display: flex;
|
||||
@ -76,7 +101,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
|
||||
|
||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||
font-size: 12px;
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.isPastDue &&
|
||||
css`
|
||||
padding-left: 4px;
|
||||
@ -91,7 +116,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
|
||||
padding: 0 4px 0 6px;
|
||||
vertical-align: top;
|
||||
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 }>`
|
||||
@ -102,7 +127,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
|
||||
cursor: pointer !important;
|
||||
position: relative;
|
||||
|
||||
background-color: ${props =>
|
||||
background-color: ${(props) =>
|
||||
props.isActive && !props.editable
|
||||
? mixin.darken(props.theme.colors.bg.secondary, 0.1)
|
||||
: `${props.theme.colors.bg.secondary}`};
|
||||
@ -119,7 +144,7 @@ export const ListCardDetails = styled.div<{ complete: boolean }>`
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
${props => props.complete && 'opacity: 0.6;'}
|
||||
${(props) => props.complete && 'opacity: 0.6;'}
|
||||
`;
|
||||
|
||||
const labelVariantExpandAnimation = keyframes`
|
||||
@ -157,7 +182,7 @@ export const ListCardLabelsWrapper = styled.div`
|
||||
`;
|
||||
|
||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.variant === 'small'
|
||||
? css`
|
||||
height: 8px;
|
||||
@ -183,14 +208,14 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||
color: #fff;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: ${props => props.color};
|
||||
background-color: ${(props) => props.color};
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.toggleLabels &&
|
||||
props.toggleDirection === 'expand' &&
|
||||
css`
|
||||
@ -201,7 +226,7 @@ export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirectio
|
||||
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
|
||||
}
|
||||
`}
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.toggleLabels &&
|
||||
props.toggleDirection === 'shrink' &&
|
||||
css`
|
||||
@ -225,7 +250,7 @@ export const ListCardOperation = styled.span`
|
||||
top: 2px;
|
||||
z-index: 100;
|
||||
&: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;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
display: block;
|
||||
align-items: center;
|
||||
`;
|
||||
@ -251,7 +276,7 @@ export const CardMembers = styled.div`
|
||||
`;
|
||||
|
||||
export const CompleteIcon = styled(CheckCircle)`
|
||||
fill: ${props => props.theme.colors.success};
|
||||
fill: ${(props) => props.theme.colors.success};
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: -2px;
|
||||
|
@ -23,6 +23,8 @@ import {
|
||||
CardTitle,
|
||||
CardMembers,
|
||||
CardTitleText,
|
||||
CommentsIcon,
|
||||
CommentsBadge,
|
||||
} from './Styles';
|
||||
|
||||
type DueDate = {
|
||||
@ -47,6 +49,7 @@ type Props = {
|
||||
dueDate?: DueDate;
|
||||
checklists?: Checklist | null;
|
||||
labels?: Array<ProjectLabel>;
|
||||
comments?: { unread: boolean; total: number } | null;
|
||||
watched?: boolean;
|
||||
wrapperProps?: any;
|
||||
members?: Array<TaskUser> | null;
|
||||
@ -72,6 +75,7 @@ const Card = React.forwardRef(
|
||||
taskGroupID,
|
||||
complete,
|
||||
toggleLabels = false,
|
||||
comments,
|
||||
toggleDirection = 'shrink',
|
||||
setToggleLabels,
|
||||
onClick,
|
||||
@ -138,7 +142,7 @@ const Card = React.forwardRef(
|
||||
onMouseEnter={() => setActive(true)}
|
||||
onMouseLeave={() => setActive(false)}
|
||||
ref={$cardRef}
|
||||
onClick={e => {
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
@ -151,7 +155,7 @@ const Card = React.forwardRef(
|
||||
<ListCardInnerContainer ref={$innerCardRef}>
|
||||
{!isPublic && isActive && !editable && (
|
||||
<ListCardOperation
|
||||
onClick={e => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onContextMenu) {
|
||||
onContextMenu($innerCardRef, taskID, taskGroupID);
|
||||
@ -167,7 +171,7 @@ const Card = React.forwardRef(
|
||||
<ListCardLabels
|
||||
toggleLabels={toggleLabels}
|
||||
toggleDirection={toggleDirection}
|
||||
onClick={e => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onCardLabelClick) {
|
||||
onCardLabelClick();
|
||||
@ -177,7 +181,7 @@ const Card = React.forwardRef(
|
||||
{labels
|
||||
.slice()
|
||||
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
||||
.map(label => (
|
||||
.map((label) => (
|
||||
<ListCardLabel
|
||||
onAnimationEnd={() => {
|
||||
if (setToggleLabels) {
|
||||
@ -198,13 +202,13 @@ const Card = React.forwardRef(
|
||||
<EditorContent>
|
||||
{complete && <CompleteIcon width={16} height={16} />}
|
||||
<EditorTextarea
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setCardTitle(e.currentTarget.value);
|
||||
if (onCardTitleChange) {
|
||||
onCardTitleChange(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
onClick={e => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
@ -235,6 +239,12 @@ const Card = React.forwardRef(
|
||||
<List width={8} height={8} />
|
||||
</DescriptionBadge>
|
||||
)}
|
||||
{comments && (
|
||||
<CommentsBadge>
|
||||
<CommentsIcon color={comments.unread ? 'success' : 'normal'} width={8} height={8} />
|
||||
<ListCardBadgeText color={comments.unread ? 'success' : 'normal'}>{comments.total}</ListCardBadgeText>
|
||||
</CommentsBadge>
|
||||
)}
|
||||
{checklists && (
|
||||
<ListCardBadge>
|
||||
<ChecklistIcon
|
||||
@ -256,7 +266,7 @@ const Card = React.forwardRef(
|
||||
size={28}
|
||||
zIndex={members.length - idx}
|
||||
member={member}
|
||||
onMemberProfile={$target => {
|
||||
onMemberProfile={($target) => {
|
||||
if (onCardMemberClick) {
|
||||
onCardMemberClick($target, taskID, member.id);
|
||||
}
|
||||
|
@ -111,24 +111,16 @@ function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
||||
const TODAY = REFERENCE.clone().startOf('day');
|
||||
return completedAt.isSame(TODAY, 'd');
|
||||
case TaskSince.YESTERDAY:
|
||||
const YESTERDAY = REFERENCE.clone()
|
||||
.subtract(1, 'day')
|
||||
.startOf('day');
|
||||
const YESTERDAY = REFERENCE.clone().subtract(1, 'day').startOf('day');
|
||||
return completedAt.isSameOrAfter(YESTERDAY, 'd');
|
||||
case TaskSince.ONE_WEEK:
|
||||
const ONE_WEEK = REFERENCE.clone()
|
||||
.subtract(7, 'day')
|
||||
.startOf('day');
|
||||
const ONE_WEEK = REFERENCE.clone().subtract(7, 'day').startOf('day');
|
||||
return completedAt.isSameOrAfter(ONE_WEEK, 'd');
|
||||
case TaskSince.TWO_WEEKS:
|
||||
const TWO_WEEKS = REFERENCE.clone()
|
||||
.subtract(14, 'day')
|
||||
.startOf('day');
|
||||
const TWO_WEEKS = REFERENCE.clone().subtract(14, 'day').startOf('day');
|
||||
return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
|
||||
case TaskSince.THREE_WEEKS:
|
||||
const THREE_WEEKS = REFERENCE.clone()
|
||||
.subtract(21, 'day')
|
||||
.startOf('day');
|
||||
const THREE_WEEKS = REFERENCE.clone().subtract(21, 'day').startOf('day');
|
||||
return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
|
||||
default:
|
||||
return true;
|
||||
@ -203,14 +195,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
let beforeDropDraggables: Array<DraggableElement> | null = null;
|
||||
|
||||
if (isList) {
|
||||
const droppedGroup = taskGroups.find(taskGroup => taskGroup.id === draggableId);
|
||||
const droppedGroup = taskGroups.find((taskGroup) => taskGroup.id === draggableId);
|
||||
if (droppedGroup) {
|
||||
droppedDraggable = {
|
||||
id: draggableId,
|
||||
position: droppedGroup.position,
|
||||
};
|
||||
beforeDropDraggables = getSortedDraggables(
|
||||
taskGroups.map(taskGroup => {
|
||||
taskGroups.map((taskGroup) => {
|
||||
return { id: taskGroup.id, position: taskGroup.position };
|
||||
}),
|
||||
);
|
||||
@ -234,13 +226,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
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) {
|
||||
droppedDraggable = {
|
||||
@ -248,7 +240,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
position: droppedTask.position,
|
||||
};
|
||||
beforeDropDraggables = getSortedDraggables(
|
||||
taskGroups[targetTaskGroup].tasks.map(task => {
|
||||
taskGroups[targetTaskGroup].tasks.map((task) => {
|
||||
return { id: task.id, position: task.position };
|
||||
}),
|
||||
);
|
||||
@ -286,7 +278,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
<BoardWrapper>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="horizontal" type="column" droppableId="root">
|
||||
{provided => (
|
||||
{(provided) => (
|
||||
<Container {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{taskGroups
|
||||
.slice()
|
||||
@ -294,14 +286,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
.map((taskGroup: TaskGroup, index: number) => {
|
||||
return (
|
||||
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
|
||||
{columnDragProvided => (
|
||||
{(columnDragProvided) => (
|
||||
<Droppable type="tasks" droppableId={taskGroup.id}>
|
||||
{(columnDropProvided, snapshot) => (
|
||||
<List
|
||||
name={taskGroup.name}
|
||||
onOpenComposer={id => setCurrentComposer(id)}
|
||||
onOpenComposer={(id) => setCurrentComposer(id)}
|
||||
isComposerOpen={currentComposer === taskGroup.id}
|
||||
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
|
||||
onSaveName={(name) => onChangeTaskGroupName(taskGroup.id, name)}
|
||||
isPublic={isPublic}
|
||||
ref={columnDragProvided.innerRef}
|
||||
wrapperProps={columnDragProvided.draggableProps}
|
||||
@ -314,8 +306,8 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
|
||||
{taskGroup.tasks
|
||||
.slice()
|
||||
.filter(t => shouldStatusFilter(t, taskStatusFilter))
|
||||
.filter(t => shouldMetaFilter(t, taskMetaFilters))
|
||||
.filter((t) => shouldStatusFilter(t, taskStatusFilter))
|
||||
.filter((t) => shouldMetaFilter(t, taskMetaFilters))
|
||||
.sort((a: any, b: any) => a.position - b.position)
|
||||
.sort((a: any, b: any) => sortTasks(a, b, taskSorting))
|
||||
.map((task: Task, taskIndex: any) => {
|
||||
@ -326,7 +318,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
index={taskIndex}
|
||||
isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
|
||||
>
|
||||
{taskProvided => {
|
||||
{(taskProvided) => {
|
||||
return (
|
||||
<Card
|
||||
toggleDirection={toggleDirection}
|
||||
@ -352,7 +344,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
complete={task.complete ?? false}
|
||||
taskGroupID={taskGroup.id}
|
||||
description=""
|
||||
labels={task.labels.map(label => label.projectLabel)}
|
||||
labels={task.labels.map((label) => label.projectLabel)}
|
||||
dueDate={
|
||||
task.dueDate
|
||||
? {
|
||||
@ -367,6 +359,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
onTaskClick(task);
|
||||
}}
|
||||
checklists={task.badges && task.badges.checklist}
|
||||
comments={task.badges && task.badges.comments}
|
||||
onCardMemberClick={onCardMemberClick}
|
||||
onContextMenu={onQuickEditorOpen}
|
||||
/>
|
||||
@ -381,7 +374,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
onClose={() => {
|
||||
setCurrentComposer('');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
onCreateCard={(name) => {
|
||||
onCreateTask(taskGroup.id, name);
|
||||
}}
|
||||
isOpen
|
||||
@ -402,7 +395,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
</DragDropContext>
|
||||
{!isPublic && (
|
||||
<AddList
|
||||
onSave={listName => {
|
||||
onSave={(listName) => {
|
||||
onCreateTaskGroup(listName);
|
||||
}}
|
||||
/>
|
||||
|
@ -22,14 +22,14 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
|
||||
position: relative;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: ${props => props.theme.borderRadius.alternate};
|
||||
border-radius: ${(props) => props.theme.borderRadius.alternate};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
& span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.invert
|
||||
? css`
|
||||
background: ${props.theme.colors.success};
|
||||
@ -63,7 +63,7 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
|
||||
color: ${props.theme.colors.success};
|
||||
}
|
||||
`}
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.invert &&
|
||||
css`
|
||||
opacity: 0.6;
|
||||
@ -89,7 +89,7 @@ export const SidebarTitle = styled.div`
|
||||
font-size: 12px;
|
||||
min-height: 24px;
|
||||
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;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
@ -110,12 +110,12 @@ export const skeletonKeyframes = keyframes`
|
||||
|
||||
export const SidebarButton = styled.div<{ $loading?: boolean }>`
|
||||
font-size: 14px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.$loading
|
||||
? css`
|
||||
background: ${props.theme.colors.bg.primary};
|
||||
@ -183,7 +183,7 @@ export const TaskDetailsTitleWrapper = styled.div<{ $loading?: boolean }>`
|
||||
margin: 8px 0 4px 0;
|
||||
display: flex;
|
||||
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 }>`
|
||||
@ -201,7 +201,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.$loading
|
||||
? css`
|
||||
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
|
||||
@ -226,7 +226,7 @@ export const DueDateTitle = styled.div`
|
||||
font-size: 12px;
|
||||
min-height: 24px;
|
||||
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;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
@ -237,7 +237,7 @@ export const AssignedUsersSection = styled.div`
|
||||
padding-right: 32px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid ${props => props.theme.colors.alternate};
|
||||
border-bottom: 1px solid ${(props) => props.theme.colors.alternate};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
@ -255,10 +255,10 @@ export const AssignUserIcon = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&: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 {
|
||||
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;
|
||||
border: 1px solid transparent;
|
||||
&: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} {
|
||||
border: 1px solid ${props => props.theme.colors.alternate};
|
||||
border: 1px solid ${(props) => props.theme.colors.alternate};
|
||||
}
|
||||
`;
|
||||
|
||||
export const AssignUserLabel = styled.span`
|
||||
flex: 1 1 auto;
|
||||
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`
|
||||
@ -295,7 +295,7 @@ export const ExtraActionsSection = styled.div`
|
||||
`;
|
||||
|
||||
export const ActionButtonsTitle = styled.h3`
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
@ -305,7 +305,7 @@ export const ActionButton = styled(Button)`
|
||||
margin-top: 8px;
|
||||
margin-left: -10px;
|
||||
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;
|
||||
transition: transform 0.2s ease;
|
||||
& span {
|
||||
@ -314,7 +314,7 @@ export const ActionButton = styled(Button)`
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
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;
|
||||
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 {
|
||||
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`
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
@ -412,7 +412,7 @@ export const MetaDetailContent = styled.div`
|
||||
`;
|
||||
export const TaskDetailsAddLabel = styled.div`
|
||||
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;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
@ -427,7 +427,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
@ -443,7 +443,7 @@ export const TaskDetailLabel = styled.div<{ color: string }>`
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
background-color: ${props => props.color};
|
||||
background-color: ${(props) => props.color};
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@ -496,17 +496,22 @@ export const TabBarSection = styled.div`
|
||||
margin-top: 2px;
|
||||
padding-left: 23px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-transform: uppercase;
|
||||
min-height: 35px;
|
||||
border-bottom: 1px solid #414561;
|
||||
`;
|
||||
|
||||
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;
|
||||
margin-bottom: -1px;
|
||||
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`
|
||||
@ -542,13 +547,13 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
|
||||
line-height: 28px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
background: #1f243e;
|
||||
border: none;
|
||||
transition: max-height 200ms, height 200ms, min-height 200ms;
|
||||
min-height: 36px;
|
||||
max-height: 36px;
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.$showCommentActions
|
||||
? css`
|
||||
min-height: 80px;
|
||||
@ -561,7 +566,7 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
|
||||
`;
|
||||
|
||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
||||
display: ${props => (props.visible ? 'flex' : 'none')};
|
||||
display: ${(props) => (props.visible ? 'flex' : 'none')};
|
||||
align-items: center;
|
||||
padding: 5px 5px 5px 9px;
|
||||
border-top: 1px solid #414561;
|
||||
@ -594,7 +599,7 @@ export const ActivityItemCommentAction = styled.div`
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
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;
|
||||
flex-direction: column;
|
||||
padding-left: 8px;
|
||||
${props => props.editable && 'width: 100%;'}
|
||||
${(props) => props.editable && 'width: 100%;'}
|
||||
`;
|
||||
export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
||||
align-items: start;
|
||||
@ -623,7 +628,7 @@ export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
||||
export const ActivityItemHeaderTitle = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
padding-bottom: 2px;
|
||||
`;
|
||||
|
||||
@ -634,8 +639,8 @@ export const ActivityItemHeaderTitleName = styled.span`
|
||||
|
||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
|
||||
font-size: 12px;
|
||||
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)};
|
||||
margin-left: ${props => props.margin}px;
|
||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.65)};
|
||||
margin-left: ${(props) => props.margin}px;
|
||||
`;
|
||||
|
||||
export const ActivityItemDetails = styled.div`
|
||||
@ -649,11 +654,11 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
|
||||
border-radius: 3px;
|
||||
${mixin.boxShadowCard}
|
||||
position: relative;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
|
||||
${props => props.editable && 'width: 100%;'}
|
||||
background-color: ${(props) => mixin.darken(props.theme.colors.alternate, 0.1)};
|
||||
${(props) => props.editable && 'width: 100%;'}
|
||||
|
||||
& span {
|
||||
display: inline-flex;
|
||||
@ -683,7 +688,7 @@ export const ActivityItemCommentActions = styled.div`
|
||||
|
||||
export const ActivityItemLog = styled.span`
|
||||
margin-left: 2px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
`;
|
||||
|
||||
export const ViewRawButton = styled.button`
|
||||
@ -694,9 +699,9 @@ export const ViewRawButton = styled.button`
|
||||
right: 4px;
|
||||
bottom: -24px;
|
||||
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 {
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -79,6 +79,7 @@ import {
|
||||
ActivityItemHeaderTitle,
|
||||
ActivityItemHeaderTitleName,
|
||||
ActivityItemComment,
|
||||
TabBarButton,
|
||||
} from './Styles';
|
||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||
import onDragEnd from './onDragEnd';
|
||||
|
@ -69,6 +69,12 @@ export type ChecklistBadge = {
|
||||
total: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type CommentsBadge = {
|
||||
__typename?: 'CommentsBadge';
|
||||
total: Scalars['Int'];
|
||||
unread: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CreateTaskChecklist = {
|
||||
taskID: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
@ -1027,6 +1033,7 @@ export type TaskActivityData = {
|
||||
export type TaskBadges = {
|
||||
__typename?: 'TaskBadges';
|
||||
checklist?: Maybe<ChecklistBadge>;
|
||||
comments?: Maybe<CommentsBadge>;
|
||||
};
|
||||
|
||||
export type TaskChecklist = {
|
||||
@ -1592,6 +1599,9 @@ export type TaskFieldsFragment = (
|
||||
& { checklist?: Maybe<(
|
||||
{ __typename?: 'ChecklistBadge' }
|
||||
& Pick<ChecklistBadge, 'complete' | 'total'>
|
||||
)>, comments?: Maybe<(
|
||||
{ __typename?: 'CommentsBadge' }
|
||||
& Pick<CommentsBadge, 'unread' | 'total'>
|
||||
)> }
|
||||
), taskGroup: (
|
||||
{ __typename?: 'TaskGroup' }
|
||||
@ -2693,6 +2703,10 @@ export const TaskFieldsFragmentDoc = gql`
|
||||
complete
|
||||
total
|
||||
}
|
||||
comments {
|
||||
unread
|
||||
total
|
||||
}
|
||||
}
|
||||
taskGroup {
|
||||
id
|
||||
|
@ -15,6 +15,10 @@ const TASK_FRAGMENT = gql`
|
||||
complete
|
||||
total
|
||||
}
|
||||
comments {
|
||||
unread
|
||||
total
|
||||
}
|
||||
}
|
||||
taskGroup {
|
||||
id
|
||||
|
12
frontend/src/shared/icons/Bubble.tsx
Normal file
12
frontend/src/shared/icons/Bubble.tsx
Normal 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;
|
@ -1,6 +1,7 @@
|
||||
import Cross from './Cross';
|
||||
import Cog from './Cog';
|
||||
import Cogs from './Cogs';
|
||||
import Bubble from './Bubble';
|
||||
import ArrowDown from './ArrowDown';
|
||||
import CheckCircleOutline from './CheckCircleOutline';
|
||||
import Briefcase from './Briefcase';
|
||||
@ -110,5 +111,6 @@ export {
|
||||
Briefcase,
|
||||
DotCircle,
|
||||
ChevronRight,
|
||||
Bubble,
|
||||
Cogs,
|
||||
};
|
||||
|
5
frontend/src/types.d.ts
vendored
5
frontend/src/types.d.ts
vendored
@ -59,8 +59,13 @@ type ChecklistBadge = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
type CommentsBadge = {
|
||||
total: number;
|
||||
unread: boolean;
|
||||
};
|
||||
type TaskBadges = {
|
||||
checklist?: ChecklistBadge | null;
|
||||
comments?: CommentsBadge | null;
|
||||
};
|
||||
|
||||
type TaskActivityData = {
|
||||
|
@ -74,6 +74,7 @@ type Querier interface {
|
||||
GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error)
|
||||
GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, 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)
|
||||
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
|
||||
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
|
||||
|
@ -95,3 +95,6 @@ SELECT task.* FROM task_assigned
|
||||
)
|
||||
)
|
||||
ORDER BY task.due_date DESC, task_group.project_id DESC;
|
||||
|
||||
-- name: GetCommentCountForTask :one
|
||||
SELECT COUNT(*) FROM task_comment WHERE task_id = $1;
|
||||
|
@ -316,6 +316,17 @@ func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetA
|
||||
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
|
||||
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
|
||||
`
|
||||
|
@ -73,6 +73,11 @@ type ComplexityRoot struct {
|
||||
Total func(childComplexity int) int
|
||||
}
|
||||
|
||||
CommentsBadge struct {
|
||||
Total func(childComplexity int) int
|
||||
Unread func(childComplexity int) int
|
||||
}
|
||||
|
||||
CreateTaskCommentPayload struct {
|
||||
Comment func(childComplexity int) int
|
||||
TaskID func(childComplexity int) int
|
||||
@ -419,6 +424,7 @@ type ComplexityRoot struct {
|
||||
|
||||
TaskBadges struct {
|
||||
Checklist func(childComplexity int) int
|
||||
Comments func(childComplexity int) int
|
||||
}
|
||||
|
||||
TaskChecklist struct {
|
||||
@ -768,6 +774,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
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":
|
||||
if e.complexity.CreateTaskCommentPayload.Comment == nil {
|
||||
break
|
||||
@ -2553,6 +2573,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
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":
|
||||
if e.complexity.TaskChecklist.ID == nil {
|
||||
break
|
||||
@ -3212,8 +3239,14 @@ type ChecklistBadge {
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type CommentsBadge {
|
||||
total: Int!
|
||||
unread: Boolean!
|
||||
}
|
||||
|
||||
type TaskBadges {
|
||||
checklist: ChecklistBadge
|
||||
comments: CommentsBadge
|
||||
}
|
||||
|
||||
type CausedBy {
|
||||
@ -5280,6 +5313,76 @@ func (ec *executionContext) _ChecklistBadge_total(ctx context.Context, field gra
|
||||
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) {
|
||||
defer func() {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -20124,6 +20259,38 @@ func (ec *executionContext) _ChecklistBadge(ctx context.Context, sel ast.Selecti
|
||||
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"}
|
||||
|
||||
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")
|
||||
case "checklist":
|
||||
out.Values[i] = ec._TaskBadges_checklist(ctx, field, obj)
|
||||
case "comments":
|
||||
out.Values[i] = ec._TaskBadges_comments(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@ -26306,6 +26475,13 @@ func (ec *executionContext) marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknott
|
||||
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) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
|
@ -33,6 +33,11 @@ type ChecklistBadge struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type CommentsBadge struct {
|
||||
Total int `json:"total"`
|
||||
Unread bool `json:"unread"`
|
||||
}
|
||||
|
||||
type CreateTaskChecklist struct {
|
||||
TaskID uuid.UUID `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
@ -431,6 +436,7 @@ type TaskActivityData struct {
|
||||
|
||||
type TaskBadges struct {
|
||||
Checklist *ChecklistBadge `json:"checklist"`
|
||||
Comments *CommentsBadge `json:"comments"`
|
||||
}
|
||||
|
||||
type TaskPositionUpdate struct {
|
||||
|
@ -138,8 +138,14 @@ type ChecklistBadge {
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type CommentsBadge {
|
||||
total: Int!
|
||||
unread: Boolean!
|
||||
}
|
||||
|
||||
type TaskBadges {
|
||||
checklist: ChecklistBadge
|
||||
comments: CommentsBadge
|
||||
}
|
||||
|
||||
type CausedBy {
|
||||
@ -994,4 +1000,3 @@ type DeleteUserAccountPayload {
|
||||
ok: Boolean!
|
||||
userAccount: UserAccount!
|
||||
}
|
||||
|
||||
|
@ -1721,8 +1721,9 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
|
||||
if err != nil {
|
||||
return &TaskBadges{}, err
|
||||
}
|
||||
if len(checklists) == 0 {
|
||||
return &TaskBadges{Checklist: nil}, err
|
||||
comments, err := r.Repository.GetCommentCountForTask(ctx, obj.TaskID)
|
||||
if err != nil {
|
||||
return &TaskBadges{}, err
|
||||
}
|
||||
complete := 0
|
||||
total := 0
|
||||
@ -1738,10 +1739,15 @@ func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, e
|
||||
}
|
||||
}
|
||||
}
|
||||
if complete == 0 && total == 0 {
|
||||
return &TaskBadges{Checklist: nil}, nil
|
||||
var taskChecklist *ChecklistBadge
|
||||
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) {
|
||||
|
@ -138,8 +138,14 @@ type ChecklistBadge {
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type CommentsBadge {
|
||||
total: Int!
|
||||
unread: Boolean!
|
||||
}
|
||||
|
||||
type TaskBadges {
|
||||
checklist: ChecklistBadge
|
||||
comments: CommentsBadge
|
||||
}
|
||||
|
||||
type CausedBy {
|
||||
|
Loading…
Reference in New Issue
Block a user