Skip to content

Commit

Permalink
feat: SB-765 Migrate notifications list query to Apollo
Browse files Browse the repository at this point in the history
  • Loading branch information
mkleszcz committed Jan 25, 2023
1 parent bdbad86 commit 55c9930
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 194 deletions.
18 changes: 16 additions & 2 deletions packages/webapp/src/mocks/factories/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { OperationDescriptor } from 'react-relay/hooks';
import { MockPayloadGenerator, RelayMockEnvironment } from 'relay-test-utils';

import { NotificationTypes } from '../../shared/components/notifications/notifications.types';
import { NotificationType } from '../../shared/services/graphqlApi/__generated/types';
import { connectionFromArray, makeId } from '../../tests/utils/fixtures';
import { NotificationType } from '../../shared/services/graphqlApi';
import { composeMockedPaginatedListQueryResult, connectionFromArray, makeId } from '../../tests/utils/fixtures';
import { ExtractNodeType } from '../../shared/utils/graphql';
import { notificationsListContent$data } from '../../shared/components/notifications/notificationsList/__generated__/notificationsListContent.graphql';
import notificationsListQueryGraphql from '../../shared/components/notifications/__generated__/notificationsListQuery.graphql';
import { NOTIFICATIONS_LIST_QUERY } from '../../shared/components/notifications/notifications.graphql';

import { createFactory } from './factoryCreators';
import { currentUserFactory } from './auth';
Expand All @@ -30,4 +31,17 @@ export const fillNotificationsListQuery = (
})
);
env.mock.queuePendingOperation(notificationsListQueryGraphql, {});

return composeMockedPaginatedListQueryResult(
NOTIFICATIONS_LIST_QUERY,
'allNotifications',
'NotificationType',
{
data: notifications,
variables: {
count: 20,
},
},
{ endCursor: 'test', hasNextPage: false }
);
};
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import { Suspense, useEffect } from 'react';
import ClickAwayListener from 'react-click-away-listener';
import { useQueryLoader } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
import { NetworkStatus, useQuery } from '@apollo/client';
import { useOpenState } from '../../hooks/useOpenState';
import { notificationsListQuery } from './__generated__/notificationsListQuery.graphql';
import { NotificationsButton } from './notificationsButton';
import { NotificationsList } from './notificationsList';
import { NOTIFICATIONS_LIST_QUERY } from './notifications.graphql';
import { NOTIFICATIONS_PER_PAGE } from './notificationsList/notificationsList.constants';

