feat: add bell notification system for task assignment
This commit is contained in:
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -225,7 +225,7 @@ const Card = React.forwardRef(
|
||||
<ListCardBadges>
|
||||
{watched && (
|
||||
<ListCardBadge>
|
||||
<Eye width={8} height={8} />
|
||||
<Eye width={12} height={12} />
|
||||
</ListCardBadge>
|
||||
)}
|
||||
{dueDate && (
|
||||
|
@ -329,6 +329,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
toggleLabels={toggleLabels}
|
||||
isPublic={isPublic}
|
||||
labelVariant={cardLabelVariant}
|
||||
watched={task.watched}
|
||||
wrapperProps={{
|
||||
...taskProvided.draggableProps,
|
||||
...taskProvided.dragHandleProps,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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};
|
||||
`;
|
||||
|
@ -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} />
|
||||
|
@ -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(
|
||||
|
@ -2,6 +2,7 @@ query findTask($taskID: UUID!) {
|
||||
findTask(input: {taskID: $taskID}) {
|
||||
id
|
||||
name
|
||||
watched
|
||||
description
|
||||
dueDate
|
||||
position
|
||||
|
@ -8,6 +8,7 @@ const TASK_FRAGMENT = gql`
|
||||
dueDate
|
||||
hasTime
|
||||
complete
|
||||
watched
|
||||
completedAt
|
||||
position
|
||||
badges {
|
||||
|
13
frontend/src/shared/graphql/notificationToggleRead.ts
Normal file
13
frontend/src/shared/graphql/notificationToggleRead.ts
Normal 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;
|
34
frontend/src/shared/graphql/notifications.ts
Normal file
34
frontend/src/shared/graphql/notifications.ts
Normal 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;
|
26
frontend/src/shared/graphql/onNotificationAdded.ts
Normal file
26
frontend/src/shared/graphql/onNotificationAdded.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
12
frontend/src/shared/graphql/task/toggleTaskWatcher.ts
Normal file
12
frontend/src/shared/graphql/task/toggleTaskWatcher.ts
Normal 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;
|
11
frontend/src/shared/graphql/unreadNotifications.ts
Normal file
11
frontend/src/shared/graphql/unreadNotifications.ts
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
12
frontend/src/shared/icons/Circle.tsx
Normal file
12
frontend/src/shared/icons/Circle.tsx
Normal 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;
|
12
frontend/src/shared/icons/CircleSolid.tsx
Normal file
12
frontend/src/shared/icons/CircleSolid.tsx
Normal 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;
|
12
frontend/src/shared/icons/UserCircle.tsx
Normal file
12
frontend/src/shared/icons/UserCircle.tsx
Normal 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;
|
@ -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,
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ const polling = {
|
||||
MEMBERS: resolve(3000),
|
||||
TEAM_PROJECTS: resolve(3000),
|
||||
TASK_DETAILS: resolve(3000),
|
||||
UNREAD_NOTIFICATIONS: resolve(30000),
|
||||
};
|
||||
|
||||
export default polling;
|
||||
|
1
frontend/src/types.d.ts
vendored
1
frontend/src/types.d.ts
vendored
@ -105,6 +105,7 @@ type Task = {
|
||||
id: string;
|
||||
taskGroup: InnerTaskGroup;
|
||||
name: string;
|
||||
watched?: boolean;
|
||||
badges?: TaskBadges;
|
||||
position: number;
|
||||
hasTime?: boolean;
|
||||
|
Reference in New Issue
Block a user