Skip to content

Commit

Permalink
Merge pull request #3091 from PenghaiZhang/feature/navigate-attachmen…
Browse files Browse the repository at this point in the history
…ts-in-gallery

Feature/navigate attachments in gallery
  • Loading branch information
PenghaiZhang authored Jun 15, 2021
2 parents 05204a6 + e522403 commit 2ffd87c
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 75 deletions.
12 changes: 12 additions & 0 deletions react-front-end/__mocks__/GallerySearchModule.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/fe79c485-a6dd-4743-81e8-52de66494633/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78771d6937",
mimeType: "image/jpeg",
name: "Kelpie1.jpg",
thumbnailSmall:
Expand All @@ -497,6 +498,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/40e879db-393b-4256-bfe2-9a78771d6937/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78771d6932",
mimeType: "image/jpeg",
name: "Kelpie2.jpg",
thumbnailSmall:
Expand All @@ -507,6 +509,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
},
additionalEntries: [
{
id: "40e879db-393b-4256-bfe2-9a78771d6933",
mimeType: "image/jpeg",
name: "Kelpie1.jpg",
thumbnailSmall:
Expand All @@ -528,6 +531,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/8d25bfcc-f877-4cb6-84cd-391a79c7c67a/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78771d6930",
mimeType: "image/jpeg",
name: "Wilkie Collins.jpg",
thumbnailSmall:
Expand All @@ -539,6 +543,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
},
additionalEntries: [
{
id: "40e879db-393b-4256-bfe2-9a78771d6939",
mimeType: "image/jpeg",
name: "Dickens.jpg",
thumbnailSmall:
Expand All @@ -548,6 +553,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
directUrl: "file/8d25bfcc-f877-4cb6-84cd-391a79c7c67a/1/Dickens.jpg",
},
{
id: "40e879db-393b-4256-bfe2-9a78771d6938",
mimeType: "image/jpeg",
name: "Conrad.jpg",
thumbnailSmall:
Expand All @@ -557,6 +563,7 @@ export const transformedBasicImageSearchResponse: OEQ.Search.SearchResult<Galler
directUrl: "file/8d25bfcc-f877-4cb6-84cd-391a79c7c67a/1/Conrad.jpg",
},
{
id: "40e879db-393b-4256-bfe2-9a78771d6922",
mimeType: "image/jpeg",
name: "Eliot.jpg",
thumbnailSmall:
Expand Down Expand Up @@ -587,6 +594,7 @@ export const transformedBasicVideoSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/de8fcb0b-0b1c-4c34-9173-a83d1b0be6b5/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78771d6117",
mimeType: "openequella/youtube",
name: "6 Tips For Caring for African Violets",
thumbnailSmall: "https://i.ytimg.com/vi/9VCudo90K5I/default.jpg",
Expand All @@ -606,6 +614,7 @@ export const transformedBasicVideoSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/59139c45-788b-4200-a9cb-e4a39e76ad35/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78771d1937",
mimeType: "video/mp4",
name: "Quokka-2021-03-24_14.42.37.mp4",
thumbnailSmall:
Expand All @@ -628,6 +637,7 @@ export const transformedBasicVideoSearchResponse: OEQ.Search.SearchResult<Galler
"http://localhost:8080/ian/api/item/9d5112d4-87b6-4ac1-b773-ceaa4a6c5205/1/",
},
mainEntry: {
id: "40e879db-393b-4256-bfe2-9a78773d6937",
mimeType: "openequella/youtube",
name:
"These Simple Words Will Help You Through Life's Most Difficult Situations | Ryan Holiday",
Expand All @@ -637,13 +647,15 @@ export const transformedBasicVideoSearchResponse: OEQ.Search.SearchResult<Galler
},
additionalEntries: [
{
id: "40e879db-393b-4256-bfe2-9a78771d6237",
mimeType: "openequella/youtube",
name: "Stoicism and the Art of Resilience | Ryan Holiday | Epictetus",
thumbnailSmall: "https://i.ytimg.com/vi/6-UQYo1YabY/default.jpg",
thumbnailLarge: "https://i.ytimg.com/vi/6-UQYo1YabY/hqdefault.jpg",
directUrl: "https://www.youtube.com/watch?v=6-UQYo1YabY",
},
{
id: "40e879db-393b-4256-bfe2-9a78771d5937",
mimeType: "openequella/youtube",
name:
"Stoicism's Simple Secret To Being Happier | Ryan Holiday | Daily Stoic",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
common: {
action: { openInNewWindow },
},
lightboxComponent: { viewNext, viewPrevious },
} = languageStrings;

/*
Expand All @@ -45,7 +46,7 @@ jest.mock("react-router", () => ({

describe("<GallerySearchResult />", () => {
const renderGallery = () =>
render(<GallerySearchResult items={buildItems(5)} />);
render(<GallerySearchResult items={buildItems(5)} />); // 16 entries in total.

it("displays the lightbox when the image is clicked on", () => {
const { getAllByLabelText, queryAllByLabelText } = renderGallery();
Expand All @@ -62,4 +63,24 @@ describe("<GallerySearchResult />", () => {
expect(mockUseHistoryPush).toHaveBeenCalled();
expect(mockUseHistoryPush.mock.calls[0][0].match("/items/")).toBeTruthy();
});

describe("viewing gallery entries in a loop", () => {
it.each([
["first", "next", "last", 15, viewNext],
["last", "previous", "first", 0, viewPrevious],
])(
"shows the %s entry when navigate to the %s entry of the %s entry",
(
currentPosition: string,
direction: string,
newPosition: string,
index: number,
arrowButtonLabel: string
) => {
const { getAllByLabelText, queryByLabelText } = renderGallery();
userEvent.click(getAllByLabelText(ariaLabel)[index]);
expect(queryByLabelText(arrowButtonLabel)).toBeInTheDocument();
}
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from "../../../../tsrc/modules/GallerySearchModule";

const buildGalleryEntry = (name: string): GalleryEntry => ({
id: "40e879db-393b-4256-bfe2-9a78771d6437",
mimeType: "image/png",
name,
thumbnailSmall: "./placeholder-135x135.png",
Expand Down
33 changes: 11 additions & 22 deletions react-front-end/tsrc/components/Lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,7 @@ import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import { pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as React from "react";
import {
ReactNode,
SyntheticEvent,
useCallback,
useEffect,
useState,
} from "react";
import { ReactNode, SyntheticEvent, useEffect, useState } from "react";
import { Literal, match, Unknown } from "runtypes";
import {
CustomMimeTypes,
Expand Down Expand Up @@ -140,22 +134,17 @@ const Lightbox = ({ open, onClose, config }: LightboxProps) => {
const [lightBoxConfig, setLightBoxConfig] = useState<LightboxConfig>(config);
const { src, title, mimeType, onPrevious, onNext } = lightBoxConfig;

const handleOnPrevious = useCallback(() => {
setContent(undefined);
onPrevious && setLightBoxConfig(onPrevious());
}, [onPrevious]);

const handleOnNext = useCallback(() => {
const handleNav = (getLightboxConfig: () => LightboxConfig) => {
setContent(undefined);
onNext && setLightBoxConfig(onNext());
}, [onNext]);
setLightBoxConfig(getLightboxConfig());
};

useEffect(() => {
const keyDownHandler = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
handleOnPrevious();
} else if (e.key === "ArrowRight") {
handleOnNext();
if (onPrevious && e.key === "ArrowLeft") {
handleNav(onPrevious);
} else if (onNext && e.key === "ArrowRight") {
handleNav(onNext);
} else if (e.key === "Escape") {
onClose();
}
Expand All @@ -164,7 +153,7 @@ const Lightbox = ({ open, onClose, config }: LightboxProps) => {
return () => {
window.removeEventListener("keydown", keyDownHandler);
};
}, [handleOnPrevious, handleOnNext, onClose]);
}, [onPrevious, onNext, onClose]);

// Update content when config is updated.
useEffect(() => {
Expand Down Expand Up @@ -298,7 +287,7 @@ const Lightbox = ({ open, onClose, config }: LightboxProps) => {
title={viewPreviousString}
onClick={(e) => {
e.stopPropagation();
handleOnPrevious();
handleNav(onPrevious);
}}
>
<NavigateBeforeIcon className={classes.arrowButton} />
Expand All @@ -315,7 +304,7 @@ const Lightbox = ({ open, onClose, config }: LightboxProps) => {
title={viewNextString}
onClick={(e) => {
e.stopPropagation();
handleOnNext();
handleNav(onNext);
}}
>
<NavigateNextIcon className={classes.arrowButton} />
Expand Down
5 changes: 5 additions & 0 deletions react-front-end/tsrc/modules/GallerySearchModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import * as yt from "./YouTubeModule";
* the asset and the associated MIME Type.
*/
export interface GalleryEntry {
/**
* The ID of the attachment this points to.
*/
id: string;
/**
* The MIME type for the attachment being represented. Note however that it really only matches
* the MIME type for `imagePathFull` - as thumbnails generated for small and medium seem to mainly
Expand Down Expand Up @@ -243,6 +247,7 @@ export const buildGalleryEntry = (
Do(E.either)
.do(validateAttachmentType(attachment))
.do(validateThumbnailRequirements(attachment))
.bind("id", E.right(attachment.id))
.bind("mimeType", mimeType(attachment))
.bind("name", E.right(attachment.description ?? attachment.id))
.bind("thumbnailSmall", thumbnailLink(attachment, "small"))
Expand Down
83 changes: 54 additions & 29 deletions react-front-end/tsrc/modules/ViewerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ export interface ViewerLightboxConfig {
config: LightboxConfig;
}

/**
* Represent either an Attachment or a GalleryEntry to be displayed in the Lightbox.
*/
export interface LightboxEntry {
/** ID of the entry. */
id: string;
/** URL for the item to display in the Lightbox. */
src: string;
/** Title of the entry displayed at the top of the Lightbox. */
title?: string;
/** MIME type of the entry specified by `src` */
mimeType: string;
}

export type ViewerConfig = ViewerLinkConfig | ViewerLightboxConfig;

export const isViewerLightboxConfig = (
Expand Down Expand Up @@ -187,39 +201,50 @@ export const getViewerDefinitionForAttachment = (
};

/**
* Build a function to handler navigation between Lightbox attachments.
* @param lightboxAttachments All attachments that can be viewed in Lightbox and their Viewer definitions.
* @param attachmentIndex Index of the attachment to be viewed
* Build a function to handle navigation between Lightbox entries.
* @param entries A list of Attachment or GalleryEntry that can be viewed in Lightbox.
* @param entryIndex Index of the current lightbox entry.
* @param isLoopBack `true` to make the navigation looping.
*/
export const buildLightboxNavigationHandler = (
lightboxAttachments: AttachmentAndViewerDefinition[],
attachmentIndex: number
): (() => LightboxConfig) | undefined =>
pipe(
lightboxAttachments,
A.lookup(attachmentIndex),
entries: LightboxEntry[],
entryIndex: number,
isLoopBack = false
): (() => LightboxConfig) | undefined => {
const buildIndexForLoop = (index: number, start: number, end: number) => {
if (index < start) {
return end;
} else if (index > end) {
return start;
}
return index;
};
const index = isLoopBack
? buildIndexForLoop(entryIndex, 0, entries.length - 1)
: entryIndex;

return pipe(
entries,
A.lookup(index),
O.fold(
() => undefined,
({
attachment: { description, mimeType },
viewerDefinition: [viewer, viewUrl],
}) => {
if (viewer === "lightbox") {
return () => ({
src: viewUrl,
title: description,
mimeType: mimeType ?? "",
onNext: buildLightboxNavigationHandler(
lightboxAttachments,
attachmentIndex + 1
),
onPrevious: buildLightboxNavigationHandler(
lightboxAttachments,
attachmentIndex - 1
),
});
}
throw new TypeError(`Unexpected viewer configuration: ${viewer}`);
({ title, mimeType, src }) => {
return () => ({
src,
title,
mimeType: mimeType ?? "",
onNext: buildLightboxNavigationHandler(
entries,
index + 1,
isLoopBack
),
onPrevious: buildLightboxNavigationHandler(
entries,
index - 1,
isLoopBack
),
});
}
)
);
};
Loading

0 comments on commit 2ffd87c

Please sign in to comment.