feat: add notification UI
showPopup was also refactored to be better
This commit is contained in:
@ -28,6 +28,7 @@
|
||||
"@types/react-router": "^5.1.4",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/react-select": "^3.0.13",
|
||||
"@types/react-timeago": "^4.1.1",
|
||||
"@types/styled-components": "^5.0.0",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
@ -60,6 +61,8 @@
|
||||
"react-scripts": "3.4.0",
|
||||
"react-select": "^3.1.0",
|
||||
"rich-markdown-editor": "^10.6.5",
|
||||
"react-timeago": "^4.4.0",
|
||||
"react-toastify": "^6.0.8",
|
||||
"styled-components": "^5.0.1",
|
||||
"typescript": "~3.7.2"
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ import { useHistory } from 'react-router';
|
||||
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||
import {
|
||||
RoleCode,
|
||||
useMeQuery,
|
||||
useTopNavbarQuery,
|
||||
useDeleteProjectMutation,
|
||||
useGetProjectsQuery,
|
||||
GetProjectsDocument,
|
||||
@ -19,6 +19,7 @@ import { Link } from 'react-router-dom';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import cache from 'App/cache';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
|
||||
|
||||
const TeamContainer = styled.div`
|
||||
display: flex;
|
||||
@ -197,7 +198,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
|
||||
<Popup title={null} tab={0}>
|
||||
<ProjectSettings
|
||||
onDeleteProject={() => {
|
||||
setTab(1, 300);
|
||||
setTab(1, { width: 300 });
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
@ -250,7 +251,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
onRemoveFromBoard,
|
||||
}) => {
|
||||
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||
const { data } = useMeQuery({
|
||||
const { loading, data } = useTopNavbarQuery({
|
||||
onCompleted: response => {
|
||||
if (user && user.roles) {
|
||||
setUserRoles({
|
||||
@ -300,13 +301,31 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
195,
|
||||
{ width: 195 },
|
||||
);
|
||||
};
|
||||
|
||||
const onOpenSettings = ($target: React.RefObject<HTMLElement>) => {
|
||||
if (popupContent) {
|
||||
showPopup($target, popupContent, 185);
|
||||
showPopup($target, popupContent, { width: 185 });
|
||||
}
|
||||
};
|
||||
|
||||
const onNotificationClick = ($target: React.RefObject<HTMLElement>) => {
|
||||
if (data) {
|
||||
showPopup(
|
||||
$target,
|
||||
<NotificationPopup>
|
||||
{data.notifications.map(notification => (
|
||||
<NotificationItem
|
||||
title={notification.entity.name}
|
||||
description={`${notification.actor.name} added you as a meber to the task "${notification.entity.name}"`}
|
||||
createdAt={notification.createdAt}
|
||||
/>
|
||||
))}
|
||||
</NotificationPopup>,
|
||||
{ width: 415, borders: false, diamondColor: '#7367f0' },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -366,7 +385,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
onInviteUser={onInviteUser}
|
||||
onChangeRole={onChangeRole}
|
||||
onChangeProjectOwner={onChangeProjectOwner}
|
||||
onNotificationClick={NOOP}
|
||||
onNotificationClick={onNotificationClick}
|
||||
onSetTab={onSetTab}
|
||||
onRemoveFromBoard={onRemoveFromBoard}
|
||||
onDashboardClick={() => {
|
||||
|
@ -3,6 +3,7 @@ import jwtDecode from 'jwt-decode';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { Router } from 'react-router';
|
||||
import { PopupProvider } from 'shared/components/PopupMenu';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import NormalizeStyles from './NormalizeStyles';
|
||||
@ -11,6 +12,39 @@ import theme from './ThemeStyles';
|
||||
import Routes from './Routes';
|
||||
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
const StyledContainer = styled(ToastContainer).attrs({
|
||||
// custom props
|
||||
})`
|
||||
.Toastify__toast-container {
|
||||
}
|
||||
.Toastify__toast {
|
||||
padding: 5px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 10px;
|
||||
background: #7367f0;
|
||||
color: #fff;
|
||||
}
|
||||
.Toastify__toast--error {
|
||||
background: rgba(${props => props.theme.colors.danger});
|
||||
}
|
||||
.Toastify__toast--warning {
|
||||
background: rgba(${props => props.theme.colors.warning});
|
||||
}
|
||||
.Toastify__toast--success {
|
||||
background: rgba(${props => props.theme.colors.success});
|
||||
}
|
||||
.Toastify__toast-body {
|
||||
}
|
||||
.Toastify__progress-bar {
|
||||
}
|
||||
.Toastify__close-button {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const history = createBrowserHistory();
|
||||
type RefreshTokenResponse = {
|
||||
accessToken: string;
|
||||
@ -72,6 +106,18 @@ const App = () => {
|
||||
)}
|
||||
</PopupProvider>
|
||||
</Router>
|
||||
<StyledContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
limit={5}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</UserContext.Provider>
|
||||
</>
|
||||
|
@ -7,6 +7,13 @@ import { useMeQuery, useClearProfileAvatarMutation, useUpdateUserPasswordMutatio
|
||||
import axios from 'axios';
|
||||
import { useCurrentUser } from 'App/context';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const MainContent = styled.div`
|
||||
padding: 0 0 50px 80px;
|
||||
height: 100%;
|
||||
background: #262c49;
|
||||
`;
|
||||
|
||||
const Projects = () => {
|
||||
const $fileUpload = useRef<HTMLInputElement>(null);
|
||||
@ -59,6 +66,7 @@ const Projects = () => {
|
||||
}}
|
||||
onResetPassword={(password, done) => {
|
||||
updateUserPassword({ variables: { userID: user.id, password } });
|
||||
toast('Password was changed!');
|
||||
done();
|
||||
}}
|
||||
onProfileAvatarRemove={() => {
|
||||
|
@ -460,7 +460,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
185,
|
||||
{ width: 185 },
|
||||
);
|
||||
}}
|
||||
>
|
||||
@ -479,7 +479,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
185,
|
||||
{ width: 185 },
|
||||
);
|
||||
}}
|
||||
>
|
||||
@ -499,7 +499,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
labels={labelsRef}
|
||||
members={membersRef}
|
||||
/>,
|
||||
200,
|
||||
{ width: 200 },
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
@ -290,10 +290,10 @@ const Details: React.FC<DetailsProps> = ({
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <div>loading</div>;
|
||||
return null;
|
||||
}
|
||||
if (!data) {
|
||||
return <div>loading</div>;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@ -501,6 +501,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
onCancel={NOOP}
|
||||
/>
|
||||
</Popup>,
|
||||
{ showDiamond: false, targetPadding: '0' },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -57,7 +57,7 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
|
||||
<Popup title={null} tab={0}>
|
||||
<TeamSettings
|
||||
onDeleteTeam={() => {
|
||||
setTab(1, 340);
|
||||
setTab(1, { width: 340 });
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
|
111
frontend/src/shared/components/NotifcationPopup/index.tsx
Normal file
111
frontend/src/shared/components/NotifcationPopup/index.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Popup } from 'shared/components/PopupMenu';
|
||||
|
||||
const ItemWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #414561;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
background: #10163a;
|
||||
}
|
||||
`;
|
||||
const ItemWrapperContent = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const ItemIconContainer = styled.span`
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ItemTextContainer = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const ItemTextTitle = styled.span`
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
color: rgba(${props => props.theme.colors.primary});
|
||||
font-size: 14px;
|
||||
`;
|
||||
const ItemTextDesc = styled.span`
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
const ItemTimeAgo = styled.span`
|
||||
margin-top: 0.25rem;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
`;
|
||||
|
||||
type NotificationItemProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export const NotificationItem: React.FC<NotificationItemProps> = ({ title, description, createdAt }) => {
|
||||
return (
|
||||
<ItemWrapper>
|
||||
<ItemWrapperContent>
|
||||
<ItemIconContainer />
|
||||
<ItemTextContainer>
|
||||
<ItemTextTitle>{title}</ItemTextTitle>
|
||||
<ItemTextDesc>{description}</ItemTextDesc>
|
||||
</ItemTextContainer>
|
||||
</ItemWrapperContent>
|
||||
<TimeAgo date={createdAt} component={ItemTimeAgo} />
|
||||
</ItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationHeader = styled.div`
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
background: rgba(${props => props.theme.colors.primary});
|
||||
`;
|
||||
|
||||
const NotificationHeaderTitle = styled.span`
|
||||
font-size: 14px;
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
`;
|
||||
|
||||
const NotificationFooter = styled.div`
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
color: rgba(${props => props.theme.colors.primary});
|
||||
&:hover {
|
||||
background: #10163a;
|
||||
}
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
`;
|
||||
|
||||
const NotificationPopup: React.FC = ({ children }) => {
|
||||
return (
|
||||
<Popup title={null} tab={0} borders={false} padding={false}>
|
||||
<NotificationHeader>
|
||||
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
|
||||
</NotificationHeader>
|
||||
<ul>{children}</ul>
|
||||
<NotificationFooter>View All</NotificationFooter>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationPopup;
|
@ -5,6 +5,7 @@ import ControlledInput from 'shared/components/ControlledInput';
|
||||
export const Container = styled.div<{
|
||||
invertY: boolean;
|
||||
invert: boolean;
|
||||
targetPadding: string;
|
||||
top: number;
|
||||
left: number;
|
||||
ref: any;
|
||||
@ -15,7 +16,7 @@ export const Container = styled.div<{
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: ${props => props.width}px;
|
||||
padding-top: 10px;
|
||||
padding-top: ${props => props.targetPadding};
|
||||
height: auto;
|
||||
z-index: 40000;
|
||||
${props =>
|
||||
@ -28,14 +29,18 @@ export const Container = styled.div<{
|
||||
css`
|
||||
top: auto;
|
||||
padding-top: 0;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: ${props.targetPadding};
|
||||
bottom: ${props.top}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 5px;
|
||||
padding-top: 8px;
|
||||
export const Wrapper = styled.div<{ padding: boolean; borders: boolean }>`
|
||||
${props =>
|
||||
props.padding &&
|
||||
css`
|
||||
padding: 5px;
|
||||
padding-top: 8px;
|
||||
`}
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
@ -43,8 +48,12 @@ export const Wrapper = styled.div`
|
||||
|
||||
color: #c2c6dc;
|
||||
background: #262c49;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-color: #414561;
|
||||
${props =>
|
||||
props.borders &&
|
||||
css`
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-color: #414561;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Header = styled.div`
|
||||
@ -326,7 +335,7 @@ export const PreviousButton = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const ContainerDiamond = styled.div<{ invert: boolean; invertY: boolean }>`
|
||||
export const ContainerDiamond = styled.div<{ borders: boolean; color: string; invert: boolean; invertY: boolean }>`
|
||||
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
@ -347,6 +356,10 @@ export const ContainerDiamond = styled.div<{ invert: boolean; invertY: boolean }
|
||||
transform: rotate(45deg) translate(-7px);
|
||||
z-index: 10;
|
||||
|
||||
background: #262c49;
|
||||
border-color: #414561;
|
||||
background: ${props => props.color};
|
||||
${props =>
|
||||
props.borders &&
|
||||
css`
|
||||
border-color: #414561;
|
||||
`}
|
||||
`;
|
||||
|
@ -15,9 +15,37 @@ import {
|
||||
Wrapper,
|
||||
} from './Styles';
|
||||
|
||||
function getPopupOptions(options?: PopupOptions) {
|
||||
const popupOptions = {
|
||||
borders: true,
|
||||
diamondColor: '#262c49',
|
||||
targetPadding: '10px',
|
||||
showDiamond: true,
|
||||
width: 316,
|
||||
};
|
||||
if (options) {
|
||||
if (options.borders) {
|
||||
popupOptions.borders = options.borders;
|
||||
}
|
||||
if (options.width) {
|
||||
popupOptions.width = options.width;
|
||||
}
|
||||
if (options.targetPadding) {
|
||||
popupOptions.targetPadding = options.targetPadding;
|
||||
}
|
||||
if (typeof options.showDiamond !== 'undefined' && options.showDiamond !== null) {
|
||||
popupOptions.showDiamond = options.showDiamond;
|
||||
}
|
||||
if (options.diamondColor) {
|
||||
popupOptions.diamondColor = options.diamondColor;
|
||||
}
|
||||
}
|
||||
return popupOptions;
|
||||
}
|
||||
|
||||
type PopupContextState = {
|
||||
show: (target: RefObject<HTMLElement>, content: JSX.Element, width?: string | number) => void;
|
||||
setTab: (newTab: number, width?: number | string) => void;
|
||||
show: (target: RefObject<HTMLElement>, content: JSX.Element, options?: PopupOptions) => void;
|
||||
setTab: (newTab: number, options?: PopupOptions) => void;
|
||||
getCurrentTab: () => number;
|
||||
hide: () => void;
|
||||
};
|
||||
@ -26,23 +54,44 @@ type PopupProps = {
|
||||
title: string | null;
|
||||
onClose?: () => void;
|
||||
tab: number;
|
||||
padding?: boolean;
|
||||
borders?: boolean;
|
||||
diamondColor?: string;
|
||||
};
|
||||
|
||||
type PopupContainerProps = {
|
||||
top: number;
|
||||
left: number;
|
||||
invert: boolean;
|
||||
targetPadding: string;
|
||||
invertY: boolean;
|
||||
onClose: () => void;
|
||||
width?: string | number;
|
||||
};
|
||||
|
||||
const PopupContainer: React.FC<PopupContainerProps> = ({ width, top, left, onClose, children, invert, invertY }) => {
|
||||
const PopupContainer: React.FC<PopupContainerProps> = ({
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
onClose,
|
||||
children,
|
||||
invert,
|
||||
invertY,
|
||||
targetPadding,
|
||||
}) => {
|
||||
const $containerRef = useRef<HTMLDivElement>(null);
|
||||
const [currentTop, setCurrentTop] = useState(top);
|
||||
useOnOutsideClick($containerRef, true, onClose, null);
|
||||
return (
|
||||
<Container width={width ?? 316} left={left} top={currentTop} ref={$containerRef} invert={invert} invertY={invertY}>
|
||||
<Container
|
||||
targetPadding={targetPadding}
|
||||
width={width ?? 316}
|
||||
left={left}
|
||||
top={currentTop}
|
||||
ref={$containerRef}
|
||||
invert={invert}
|
||||
invertY={invertY}
|
||||
>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
@ -73,13 +122,28 @@ type PopupState = {
|
||||
currentTab: number;
|
||||
previousTab: number;
|
||||
content: JSX.Element | null;
|
||||
width?: string | number;
|
||||
options: PopupOptionsInternal | null;
|
||||
};
|
||||
|
||||
const { Provider, Consumer } = PopupContext;
|
||||
|
||||
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
|
||||
|
||||
type PopupOptionsInternal = {
|
||||
width: number;
|
||||
borders: boolean;
|
||||
targetPadding: string;
|
||||
diamondColor: string;
|
||||
showDiamond: boolean;
|
||||
};
|
||||
|
||||
type PopupOptions = {
|
||||
targetPadding?: string | null;
|
||||
showDiamond?: boolean | null;
|
||||
width?: number | null;
|
||||
borders?: boolean | null;
|
||||
diamondColor?: string | null;
|
||||
};
|
||||
const defaultState = {
|
||||
isOpen: false,
|
||||
left: 0,
|
||||
@ -89,11 +153,12 @@ const defaultState = {
|
||||
currentTab: 0,
|
||||
previousTab: 0,
|
||||
content: null,
|
||||
options: null,
|
||||
};
|
||||
|
||||
export const PopupProvider: React.FC = ({ children }) => {
|
||||
const [currentState, setState] = useState<PopupState>(defaultState);
|
||||
const show = (target: RefObject<HTMLElement>, content: JSX.Element, width?: number | string) => {
|
||||
const show = (target: RefObject<HTMLElement>, content: JSX.Element, options?: PopupOptions) => {
|
||||
if (target && target.current) {
|
||||
const bounds = target.current.getBoundingClientRect();
|
||||
let top = bounds.top + bounds.height;
|
||||
@ -102,6 +167,7 @@ export const PopupProvider: React.FC = ({ children }) => {
|
||||
top = window.innerHeight - bounds.top;
|
||||
invertY = true;
|
||||
}
|
||||
const popupOptions = getPopupOptions(options);
|
||||
if (bounds.left + 304 + 30 > window.innerWidth) {
|
||||
setState({
|
||||
isOpen: true,
|
||||
@ -112,7 +178,7 @@ export const PopupProvider: React.FC = ({ children }) => {
|
||||
currentTab: 0,
|
||||
previousTab: 0,
|
||||
content,
|
||||
width: width ?? 316,
|
||||
options: popupOptions,
|
||||
});
|
||||
} else {
|
||||
setState({
|
||||
@ -124,7 +190,7 @@ export const PopupProvider: React.FC = ({ children }) => {
|
||||
currentTab: 0,
|
||||
previousTab: 0,
|
||||
content,
|
||||
width: width ?? 316,
|
||||
options: popupOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -139,20 +205,21 @@ export const PopupProvider: React.FC = ({ children }) => {
|
||||
currentTab: 0,
|
||||
previousTab: 0,
|
||||
content: null,
|
||||
options: null,
|
||||
});
|
||||
};
|
||||
const portalTarget = canUseDOM ? document.body : null; // appease flow
|
||||
|
||||
const setTab = (newTab: number, width?: number | string) => {
|
||||
const newWidth = width ?? currentState.width;
|
||||
setState((prevState: PopupState) => {
|
||||
return {
|
||||
...prevState,
|
||||
previousTab: currentState.currentTab,
|
||||
currentTab: newTab,
|
||||
width: newWidth,
|
||||
};
|
||||
});
|
||||
const setTab = (newTab: number, options?: PopupOptions) => {
|
||||
setState((prevState: PopupState) =>
|
||||
produce(prevState, draftState => {
|
||||
draftState.previousTab = currentState.currentTab;
|
||||
draftState.currentTab = newTab;
|
||||
if (options) {
|
||||
draftState.options = getPopupOptions(options);
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getCurrentTab = () => {
|
||||
@ -163,17 +230,26 @@ export const PopupProvider: React.FC = ({ children }) => {
|
||||
<Provider value={{ hide, show, setTab, getCurrentTab }}>
|
||||
{portalTarget &&
|
||||
currentState.isOpen &&
|
||||
currentState.options &&
|
||||
createPortal(
|
||||
<PopupContainer
|
||||
invertY={currentState.invertY}
|
||||
invert={currentState.invert}
|
||||
top={currentState.top}
|
||||
targetPadding={currentState.options.targetPadding}
|
||||
left={currentState.left}
|
||||
onClose={() => setState(defaultState)}
|
||||
width={currentState.width ?? 316}
|
||||
width={currentState.options.width}
|
||||
>
|
||||
{currentState.content}
|
||||
<ContainerDiamond invertY={currentState.invertY} invert={currentState.invert} />
|
||||
{currentState.options.showDiamond && (
|
||||
<ContainerDiamond
|
||||
color={currentState.options.diamondColor}
|
||||
borders={currentState.options.borders}
|
||||
invertY={currentState.invertY}
|
||||
invert={currentState.invert}
|
||||
/>
|
||||
)}
|
||||
</PopupContainer>,
|
||||
portalTarget,
|
||||
)}
|
||||
@ -197,8 +273,16 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
|
||||
useOnOutsideClick($containerRef, true, onClose, null);
|
||||
|
||||
return (
|
||||
<Container invertY={false} width={width ?? 316} invert={false} left={left} top={top} ref={$containerRef}>
|
||||
<Wrapper>
|
||||
<Container
|
||||
targetPadding="10px"
|
||||
invertY={false}
|
||||
width={width ?? 316}
|
||||
invert={false}
|
||||
left={left}
|
||||
top={top}
|
||||
ref={$containerRef}
|
||||
>
|
||||
<Wrapper padding borders>
|
||||
{onPrevious && (
|
||||
<PreviousButton onClick={onPrevious}>
|
||||
<AngleLeft color="#c2c6dc" />
|
||||
@ -222,7 +306,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
|
||||
);
|
||||
};
|
||||
|
||||
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
|
||||
export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, title, onClose, tab, children }) => {
|
||||
const { getCurrentTab, setTab } = usePopup();
|
||||
if (getCurrentTab() !== tab) {
|
||||
return null;
|
||||
@ -230,7 +314,7 @@ export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) =
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper>
|
||||
<Wrapper borders={borders} padding={padding}>
|
||||
{tab > 0 && (
|
||||
<PreviousButton
|
||||
onClick={() => {
|
||||
|
@ -5,6 +5,7 @@ import Button from 'shared/components/Button';
|
||||
import { Taskcafe } from 'shared/icons';
|
||||
import { NavLink, Link } from 'react-router-dom';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||
z-index: ${props => props.zIndex};
|
||||
@ -65,7 +66,7 @@ export const ProfileNameWrapper = styled.div`
|
||||
line-height: 1.25;
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div<{ disabled?: boolean }>`
|
||||
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
${props =>
|
||||
@ -86,7 +87,10 @@ export const ProfileNameSecondary = styled.small`
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const ProfileIcon = styled.div<{ bgColor: string | null; backgroundURL: string | null }>`
|
||||
export const ProfileIcon = styled.div<{
|
||||
bgColor: string | null;
|
||||
backgroundURL: string | null;
|
||||
}>`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 9999px;
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import ProfileIcon from 'shared/components/ProfileIcon';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { usePopup } from 'shared/components/PopupMenu';
|
||||
import { RoleCode } from 'shared/generated/graphql';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import {
|
||||
TaskcafeLogo,
|
||||
TaskcafeTitle,
|
||||
ProjectFinder,
|
||||
LogoContainer,
|
||||
NavSeparator,
|
||||
IconContainer,
|
||||
IconContainerWrapper,
|
||||
ProjectNameTextarea,
|
||||
InviteButton,
|
||||
GlobalActions,
|
||||
@ -33,6 +31,28 @@ import {
|
||||
ProjectMembers,
|
||||
} from './Styles';
|
||||
|
||||
type IconContainerProps = {
|
||||
disabled?: boolean;
|
||||
onClick?: ($target: React.RefObject<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
const IconContainer: React.FC<IconContainerProps> = ({ onClick, disabled = false, children }) => {
|
||||
const $container = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<IconContainerWrapper
|
||||
ref={$container}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick($container);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IconContainerWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const HomeDashboard = styled(Home)``;
|
||||
|
||||
type ProjectHeadingProps = {
|
||||
@ -145,7 +165,7 @@ type NavBarProps = {
|
||||
onFavorite?: () => void;
|
||||
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onSaveName?: (name: string) => void;
|
||||
onNotificationClick: () => void;
|
||||
onNotificationClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
canEditProjectName?: boolean;
|
||||
canInviteUser?: boolean;
|
||||
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
||||
@ -257,16 +277,16 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<ProjectFinder onClick={onOpenProjectFinder} variant="gradient">
|
||||
Projects
|
||||
</ProjectFinder>
|
||||
<IconContainer onClick={onDashboardClick}>
|
||||
<IconContainer onClick={() => onDashboardClick()}>
|
||||
<HomeDashboard width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<CheckCircle width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer onClick={onNotificationClick}>
|
||||
<IconContainer disabled onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<BarChart width={20} height={20} />
|
||||
</IconContainer>
|
||||
|
||||
|
@ -213,22 +213,18 @@ export enum ObjectType {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
organizations: Array<Organization>;
|
||||
users: Array<UserAccount>;
|
||||
findUser: UserAccount;
|
||||
findProject: Project;
|
||||
findTask: Task;
|
||||
projects: Array<Project>;
|
||||
findTeam: Team;
|
||||
teams: Array<Team>;
|
||||
findUser: UserAccount;
|
||||
labelColors: Array<LabelColor>;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
me: MePayload;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindUserArgs = {
|
||||
input: FindUser;
|
||||
notifications: Array<Notification>;
|
||||
organizations: Array<Organization>;
|
||||
projects: Array<Project>;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
teams: Array<Team>;
|
||||
users: Array<UserAccount>;
|
||||
};
|
||||
|
||||
|
||||
@ -242,13 +238,18 @@ export type QueryFindTaskArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryProjectsArgs = {
|
||||
input?: Maybe<ProjectsFilter>;
|
||||
export type QueryFindTeamArgs = {
|
||||
input: FindTeam;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindTeamArgs = {
|
||||
input: FindTeam;
|
||||
export type QueryFindUserArgs = {
|
||||
input: FindUser;
|
||||
};
|
||||
|
||||
|
||||
export type QueryProjectsArgs = {
|
||||
input?: Maybe<ProjectsFilter>;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
@ -577,6 +578,42 @@ export type FindTeam = {
|
||||
teamID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export enum EntityType {
|
||||
Task = 'TASK'
|
||||
}
|
||||
|
||||
export enum ActorType {
|
||||
User = 'USER'
|
||||
}
|
||||
|
||||
export enum ActionType {
|
||||
TaskMemberAdded = 'TASK_MEMBER_ADDED'
|
||||
}
|
||||
|
||||
export type NotificationActor = {
|
||||
__typename?: 'NotificationActor';
|
||||
id: Scalars['UUID'];
|
||||
type: ActorType;
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type NotificationEntity = {
|
||||
__typename?: 'NotificationEntity';
|
||||
id: Scalars['UUID'];
|
||||
type: EntityType;
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Notification = {
|
||||
__typename?: 'Notification';
|
||||
id: Scalars['ID'];
|
||||
entity: NotificationEntity;
|
||||
actionType: ActionType;
|
||||
actor: NotificationActor;
|
||||
read: Scalars['Boolean'];
|
||||
createdAt: Scalars['Time'];
|
||||
};
|
||||
|
||||
export type NewProject = {
|
||||
userID: Scalars['UUID'];
|
||||
teamID: Scalars['UUID'];
|
||||
@ -1755,6 +1792,40 @@ export type ToggleTaskLabelMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type TopNavbarQueryVariables = {};
|
||||
|
||||
|
||||
export type TopNavbarQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { notifications: Array<(
|
||||
{ __typename?: 'Notification' }
|
||||
& Pick<Notification, 'createdAt' | 'read' | 'id' | 'actionType'>
|
||||
& { entity: (
|
||||
{ __typename?: 'NotificationEntity' }
|
||||
& Pick<NotificationEntity, 'id' | 'type' | 'name'>
|
||||
), actor: (
|
||||
{ __typename?: 'NotificationActor' }
|
||||
& Pick<NotificationActor, 'id' | 'type' | 'name'>
|
||||
) }
|
||||
)>, me: (
|
||||
{ __typename?: 'MePayload' }
|
||||
& { user: (
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||
) }
|
||||
), teamRoles: Array<(
|
||||
{ __typename?: 'TeamRole' }
|
||||
& Pick<TeamRole, 'teamID' | 'roleCode'>
|
||||
)>, projectRoles: Array<(
|
||||
{ __typename?: 'ProjectRole' }
|
||||
& Pick<ProjectRole, 'projectID' | 'roleCode'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type UnassignTaskMutationVariables = {
|
||||
taskID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
@ -3613,6 +3684,70 @@ export function useToggleTaskLabelMutation(baseOptions?: ApolloReactHooks.Mutati
|
||||
export type ToggleTaskLabelMutationHookResult = ReturnType<typeof useToggleTaskLabelMutation>;
|
||||
export type ToggleTaskLabelMutationResult = ApolloReactCommon.MutationResult<ToggleTaskLabelMutation>;
|
||||
export type ToggleTaskLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<ToggleTaskLabelMutation, ToggleTaskLabelMutationVariables>;
|
||||
export const TopNavbarDocument = gql`
|
||||
query topNavbar {
|
||||
notifications {
|
||||
createdAt
|
||||
read
|
||||
id
|
||||
entity {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
actor {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
actionType
|
||||
}
|
||||
me {
|
||||
user {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
teamRoles {
|
||||
teamID
|
||||
roleCode
|
||||
}
|
||||
projectRoles {
|
||||
projectID
|
||||
roleCode
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTopNavbarQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTopNavbarQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTopNavbarQuery` 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 } = useTopNavbarQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTopNavbarQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<TopNavbarQuery, TopNavbarQueryVariables>) {
|
||||
return ApolloReactHooks.useQuery<TopNavbarQuery, TopNavbarQueryVariables>(TopNavbarDocument, baseOptions);
|
||||
}
|
||||
export function useTopNavbarLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<TopNavbarQuery, TopNavbarQueryVariables>) {
|
||||
return ApolloReactHooks.useLazyQuery<TopNavbarQuery, TopNavbarQueryVariables>(TopNavbarDocument, baseOptions);
|
||||
}
|
||||
export type TopNavbarQueryHookResult = ReturnType<typeof useTopNavbarQuery>;
|
||||
export type TopNavbarLazyQueryHookResult = ReturnType<typeof useTopNavbarLazyQuery>;
|
||||
export type TopNavbarQueryResult = ApolloReactCommon.QueryResult<TopNavbarQuery, TopNavbarQueryVariables>;
|
||||
export const UnassignTaskDocument = gql`
|
||||
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
|
||||
unassignTask(input: {taskID: $taskID, userID: $userID}) {
|
||||
|
43
frontend/src/shared/graphql/topNavbar.ts
Normal file
43
frontend/src/shared/graphql/topNavbar.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const TOP_NAVBAR_QUERY = gql`
|
||||
query topNavbar {
|
||||
notifications {
|
||||
createdAt
|
||||
read
|
||||
id
|
||||
entity {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
actor {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
actionType
|
||||
}
|
||||
me {
|
||||
user {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
teamRoles {
|
||||
teamID
|
||||
roleCode
|
||||
}
|
||||
projectRoles {
|
||||
projectID
|
||||
roleCode
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default TOP_NAVBAR_QUERY;
|
@ -3290,6 +3290,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-timeago@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-timeago/-/react-timeago-4.1.1.tgz#14c24cb8c1299379d9c2941ca3373f849d8bc78a"
|
||||
integrity sha512-Rokr09qNqkKXpKwrDVYzGz/mGlmgX5ZdxAJToUehMyRHXEwtXUUNaAZur5RoZ5JtukBwXKzDevS72Axtjm+yKg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@*":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
|
||||
@ -13986,6 +13993,20 @@ react-textarea-autosize@^7.1.0:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-timeago@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-timeago/-/react-timeago-4.4.0.tgz#4520dd9ba63551afc4d709819f52b14b9343ba2b"
|
||||
integrity sha512-Zj8RchTqZEH27LAANemzMR2RpotbP2aMd+UIajfYMZ9KW4dMcViUVKzC7YmqfiqlFfz8B0bjDw2xUBjmcxDngA==
|
||||
|
||||
react-toastify@^6.0.8:
|
||||
version "6.0.8"
|
||||
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-6.0.8.tgz#84625d81d0fd01902a7f4c6f317eb074cb3bba67"
|
||||
integrity sha512-NSqCNwv+C4IfR+c92PFZiNyeBwOJvigrP2bcRi2f6Hg3WqcHhEHOknbSQOs9QDFuqUjmK3SOrdvScQ3z63ifXg==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
prop-types "^15.7.2"
|
||||
react-transition-group "^4.4.1"
|
||||
|
||||
react-transition-group@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
|
||||
@ -13996,6 +14017,16 @@ react-transition-group@^4.3.0:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-transition-group@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^16.12.0, react@^16.8.3:
|
||||
version "16.12.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"
|
||||
|
Reference in New Issue
Block a user