diff --git a/src/components/molecules/CurrentDirectoryView/index.tsx b/src/components/molecules/CurrentDirectoryView/index.tsx index cdf997490a42fc9a0534eecdec3807daded17f76..bd5696bb7cc89293c2630270632fd04ce8458219 100644 --- a/src/components/molecules/CurrentDirectoryView/index.tsx +++ b/src/components/molecules/CurrentDirectoryView/index.tsx @@ -2,10 +2,13 @@ import React from "react"; import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import ResourceSectionHeader from "../ResourceSectionHeader"; +import { api, methods } from "../../../constants/routes"; +import { request } from "../../../utils/api"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; import { faFileAlt, faFileVideo, faFilePdf, IconDefinition } from "@fortawesome/free-solid-svg-icons"; import FileCard from "components/atoms/FileCard"; +// TODO: Refactor out duplication with QuickAccessView export interface CurrentDirectoryViewProps { documentItems: { title: string; @@ -13,6 +16,7 @@ export interface CurrentDirectoryViewProps { tags: string[]; id: number; }[]; + moduleCode?: string; } type idBooleanMap = { [key: number]: boolean }; @@ -65,6 +69,49 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My this.setState({ isSelected, isHoveringOver }); } + handleDownloadClick() { + const onSuccess = (filename: string, data: any) => { + data.blob().then((blob: any) => { + let url = URL.createObjectURL(blob); + let a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + a.remove(); + }); + }; + // Partial application utility + const downloadFilename = (filename: string) => { + return (data: any) => { + return onSuccess(filename, data); + }; + }; + const onFailure = (error: { text: () => Promise<any> }) => { + error.text().then((errorText) => { + console.log(errorText); + }); + }; + + let indices : number[] = []; + for (let key in this.state.isSelected) { + if (this.state.isSelected[key]) { + indices.push(parseInt(key)); + } + } + + if (indices.length === 1) { + // Only one file to download, call single file endpoint + let filename = this.props.documentItems.filter(document => document.id === indices[0])[0].title; + request(api.MATERIALS_RESOURCES_FILE(indices[0]), methods.GET, downloadFilename(filename), onFailure); + } else { + // Multiple files to download, call zipped selection endpoint + request(api.MATERIALS_ZIPPED_SELECTION, methods.GET, downloadFilename("materials.zip"), onFailure, { + ids: indices, + course: this.props.moduleCode, + }); + } + } + handleSelectAllClick() { let items = this.props.documentItems; let isSelected = JSON.parse(JSON.stringify(this.state.isSelected)); @@ -76,6 +123,23 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My } handleCardClick(id: number) { + const onSuccess = (data: any) => { + data.blob().then((blob: any) => { + let url = URL.createObjectURL(blob); + let a = document.createElement("a"); + a.target = "_blank"; + a.href = url; + a.click(); + a.remove(); + }); + }; + const onFailure = (error: { text: () => Promise<any> }) => { + error.text().then((errorText) => { + console.log(errorText); + }); + }; + request(api.MATERIALS_RESOURCES_FILE(id), methods.GET, onSuccess, onFailure); + if (this.isAnySelected()) { this.handleIconClick(id); } @@ -99,6 +163,7 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My <ResourceSectionHeader heading="Files" showDownload={this.isAnySelected()} + onDownloadClick={() => this.handleDownloadClick()} onSelectAllClick={() => this.handleSelectAllClick()} selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare} checkBoxColur={this.isAnySelected() ? "#495057" : "#dee2e6"} diff --git a/src/components/molecules/QuickAccessView/index.tsx b/src/components/molecules/QuickAccessView/index.tsx index dabff7cdd9ecefb9e51bbfbe805ecdfd2d34212e..22d86264326bab09f073865466df70f5f0333b86 100644 --- a/src/components/molecules/QuickAccessView/index.tsx +++ b/src/components/molecules/QuickAccessView/index.tsx @@ -4,6 +4,8 @@ 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 { api, methods } from "../../../constants/routes"; +import { request } from "../../../utils/api"; import ResourceSectionHeader from "../ResourceSectionHeader"; import FileCard from "components/atoms/FileCard"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; @@ -16,6 +18,7 @@ export interface QuickAccessProps { tags: string[]; id: number; }[]; + moduleCode?: string; } type idBooleanMap = { [key: number]: boolean }; @@ -70,6 +73,50 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> { this.setState({ isSelected, isHoveringOver }); } + handleDownloadClick() { + const onSuccess = (filename: string, data: any) => { + // TODO: Try to navigate straight to the endpoint url instead of creating an object url + data.blob().then((blob: any) => { + let url = URL.createObjectURL(blob); + let a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + a.remove(); + }); + }; + // Partial application utility + const downloadFilename = (filename: string) => { + return (data: any) => { + return onSuccess(filename, data); + }; + }; + const onFailure = (error: { text: () => Promise<any> }) => { + error.text().then((errorText) => { + console.log(errorText); + }); + }; + + let indices : number[] = []; + for (let key in this.state.isSelected) { + if (this.state.isSelected[key]) { + indices.push(parseInt(key)); + } + } + + if (indices.length === 1) { + // Only one file to download, call single file endpoint + let filename = this.props.quickAccessItems.filter(document => document.id === indices[0])[0].title; + request(api.MATERIALS_RESOURCES_FILE(indices[0]), methods.GET, downloadFilename(filename), onFailure); + } else { + // Multiple files to download, call zipped selection endpoint + request(api.MATERIALS_ZIPPED_SELECTION, methods.GET, downloadFilename("materials.zip"), onFailure, { + ids: indices, + course: this.props.moduleCode, + }); + } + } + handleSelectAllClick() { let items = this.props.quickAccessItems; let isSelected = JSON.parse(JSON.stringify(this.state.isSelected)); @@ -81,6 +128,24 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> { } handleCardClick(id: number) { + const onSuccess = (data: any) => { + // TODO: Try to navigate straight to the endpoint url instead of creating an object url + data.blob().then((blob: any) => { + let url = URL.createObjectURL(blob); + let a = document.createElement("a"); + a.target = "_blank"; + a.href = url; + a.click(); + a.remove(); + }); + }; + const onFailure = (error: { text: () => Promise<any> }) => { + error.text().then((errorText) => { + console.log(errorText); + }); + }; + request(api.MATERIALS_RESOURCES_FILE(id), methods.GET, onSuccess, onFailure); + if (this.isAnySelected()) { this.handleIconClick(id); } @@ -103,6 +168,7 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> { <> <ResourceSectionHeader heading="Quick Access" + onDownloadClick={() => this.handleDownloadClick()} showDownload={this.isAnySelected()} onSelectAllClick={() => this.handleSelectAllClick()} selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare} diff --git a/src/components/molecules/ResourceSectionHeader/index.tsx b/src/components/molecules/ResourceSectionHeader/index.tsx index cc89bdc5b717b609e0b56414cce7e86b2e0cc426..7eccc3006ceeee6ca82ce869bda3f54bb383cbad 100644 --- a/src/components/molecules/ResourceSectionHeader/index.tsx +++ b/src/components/molecules/ResourceSectionHeader/index.tsx @@ -10,6 +10,7 @@ export interface SectionHeaderProps { heading: string; selectAllIcon: IconDefinition; showDownload: Boolean; + onDownloadClick: (event: React.MouseEvent) => void; onSelectAllClick: (event: React.MouseEvent) => void; checkBoxColur: string; } @@ -18,6 +19,7 @@ const ResourceSectionHeader: React.FC<SectionHeaderProps> = ({ heading, showDownload, selectAllIcon, + onDownloadClick, onSelectAllClick, checkBoxColur, }: SectionHeaderProps) => { @@ -37,7 +39,7 @@ const ResourceSectionHeader: React.FC<SectionHeaderProps> = ({ style={{ color: checkBoxColur }} variant="secondary" className={styles.sectionHeaderButton} - onClick={() => {}} + onClick={onDownloadClick} > <FontAwesomeIcon className={styles.buttonIcon} diff --git a/src/components/molecules/ResourcesFolderView/index.tsx b/src/components/molecules/ResourcesFolderView/index.tsx index b4fdc8c9d9bb65fbfee50190f7694cd78f27c4de..4184388fbbd0f3a18a7f0fa2c42766c3a8991f78 100644 --- a/src/components/molecules/ResourcesFolderView/index.tsx +++ b/src/components/molecules/ResourcesFolderView/index.tsx @@ -116,6 +116,7 @@ class ResourcesFolderView extends React.Component<PropsType, MyState> { <ResourceSectionHeader heading="Folders" showDownload={this.isAnySelected()} + onDownloadClick={() => {}} onSelectAllClick={() => this.handleSelectAllClick()} selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare} checkBoxColur={this.isAnySelected() ? "#495057" : "#dee2e6"} diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx index 0283b05eb7cfc85efc0eb6d79dd422846aca1d45..75476cffbfa23155902d05ec27e127c82bf25cff 100644 --- a/src/components/pages/ModuleResources/index.tsx +++ b/src/components/pages/ModuleResources/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import styles from "./style.module.scss"; import { request } from "../../../utils/api"; -import { api } from "../../../constants/routes"; +import { api, methods } from "../../../constants/routes"; import MyBreadcrumbs from "components/atoms/MyBreadcrumbs"; import InputGroup from "react-bootstrap/InputGroup"; @@ -46,11 +46,12 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }; } + moduleCode = this.props.moduleID.startsWith("CO") + ? this.props.moduleID.slice(2) + : this.props.moduleID; + componentDidMount() { this.setState({ isLoaded: false }); - let moduleCode = this.props.moduleID.startsWith("CO") - ? this.props.moduleID.slice(2) - : this.props.moduleID; const onSuccess = (data: { json: () => Promise<any> }) => { let resourceArr: Resource[] = []; @@ -75,9 +76,9 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); }; - request(api.MATERIALS_RESOURCES, "GET", onSuccess, onFailure, { + request(api.MATERIALS_RESOURCES, methods.GET, onSuccess, onFailure, { year: this.props.year, - course: moduleCode, + course: this.moduleCode, }); } @@ -170,12 +171,12 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { <ResourcesFolderView folderItems={folders} /> ) : null} {scope !== "" || this.state.searchText !== "" ? ( - <CurrentDirectoryView documentItems={filesContent} /> + <CurrentDirectoryView documentItems={filesContent} moduleCode={this.moduleCode}/> ) : null} {this.state.searchText === "" && scope === "" && quickAccessItems.length > 0 ? ( - <QuickAccessView quickAccessItems={quickAccessItems} /> + <QuickAccessView quickAccessItems={quickAccessItems} moduleCode={this.moduleCode}/> ) : null} </> ) diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx index dd37f6e245d5e0516af3a876b3a631900a99b536..0781c6598444c590e0aed35bf0af81a33fa87ddf 100644 --- a/src/constants/routes.tsx +++ b/src/constants/routes.tsx @@ -8,8 +8,15 @@ const prod = { const config = process.env.NODE_ENV === "development" ? dev : prod; +export const methods = { + GET: "GET", + POST: "POST", +} + export const api = { MATERIALS_LOGIN: config.MATERIALS_URL + "/auth/login", MATERIALS_COURSES: config.MATERIALS_URL + "/courses/1819", - MATERIALS_RESOURCES: config.MATERIALS_URL + "/resources" + MATERIALS_RESOURCES: config.MATERIALS_URL + "/resources", + MATERIALS_RESOURCES_FILE: (id: number) => { return config.MATERIALS_URL + "/resources/" + id + "/file"; }, + MATERIALS_ZIPPED_SELECTION: config.MATERIALS_URL + "/resources/zipped/selection", } \ No newline at end of file diff --git a/src/utils/api.tsx b/src/utils/api.tsx index b21d81177ec0c48f5daa7c51ac1f72f2f25990f3..e84733d16781c7e2e07fa5bdd0a076eb0d15ea98 100644 --- a/src/utils/api.tsx +++ b/src/utils/api.tsx @@ -1,6 +1,6 @@ import authConstants from "../constants/auth"; import authenticationService from "../utils/auth"; -import { api } from "../constants/routes" +import { api, methods } from "../constants/routes" interface RequestOptions { [key: string]: any @@ -26,7 +26,7 @@ export async function request(url: string, method: string, onSuccess: any, onErr }, }; - if (method === "GET") { + if (method === methods.GET) { url = url + "?" + new URLSearchParams(body); } else { options.body = JSON.stringify(body);