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