Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions src/components/CitationExporter/CitationExporter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,45 +279,3 @@ const AdvancedControls = ({
}
return null;
};

/**
* Static component for SSR
*/
const Static = (props: Omit<ICitationExporterProps, 'page' | 'nextPage'>): ReactElement => {
const { records, initialFormat, singleMode, totalRecords, sort, ...divProps } = props;

const { data, state } = useCitationExporter({
format: initialFormat,
records,
singleMode: true,
sort,
});
const ctx = state.context;

const { getFormatById } = useExportFormats();
const format = getFormatById(ctx.params.format);

if (singleMode) {
return (
<ExportContainer header={<>Exporting record in {format.name} format</>} {...divProps}>
<ResultArea result={data?.export} format={ctx.params.format} />
</ExportContainer>
);
}

return (
<ExportContainer
header={
<>
Exporting record{ctx.range[1] - ctx.range[0] > 1 ? 's' : ''} {ctx.range[0] + 1} to {ctx.range[1]} (total:{' '}
{totalRecords.toLocaleString()})
</>
}
{...divProps}
>
<ResultArea result={data?.export} format={ctx.params.format} />
</ExportContainer>
);
};

CitationExporter.Static = Static;
15 changes: 15 additions & 0 deletions src/components/CitationExporter/components/ExportSkeleton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { render, screen } from '@/test-utils';
import { describe, expect, test } from 'vitest';
import { ExportSkeleton } from './ExportSkeleton';

describe('ExportSkeleton', () => {
test('renders the export container chrome with a heading slot', () => {
render(<ExportSkeleton />);
expect(screen.getByTestId('export-heading')).toBeInTheDocument();
});

test('renders skeleton placeholders', () => {
const { container } = render(<ExportSkeleton />);
expect(container.querySelectorAll('.chakra-skeleton').length).toBeGreaterThan(0);
});
});
23 changes: 23 additions & 0 deletions src/components/CitationExporter/components/ExportSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Grid, GridItem, Skeleton, Stack } from '@chakra-ui/react';
import { ReactElement } from 'react';
import { ExportContainer } from './ExportContainer';

export const ExportSkeleton = (): ReactElement => {
return (
<ExportContainer header={<Skeleton height="20px" width="240px" />} isLoading>
<Grid templateColumns={{ base: 'auto', md: 'repeat(2, 1fr)' }} gap={4}>
<GridItem>
<Stack spacing={4}>
<Skeleton height="36px" />
<Skeleton height="36px" />
<Skeleton height="36px" />
<Skeleton height="40px" />
</Stack>
</GridItem>
<GridItem>
<Skeleton height="240px" />
</GridItem>
</Grid>
</ExportContainer>
);
};
99 changes: 24 additions & 75 deletions src/pages/search/exportcitation/[format].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Alert, AlertIcon, Box, Flex, Heading, HStack } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';

import { getExportCitationDefaultContext } from '@/components/CitationExporter/CitationExporter.machine';
import { APP_DEFAULTS, BRAND_NAME_FULL } from '@/config';
import { useIsClient } from '@/lib/useIsClient';
import axios from 'axios';
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
Expand All @@ -15,14 +13,15 @@ import { useSettings } from '@/lib/useSettings';
import { logger } from '@/logger';
import { SimpleLink } from '@/components/SimpleLink';
import { CitationExporter } from '@/components/CitationExporter';
import { ExportSkeleton } from '@/components/CitationExporter/components/ExportSkeleton';
import { JournalFormatMap } from '@/components/Settings';
import { parseQueryFromUrl } from '@/utils/common/search';
import { unwrapStringValue } from '@/utils/common/formatters';
import { parseAPIError } from '@/utils/common/parseAPIError';
import { ExportApiFormatKey } from '@/api/export/types';
import { IADSApiSearchParams } from '@/api/search/types';
import { fetchSearchInfinite, searchKeys, useSearchInfinite } from '@/api/search/search';
import { exportCitationKeys, fetchExportCitation, fetchExportFormats } from '@/api/export/export';
import { useSearchInfinite } from '@/api/search/search';
import { exportCitationKeys, fetchExportFormats } from '@/api/export/export';

