From 6ffefe7d8d4584ca9dd95bb4fc64478e5c6a70e6 Mon Sep 17 00:00:00 2001 From: Wilson Chua <wwc4618@ic.ac.uk> Date: Thu, 20 Aug 2020 17:16:49 +0800 Subject: [PATCH] Create new list view for ModuleResources, and related subcomponents. Connect more Materials resource download endpoints --- src/assets/scss/global.scss | 23 +++++ .../atoms/FileCard/style.module.scss | 22 +---- .../molecules/CategoryList/index.tsx | 82 ++++++++++++++++ .../molecules/CategoryList/style.module.scss | 1 + .../components/CurrentDirectoryView.tsx | 35 +------ .../components/FoldersView.tsx | 14 +-- .../ModuleResources/components/ListView.tsx | 47 +++++++++ .../pages/ModuleResources/index.tsx | 96 +++++++++++++++++-- 8 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 src/assets/scss/global.scss create mode 100644 src/components/molecules/CategoryList/index.tsx create mode 100644 src/components/molecules/CategoryList/style.module.scss create mode 100644 src/components/pages/ModuleResources/components/ListView.tsx diff --git a/src/assets/scss/global.scss b/src/assets/scss/global.scss new file mode 100644 index 000000000..2f9b6ca2b --- /dev/null +++ b/src/assets/scss/global.scss @@ -0,0 +1,23 @@ +@import "./custom"; + +// Tags styling +$blue-tag-background: transparentize($blue-100, 0.5); +$teal-tag-background: transparentize($teal-100, 0.5); + +.fileTag { + text-transform: uppercase; + font-size: 0.8rem; + font-weight: 500; + border-radius: 0.33rem; + margin-right: 0.5rem; +} + +.tagBlue { + color: $blue-700; + background: $blue-tag-background; +} + +.tagTeal { + color: $teal-700; + background: $teal-tag-background; +} \ No newline at end of file diff --git a/src/components/atoms/FileCard/style.module.scss b/src/components/atoms/FileCard/style.module.scss index e2e487a06..07cad1752 100644 --- a/src/components/atoms/FileCard/style.module.scss +++ b/src/components/atoms/FileCard/style.module.scss @@ -1,4 +1,5 @@ @import "assets/scss/custom"; +@import "assets/scss/global"; .fileCard { border-radius: 0.5rem; @@ -47,27 +48,6 @@ border-top-right-radius: 0.4rem; } -$blue-tag-background: transparentize($blue-100, 0.5); -$teal-tag-background: transparentize($teal-100, 0.5); - -.fileTag { - text-transform: uppercase; - font-size: 0.8rem; - font-weight: 500; - border-radius: 0.33rem; - margin-right: 0.5rem; -} - -.tagBlue { - color: $blue-700; - background: $blue-tag-background; -} - -.tagTeal { - color: $teal-700; - background: $teal-tag-background; -} - .fileRow { scrollbar-width: thin; scrollbar-color: $white $white; diff --git a/src/components/molecules/CategoryList/index.tsx b/src/components/molecules/CategoryList/index.tsx new file mode 100644 index 000000000..d24a67b84 --- /dev/null +++ b/src/components/molecules/CategoryList/index.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import styles from "./style.module.scss"; +import classNames from "classnames"; + +import Row from "react-bootstrap/esm/Row"; +import Col from "react-bootstrap/esm/Col"; +import Badge from "react-bootstrap/Badge"; +import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; +import { + faFileAlt, + IconDefinition, + faFilePdf, + faFileVideo, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { SelectionProps } from "components/molecules/SelectionView"; + +const CategoryList: React.FC<{ select: SelectionProps }> = ({ + select, +}) => { + return ( + <> + {select.selectionItems.map(({ title, type, tags, id }) => { + let normalIcon: IconDefinition; + if (type === undefined || tags === undefined) return null; + switch (type) { + case "pdf": + normalIcon = faFilePdf; + break; + case "video": + normalIcon = faFileVideo; + break; + default: + normalIcon = faFileAlt; + break; + } + + let icon = select.isAnySelected() || select.state.isHoveringOver[id] + ? select.state.isSelected[id] + ? faCheckSquare + : faSquare + : normalIcon; + + return ( + <Row + style={{ marginTop: "10px", marginLeft: "-10px", marginRight: "-10px", cursor: "pointer" }} + onClick={() => select.handleCardClick(id)} + onMouseOver={() => select.handleMouseOver(id)} + onMouseOut={() => select.handleMouseOut(id)} + > + <Col>{title}</Col> + <Col md="auto"> + {tags.map((tag) => ( + <Badge + pill + key={tag} + className={classNames( + styles.fileTag, + tag === "new" ? styles.tagTeal : styles.tagBlue + )}> + {tag} + </Badge> + ))} + </Col> + <Col md="auto"> + <FontAwesomeIcon + style={{ marginLeft: "8px", fontSize: "1.125rem", cursor: "default"}} + icon={icon} + onClick={(e) => { + e.stopPropagation(); + select.handleIconClick(id); + }} + /> + </Col> + </Row> + ); + })} + </> + ); +}; + +export default CategoryList; diff --git a/src/components/molecules/CategoryList/style.module.scss b/src/components/molecules/CategoryList/style.module.scss new file mode 100644 index 000000000..d098ba485 --- /dev/null +++ b/src/components/molecules/CategoryList/style.module.scss @@ -0,0 +1 @@ +@import "assets/scss/global"; \ No newline at end of file diff --git a/src/components/pages/ModuleResources/components/CurrentDirectoryView.tsx b/src/components/pages/ModuleResources/components/CurrentDirectoryView.tsx index 5e4f35fde..b8d1c5e02 100644 --- a/src/components/pages/ModuleResources/components/CurrentDirectoryView.tsx +++ b/src/components/pages/ModuleResources/components/CurrentDirectoryView.tsx @@ -11,39 +11,7 @@ export interface CurrentDirectoryViewProps { searchText: string; onDownloadClick: (identifiers: number[]) => void; onItemClick: (identifier: number) => void; -} - -function includeInSearchResult(item: Resource, searchText: string) { - let rx = /([a-z]+)\(([^)]+)\)/gi; - let match: RegExpExecArray | null; - - let title = item.title.toLowerCase(); - let tags = item.tags.map((tag) => tag.toLowerCase()); - let type = item.type.toLowerCase(); - - while ((match = rx.exec(searchText)) !== null) { - - switch (match[1]) { - case "type": - if (type !== match[2]) { - return false; - } - break; - case "tag": - let matchSafe = match as RegExpExecArray; - if (!tags.some((tag) => tag === matchSafe[2])) { - return false; - } - break; - default: - break; - } - } - let rest = searchText.replace(rx, "").trim(); - if (tags.some((tag) => tag.indexOf(rest) !== -1)) { - return true; - } - return title.indexOf(rest) !== -1; + includeInSearchResult: (item: Resource, searchText: string) => boolean; } const CurrentDirectoryView: React.FC<CurrentDirectoryViewProps> = ({ @@ -52,6 +20,7 @@ const CurrentDirectoryView: React.FC<CurrentDirectoryViewProps> = ({ searchText, onDownloadClick, onItemClick, + includeInSearchResult }) => { let filesContent: Resource[] = resources; if (scope !== "") { diff --git a/src/components/pages/ModuleResources/components/FoldersView.tsx b/src/components/pages/ModuleResources/components/FoldersView.tsx index 691f2f3af..44dfdb879 100644 --- a/src/components/pages/ModuleResources/components/FoldersView.tsx +++ b/src/components/pages/ModuleResources/components/FoldersView.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Resource } from "../index"; import SelectionView, { SelectionProps, } from "components/molecules/SelectionView"; @@ -7,24 +6,21 @@ import FoldersRow from "components/molecules/FoldersRow"; import { useHistory, useLocation } from "react-router-dom"; export interface FoldersViewProps { - resources: Resource[]; + folders: { + title: string; + id: number; + }[]; scope: string; searchText: string; } const FoldersView: React.FC<FoldersViewProps> = ({ - resources, + folders, scope, searchText, }) => { let history = useHistory(); let location = useLocation(); - let folders: { title: string; id: number }[] = Array.from( - new Set<string>(resources.map((res: Resource) => res.folder)) - ).map((title: string, id: number) => ({ - title: title, - id: id, - })); function handleFolderClick(foldersId: number) { let title = folders.filter(({ id }) => id === foldersId)[0].title; diff --git a/src/components/pages/ModuleResources/components/ListView.tsx b/src/components/pages/ModuleResources/components/ListView.tsx new file mode 100644 index 000000000..67ce2477f --- /dev/null +++ b/src/components/pages/ModuleResources/components/ListView.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Resource } from "../index"; +import SelectionView, { + SelectionProps, +} from "components/molecules/SelectionView"; +import CategoryList from "components/molecules/CategoryList"; + +export interface ListViewProps { + folders: { + title: string; + id: number; + }[]; + resources: Resource[]; + searchText: string; + onDownloadClick: (identifiers: number[]) => void; + onItemClick: (identifier: number) => void; + includeInSearchResult: (item: Resource, searchText: string) => boolean; +} + +const ListView: React.FC<ListViewProps> = ({ + folders, + resources, + searchText, + onDownloadClick, + onItemClick, + includeInSearchResult +}) => { + let filesContent: Resource[] = resources; + if (searchText !== "") { + filesContent = filesContent.filter((item) => + includeInSearchResult(item, searchText.toLowerCase()) + ); + } + return ( + <SelectionView + heading="Files" + onItemClick={onItemClick} + onDownloadClick={onDownloadClick} + selectionItems={filesContent} + render={(select: SelectionProps) => ( + <CategoryList select={select} /> + )} + /> + ); +}; + +export default ListView; \ No newline at end of file diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx index 03b8bc6b8..08551fa72 100644 --- a/src/components/pages/ModuleResources/index.tsx +++ b/src/components/pages/ModuleResources/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Button from "react-bootstrap/Button"; import { request } from "../../../utils/api"; import { api, methods } from "../../../constants/routes"; @@ -7,6 +8,7 @@ import SearchBox from "components/molecules/SearchBox"; import QuickAccessView from "./components/QuickAccessView"; import CurrentDirectoryView from "./components/CurrentDirectoryView"; import FoldersView from "./components/FoldersView"; +import ListView from "./components/ListView"; export interface Resource { title: string; @@ -25,6 +27,7 @@ export interface ResourcesProps { export interface ResourceState { error: any; isLoaded: Boolean; + view: string; resources: Resource[]; searchText: string; } @@ -39,6 +42,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { this.state = { error: null, isLoaded: false, + view: "folder", resources: [], searchText: "", }; @@ -150,6 +154,37 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { ); } + includeInSearchResult(item: Resource, searchText: string) { + let rx = /([a-z]+)\(([^)]+)\)/gi; + let match: RegExpExecArray | null; + let title = item.title.toLowerCase(); + let tags = item.tags.map((tag) => tag.toLowerCase()); + let type = item.type.toLowerCase(); + + while ((match = rx.exec(searchText)) !== null) { + switch (match[1]) { + case "type": + if (type !== match[2]) { + return false; + } + break; + case "tag": + let matchSafe = match as RegExpExecArray; + if (!tags.some((tag) => tag === matchSafe[2])) { + return false; + } + break; + default: + break; + } + } + let rest = searchText.replace(rx, "").trim(); + if (tags.some((tag) => tag.indexOf(rest) !== -1)) { + return true; + } + return title.indexOf(rest) !== -1; + } + getloadedItems() { if (!this.state.isLoaded) return <>Loading...</>; if (this.state.error) @@ -157,19 +192,30 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { return null; } + toggleView() { + if (this.state.view === "folder") { + this.setState({ view: "list" }); + } else { + this.setState({ view: "folder" }); + } + } + render() { let scope = this.props.scope || ""; - return ( - <> - <MyBreadcrumbs /> - <SearchBox - searchText={this.state.searchText} - onSearchTextChange={(text) => this.setState({ searchText: text })} - /> - {this.getloadedItems() || ( + + let folders: { title: string; id: number }[] = Array.from( + new Set<string>(this.state.resources.map((res: Resource) => res.folder)) + ).map((title: string, id: number) => ({ + title: title, + id: id, + })); + + const view = () => { + switch(this.state.view) { + case "folder": return ( <> <FoldersView - resources={this.state.resources} + folders={folders} scope={scope} searchText={this.state.searchText} /> @@ -180,6 +226,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { searchText={this.state.searchText} onDownloadClick={(ids) => this.handleFileDownload(ids)} onItemClick={(id) => this.handleFileClick(id)} + includeInSearchResult={this.includeInSearchResult} /> <QuickAccessView @@ -190,6 +237,37 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { onItemClick={(id) => this.handleFileClick(id)} /> </> + ); + case "list": return ( + <> + <ListView + folders={folders} + resources={this.state.resources} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onItemClick={(id) => this.handleFileClick(id)} + includeInSearchResult={this.includeInSearchResult} + /> + </> + ); + } + } + return ( + <> + <MyBreadcrumbs /> + <SearchBox + searchText={this.state.searchText} + onSearchTextChange={(text) => this.setState({ searchText: text })} + /> + {this.getloadedItems() || ( + <> + { view() } + <Button + variant="secondary" + className="mt-5" + onClick={this.state.isLoaded ? () => this.toggleView() : undefined} + >Toggle view</Button> + </> )} </> ); -- GitLab