Skip to content

Commit

Permalink
feat(app): support page parameter (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
danadajian authored Jun 9, 2023
1 parent 8b4ddbe commit 677c65b
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 87 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'no-console': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'react/jsx-filename-extension': ['warn', { extensions: ['.tsx'] }],
'no-shadow': 'off',
Expand Down
10 changes: 9 additions & 1 deletion app/backend/src/fetchCurrentPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import { getBase64StringFromS3 } from './getBase64StringFromS3';
import { FetchCurrentPageInput } from './schema';
import { parse } from 'path';
import { getGroupedKeys } from './getGroupedKeys';
import { TRPCError } from '@trpc/server';

export const fetchCurrentPage = async ({ hash, bucket, page }: FetchCurrentPageInput) => {
const paginatedKeys = await getGroupedKeys(hash, bucket);
const { keys, title } = paginatedKeys[page - 1];
const currentPage = paginatedKeys[page - 1];
if (!currentPage) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Page ${page} does not exist. Only ${paginatedKeys.length} pages were found.`
});
}
const { keys, title } = currentPage;
const images = await Promise.all(
keys.map(async key => ({
name: parse(key).name,
Expand Down
10 changes: 10 additions & 0 deletions app/backend/test/fetchCurrentPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,14 @@ describe('fetchCurrentPage', () => {
nextPage: undefined
});
});

it('should throw when page is not found', async () => {
await expect(() =>
fetchCurrentPage({
hash: 'hash',
bucket: 'bucket',
page: 12
})
).rejects.toThrow('Page 12 does not exist. Only 2 pages were found.');
});
});
12 changes: 4 additions & 8 deletions app/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import * as React from 'react';
import './App.css';
import { MainPage } from './components/main-page';
import { ClientProvider } from './providers/client-provider';
import { QueryParamProvider } from 'use-query-params';
import { WindowHistoryAdapter } from 'use-query-params/adapters/window';
import { BaseImageStateProvider } from './providers/base-image-state-provider';

function App({ queryParamAdapter = WindowHistoryAdapter }) {
function App() {
return (
<ClientProvider>
<QueryParamProvider adapter={queryParamAdapter}>
<BaseImageStateProvider>
<MainPage />
</BaseImageStateProvider>
</QueryParamProvider>
<BaseImageStateProvider>
<MainPage />
</BaseImageStateProvider>
</ClientProvider>
);
}
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/components/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type ButtonProps = {
const commonStyles = 'uppercase rounded-md px-4 py-2 font-medium';

const getButton = (props: ButtonProps, styles: string) => {
const { className: extraStyles, ...rest } = props;
const { className: extraStyles, backgroundFilled, ...rest } = props;

return (
<button className={`${commonStyles} ${styles} ${extraStyles}`} {...rest}>
Expand Down
32 changes: 19 additions & 13 deletions app/frontend/components/main-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@ import { ViewToggle, ViewType } from './view-toggle';
import { UpdateImagesButton } from './update-images-button';
import { SideBySideImageView, SingleImageView } from './image-views';
import { RouterOutput, trpc } from '../utils/trpc';
import { useQueryParams } from 'use-query-params';
import { URL_PARAMS } from '../constants';
import { createSearchParams, useNavigate, useSearchParams } from 'react-router-dom';
import { ArrowBackIcon, ArrowForwardIcon } from './arrows';

export const MainPage = () => {
const [{ hash, bucket }] = useQueryParams(URL_PARAMS);

const [specIndex, setSpecIndex] = React.useState(0);
const [viewType, setViewType] = React.useState<ViewType | undefined>();
const [singleImageViewIndex, setSingleImageViewIndex] = React.useState(0);

const [searchParams] = useSearchParams();
const params: Record<string, string | undefined> = Object.fromEntries(searchParams.entries());
const { hash, bucket, page: pageParam } = params;
if (!hash || !bucket) {
return <LandingPage />;
}

const page = specIndex + 1;
const page = Number(pageParam ?? 1);
const { isLoading, data, isFetching, refetch, error } = trpc.fetchCurrentPage.useQuery({ hash, bucket, page });

const nextPageExists = Boolean(data?.nextPage);

const navigate = useNavigate();
const utils = trpc.useContext();
if (data) {
if (nextPageExists) {
utils.fetchCurrentPage.prefetch({ hash, bucket, page: page + 1 });
}

Expand All @@ -45,12 +47,18 @@ export const MainPage = () => {
}

const onClickBackArrow = () => {
setSpecIndex(specIndex - 1);
navigate({
pathname: '/',
search: `?${createSearchParams({ ...params, page: String(page - 1) })}`
});
refetch();
};

const onClickForwardArrow = () => {
setSpecIndex(specIndex + 1);
navigate({
pathname: '/',
search: `?${createSearchParams({ ...params, page: String(page + 1) })}`
});
refetch();
};

Expand All @@ -67,14 +75,12 @@ export const MainPage = () => {
return <div className="mt-8">{imageView}</div>;
};

const nextPageExists = Boolean(data.nextPage);

return (
<>
<div className="mt-10 flex flex-col items-center justify-center">
<div className="flex w-4/5 items-center justify-between">
<button disabled={specIndex <= 0} onClick={onClickBackArrow} aria-label="back-arrow">
<ArrowBackIcon disabled={specIndex <= 0} />
<button disabled={page <= 1} onClick={onClickBackArrow} aria-label="back-arrow">
<ArrowBackIcon disabled={page <= 1} />
</button>
<h1 className="text-center text-4xl font-medium">{data.title}</h1>
<button disabled={!nextPageExists} onClick={onClickForwardArrow} aria-label="forward-arrow">
Expand Down
7 changes: 4 additions & 3 deletions app/frontend/components/update-images-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ import { Fragment, useContext, useState } from 'react';
import { Error } from './error';
import { BaseImageStateContext, UpdateBaseImagesText } from '../providers/base-image-state-provider';
import { trpc } from '../utils/trpc';
import { useQueryParams } from 'use-query-params';
import { URL_PARAMS } from '../constants';
import { Dialog, Transition } from '@headlessui/react';
import { PrimaryButton, TertiaryButton } from './buttons';
import { useSearchParams } from 'react-router-dom';

const UPDATE_TEXT =
'WARNING: This will update the base images in S3 and will set the visual regression status to passed. You can only do this if you are about to merge your PR and all other checks have passed.';

export const UpdateImagesButton = () => {
const [{ hash, bucket, repo, owner, baseImagesDirectory }] = useQueryParams(URL_PARAMS);
const [dialogIsOpen, setDialogIsOpen] = useState(false);
const { baseImageState, setBaseImageState } = useContext(BaseImageStateContext);

const { error: updateBaseImagesError, mutateAsync: updateBaseImages } = trpc.updateBaseImages.useMutation();
const { error: updateCommitStatusError, mutateAsync: updateCommitStatus } = trpc.updateCommitStatus.useMutation();

const [searchParams] = useSearchParams();
const params: Record<string, string | undefined> = Object.fromEntries(searchParams.entries());
const { hash, bucket, repo, owner, baseImagesDirectory } = params;
if (!hash || !bucket || !owner || !repo) {
return null;
}
Expand Down
9 changes: 0 additions & 9 deletions app/frontend/constants.ts

This file was deleted.

32 changes: 24 additions & 8 deletions app/frontend/cypress/component/App.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import * as React from 'react';
import { makeMockAdapter } from '../utils/makeMockAdapter';
import App from '../../App';
import { UPDATE_BASE_IMAGES_ERROR_MESSAGE } from 'shared';
import { firstPage, noNewImagesPage, onlyNewImagesFirstPage, onlyNewImagesSecondPage, secondPage } from '../mocks/pages';
import { CyHttpMessages } from 'cypress/types/net-stubbing';
import { baseImageUpdateRejection } from '../mocks/base-image-update-rejection';
import { mutationResponse } from '../mocks/mutation';
import { MemoryRouter } from 'react-router-dom';

const getPageFromRequest = (req: CyHttpMessages.IncomingHttpRequest) => JSON.parse(req.query.input as string)['0'].page;

describe('App', () => {
describe('homepage', () => {
it('should redirect to homepage when parameters are omitted', () => {
cy.mount(<App queryParamAdapter={makeMockAdapter({ search: '' })} />);
cy.mount(
<MemoryRouter initialEntries={['/']}>
<App />
</MemoryRouter>
);
cy.findByText(/Welcome to Comparadise/);
});
});
Expand All @@ -21,12 +25,16 @@ describe('App', () => {
beforeEach(() => {
cy.intercept('/trpc/fetchCurrentPage*', req => {
const page = getPageFromRequest(req);
const body = page === 1 ? firstPage : secondPage;
const body = page === 2 ? secondPage : firstPage;
req.reply(body);
});
cy.intercept('/trpc/updateBaseImages*', { body: mutationResponse }).as('base-images');
cy.intercept('/trpc/updateCommitStatus*', { body: mutationResponse }).as('commit-status');
cy.mount(<App queryParamAdapter={makeMockAdapter({ search: '?hash=123&bucket=bucket&repo=repo&owner=owner' })} />);
cy.mount(
<MemoryRouter initialEntries={['/?hash=123&bucket=bucket&repo=repo&owner=owner']}>
<App />
</MemoryRouter>
);
});

it('should default to the base image view of the first spec in the response list', () => {
Expand Down Expand Up @@ -118,10 +126,14 @@ describe('App', () => {
beforeEach(() => {
cy.intercept('/trpc/fetchCurrentPage*', req => {
const page = getPageFromRequest(req);
const body = page === 1 ? onlyNewImagesFirstPage : onlyNewImagesSecondPage;
const body = page === 2 ? onlyNewImagesSecondPage : onlyNewImagesFirstPage;
req.reply(body);
});
cy.mount(<App queryParamAdapter={makeMockAdapter({ search: '?hash=123&bucket=bucket&repo=repo&owner=owner' })} />);
cy.mount(
<MemoryRouter initialEntries={['?hash=123&bucket=bucket&repo=repo&owner=owner']}>
<App />
</MemoryRouter>
);
});

it('should display the new image with side-by-side view disabled', () => {
Expand All @@ -142,10 +154,14 @@ describe('App', () => {
beforeEach(() => {
cy.intercept('/trpc/fetchCurrentPage*', req => {
const page = getPageFromRequest(req);
const body = page === 1 ? firstPage : noNewImagesPage;
const body = page === 2 ? noNewImagesPage : firstPage;
req.reply(body);
});
cy.mount(<App queryParamAdapter={makeMockAdapter({ search: '?hash=123&bucket=bucket&repo=repo&owner=owner' })} />);
cy.mount(
<MemoryRouter initialEntries={['?hash=123&bucket=bucket&repo=repo&owner=owner']}>
<App />
</MemoryRouter>
);
});

it('should default to base when no new image was found and the currently selected image is new', () => {
Expand Down
16 changes: 0 additions & 16 deletions app/frontend/cypress/utils/makeMockAdapter.ts

This file was deleted.

10 changes: 9 additions & 1 deletion app/frontend/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
{
path: '/',
element: <App />
}
]);

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
<RouterProvider router={router} />
</React.StrictMode>
);
4 changes: 2 additions & 2 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
"dependencies": {
"@headlessui/react": "1.7.14",
"@tanstack/react-query": "4.29.12",
"@testing-library/cypress": "9.0.0",
"@trpc/client": "10.18.0",
"@trpc/react-query": "10.18.0",
"@trpc/server": "10.18.0",
"@testing-library/cypress": "9.0.0",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.8",
"@types/testing-library__cypress": "5.0.9",
"@vitejs/plugin-react-swc": "3.3.2",
"prettier-plugin-tailwindcss": "0.3.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "6.12.1",
"shared": "workspace:*",
"tailwindcss": "3.3.2",
"use-query-params": "2.2.1",
"vite": "4.3.9"
},
"scripts": {
Expand Down
Loading

0 comments on commit 677c65b

Please sign in to comment.