interface IExportCitationPageProps {
format: string;
Expand All @@ -35,10 +34,9 @@ interface IExportCitationPageProps {
}
Comment on lines 26 to 34

const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
const { format, query, referrer } = props;
const isClient = useIsClient();
const { format, query, referrer, error } = props;
const router = useRouter();

// get export related user settings
const { settings } = useSettings({
suspense: false,
});
Expand All @@ -58,22 +56,18 @@ const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
maxauthor: parseInt(settings.bibtexMaxAuthors),
};

const router = useRouter();
const { data, fetchNextPage, hasNextPage, error } = useSearchInfinite(query);

// TODO: add more error handling here
if (!data) {
return null;
}
const { data, fetchNextPage, hasNextPage, isLoading, error: searchError } = useSearchInfinite(query);

const res = last(data?.pages).response;
const records = res.docs.map((d) => d.bibcode);
const numFound = res.numFound;
const lastPage = data ? last(data.pages) : null;
const records = lastPage ? lastPage.response.docs.map((d) => d.bibcode) : [];
const numFound = lastPage ? lastPage.response.numFound : 0;

const handleNextPage = () => {
void fetchNextPage();
};

const errorMessage = error?.message ?? (searchError instanceof Error ? searchError.message : undefined);

return (
<>
<Head>
Expand All @@ -96,12 +90,14 @@ const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
</Heading>
</HStack>
<Box pt="1">
{error ? (
{errorMessage ? (
<Alert status="error">
<AlertIcon />
{error.message}
{errorMessage}
</Alert>
) : isClient ? (
) : isLoading || !data ? (
<ExportSkeleton />
) : (
<CitationExporter
initialFormat={format}
keyformat={keyformat}
Expand All @@ -115,13 +111,6 @@ const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
page={data.pages.length - 1}
sort={query.sort}
/>
) : (
<CitationExporter.Static
initialFormat={format}
records={records}
totalRecords={numFound}
sort={query.sort}
/>
)}
</Box>
</Flex>
Expand All @@ -131,83 +120,43 @@ const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx) => {
const {
qid = null,
p,
referrer = null,
...query
} = parseQueryFromUrl<{ qid: string; format: string }>(ctx.req.url, { sortPostfix: 'id asc' });

const { format } = ctx.params as { format: string };

if (!query && !qid) {
return {
props: {
format,
query,
qid,
referrer,
error: 'No Records',
},
};
}

const queryClient = new QueryClient();
const params: IADSApiSearchParams = {
const searchParams: IADSApiSearchParams = {
rows: APP_DEFAULTS.EXPORT_PAGE_SIZE,
fl: ['bibcode'],
sort: query.sort ?? APP_DEFAULTS.SORT,
...(qid ? { q: `docs(${qid})` } : query),
};
Comment on lines 120 to 134

try {
// primary search, this is based on query params
const data = await queryClient.fetchInfiniteQuery({
queryKey: searchKeys.infinite(params),
queryFn: fetchSearchInfinite,
meta: { params },
});
const queryClient = new QueryClient();

try {
const formatsData = await queryClient.fetchQuery({
queryKey: exportCitationKeys.manifest(),
queryFn: fetchExportFormats,
});

const formats = map(prop('route'), formatsData).map((r) => r.substring(1));

// extract bibcodes to use for export
const records = data.pages[0].response.docs.map((d) => d.bibcode);

const { params: exportParams } = getExportCitationDefaultContext({
format: formats.includes(format) ? format : ExportApiFormatKey.bibtex,
records,
singleMode: false,
sort: params.sort,
});

// fetch export string, format is pulled from the url
void (await queryClient.prefetchQuery({
queryKey: exportCitationKeys.primary(exportParams),
queryFn: fetchExportCitation,
meta: { params: exportParams },
}));

// react-query infinite queries cannot be serialized by next, currently.
// see https://github.com/tannerlinsley/react-query/issues/3301#issuecomment-1041374043

const dehydratedState = JSON.parse(JSON.stringify(dehydrate(queryClient)));
const resolvedFormat = formats.includes(format) ? format : ExportApiFormatKey.bibtex;

return {
props: {
format: exportParams.format,
query: params,
format: resolvedFormat,
query: searchParams,
referrer,
dehydratedState,
dehydratedState: dehydrate(queryClient),
},
};
} catch (error) {
logger.error({ msg: 'GSSP error in export citation page', error });
return {
props: {
query: params,
query: searchParams,
pageError: parseAPIError(error),
error: axios.isAxiosError(error) ? error.message : 'Unable to fetch data',
},
Expand Down
Loading