export const Notifications = () => {
const notifications = useOpenState(false);

const [listQueryRef, loadListQuery] = useQueryLoader<notificationsListQuery>(
graphql`
query notificationsListQuery($count: Int = 20, $cursor: String) {
...notificationsListContent
...notificationsButtonContent
}
`
);
const { loading, data, fetchMore, networkStatus } = useQuery(NOTIFICATIONS_LIST_QUERY);

useEffect(() => {
loadListQuery({});
}, [loadListQuery]);
if (loading && networkStatus === NetworkStatus.loading) {
return <NotificationsButton.Fallback />;
}

if (!listQueryRef) return null;
const onLoadMore = (cursor, count = NOTIFICATIONS_PER_PAGE) => {
fetchMore({
variables: {
cursor,
count,
},
});
};

return (
<Suspense fallback={<NotificationsButton.Fallback />}>
<NotificationsButton listQueryRef={listQueryRef} onClick={notifications.toggle} />
<>
<NotificationsButton queryResult={data} onClick={notifications.toggle} />
<ClickAwayListener onClickAway={notifications.clickAway}>
<NotificationsList isOpen={notifications.isOpen} listQueryRef={listQueryRef} />
<NotificationsList isOpen={notifications.isOpen} queryResult={data} loading={loading} onLoadMore={onLoadMore} />
</ClickAwayListener>
</Suspense>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { gql } from '../../services/graphqlApi/__generated/gql';

export const NOTIFICATIONS_LIST_QUERY = gql(/* GraphQL */ `
query notificationsListQuery($count: Int = 20, $cursor: String) {
...notificationsListContentFragment
...notificationsButtonContent
}
`);
Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
import mailOutlineIcon from '@iconify-icons/ion/mail-outline';
import mailUnreadOutlineIcon from '@iconify-icons/ion/mail-unread-outline';
import { PreloadedQuery, useFragment, usePreloadedQuery } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
import { useIntl } from 'react-intl';
import { ButtonProps, ButtonVariant } from '../../forms/button';
import { Icon } from '../../icon';
import notificationsListQueryGraphql, {
notificationsListQuery,
notificationsListQuery$data,
} from '../__generated__/notificationsListQuery.graphql';
import { notificationsButtonContent$key } from './__generated__/notificationsButtonContent.graphql';
import { gql, useFragment, FragmentType } from '../../../services/graphqlApi/__generated/gql';
import { Button } from './notificationsButton.styles';

export type NotificationsButtonProps = Omit<ButtonProps, 'children' | 'variant'> & {
listQueryRef: PreloadedQuery<notificationsListQuery>;
};
export const NOTIFICATIONS_BUTTON_CONTENT_FRAGMENT = gql(/* GraphQL */ `
fragment notificationsButtonContent on Query {
hasUnreadNotifications
}
`);

export const NotificationsButton = ({ listQueryRef, ...props }: NotificationsButtonProps) => {
const queryResponse = usePreloadedQuery(notificationsListQueryGraphql, listQueryRef);

return <Wrapper queryResponse={queryResponse} {...props} />;
};

type WrapperProps = Omit<NotificationsButtonProps, 'listQueryRef'> & {
queryResponse: notificationsListQuery$data;
export type NotificationsButtonProps = Omit<ButtonProps, 'children' | 'variant'> & {
queryResult?: FragmentType<typeof NOTIFICATIONS_BUTTON_CONTENT_FRAGMENT>;
};

export const Wrapper = ({ queryResponse, ...props }: WrapperProps) => {
const data = useFragment<notificationsButtonContent$key>(
graphql`
fragment notificationsButtonContent on Query {
hasUnreadNotifications
}
`,
queryResponse
);
export const NotificationsButton = ({ queryResult, ...props }: NotificationsButtonProps) => {
const data = useFragment(NOTIFICATIONS_BUTTON_CONTENT_FRAGMENT, queryResult);

return <Content hasUnreadNotifications={data.hasUnreadNotifications ?? false} {...props} />;
return <Content hasUnreadNotifications={data?.hasUnreadNotifications ?? false} {...props} />;
};

type ContentProps = Omit<NotificationsButtonProps, 'listQueryRef'> & {
type ContentProps = Omit<NotificationsButtonProps, 'queryResult'> & {
hasUnreadNotifications: boolean;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,69 @@
import { screen } from '@testing-library/react';
import { times } from 'ramda';
import { useQuery } from '@apollo/client';
import { render } from '../../../../../tests/utils/rendering';
import { NotificationsList, NotificationsListProps } from '../notificationsList.component';
import { fillNotificationsListQuery, notificationFactory } from '../../../../../mocks/factories';
import { ExtractNodeType } from '../../../../utils/graphql';
import { notificationsListContent$data } from '../__generated__/notificationsListContent.graphql';
import { getRelayEnv } from '../../../../../tests/utils/relay';
import { NOTIFICATIONS_LIST_QUERY } from '../../notifications.graphql';

describe('NotificationsList: Component', () => {
const Component = (props: Partial<NotificationsListProps>) => (
<NotificationsList isOpen listQueryRef={{} as any} {...props} />
);
const Component = (props: Partial<NotificationsListProps>) => {
const { loading, data } = useQuery(NOTIFICATIONS_LIST_QUERY);
return <NotificationsList isOpen onLoadMore={() => null} loading={loading} queryResult={data} {...props} />;
};

const renderWithNotifications = (
notifications: Array<Partial<ExtractNodeType<notificationsListContent$data['allNotifications']>>>
) => {
const env = getRelayEnv();
fillNotificationsListQuery(env, notifications);
const mockRequest = fillNotificationsListQuery(env, notifications);

if (mockRequest.result?.data) {
mockRequest.result.data.hasUnreadNotifications = false;
}
const apolloMocks = [mockRequest];

render(
<Component
listQueryRef={
{
environment: env,
isDisposed: false,
} as any
}
/>,
{ relayEnvironment: env }
);
render(<Component />, { relayEnvironment: env, apolloMocks });
};

it('should render no items correctly', () => {
it('should render no items correctly', async () => {
renderWithNotifications([]);

expect(screen.getAllByLabelText('Loading notification')).toHaveLength(2);
expect(await screen.findByText('Mark all as read')).toBeInTheDocument();
expect(await screen.findByText('No notifications')).toBeInTheDocument();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});

it('should not render non registered notifications', () => {
it('should not render non registered notifications', async () => {
renderWithNotifications([
{
notificationFactory({
type: 'some_random_type_that_doesnt_exist',
},
}),
]);

expect(screen.queryByRole('link')).not.toBeInTheDocument();
});

it('should render correct notifications', () => {
it('should render correct notifications', async () => {
const notifications = times(() => notificationFactory(), 3);
renderWithNotifications(notifications);

expect(screen.getAllByRole('link')).toHaveLength(notifications.length);
expect(screen.getAllByLabelText('Loading notification')).toHaveLength(2);
expect(await screen.findByText('Mark all as read')).toBeInTheDocument();
expect(await screen.findAllByRole('link')).toHaveLength(notifications.length);
});

it('should not render wrong notifications', () => {
it('should not render wrong notifications', async () => {
const correctNotifications = times(() => notificationFactory(), 3);
const malformedNotification = notificationFactory({
data: null,
});
renderWithNotifications([...correctNotifications, malformedNotification]);

expect(screen.getAllByRole('link')).toHaveLength(correctNotifications.length);
expect(await screen.findAllByRole('link')).toHaveLength(correctNotifications.length);
});
});
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import { ElementType, Suspense } from 'react';
import { ElementType } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { PreloadedQuery, usePreloadedQuery } from 'react-relay';
import useInfiniteScroll from 'react-infinite-scroll-hook';
import { isEmpty } from 'ramda';
import { ButtonVariant } from '../../forms/button';
import { NotificationSkeleton } from '../notification';
import { EmptyState } from '../../emptyState';
import { NotificationTypes } from '../notifications.types';
import { NOTIFICATIONS_STRATEGY } from '../notifications.constants';
import notificationsListQueryGraphql, {
notificationsListQuery,
notificationsListQuery$data,
} from '../__generated__/notificationsListQuery.graphql';
import { Container, List, MarkAllAsReadButton, Title } from './notificationsList.styles';
import { FragmentType } from '../../../services/graphqlApi/__generated/gql';
import { NOTIFICATIONS_PER_PAGE } from './notificationsList.constants';
import { Container, List, MarkAllAsReadButton, Title } from './notificationsList.styles';
import { useMarkAllAsRead, useNotificationsListContent } from './notificationsList.hooks';
import { NotificationErrorBoundary } from './notificationErrorBoundary';
import { notificationsListContentFragment } from './notificationsList.graphql';

export type NotificationsListProps = {
isOpen: boolean;
listQueryRef: PreloadedQuery<notificationsListQuery>;
queryResult?: FragmentType<typeof notificationsListContentFragment>;
loading: boolean;
onLoadMore: (cursor: string, count: number) => void;
};

export const NotificationsList = ({ listQueryRef, isOpen }: NotificationsListProps) => {
export const NotificationsList = ({ isOpen, ...props }: NotificationsListProps) => {
const intl = useIntl();
const queryResponse = usePreloadedQuery(notificationsListQueryGraphql, listQueryRef);

const markAllAsRead = useMarkAllAsRead(
intl.formatMessage({
Expand All @@ -45,33 +43,31 @@ export const NotificationsList = ({ listQueryRef, isOpen }: NotificationsListPro
/>
</MarkAllAsReadButton>
<List>
<Suspense
fallback={
<>
<NotificationSkeleton />
<NotificationSkeleton />
</>
}
>
<Content queryResponse={queryResponse} />
</Suspense>
{props.loading ? (
<>
<NotificationSkeleton />
<NotificationSkeleton />
</>
) : (
<Content {...props} />
)}
</List>
</Container>
);
};

type ContentProps = {
queryResponse: notificationsListQuery$data;
};
type ContentProps = Pick<NotificationsListProps, 'queryResult' | 'loading' | 'onLoadMore'>;

const Content = ({ queryResponse }: ContentProps) => {
const { allNotifications, loadNext, hasNext, isLoadingNext } = useNotificationsListContent(queryResponse);
const Content = ({ queryResult, loading, onLoadMore }: ContentProps) => {
const { allNotifications, hasNext, endCursor } = useNotificationsListContent(queryResult);

const [scrollSensorRef] = useInfiniteScroll({
loading: isLoadingNext,
loading,
hasNextPage: hasNext,
onLoadMore: () => {
loadNext(NOTIFICATIONS_PER_PAGE);
if (hasNext && endCursor) {
onLoadMore(endCursor, NOTIFICATIONS_PER_PAGE);
}
},
disabled: false,
});
Expand All @@ -90,7 +86,7 @@ const Content = ({ queryResponse }: ContentProps) => {
const NotificationComponent = NOTIFICATIONS_STRATEGY[notification.type as NotificationTypes] as
| ElementType
| undefined;
if (!NotificationComponent) {
if (!notification.data || !NotificationComponent) {
return null;
}
return (
Expand All @@ -99,7 +95,7 @@ const Content = ({ queryResponse }: ContentProps) => {
</NotificationErrorBoundary>
);
})}
{(hasNext || isLoadingNext) && (
{(hasNext || loading) && (
<>
<NotificationSkeleton ref={scrollSensorRef} />
<NotificationSkeleton />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { gql } from '../../../services/graphqlApi/__generated/gql';

export const notificationsListContentFragment = gql(/* GraphQL */ `
fragment notificationsListContentFragment on Query {
hasUnreadNotifications
allNotifications(first: $count, after: $cursor) {
edges {
node {
id
data
createdAt
readAt
type
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`);
Loading

0 comments on commit 55c9930

Please sign in to comment.