diff --git a/src/assets/scss/global.scss b/src/assets/scss/global.scss index 2f9b6ca2b61c5ea7a9fd462cf53b735800d90085..575583219de3847a51fd3faec10f6d18872bb29e 100644 --- a/src/assets/scss/global.scss +++ b/src/assets/scss/global.scss @@ -20,4 +20,52 @@ $teal-tag-background: transparentize($teal-100, 0.5); .tagTeal { color: $teal-700; background: $teal-tag-background; +} + +// Section header with checkbox settings +.sectionHeaderContainer { + display: flex; + justify-content: space-between; + margin-top: 1.875rem; + align-items: center; +} + +.sectionHeader { + font-weight: 500; + font-size: 20px; + text-transform: uppercase; +} + +.sectionHeaderButton { + background-color: $white; + color: $gray-500; + border-width: 0px; + border-radius: 8px; + margin-left: 20px; + justify-content: space-between; + height: 2.25rem; + transition: 0.2s background-color; + -webkit-transition: 0.2s background-color; + -moz-transition: 0.2s background-color; + font-size: 1.05rem; +} + +.buttonIcon { + margin-top: 0.22rem; +} + +.sectionHeaderButton:global(.active), +.sectionHeaderButton:active { + background: $gray-400 !important; + font-weight: 500; + text-align: left; + border-width: 0rem; + height: 2.25rem; + line-height: 1.375rem; +} + +.sectionHeaderButton:hover, .sectionHeaderButton:focus { + background-color: $gray-200; + color: $gray-700 !important; + box-shadow: none !important; } \ No newline at end of file diff --git a/src/components/molecules/CategoryHeader/index.tsx b/src/components/molecules/CategoryHeader/index.tsx index 25082f656fd78f03e580e6e00df29cc7bc123db4..8aa7ac14d5d3e54a9681c76d2337822495ceda02 100644 --- a/src/components/molecules/CategoryHeader/index.tsx +++ b/src/components/molecules/CategoryHeader/index.tsx @@ -3,16 +3,20 @@ import styles from "./style.module.scss"; import Button from "react-bootstrap/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faDownload } from "@fortawesome/free-solid-svg-icons"; +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; export interface CategoryHeaderProps { heading: string; - onDownloadClick: (event: React.MouseEvent) => void; + onSelectAllClick: (event: React.MouseEvent) => void; + selectAllIcon: IconDefinition; + checkBoxColor: string; } const CategoryHeader: React.FC<CategoryHeaderProps> = ({ heading, - onDownloadClick, + onSelectAllClick, + selectAllIcon, + checkBoxColor }: CategoryHeaderProps) => { return ( <> @@ -21,19 +25,17 @@ const CategoryHeader: React.FC<CategoryHeaderProps> = ({ {heading} </span> <div className={styles.sectionHeaderButtonGroup}> - <span id="download-button"> - <Button - variant="secondary" - className={styles.sectionHeaderButton} - onClick={onDownloadClick} - > - <FontAwesomeIcon - className={styles.buttonIcon} - icon={faDownload} - /> - Download Section - </Button> - </span> + <Button + style={{ color: checkBoxColor }} + className={styles.sectionHeaderButton} + onClick={onSelectAllClick} + variant="secondary" + > + <FontAwesomeIcon + className={styles.buttonIcon} + icon={selectAllIcon} + /> + </Button> </div> </div> </> diff --git a/src/components/molecules/CategoryHeader/style.module.scss b/src/components/molecules/CategoryHeader/style.module.scss index 35998888521e09f5abcdd730b4e854d7fcc1df9b..e2a1338b52a22d9c9a0018fc9f01e0dbdac30737 100644 --- a/src/components/molecules/CategoryHeader/style.module.scss +++ b/src/components/molecules/CategoryHeader/style.module.scss @@ -1,48 +1,2 @@ @import "assets/scss/custom"; - -.sectionHeaderContainer { - display: flex; - justify-content: space-between; - margin-top: 1.875rem; - align-items: center; -} - -.sectionHeader { - font-size: 20px; - text-transform: uppercase; -} - -.sectionHeaderButton { - background-color: $white; - color: $gray-500; - border-width: 0px; - border-radius: 8px; - margin-left: 20px; - justify-content: space-between; - height: 2.25rem; - transition: 0.2s background-color; - -webkit-transition: 0.2s background-color; - -moz-transition: 0.2s background-color; - font-size: 1.05rem; -} - -.buttonIcon { - margin-top: 0.22rem; - margin-right: 0.22rem; -} - -.sectionHeaderButton:global(.active), -.sectionHeaderButton:active { - background: $gray-400 !important; - font-weight: 500; - text-align: left; - border-width: 0rem; - height: 2.25rem; - line-height: 1.375rem; -} - -.sectionHeaderButton:hover, .sectionHeaderButton:focus { - background-color: $gray-200; - color: $gray-700 !important; - box-shadow: none !important; -} +@import "assets/scss/global"; diff --git a/src/components/molecules/CategoryList/index.tsx b/src/components/molecules/CategoryList/index.tsx index f74bd06753d9955caedf960adba01424d5cc13d3..e61877f0d4fc60bbb4018dff1179bed81f0a9a78 100644 --- a/src/components/molecules/CategoryList/index.tsx +++ b/src/components/molecules/CategoryList/index.tsx @@ -6,14 +6,9 @@ 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"; +import { resourceTypeToIcon } from "../../pages/ModuleResources" const CategoryList: React.FC<{ select: SelectionProps }> = ({ select, @@ -21,29 +16,17 @@ const CategoryList: React.FC<{ select: SelectionProps }> = ({ 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; + : resourceTypeToIcon(type); return ( <Row - style={{ marginTop: "10px", marginLeft: "-10px", marginRight: "-10px", cursor: "pointer" }} + style={{ marginTop: "10px", marginLeft: "0px", marginRight: "0px", cursor: "pointer" }} onClick={() => select.handleCardClick(id)} onMouseOver={() => select.handleMouseOver(id)} onMouseOut={() => select.handleMouseOut(id)} @@ -62,9 +45,9 @@ const CategoryList: React.FC<{ select: SelectionProps }> = ({ </Badge> ))} </Col> - <Col md="auto"> + <Col md="auto" className="px-0"> <FontAwesomeIcon - style={{ fontSize: "1.125rem" }} + style={{ fontSize: "1.125rem", marginRight: "0.5rem" }} icon={icon} onClick={(e) => { e.stopPropagation(); diff --git a/src/components/molecules/CurrentDirectoryRow/index.tsx b/src/components/molecules/CurrentDirectoryRow/index.tsx index e3d51b5cde7f0e9a928bde5f23c07b06335465a0..a606063adb53214ab0a4f5cfe1353f7c29fbc05f 100644 --- a/src/components/molecules/CurrentDirectoryRow/index.tsx +++ b/src/components/molecules/CurrentDirectoryRow/index.tsx @@ -4,13 +4,8 @@ import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import FileCard from "components/atoms/FileCard"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; -import { - faFileAlt, - IconDefinition, - faFilePdf, - faFileVideo, -} from "@fortawesome/free-solid-svg-icons"; import { SelectionProps } from "components/molecules/SelectionView"; +import { resourceTypeToIcon } from "../../pages/ModuleResources" const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ select, @@ -20,19 +15,8 @@ const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ style={{ marginTop: "10px", marginLeft: "-10px", marginRight: "-10px" }} > {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; - } + return ( <Col xs={6} @@ -57,7 +41,7 @@ const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ ? select.state.isSelected[id] ? faCheckSquare : faSquare - : normalIcon + : resourceTypeToIcon(type) } onClick={() => select.handleCardClick(id)} onIconClick={(e) => { diff --git a/src/components/molecules/QuickAccessRow/index.tsx b/src/components/molecules/QuickAccessRow/index.tsx index 884f21a2065eccb18b9476d379b33b4d6cea4544..34d1ab888ae6ed2fb977fcc3a21affab7ec39b2b 100644 --- a/src/components/molecules/QuickAccessRow/index.tsx +++ b/src/components/molecules/QuickAccessRow/index.tsx @@ -6,13 +6,8 @@ import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import FileCard from "components/atoms/FileCard"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; -import { - faFileAlt, - IconDefinition, - faFilePdf, - faFileVideo, -} from "@fortawesome/free-solid-svg-icons"; import { SelectionProps } from "components/molecules/SelectionView"; +import { resourceTypeToIcon } from "../../pages/ModuleResources" const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ select, @@ -27,19 +22,8 @@ const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ )} > {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; - } + return ( <Col xs={7} @@ -64,7 +48,7 @@ const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ ? select.state.isSelected[id] ? faCheckSquare : faSquare - : normalIcon + : resourceTypeToIcon(type) } onClick={() => select.handleCardClick(id)} onIconClick={(e) => { diff --git a/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss b/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss index c74607afbf4941c77272d62edad3f60081f3c864..9fee92a7869d578f2c303a30b06ad6dccc395e4b 100644 --- a/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss +++ b/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss @@ -1,55 +1,5 @@ @import "assets/scss/custom"; - -.sectionHeaderContainer { - display: flex; - justify-content: space-between; - margin-top: 1.875rem; - align-items: center; -} - -.sectionHeader { - font-weight: 500; - font-size: 20px; - cursor: pointer; -} - -.sectionHeaderButton { - background-color: $white; - color: $gray-500; - border-width: 0px; - border-radius: 8px; - margin-left: 20px; - justify-content: space-between; - height: 2.25rem; - transition: 0.2s background-color; - -webkit-transition: 0.2s background-color; - -moz-transition: 0.2s background-color; - font-size: 1.05rem; -} - -.buttonIcon { - margin-top: 0.22rem; -} - -.buttonCheckbox :global(.form-check-input) { - margin: 0px; -} - -.sectionHeaderButton:global(.active), -.sectionHeaderButton:active { - background: $gray-400 !important; - font-weight: 500; - text-align: left; - border-width: 0rem; - height: 2.25rem; - line-height: 1.375rem; -} - -.sectionHeaderButton:hover, .sectionHeaderButton:focus { - background-color: $gray-200; - color: $gray-700 !important; - box-shadow: none !important; -} +@import "assets/scss/global"; .alert-enter { opacity: 0; diff --git a/src/components/molecules/SelectionView/index.tsx b/src/components/molecules/SelectionView/index.tsx index ffdda456edd2397b2e7b8665b9b5bf19e6e1990f..b920facb16fd1cf09109fd1c39afcc733d133810 100644 --- a/src/components/molecules/SelectionView/index.tsx +++ b/src/components/molecules/SelectionView/index.tsx @@ -15,6 +15,7 @@ type idBooleanMap = { [key: number]: boolean }; export interface SelectionProps { selectionItems: SelectionItem[]; state: MyState; + setIsSelected: (selection: idBooleanMap) => void; isAnySelected: () => boolean; handleCardClick: (id: number) => void; handleIconClick: (id: number) => void; @@ -126,6 +127,7 @@ class SelectionView extends React.Component<MyProps, MyState> { let selection: SelectionProps = { selectionItems: this.props.selectionItems, state: this.state, + setIsSelected: (selection) => this.setState({ isSelected: selection }), isAnySelected: () => this.isAnySelected(), handleCardClick: (id: number) => this.handleCardClick(id), handleIconClick: (id: number) => this.handleIconClick(id), diff --git a/src/components/pages/ModuleResources/components/FoldersView.tsx b/src/components/pages/ModuleResources/components/FoldersView.tsx index 44dfdb87971315a42bf9802471dc24675559ac9e..48455eb46769339c5a6f0c334dc2f8c1b21cfd4e 100644 --- a/src/components/pages/ModuleResources/components/FoldersView.tsx +++ b/src/components/pages/ModuleResources/components/FoldersView.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Folder } from "../index"; import SelectionView, { SelectionProps, } from "components/molecules/SelectionView"; @@ -6,18 +7,17 @@ import FoldersRow from "components/molecules/FoldersRow"; import { useHistory, useLocation } from "react-router-dom"; export interface FoldersViewProps { - folders: { - title: string; - id: number; - }[]; + folders: Folder[]; scope: string; searchText: string; + handleFolderDownload: (ids: number[]) => void; } const FoldersView: React.FC<FoldersViewProps> = ({ folders, scope, searchText, + handleFolderDownload }) => { let history = useHistory(); let location = useLocation(); @@ -31,7 +31,7 @@ const FoldersView: React.FC<FoldersViewProps> = ({ return ( <SelectionView heading="Folders" - onDownloadClick={() => {}} + onDownloadClick={handleFolderDownload} onItemClick={handleFolderClick} selectionItems={folders} render={(select: SelectionProps) => <FoldersRow select={select} />} diff --git a/src/components/pages/ModuleResources/components/ListView.tsx b/src/components/pages/ModuleResources/components/ListView.tsx index dc8d5045657187c20bee8de81443d0175db45652..8ac1e21676047034d081e59469fc98d553ad2a4e 100644 --- a/src/components/pages/ModuleResources/components/ListView.tsx +++ b/src/components/pages/ModuleResources/components/ListView.tsx @@ -1,16 +1,14 @@ import React from "react"; -import { Resource } from "../index"; +import { Resource, Folder } from "../index"; import SelectionView, { SelectionProps, } from "components/molecules/SelectionView"; import CategoryList from "components/molecules/CategoryList"; import CategoryHeader from "components/molecules/CategoryHeader"; +import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; export interface ListViewProps { - folders: { - title: string; - id: number; - }[]; + folders: Folder[]; resources: Resource[]; searchText: string; onDownloadClick: (identifiers: number[]) => void; @@ -23,8 +21,7 @@ const ListView: React.FC<ListViewProps> = ({ folders, resources, searchText, - onDownloadClick, - onSectionDownloadClick, + onDownloadClick, onItemClick, includeInSearchResult }) => { @@ -47,14 +44,36 @@ const ListView: React.FC<ListViewProps> = ({ let categorySelect : SelectionProps = { selectionItems: select.selectionItems.filter(res => res.folder === title), state: select.state, + setIsSelected: select.setIsSelected, isAnySelected: select.isAnySelected, handleCardClick: select.handleCardClick, handleIconClick: select.handleIconClick, handleMouseOver: select.handleMouseOver, handleMouseOut: select.handleMouseOut } + + function isAllSelected() : boolean { + let isSelected = categorySelect.state.isSelected; + return categorySelect.selectionItems.every(item => isSelected[item.id]); + } + + function onSelectAllClick() { + let setValue = !isAllSelected(); + let isSelected = JSON.parse(JSON.stringify(select.state.isSelected)); + let items = categorySelect.selectionItems; + for (let item in items) { + isSelected[items[item].id] = setValue; + } + select.setIsSelected(isSelected); + } + return (<> - <CategoryHeader heading={title} onDownloadClick={() => onSectionDownloadClick(title)}/> + <CategoryHeader + heading={title} + onSelectAllClick={onSelectAllClick} + selectAllIcon={isAllSelected() ? faCheckSquare : faSquare} + checkBoxColor={select.isAnySelected() ? "#495057" : "#e9ecef"} + /> <CategoryList select={categorySelect} /> </>) })} diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx index 2312c4980b7c9ee0ee31ad7845193c5a8fb87ec0..d7cd9cb426d8332f4e3ab39e768b0781e884813f 100644 --- a/src/components/pages/ModuleResources/index.tsx +++ b/src/components/pages/ModuleResources/index.tsx @@ -7,6 +7,13 @@ import CurrentDirectoryView from "./components/CurrentDirectoryView"; import FoldersView from "./components/FoldersView"; import ListView from "./components/ListView"; import TopSection from "./components/TopSection"; +import { + faFileAlt, + faFilePdf, + faFileVideo, + faLink, + IconDefinition, +} from "@fortawesome/free-solid-svg-icons"; export interface Resource { title: string; @@ -14,6 +21,12 @@ export interface Resource { tags: string[]; folder: string; id: number; + path?: string; +} + +export interface Folder { + title: string; + id: number; } export interface ResourcesProps { @@ -30,6 +43,19 @@ export interface ResourceState { searchText: string; } +export function resourceTypeToIcon(type: string): IconDefinition { + switch (type) { + case "pdf": + return faFilePdf; + case "video": + return faFileVideo; + case "link": + return faLink; + default: + return faFileAlt; + } +} + class ModuleResources extends React.Component<ResourcesProps, ResourceState> { moduleCode = this.props.moduleID.startsWith("CO") ? this.props.moduleID.slice(2) @@ -60,6 +86,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { tags: resource.tags, folder: resource.category, id: resource.id, + path: resource.path, } as Resource); } this.setState({ resources: resourceArr, isLoaded: true }); @@ -77,6 +104,13 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); } + // Gets the unique categories/folders that have been assigned for the resources + folders(): Folder[] { + return Array.from(new Set<string>( + this.state.resources.map((res: Resource) => res.folder)) + ).map((title, id) => ({ title: title, id: id })); + } + handleFileDownload(indices: number[]) { if (indices.length === 1) { // Only one file to download, call single file endpoint @@ -94,6 +128,20 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { } } + handleFolderDownload(ids: number[]) { + let categories = this.folders().filter(folder => folder.id in ids) + .map(folder => folder.title); + if (categories.length === 1) { + this.handleSectionDownload(categories[0]); + } else { + // No endpoint for multiple category download, reuse zipped selection instead + let resourceIds = this.state.resources + .filter(resource => resource.folder in categories) + .map(resource => resource.id); + this.handleFileDownload(resourceIds); + } + } + handleSectionDownload(category: string) { download(api.MATERIALS_ZIPPED, methods.GET, category + ".zip", { year: this.props.year, @@ -102,7 +150,17 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); } - handleFileClick(id: number) { + handleResourceClick(id: number) { + let resource = this.state.resources.find(resource => resource.id === id); + if (resource === undefined) { + return; + } + if (resource.type === "link") { + window.open(resource.path, "_blank"); + return; + } + + // Resource is of file type, get from Materials const onSuccess = (data: any) => { // TODO: Try to navigate straight to the endpoint url instead of creating an object url data.blob().then((blob: any) => { @@ -176,58 +234,48 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { render() { let scope = this.props.scope || ""; - 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 - folders={folders} - scope={scope} - searchText={this.state.searchText} - /> - - <CurrentDirectoryView - resources={this.state.resources} - scope={scope} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onItemClick={(id) => this.handleFileClick(id)} - includeInSearchResult={this.includeInSearchResult} - /> - - <QuickAccessView - resources={this.state.resources} - scope={scope} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onItemClick={(id) => this.handleFileClick(id)} - /> - </> - ); - case "list": - return ( - <> - <ListView - folders={folders} - resources={this.state.resources} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onSectionDownloadClick={(category) => - this.handleSectionDownload(category) - } - onItemClick={(id) => this.handleFileClick(id)} - includeInSearchResult={this.includeInSearchResult} - /> - </> - ); + switch(this.state.view) { + case "folder": return ( + <> + <FoldersView + folders={this.folders()} + scope={scope} + searchText={this.state.searchText} + handleFolderDownload={(ids) => this.handleFolderDownload(ids)} + /> + + <CurrentDirectoryView + resources={this.state.resources} + scope={scope} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onItemClick={(id) => this.handleResourceClick(id)} + includeInSearchResult={this.includeInSearchResult} + /> + + <QuickAccessView + resources={this.state.resources} + scope={scope} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onItemClick={(id) => this.handleResourceClick(id)} + /> + </> + ); + case "list": return ( + <> + <ListView + folders={this.folders()} + resources={this.state.resources} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onSectionDownloadClick={(category) => this.handleSectionDownload(category)} + onItemClick={(id) => this.handleResourceClick(id)} + includeInSearchResult={this.includeInSearchResult} + /> + </> + ); } }; return (