Skip to content

Commit

Permalink
chore: Styling fixes from #2874 (#2875)
Browse files Browse the repository at this point in the history
Styling fixes from #2874
  • Loading branch information
StaNov committed Feb 3, 2025
1 parent 2238d10 commit 9e1f094
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 159 deletions.
4 changes: 2 additions & 2 deletions webapp/src/component/layout/Notifications/MfaDisabledItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import {
NotificationItem,
NotificationItemProps,
} from 'tg.component/layout/Notifications/NotificationItem';
import { LINKS } from 'tg.constants/links';

type MfaDisabledItemProps = NotificationItemProps;

export const MfaDisabledItem: FunctionComponent<MfaDisabledItemProps> = ({
notification,
...props
}) => {
const destinationUrl = '/account/security';
return (
<NotificationItem
notification={notification}
destinationUrl={destinationUrl}
destinationUrl={LINKS.USER_ACCOUNT_SECURITY.build()}
{...props}
>
<b>{notification.originatingUser?.name}</b>&nbsp;
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/component/layout/Notifications/MfaEnabledItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import {
NotificationItem,
NotificationItemProps,
} from 'tg.component/layout/Notifications/NotificationItem';
import { LINKS } from 'tg.constants/links';

type MfaEnabledItemProps = NotificationItemProps;

export const MfaEnabledItem: FunctionComponent<MfaEnabledItemProps> = ({
notification,
...props
}) => {
const destinationUrl = '/account/security';
return (
<NotificationItem
notification={notification}
destinationUrl={destinationUrl}
destinationUrl={LINKS.USER_ACCOUNT_SECURITY.build()}
{...props}
>
<b>{notification.originatingUser?.name}</b>&nbsp;
Expand Down
46 changes: 19 additions & 27 deletions webapp/src/component/layout/Notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { default as React, FunctionComponent } from 'react';
import { default as React } from 'react';
import {
Box,
ListItemButton,
ListItemButtonProps,
styled,
} from '@mui/material';
import { useHistory } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { components } from 'tg.service/apiSchema.generated';
import { useCurrentLanguage } from 'tg.hooks/useCurrentLanguage';
import { locales } from '../../../locales';
import { formatDistanceToNowStrict } from 'date-fns';
import { AvatarImg } from 'tg.component/common/avatar/AvatarImg';

const Item = styled(ListItemButton)`
const StyledItem = styled(ListItemButton)`
display: grid;
column-gap: 10px;
grid-template-columns: 30px 1fr 120px;
Expand All @@ -23,25 +23,22 @@ const Item = styled(ListItemButton)`
line-height: 1.3;
`;

const Detail = styled(Box)`
const StyledDetail = styled(Box)`
grid-area: notification-detail;
`;

const Avatar = styled(Box)`
const StyledAvatar = styled(Box)`
grid-area: notification-avatar;
`;

const Time = styled(Box)`
const StyledTime = styled(Box)`
font-size: 13px;
grid-area: notification-time;
text-align: right;
color: ${({ theme }) =>
theme.palette.mode === 'light'
? theme.palette.emphasis[400]
: theme.palette.emphasis[600]};
color: ${({ theme }) => theme.palette.text.secondary};
`;

const Project = styled(Time)`
const StyledProject = styled(StyledTime)`
grid-area: notification-project;
`;

Expand All @@ -51,32 +48,27 @@ export type NotificationItemProps = {
destinationUrl?: string;
} & ListItemButtonProps;

export const NotificationItem: FunctionComponent<NotificationItemProps> = ({
export const NotificationItem: React.FC<NotificationItemProps> = ({
notification,
key,
isLast,
destinationUrl,
children,
}) => {
const history = useHistory();
const language = useCurrentLanguage();
const createdAt = notification.createdAt;
const originatingUser = notification.originatingUser;
const project = notification.project;
return (
<Item
<StyledItem
key={key}
divider={!isLast}
//@ts-ignore
href={destinationUrl}
onClick={(event) => {
if (!destinationUrl) return;
event.preventDefault();
history.push(destinationUrl);
}}
component={Link}
to={destinationUrl}
data-cy="notifications-list-item"
>
<Avatar>
<StyledAvatar>
{originatingUser && (
<AvatarImg
owner={{
Expand All @@ -88,17 +80,17 @@ export const NotificationItem: FunctionComponent<NotificationItemProps> = ({
size={30}
/>
)}
</Avatar>
<Detail>{children}</Detail>
</StyledAvatar>
<StyledDetail>{children}</StyledDetail>
{createdAt && (
<Time>
<StyledTime>
{formatDistanceToNowStrict(new Date(createdAt), {
addSuffix: true,
locale: locales[language].dateFnsLocale,
})}
</Time>
</StyledTime>
)}
{project && <Project>{project.name}</Project>}
</Item>
{project && <StyledProject>{project.name}</StyledProject>}
</StyledItem>
);
};
21 changes: 21 additions & 0 deletions webapp/src/component/layout/Notifications/NotificationTypeMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TaskAssignedItem } from 'tg.component/layout/Notifications/TaskAssignedItem';
import { TaskCompletedItem } from 'tg.component/layout/Notifications/TaskCompletedItem';
import { MfaEnabledItem } from 'tg.component/layout/Notifications/MfaEnabledItem';
import { MfaDisabledItem } from 'tg.component/layout/Notifications/MfaDisabledItem';
import { PasswordChangedItem } from 'tg.component/layout/Notifications/PasswordChangedItem';
import { components } from 'tg.service/apiSchema.generated';
import { NotificationItemProps } from 'tg.component/layout/Notifications/NotificationItem';
import React from 'react';

type NotificationsComponentMap = Record<
components['schemas']['NotificationModel']['type'],
React.FC<NotificationItemProps>
>;

export const notificationComponents: NotificationsComponentMap = {
TASK_ASSIGNED: TaskAssignedItem,
TASK_COMPLETED: TaskCompletedItem,
MFA_ENABLED: MfaEnabledItem,
MFA_DISABLED: MfaDisabledItem,
PASSWORD_CHANGED: PasswordChangedItem,
};
127 changes: 64 additions & 63 deletions webapp/src/component/layout/Notifications/NotificationsPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { default as React, FunctionComponent, useEffect } from 'react';
import { default as React, useEffect } from 'react';
import { List, ListItem, styled } from '@mui/material';
import Menu from '@mui/material/Menu';
import {
Expand All @@ -10,27 +10,45 @@ import { useGlobalContext } from 'tg.globalContext/GlobalContext';
import { useUser } from 'tg.globalContext/helpers';
import { BoxLoading } from 'tg.component/common/BoxLoading';
import { PopoverProps } from '@mui/material/Popover';
import { TaskAssignedItem } from 'tg.component/layout/Notifications/TaskAssignedItem';
import { TaskCompletedItem } from 'tg.component/layout/Notifications/TaskCompletedItem';
import { MfaEnabledItem } from 'tg.component/layout/Notifications/MfaEnabledItem';
import { MfaDisabledItem } from 'tg.component/layout/Notifications/MfaDisabledItem';
import { PasswordChangedItem } from 'tg.component/layout/Notifications/PasswordChangedItem';
import { notificationComponents } from 'tg.component/layout/Notifications/NotificationTypeMap';
import { NotificationsChanged } from 'tg.websocket-client/WebsocketClient';
import { components } from 'tg.service/apiSchema.generated';
import { InfiniteData } from 'react-query';

type PagedModelNotificationModel =
components['schemas']['PagedModelNotificationModel'];

const FETCH_NEXT_PAGE_SCROLL_THRESHOLD_IN_PIXELS = 100;

const StyledMenu = styled(Menu)`
.MuiPaper-root {
margin-top: 5px;
}
`;

const ListItemHeader = styled(ListItem)`
const StyledListItemHeader = styled(ListItem)`
font-weight: bold;
`;

export const NotificationsPopup: FunctionComponent<{
function getNotifications(
data: InfiniteData<PagedModelNotificationModel> | undefined
) {
return data?.pages
.flatMap((it) => it?._embedded?.notificationModelList)
.filter((it) => it !== undefined);
}

type NotificationsPopupProps = {
onClose: () => void;
onNotificationsChanged: (NotificationsChanged) => void;
onNotificationsChanged: (event: NotificationsChanged) => void;
anchorEl: PopoverProps['anchorEl'];
}> = ({ onClose, onNotificationsChanged, anchorEl }) => {
};

export const NotificationsPopup: React.FC<NotificationsPopupProps> = ({
onClose,
onNotificationsChanged,
anchorEl,
}) => {
const user = useUser();
const client = useGlobalContext((c) => c.wsClient.client);

Expand All @@ -40,7 +58,10 @@ export const NotificationsPopup: FunctionComponent<{
method: 'get',
query: query,
options: {
enabled: false,
enabled: !!anchorEl,
refetchOnMount: false,
staleTime: Infinity,
cacheTime: Infinity,
getNextPageParam: (lastPage) => {
if (
lastPage.page &&
Expand All @@ -56,6 +77,18 @@ export const NotificationsPopup: FunctionComponent<{
return null;
}
},
onSuccess(data) {
const markAsSeenIds = getNotifications(data)?.map((it) => it.id);
if (!markAsSeenIds) return;

markSeenMutation.mutate({
content: {
'application/json': {
notificationIds: markAsSeenIds,
},
},
});
},
},
});

Expand All @@ -64,50 +97,26 @@ export const NotificationsPopup: FunctionComponent<{
method: 'put',
});

const notifications = notificationsLoadable.data?.pages
.flatMap((it) => it?._embedded?.notificationModelList)
.filter((it) => it !== undefined);

useEffect(() => {
if (!anchorEl) return;

const data = notificationsLoadable.data;
if (!data) {
if (!notificationsLoadable.isFetching) {
notificationsLoadable.refetch();
}
return;
}

const markAsSeenIds = notifications?.map((it) => it.id);
if (!markAsSeenIds) return;

markSeenMutation.mutate({
content: {
'application/json': {
notificationIds: markAsSeenIds,
},
},
});
}, [notificationsLoadable.data, anchorEl]);

useEffect(() => {
if (client && user) {
return client.subscribe(
`/users/${user.id}/notifications-changed`,
(e) => {
const newNotification = e.data.newNotification;
if (newNotification) notificationsLoadable.remove();
onNotificationsChanged(e);
(event) => {
if (event.data.newNotification) {
notificationsLoadable.remove();
}
onNotificationsChanged(event);
}
);
}
}, [user, client]);

const notifications = getNotifications(notificationsLoadable.data);

return (
<StyledMenu
keepMounted
open={!!anchorEl}
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
Expand All @@ -129,7 +138,8 @@ export const NotificationsPopup: FunctionComponent<{
if (
notificationsLoadable?.hasNextPage &&
!notificationsLoadable.isFetching &&
target.scrollHeight - target.clientHeight - target.scrollTop < 100
target.scrollHeight - target.clientHeight - target.scrollTop <
FETCH_NEXT_PAGE_SCROLL_THRESHOLD_IN_PIXELS
) {
notificationsLoadable.fetchNextPage();
}
Expand All @@ -138,27 +148,18 @@ export const NotificationsPopup: FunctionComponent<{
}}
>
<List id="notifications-list" data-cy="notifications-list">
<ListItemHeader divider>
<StyledListItemHeader divider>
<T keyName="notifications-header" />
</ListItemHeader>
</StyledListItemHeader>
{notifications?.map((notification, i) => {
const props = {
notification: notification,
key: notification.id,
isLast: i === notifications.length - 1,
};
switch (notification.type) {
case 'TASK_ASSIGNED':
return <TaskAssignedItem {...props} />;
case 'TASK_COMPLETED':
return <TaskCompletedItem {...props} />;
case 'MFA_ENABLED':
return <MfaEnabledItem {...props} />;
case 'MFA_DISABLED':
return <MfaDisabledItem {...props} />;
case 'PASSWORD_CHANGED':
return <PasswordChangedItem {...props} />;
}
const Component = notificationComponents[notification.type]!;
return (
<Component
notification={notification}
key={notification.id}
isLast={i === notifications.length - 1}
/>
);
})}
{notifications?.length === 0 && (
<ListItem data-cy="notifications-empty-message">
Expand Down
Loading

0 comments on commit 9e1f094

Please sign in to comment.