feat: add bell notification system for task assignment

This commit is contained in:
Jordan Knott
2021-11-02 14:45:05 -05:00
parent 3afd860534
commit 799d7f3ad0
53 changed files with 3306 additions and 163 deletions

View File

@ -1,16 +1,22 @@
import React from 'react';
import React, { useState } from 'react';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import LoggedOutNavbar from 'shared/components/TopNavbar/LoggedOut';
import { ProfileMenu } from 'shared/components/DropdownMenu';
import { useHistory, useRouteMatch } from 'react-router';
import { useCurrentUser } from 'App/context';
import { RoleCode, useTopNavbarQuery } from 'shared/generated/graphql';
import {
RoleCode,
useTopNavbarQuery,
useNotificationAddedSubscription,
useHasUnreadNotificationsQuery,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache';
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
import theme from 'App/ThemeStyles';
import ProjectFinder from './ProjectFinder';
import polling from 'shared/utils/polling';
// TODO: Move to context based navbar?
@ -49,9 +55,25 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
onRemoveInvitedFromBoard,
onRemoveFromBoard,
}) => {
const { data } = useTopNavbarQuery();
const [notifications, setNotifications] = useState<Array<{ id: string; notification: { actionType: string } }>>([]);
const { data } = useTopNavbarQuery({
onCompleted: (d) => {
setNotifications((n) => [...n, ...d.notifications]);
},
});
const { data: nData, loading } = useNotificationAddedSubscription({
onSubscriptionData: (d) => {
setNotifications((n) => {
if (d.subscriptionData.data) {
return [...n, d.subscriptionData.data.notificationAdded];
}
return n;
});
},
});
const { showPopup, hidePopup } = usePopup();
const { setUser } = useCurrentUser();
const { data: unreadData } = useHasUnreadNotificationsQuery({ pollInterval: polling.UNREAD_NOTIFICATIONS });
const history = useHistory();
const onLogout = () => {
fetch('/auth/logout', {
@ -94,21 +116,10 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
}
};
// TODO: rewrite popup to contain subscription and notification fetch
const onNotificationClick = ($target: React.RefObject<HTMLElement>) => {
if (data) {
showPopup(
$target,
<NotificationPopup>
{data.notifications.map((notification) => (
<NotificationItem
title={notification.notification.actionType}
description={`${notification.notification.causedBy.fullname} added you as a meber to the task "${notification.notification.actionType}"`}
createdAt={notification.notification.createdAt}
/>
))}
</NotificationPopup>,
{ width: 415, borders: false, diamondColor: theme.colors.primary },
);
showPopup($target, <NotificationPopup />, { width: 605, borders: false, diamondColor: theme.colors.primary });
}
};
@ -174,17 +185,12 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
}
};
if (data) {
console.log('HERE DATA');
console.log(data.me);
} else {
console.log('NO DATA');
}
const user = data ? data.me?.user : null;
return (
<>
<TopNavbar
hasUnread={unreadData ? unreadData.hasUnreadNotifications.unread : false}
name={name}
menuType={menuType}
onOpenProjectFinder={($target) => {

View File

@ -430,6 +430,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
__typename: 'Task',
id: `${Math.round(Math.random() * -1000000)}`,
name,
watched: false,
complete: false,
completedAt: null,
hasTime: false,

View File

@ -7,6 +7,7 @@ import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory, useParams } from 'react-router';
import {
useDeleteTaskChecklistMutation,
useToggleTaskWatchMutation,
useUpdateTaskChecklistNameMutation,
useUpdateTaskChecklistItemLocationMutation,
useCreateTaskChecklistMutation,
@ -216,6 +217,7 @@ const Details: React.FC<DetailsProps> = ({
);
},
});
const [toggleTaskWatch] = useToggleTaskWatchMutation();
const [createTaskComment] = useCreateTaskCommentMutation({
update: (client, response) => {
updateApolloCache<FindTaskQuery>(
@ -440,6 +442,19 @@ const Details: React.FC<DetailsProps> = ({
);
}}
task={data.findTask}
onToggleTaskWatch={(task, watched) => {
toggleTaskWatch({
variables: { taskID: task.id },
optimisticResponse: {
__typename: 'Mutation',
toggleTaskWatch: {
id: task.id,
__typename: 'Task',
watched,
},
},
});
}}
onCreateComment={(task, message) => {
createTaskComment({ variables: { taskID: task.id, message } });
}}
@ -540,7 +555,8 @@ const Details: React.FC<DetailsProps> = ({
bio="None"
onRemoveFromTask={() => {
if (user) {
unassignTask({ variables: { taskID: data.findTask.id, userID: user ?? '' } });
unassignTask({ variables: { taskID: data.findTask.id, userID: member.id ?? '' } });
hidePopup();
}
}}
/>

View File

@ -10,6 +10,8 @@ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween';
import weekday from 'dayjs/plugin/weekday';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import log from 'loglevel';
import remote from 'loglevel-plugin-remote';
import cache from './App/cache';
@ -36,6 +38,8 @@ dayjs.extend(weekday);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
dayjs.extend(updateLocale);
dayjs.extend(duration);
dayjs.extend(relativeTime);
dayjs.updateLocale('en', {
week: {
dow: 1, // First day of week is Monday

View File

@ -225,7 +225,7 @@ const Card = React.forwardRef(
<ListCardBadges>
{watched && (
<ListCardBadge>
<Eye width={8} height={8} />
<Eye width={12} height={12} />
</ListCardBadge>
)}
{dueDate && (

View File

@ -329,6 +329,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
toggleLabels={toggleLabels}
isPublic={isPublic}
labelVariant={cardLabelVariant}
watched={task.watched}
wrapperProps={{
...taskProvided.draggableProps,
...taskProvided.dragHandleProps,

View File

@ -1,8 +1,20 @@
import React from 'react';
import styled from 'styled-components';
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
import TimeAgo from 'react-timeago';
import { Link } from 'react-router-dom';
import { mixin } from 'shared/utils/styles';
import {
useNotificationsQuery,
NotificationFilter,
ActionType,
useNotificationAddedSubscription,
useNotificationToggleReadMutation,
} from 'shared/generated/graphql';
import dayjs from 'dayjs';
import { Popup } from 'shared/components/PopupMenu';
import { Popup, usePopup } from 'shared/components/PopupMenu';
import { CheckCircleOutline, Circle, CircleSolid, UserCircle } from 'shared/icons';
import produce from 'immer';
const ItemWrapper = styled.div`
cursor: pointer;
@ -37,7 +49,7 @@ const ItemTextContainer = styled.div`
const ItemTextTitle = styled.span`
font-weight: 500;
display: block;
color: ${props => props.theme.colors.primary};
color: ${(props) => props.theme.colors.primary};
font-size: 14px;
`;
const ItemTextDesc = styled.span`
@ -72,38 +84,450 @@ export const NotificationItem: React.FC<NotificationItemProps> = ({ title, descr
};
const NotificationHeader = styled.div`
padding: 0.75rem;
padding: 20px 28px;
text-align: center;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background: ${props => props.theme.colors.primary};
background: ${(props) => props.theme.colors.primary};
`;
const NotificationHeaderTitle = styled.span`
font-size: 14px;
color: ${props => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary};
`;
const Notifications = styled.div`
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
height: 448px;
overflow-y: scroll;
user-select: none;
`;
const NotificationFooter = styled.div`
cursor: pointer;
padding: 0.5rem;
text-align: center;
color: ${props => props.theme.colors.primary};
color: ${(props) => props.theme.colors.primary};
&:hover {
background: ${props => props.theme.colors.bg.primary};
background: ${(props) => props.theme.colors.bg.primary};
}
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
const NotificationTabs = styled.div`
align-items: flex-end;
align-self: stretch;
display: flex;
flex: 1 0 auto;
justify-content: flex-start;
max-width: 100%;
padding-top: 4px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
const NotificationTab = styled.div<{ active: boolean }>`
font-size: 80%;
color: ${(props) => props.theme.colors.text.primary};
font-size: 15px;
cursor: pointer;
display: flex;
user-select: none;
justify-content: center;
line-height: normal;
min-width: 1px;
transition-duration: 0.2s;
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding: 12px 16px;
&:first-child {
margin-left: 12px;
}
&:hover {
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary};
}
&:not(:last-child) {
margin-right: 12px;
}
${(props) =>
props.active &&
css`
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
color: ${props.theme.colors.secondary};
&:hover {
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
color: ${props.theme.colors.secondary};
}
`}
`;
const NotificationLink = styled(Link)`
display: flex;
align-items: center;
text-decoration: none;
padding: 16px 8px;
width: 100%;
`;
const NotificationControls = styled.div`
width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
visibility: hidden;
padding: 4px;
`;
const NotificationButtons = styled.div`
display: flex;
align-self: flex-end;
align-items: center;
margin-top: auto;
margin-bottom: 6px;
`;
const NotificationButton = styled.div`
padding: 4px 15px;
cursor: pointer;
&:hover svg {
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationWrapper = styled.li`
min-height: 112px;
display: flex;
font-size: 14px;
transition: background-color 0.1s ease-in-out;
margin: 2px 8px;
border-radius: 8px;
justify-content: space-between;
position: relative;
&:hover {
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
}
&:hover ${NotificationLink} {
color: #fff;
}
&:hover ${NotificationControls} {
visibility: visible;
}
`;
const NotificationContentFooter = styled.div`
margin-top: 8px;
display: flex;
align-items: center;
color: ${(props) => props.theme.colors.text.primary};
`;
const NotificationCausedBy = styled.div`
height: 60px;
width: 60px;
min-height: 60px;
min-width: 60px;
`;
const NotificationCausedByInitials = styled.div`
position: relative;
display: flex;
align-items: center;
text: #fff;
font-size: 18px;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
height: 100%;
width: 100%;
border: none;
background: #7367f0;
`;
const NotificationCausedByImage = styled.img`
position: relative;
display: flex;
border-radius: 50%;
flex-shrink: 0;
height: 100%;
width: 100%;
border: none;
background: #7367f0;
`;
const NotificationContent = styled.div`
display: flex;
overflow: hidden;
flex-direction: column;
margin-left: 16px;
`;
const NotificationContentHeader = styled.div`
font-weight: bold;
font-size: 14px;
color: #fff;
svg {
margin-left: 8px;
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationBody = styled.div`
margin-top: 8px;
display: flex;
align-items: center;
color: #fff;
svg {
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationPrefix = styled.span`
color: rgb(216, 93, 216);
margin: 0 4px;
`;
const NotificationSeparator = styled.span`
margin: 0 6px;
`;
type NotificationProps = {
causedBy?: { fullname: string; username: string; id: string } | null;
createdAt: string;
read: boolean;
data: Array<{ key: string; value: string }>;
actionType: ActionType;
onToggleRead: () => void;
};
const Notification: React.FC<NotificationProps> = ({ causedBy, createdAt, data, actionType, read, onToggleRead }) => {
const prefix: any = [];
const { hidePopup } = usePopup();
const dataMap = new Map<string, string>();
data.forEach((d) => dataMap.set(d.key, d.value));
let link = '#';
switch (actionType) {
case ActionType.TaskAssigned:
prefix.push(<UserCircle width={14} height={16} />);
prefix.push(<NotificationPrefix>Assigned </NotificationPrefix>);
prefix.push(<span>you to the task "{dataMap.get('TaskName')}"</span>);
link = `/projects/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
break;
default:
throw new Error('unknown action type');
}
return (
<NotificationWrapper>
<NotificationLink to={link} onClick={hidePopup}>
<NotificationCausedBy>
<NotificationCausedByInitials>
{causedBy
? causedBy.fullname
.split(' ')
.map((n) => n[0])
.join('.')
: 'RU'}
</NotificationCausedByInitials>
</NotificationCausedBy>
<NotificationContent>
<NotificationContentHeader>
{causedBy ? causedBy.fullname : 'Removed user'}
{!read && <CircleSolid width={10} height={10} />}
</NotificationContentHeader>
<NotificationBody>{prefix}</NotificationBody>
<NotificationContentFooter>
<span>{dayjs.duration(dayjs(createdAt).diff(dayjs())).humanize(true)}</span>
<NotificationSeparator></NotificationSeparator>
<span>{dataMap.get('ProjectName')}</span>
</NotificationContentFooter>
</NotificationContent>
</NotificationLink>
<NotificationControls>
<NotificationButtons>
<NotificationButton onClick={() => onToggleRead()}>
{read ? <Circle width={18} height={18} /> : <CheckCircleOutline width={18} height={18} />}
</NotificationButton>
</NotificationButtons>
</NotificationControls>
</NotificationWrapper>
);
};
const PopupContent = styled.div`
display: flex;
flex-direction: column;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 10px;
border-color: #414561;
`;
const tabs = [
{ label: 'All', key: NotificationFilter.All },
{ label: 'Unread', key: NotificationFilter.Unread },
{ label: 'I was mentioned', key: NotificationFilter.Mentioned },
{ label: 'Assigned to me', key: NotificationFilter.Assigned },
];
type NotificationEntry = {
id: string;
read: boolean;
readAt?: string | undefined | null;
notification: {
id: string;
data: Array<{ key: string; value: string }>;
actionType: ActionType;
causedBy?: { id: string; username: string; fullname: string } | undefined | null;
createdAt: string;
};
};
const NotificationPopup: React.FC = ({ children }) => {
const [filter, setFilter] = useState<NotificationFilter>(NotificationFilter.Unread);
const [data, setData] = useState<{ nodes: Array<NotificationEntry>; hasNextPage: boolean; cursor: string }>({
nodes: [],
hasNextPage: false,
cursor: '',
});
const [toggleRead] = useNotificationToggleReadMutation({
onCompleted: (data) => {
setData((prev) => {
return produce(prev, (draft) => {
const idx = draft.nodes.findIndex((n) => n.id === data.notificationToggleRead.id);
if (idx !== -1) {
draft.nodes[idx].read = data.notificationToggleRead.read;
draft.nodes[idx].readAt = data.notificationToggleRead.readAt;
}
});
});
},
});
const { data: nData, fetchMore } = useNotificationsQuery({
variables: { limit: 5, filter },
onCompleted: (d) => {
setData((prev) => ({
hasNextPage: d.notified.pageInfo.hasNextPage,
cursor: d.notified.pageInfo.endCursor,
nodes: [...prev.nodes, ...d.notified.notified],
}));
},
});
const { data: sData, loading } = useNotificationAddedSubscription({
onSubscriptionData: (d) => {
setData((n) => {
if (d.subscriptionData.data) {
return {
...n,
nodes: [d.subscriptionData.data.notificationAdded, ...n.nodes],
};
}
return n;
});
},
});
return (
<Popup title={null} tab={0} borders={false} padding={false}>
<NotificationHeader>
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
</NotificationHeader>
<ul>{children}</ul>
<NotificationFooter>View All</NotificationFooter>
<PopupContent>
<NotificationHeader>
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
</NotificationHeader>
<NotificationTabs>
{tabs.map((tab) => (
<NotificationTab
key={tab.key}
onClick={() => {
if (filter !== tab.key) {
setData({ cursor: '', hasNextPage: false, nodes: [] });
setFilter(tab.key);
}
}}
active={tab.key === filter}
>
{tab.label}
</NotificationTab>
))}
</NotificationTabs>
<Notifications
onScroll={({ currentTarget }) => {
if (currentTarget.scrollTop + currentTarget.clientHeight >= currentTarget.scrollHeight) {
if (data.hasNextPage) {
console.log(`fetching more = ${data.cursor} - ${data.hasNextPage}`);
fetchMore({
variables: {
limit: 5,
filter,
cursor: data.cursor,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
setData((d) => ({
cursor: fetchMoreResult.notified.pageInfo.endCursor,
hasNextPage: fetchMoreResult.notified.pageInfo.hasNextPage,
nodes: [...d.nodes, ...fetchMoreResult.notified.notified],
}));
return {
...prev,
notified: {
...prev.notified,
pageInfo: {
...fetchMoreResult.notified.pageInfo,
},
notified: [...prev.notified.notified, ...fetchMoreResult.notified.notified],
},
};
},
});
}
}
}}
>
{data.nodes.map((n) => (
<Notification
key={n.id}
read={n.read}
actionType={n.notification.actionType}
data={n.notification.data}
createdAt={n.notification.createdAt}
causedBy={n.notification.causedBy}
onToggleRead={() =>
toggleRead({
variables: { notifiedID: n.id },
optimisticResponse: {
__typename: 'Mutation',
notificationToggleRead: {
__typename: 'Notified',
id: n.id,
read: !n.read,
readAt: new Date().toUTCString(),
},
},
})
}
/>
))}
</Notifications>
</PopupContent>
</Popup>
);
};

View File

@ -4,6 +4,7 @@ import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
import TaskAssignee from 'shared/components/TaskAssignee';
import theme from 'App/ThemeStyles';
import { Checkmark } from 'shared/icons';
export const Container = styled.div`
display: flex;
@ -309,6 +310,7 @@ export const ActionButton = styled(Button)`
text-align: left;
transition: transform 0.2s ease;
& span {
position: unset;
justify-content: flex-start;
}
&:hover {
@ -717,3 +719,8 @@ export const TaskDetailsEditor = styled(TextareaAutosize)`
outline: none;
border: none;
`;
export const WatchedCheckmark = styled(Checkmark)`
position: absolute;
right: 16px;
`;

View File

@ -12,6 +12,7 @@ import {
CheckSquareOutline,
At,
Smile,
Eye,
} from 'shared/icons';
import { toArray } from 'react-emoji-render';
import DOMPurify from 'dompurify';
@ -80,6 +81,7 @@ import {
ActivityItemHeaderTitleName,
ActivityItemComment,
TabBarButton,
WatchedCheckmark,
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
@ -237,6 +239,7 @@ type TaskDetailsProps = {
onToggleChecklistItem: (itemID: string, complete: boolean) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onToggleTaskWatch: (task: Task, watched: boolean) => void;
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onCreateComment: (task: Task, message: string) => void;
@ -258,6 +261,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
task,
editableComment = null,
onDeleteChecklist,
onToggleTaskWatch,
onTaskNameChange,
onCommentShowActions,
onOpenAddChecklistPopup,
@ -328,6 +332,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const saveDescription = () => {
onTaskDescriptionChange(task, taskDescriptionRef.current);
};
console.log(task.watched);
return (
<Container>
<LeftSidebar>
@ -418,6 +423,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
Checklist
</ActionButton>
<ActionButton>Cover</ActionButton>
<ActionButton
onClick={() => {
onToggleTaskWatch(task, !task.watched);
}}
icon={<Eye width={12} height={12} />}
>
Watch {task.watched && <WatchedCheckmark width={18} height={18} />}
</ActionButton>
</ExtraActionsSection>
)}
</LeftSidebarContent>

View File

@ -6,11 +6,11 @@ import { NavLink, Link } from 'react-router-dom';
import TaskAssignee from 'shared/components/TaskAssignee';
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${props => props.zIndex};
z-index: ${(props) => props.zIndex};
position: relative;
box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.primary},
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
`;
export const NavbarWrapper = styled.div`
@ -27,9 +27,9 @@ export const NavbarHeader = styled.header`
display: flex;
align-items: center;
justify-content: space-between;
background: ${props => props.theme.colors.bg.primary};
background: ${(props) => props.theme.colors.bg.primary};
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)};
`;
export const Breadcrumbs = styled.div`
color: rgb(94, 108, 132);
@ -59,7 +59,7 @@ export const ProjectSwitchInner = styled.div`
flex-direction: column;
justify-content: center;
background-color: ${props => props.theme.colors.primary};
background-color: ${(props) => props.theme.colors.primary};
`;
export const ProjectSwitch = styled.div`
@ -109,10 +109,27 @@ export const NavbarLink = styled(Link)`
cursor: pointer;
`;
export const NotificationCount = styled.div`
position: absolute;
top: -6px;
right: -6px;
background: #7367f0;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid rgb(16, 22, 58);
color: #fff;
font-size: 14px;
`;
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
margin-right: 20px;
position: relative;
cursor: pointer;
${props =>
${(props) =>
props.disabled &&
css`
opacity: 0.5;
@ -142,14 +159,14 @@ export const ProfileIcon = styled.div<{
justify-content: center;
color: #fff;
font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
display: flex;
${props => !props.nameOnly && 'padding-top: 9px;'}
${(props) => !props.nameOnly && 'padding-top: 9px;'}
margin-left: -6px;
align-items: center;
max-width: 100%;
@ -167,7 +184,7 @@ export const ProjectTabs = styled.div`
export const ProjectTab = styled(NavLink)`
font-size: 80%;
color: ${props => props.theme.colors.text.primary};
color: ${(props) => props.theme.colors.text.primary};
font-size: 15px;
cursor: pointer;
display: flex;
@ -184,22 +201,22 @@ export const ProjectTab = styled(NavLink)`
}
&:hover {
box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
color: ${props => props.theme.colors.text.secondary};
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary};
}
&.active {
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${props => props.theme.colors.secondary};
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
color: ${(props) => props.theme.colors.secondary};
}
&.active:hover {
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${props => props.theme.colors.secondary};
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
color: ${(props) => props.theme.colors.secondary};
}
`;
export const ProjectName = styled.h1`
color: ${props => props.theme.colors.text.primary};
color: ${(props) => props.theme.colors.text.primary};
font-weight: 600;
font-size: 20px;
padding: 3px 10px 3px 8px;
@ -241,7 +258,7 @@ export const ProjectNameTextarea = styled.input`
font-size: 20px;
padding: 3px 10px 3px 8px;
&:focus {
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
}
`;
@ -259,7 +276,7 @@ export const ProjectSwitcher = styled.button`
color: #c2c6dc;
cursor: pointer;
&:hover {
background: ${props => props.theme.colors.primary};
background: ${(props) => props.theme.colors.primary};
}
`;
@ -283,7 +300,7 @@ export const ProjectSettingsButton = styled.button`
justify-content: center;
cursor: pointer;
&:hover {
background: ${props => props.theme.colors.primary};
background: ${(props) => props.theme.colors.primary};
}
`;
@ -309,7 +326,7 @@ export const SignIn = styled(Button)`
export const NavSeparator = styled.div`
width: 1px;
background: ${props => props.theme.colors.border};
background: ${(props) => props.theme.colors.border};
height: 34px;
margin: 0 20px;
`;
@ -326,11 +343,11 @@ export const LogoContainer = styled(Link)`
export const TaskcafeTitle = styled.h2`
margin-left: 5px;
color: ${props => props.theme.colors.text.primary};
color: ${(props) => props.theme.colors.text.primary};
font-size: 20px;
`;
export const TaskcafeLogo = styled(Taskcafe)`
fill: ${props => props.theme.colors.text.primary};
stroke: ${props => props.theme.colors.text.primary};
fill: ${(props) => props.theme.colors.text.primary};
stroke: ${(props) => props.theme.colors.text.primary};
`;

View File

@ -36,6 +36,7 @@ import {
ProjectMember,
ProjectMembers,
ProjectSwitchInner,
NotificationCount,
} from './Styles';
type IconContainerProps = {
@ -185,6 +186,7 @@ type NavBarProps = {
projectMembers?: Array<TaskUser> | null;
projectInvitedMembers?: Array<InvitedUser> | null;
hasUnread: boolean;
onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
@ -203,6 +205,7 @@ const NavBar: React.FC<NavBarProps> = ({
onOpenProjectFinder,
onFavorite,
onSetTab,
hasUnread,
projectInvitedMembers,
onChangeRole,
name,
@ -330,8 +333,9 @@ const NavBar: React.FC<NavBarProps> = ({
<IconContainer disabled onClick={NOOP}>
<ListUnordered width={20} height={20} />
</IconContainer>
<IconContainer disabled onClick={onNotificationClick}>
<IconContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
{hasUnread && <NotificationCount />}
</IconContainer>
<IconContainer disabled onClick={NOOP}>
<BarChart width={20} height={20} />

View File

@ -26,7 +26,20 @@ export enum ActionLevel {
}
export enum ActionType {
TaskMemberAdded = 'TASK_MEMBER_ADDED'
TeamAdded = 'TEAM_ADDED',
TeamRemoved = 'TEAM_REMOVED',
ProjectAdded = 'PROJECT_ADDED',
ProjectRemoved = 'PROJECT_REMOVED',
ProjectArchived = 'PROJECT_ARCHIVED',
DueDateAdded = 'DUE_DATE_ADDED',
DueDateRemoved = 'DUE_DATE_REMOVED',
DueDateChanged = 'DUE_DATE_CHANGED',
TaskAssigned = 'TASK_ASSIGNED',
TaskMoved = 'TASK_MOVED',
TaskArchived = 'TASK_ARCHIVED',
TaskAttachmentUploaded = 'TASK_ATTACHMENT_UPLOADED',
CommentMentioned = 'COMMENT_MENTIONED',
CommentOther = 'COMMENT_OTHER'
}
export enum ActivityType {
@ -280,6 +293,11 @@ export type FindUser = {
userID: Scalars['UUID'];
};
export type HasUnreadNotificationsResult = {
__typename?: 'HasUnreadNotificationsResult';
unread: Scalars['Boolean'];
};
export type InviteProjectMembers = {
projectID: Scalars['UUID'];
members: Array<MemberInvite>;
@ -394,12 +412,14 @@ export type Mutation = {
duplicateTaskGroup: DuplicateTaskGroupPayload;
inviteProjectMembers: InviteProjectMembersPayload;
logoutUser: Scalars['Boolean'];
notificationToggleRead: Notified;
removeTaskLabel: Task;
setTaskChecklistItemComplete: TaskChecklistItem;
setTaskComplete: Task;
sortTaskGroup: SortTaskGroupPayload;
toggleProjectVisibility: ToggleProjectVisibilityPayload;
toggleTaskLabel: ToggleTaskLabelPayload;
toggleTaskWatch: Task;
unassignTask: Task;
updateProjectLabel: ProjectLabel;
updateProjectLabelColor: ProjectLabel;
@ -569,6 +589,11 @@ export type MutationLogoutUserArgs = {
};
export type MutationNotificationToggleReadArgs = {
input: NotificationToggleReadInput;
};
export type MutationRemoveTaskLabelArgs = {
input?: Maybe<RemoveTaskLabelInput>;
};
@ -599,6 +624,11 @@ export type MutationToggleTaskLabelArgs = {
};
export type MutationToggleTaskWatchArgs = {
input: ToggleTaskWatch;
};
export type MutationUnassignTaskArgs = {
input?: Maybe<UnassignTaskInput>;
};
@ -784,7 +814,7 @@ export type Notification = {
__typename?: 'Notification';
id: Scalars['ID'];
actionType: ActionType;
causedBy: NotificationCausedBy;
causedBy?: Maybe<NotificationCausedBy>;
data: Array<NotificationData>;
createdAt: Scalars['Time'];
};
@ -802,6 +832,17 @@ export type NotificationData = {
value: Scalars['String'];
};
export enum NotificationFilter {
All = 'ALL',
Unread = 'UNREAD',
Assigned = 'ASSIGNED',
Mentioned = 'MENTIONED'
}
export type NotificationToggleReadInput = {
notifiedID: Scalars['UUID'];
};
export type Notified = {
__typename?: 'Notified';
id: Scalars['ID'];
@ -810,6 +851,19 @@ export type Notified = {
readAt?: Maybe<Scalars['Time']>;
};
export type NotifiedInput = {
limit: Scalars['Int'];
cursor?: Maybe<Scalars['String']>;
filter: NotificationFilter;
};
export type NotifiedResult = {
__typename?: 'NotifiedResult';
totalCount: Scalars['Int'];
notified: Array<Notified>;
pageInfo: PageInfo;
};
export enum ObjectType {
Org = 'ORG',
Team = 'TEAM',
@ -838,6 +892,12 @@ export type OwnersList = {
teams: Array<Scalars['UUID']>;
};
export type PageInfo = {
__typename?: 'PageInfo';
endCursor: Scalars['String'];
hasNextPage: Scalars['Boolean'];
};
export type ProfileIcon = {
__typename?: 'ProfileIcon';
url?: Maybe<Scalars['String']>;
@ -896,11 +956,13 @@ export type Query = {
findTask: Task;
findTeam: Team;
findUser: UserAccount;
hasUnreadNotifications: HasUnreadNotificationsResult;
invitedUsers: Array<InvitedUserAccount>;
labelColors: Array<LabelColor>;
me?: Maybe<MePayload>;
myTasks: MyTasksPayload;
notifications: Array<Notified>;
notified: NotifiedResult;
organizations: Array<Organization>;
projects: Array<Project>;
searchMembers: Array<MemberSearchResult>;
@ -935,6 +997,11 @@ export type QueryMyTasksArgs = {
};
export type QueryNotifiedArgs = {
input: NotifiedInput;
};
export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>;
};
@ -1006,6 +1073,7 @@ export type Task = {
name: Scalars['String'];
position: Scalars['Float'];
description?: Maybe<Scalars['String']>;
watched: Scalars['Boolean'];
dueDate?: Maybe<Scalars['Time']>;
hasTime: Scalars['Boolean'];
complete: Scalars['Boolean'];
@ -1132,6 +1200,10 @@ export type ToggleTaskLabelPayload = {
task: Task;
};
export type ToggleTaskWatch = {
taskID: Scalars['UUID'];
};
export type UnassignTaskInput = {
taskID: Scalars['UUID'];
@ -1520,7 +1592,7 @@ export type FindTaskQuery = (
{ __typename?: 'Query' }
& { findTask: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'>
& Pick<Task, 'id' | 'name' | 'watched' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'>
@ -1596,7 +1668,7 @@ export type FindTaskQuery = (
export type TaskFieldsFragment = (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'hasTime' | 'complete' | 'completedAt' | 'position'>
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'>
& { badges: (
{ __typename?: 'TaskBadges' }
& { checklist?: Maybe<(
@ -1725,6 +1797,74 @@ export type MyTasksQuery = (
) }
);
export type NotificationToggleReadMutationVariables = Exact<{
notifiedID: Scalars['UUID'];
}>;
export type NotificationToggleReadMutation = (
{ __typename?: 'Mutation' }
& { notificationToggleRead: (
{ __typename?: 'Notified' }
& Pick<Notified, 'id' | 'read' | 'readAt'>
) }
);
export type NotificationsQueryVariables = Exact<{
limit: Scalars['Int'];
cursor?: Maybe<Scalars['String']>;
filter: NotificationFilter;
}>;
export type NotificationsQuery = (
{ __typename?: 'Query' }
& { notified: (
{ __typename?: 'NotifiedResult' }
& Pick<NotifiedResult, 'totalCount'>
& { pageInfo: (
{ __typename?: 'PageInfo' }
& Pick<PageInfo, 'endCursor' | 'hasNextPage'>
), notified: Array<(
{ __typename?: 'Notified' }
& Pick<Notified, 'id' | 'read' | 'readAt'>
& { notification: (
{ __typename?: 'Notification' }
& Pick<Notification, 'id' | 'actionType' | 'createdAt'>
& { data: Array<(
{ __typename?: 'NotificationData' }
& Pick<NotificationData, 'key' | 'value'>
)>, causedBy?: Maybe<(
{ __typename?: 'NotificationCausedBy' }
& Pick<NotificationCausedBy, 'username' | 'fullname' | 'id'>
)> }
) }
)> }
) }
);
export type NotificationAddedSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type NotificationAddedSubscription = (
{ __typename?: 'Subscription' }
& { notificationAdded: (
{ __typename?: 'Notified' }
& Pick<Notified, 'id' | 'read' | 'readAt'>
& { notification: (
{ __typename?: 'Notification' }
& Pick<Notification, 'id' | 'actionType' | 'createdAt'>
& { data: Array<(
{ __typename?: 'NotificationData' }
& Pick<NotificationData, 'key' | 'value'>
)>, causedBy?: Maybe<(
{ __typename?: 'NotificationCausedBy' }
& Pick<NotificationCausedBy, 'username' | 'fullname' | 'id'>
)> }
) }
) }
);
export type DeleteProjectMutationVariables = Exact<{
projectID: Scalars['UUID'];
}>;
@ -1979,6 +2119,19 @@ export type SetTaskCompleteMutation = (
) }
);
export type ToggleTaskWatchMutationVariables = Exact<{
taskID: Scalars['UUID'];
}>;
export type ToggleTaskWatchMutation = (
{ __typename?: 'Mutation' }
& { toggleTaskWatch: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'watched'>
) }
);
export type UpdateTaskChecklistItemLocationMutationVariables = Exact<{
taskChecklistID: Scalars['UUID'];
taskChecklistItemID: Scalars['UUID'];
@ -2363,10 +2516,10 @@ export type TopNavbarQuery = (
& { notification: (
{ __typename?: 'Notification' }
& Pick<Notification, 'id' | 'actionType' | 'createdAt'>
& { causedBy: (
& { causedBy?: Maybe<(
{ __typename?: 'NotificationCausedBy' }
& Pick<NotificationCausedBy, 'username' | 'fullname' | 'id'>
) }
)> }
) }
)>, me?: Maybe<(
{ __typename?: 'MePayload' }
@ -2405,6 +2558,17 @@ export type UnassignTaskMutation = (
) }
);
export type HasUnreadNotificationsQueryVariables = Exact<{ [key: string]: never; }>;
export type HasUnreadNotificationsQuery = (
{ __typename?: 'Query' }
& { hasUnreadNotifications: (
{ __typename?: 'HasUnreadNotificationsResult' }
& Pick<HasUnreadNotificationsResult, 'unread'>
) }
);
export type UpdateProjectLabelMutationVariables = Exact<{
projectLabelID: Scalars['UUID'];
labelColorID: Scalars['UUID'];
@ -2700,6 +2864,7 @@ export const TaskFieldsFragmentDoc = gql`
dueDate
hasTime
complete
watched
completedAt
position
badges {
@ -3171,6 +3336,7 @@ export const FindTaskDocument = gql`
findTask(input: {taskID: $taskID}) {
id
name
watched
description
dueDate
position
@ -3505,6 +3671,146 @@ export function useMyTasksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<My
export type MyTasksQueryHookResult = ReturnType<typeof useMyTasksQuery>;
export type MyTasksLazyQueryHookResult = ReturnType<typeof useMyTasksLazyQuery>;
export type MyTasksQueryResult = Apollo.QueryResult<MyTasksQuery, MyTasksQueryVariables>;
export const NotificationToggleReadDocument = gql`
mutation notificationToggleRead($notifiedID: UUID!) {
notificationToggleRead(input: {notifiedID: $notifiedID}) {
id
read
readAt
}
}
`;
export type NotificationToggleReadMutationFn = Apollo.MutationFunction<NotificationToggleReadMutation, NotificationToggleReadMutationVariables>;
/**
* __useNotificationToggleReadMutation__
*
* To run a mutation, you first call `useNotificationToggleReadMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useNotificationToggleReadMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [notificationToggleReadMutation, { data, loading, error }] = useNotificationToggleReadMutation({
* variables: {
* notifiedID: // value for 'notifiedID'
* },
* });
*/
export function useNotificationToggleReadMutation(baseOptions?: Apollo.MutationHookOptions<NotificationToggleReadMutation, NotificationToggleReadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<NotificationToggleReadMutation, NotificationToggleReadMutationVariables>(NotificationToggleReadDocument, options);
}
export type NotificationToggleReadMutationHookResult = ReturnType<typeof useNotificationToggleReadMutation>;
export type NotificationToggleReadMutationResult = Apollo.MutationResult<NotificationToggleReadMutation>;
export type NotificationToggleReadMutationOptions = Apollo.BaseMutationOptions<NotificationToggleReadMutation, NotificationToggleReadMutationVariables>;
export const NotificationsDocument = gql`
query notifications($limit: Int!, $cursor: String, $filter: NotificationFilter!) {
notified(input: {limit: $limit, cursor: $cursor, filter: $filter}) {
totalCount
pageInfo {
endCursor
hasNextPage
}
notified {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
}
`;
/**
* __useNotificationsQuery__
*
* To run a query within a React component, call `useNotificationsQuery` and pass it any options that fit your needs.
* When your component renders, `useNotificationsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNotificationsQuery({
* variables: {
* limit: // value for 'limit'
* cursor: // value for 'cursor'
* filter: // value for 'filter'
* },
* });
*/
export function useNotificationsQuery(baseOptions: Apollo.QueryHookOptions<NotificationsQuery, NotificationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<NotificationsQuery, NotificationsQueryVariables>(NotificationsDocument, options);
}
export function useNotificationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NotificationsQuery, NotificationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<NotificationsQuery, NotificationsQueryVariables>(NotificationsDocument, options);
}
export type NotificationsQueryHookResult = ReturnType<typeof useNotificationsQuery>;
export type NotificationsLazyQueryHookResult = ReturnType<typeof useNotificationsLazyQuery>;
export type NotificationsQueryResult = Apollo.QueryResult<NotificationsQuery, NotificationsQueryVariables>;
export const NotificationAddedDocument = gql`
subscription notificationAdded {
notificationAdded {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
`;
/**
* __useNotificationAddedSubscription__
*
* To run a query within a React component, call `useNotificationAddedSubscription` and pass it any options that fit your needs.
* When your component renders, `useNotificationAddedSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNotificationAddedSubscription({
* variables: {
* },
* });
*/
export function useNotificationAddedSubscription(baseOptions?: Apollo.SubscriptionHookOptions<NotificationAddedSubscription, NotificationAddedSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<NotificationAddedSubscription, NotificationAddedSubscriptionVariables>(NotificationAddedDocument, options);
}
export type NotificationAddedSubscriptionHookResult = ReturnType<typeof useNotificationAddedSubscription>;
export type NotificationAddedSubscriptionResult = Apollo.SubscriptionResult<NotificationAddedSubscription>;
export const DeleteProjectDocument = gql`
mutation deleteProject($projectID: UUID!) {
deleteProject(input: {projectID: $projectID}) {
@ -4061,6 +4367,40 @@ export function useSetTaskCompleteMutation(baseOptions?: Apollo.MutationHookOpti
export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskCompleteMutation>;
export type SetTaskCompleteMutationResult = Apollo.MutationResult<SetTaskCompleteMutation>;
export type SetTaskCompleteMutationOptions = Apollo.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
export const ToggleTaskWatchDocument = gql`
mutation toggleTaskWatch($taskID: UUID!) {
toggleTaskWatch(input: {taskID: $taskID}) {
id
watched
}
}
`;
export type ToggleTaskWatchMutationFn = Apollo.MutationFunction<ToggleTaskWatchMutation, ToggleTaskWatchMutationVariables>;
/**
* __useToggleTaskWatchMutation__
*
* To run a mutation, you first call `useToggleTaskWatchMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useToggleTaskWatchMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [toggleTaskWatchMutation, { data, loading, error }] = useToggleTaskWatchMutation({
* variables: {
* taskID: // value for 'taskID'
* },
* });
*/
export function useToggleTaskWatchMutation(baseOptions?: Apollo.MutationHookOptions<ToggleTaskWatchMutation, ToggleTaskWatchMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ToggleTaskWatchMutation, ToggleTaskWatchMutationVariables>(ToggleTaskWatchDocument, options);
}
export type ToggleTaskWatchMutationHookResult = ReturnType<typeof useToggleTaskWatchMutation>;
export type ToggleTaskWatchMutationResult = Apollo.MutationResult<ToggleTaskWatchMutation>;
export type ToggleTaskWatchMutationOptions = Apollo.BaseMutationOptions<ToggleTaskWatchMutation, ToggleTaskWatchMutationVariables>;
export const UpdateTaskChecklistItemLocationDocument = gql`
mutation updateTaskChecklistItemLocation($taskChecklistID: UUID!, $taskChecklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(
@ -4923,6 +5263,40 @@ export function useUnassignTaskMutation(baseOptions?: Apollo.MutationHookOptions
export type UnassignTaskMutationHookResult = ReturnType<typeof useUnassignTaskMutation>;
export type UnassignTaskMutationResult = Apollo.MutationResult<UnassignTaskMutation>;
export type UnassignTaskMutationOptions = Apollo.BaseMutationOptions<UnassignTaskMutation, UnassignTaskMutationVariables>;
export const HasUnreadNotificationsDocument = gql`
query hasUnreadNotifications {
hasUnreadNotifications {
unread
}
}
`;
/**
* __useHasUnreadNotificationsQuery__
*
* To run a query within a React component, call `useHasUnreadNotificationsQuery` and pass it any options that fit your needs.
* When your component renders, `useHasUnreadNotificationsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useHasUnreadNotificationsQuery({
* variables: {
* },
* });
*/
export function useHasUnreadNotificationsQuery(baseOptions?: Apollo.QueryHookOptions<HasUnreadNotificationsQuery, HasUnreadNotificationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<HasUnreadNotificationsQuery, HasUnreadNotificationsQueryVariables>(HasUnreadNotificationsDocument, options);
}
export function useHasUnreadNotificationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HasUnreadNotificationsQuery, HasUnreadNotificationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<HasUnreadNotificationsQuery, HasUnreadNotificationsQueryVariables>(HasUnreadNotificationsDocument, options);
}
export type HasUnreadNotificationsQueryHookResult = ReturnType<typeof useHasUnreadNotificationsQuery>;
export type HasUnreadNotificationsLazyQueryHookResult = ReturnType<typeof useHasUnreadNotificationsLazyQuery>;
export type HasUnreadNotificationsQueryResult = Apollo.QueryResult<HasUnreadNotificationsQuery, HasUnreadNotificationsQueryVariables>;
export const UpdateProjectLabelDocument = gql`
mutation updateProjectLabel($projectLabelID: UUID!, $labelColorID: UUID!, $name: String!) {
updateProjectLabel(

View File

@ -2,6 +2,7 @@ query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
id
name
watched
description
dueDate
position

View File

@ -8,6 +8,7 @@ const TASK_FRAGMENT = gql`
dueDate
hasTime
complete
watched
completedAt
position
badges {

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
const CREATE_TASK_MUTATION = gql`
mutation notificationToggleRead($notifiedID: UUID!) {
notificationToggleRead(input: { notifiedID: $notifiedID }) {
id
read
readAt
}
}
`;
export default CREATE_TASK_MUTATION;

View File

@ -0,0 +1,34 @@
import gql from 'graphql-tag';
export const TOP_NAVBAR_QUERY = gql`
query notifications($limit: Int!, $cursor: String, $filter: NotificationFilter!) {
notified(input: { limit: $limit, cursor: $cursor, filter: $filter }) {
totalCount
pageInfo {
endCursor
hasNextPage
}
notified {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
}
`;
export default TOP_NAVBAR_QUERY;

View File

@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql`
subscription notificationAdded {
notificationAdded {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
`;

View File

@ -0,0 +1,12 @@
import gql from 'graphql-tag';
const CREATE_TASK_MUTATION = gql`
mutation toggleTaskWatch($taskID: UUID!) {
toggleTaskWatch(input: { taskID: $taskID }) {
id
watched
}
}
`;
export default CREATE_TASK_MUTATION;

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag';
export const TOP_NAVBAR_QUERY = gql`
query hasUnreadNotifications {
hasUnreadNotifications {
unread
}
}
`;
export default TOP_NAVBAR_QUERY;

View File

@ -8,7 +8,7 @@ type Props = {
const Bell = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512">
<path d="M439.39 362.29c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71zM67.53 368c21.22-27.97 44.42-74.33 44.53-159.42 0-.2-.06-.38-.06-.58 0-61.86 50.14-112 112-112s112 50.14 112 112c0 .2-.06.38-.06.58.11 85.1 23.31 131.46 44.53 159.42H67.53zM224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64z" />
<path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" />
</svg>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Circle: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z" />
</Icon>
);
};
export default Circle;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const CircleSolid: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" />
</Icon>
);
};
export default CircleSolid;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const UserCircle: React.FC<IconProps> = ({ width = '16px', height = '16px', className, onClick }) => {
return (
<Icon onClick={onClick} width={width} height={height} className={className} viewBox="0 0 496 512">
<path d="M248 104c-53 0-96 43-96 96s43 96 96 96 96-43 96-96-43-96-96-96zm0 144c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zm0-240C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-49.7 0-95.1-18.3-130.1-48.4 14.9-23 40.4-38.6 69.6-39.5 20.8 6.4 40.6 9.6 60.5 9.6s39.7-3.1 60.5-9.6c29.2 1 54.7 16.5 69.6 39.5-35 30.1-80.4 48.4-130.1 48.4zm162.7-84.1c-24.4-31.4-62.1-51.9-105.1-51.9-10.2 0-26 9.6-57.6 9.6-31.5 0-47.4-9.6-57.6-9.6-42.9 0-80.6 20.5-105.1 51.9C61.9 339.2 48 299.2 48 256c0-110.3 89.7-200 200-200s200 89.7 200 200c0 43.2-13.9 83.2-37.3 115.9z" />
</Icon>
);
};
export default UserCircle;

View File

@ -1,6 +1,9 @@
import Cross from './Cross';
import Cog from './Cog';
import Cogs from './Cogs';
import Circle from './Circle';
import CircleSolid from './CircleSolid';
import UserCircle from './UserCircle';
import Bubble from './Bubble';
import ArrowDown from './ArrowDown';
import CheckCircleOutline from './CheckCircleOutline';
@ -111,6 +114,9 @@ export {
Briefcase,
DotCircle,
ChevronRight,
Circle,
CircleSolid,
Bubble,
UserCircle,
Cogs,
};

View File

@ -8,6 +8,7 @@ const polling = {
MEMBERS: resolve(3000),
TEAM_PROJECTS: resolve(3000),
TASK_DETAILS: resolve(3000),
UNREAD_NOTIFICATIONS: resolve(30000),
};
export default polling;

View File

@ -105,6 +105,7 @@ type Task = {
id: string;
taskGroup: InnerTaskGroup;
name: string;
watched?: boolean;
badges?: TaskBadges;
position: number;
hasTime?: boolean;