diff --git a/api/ECNQueue.py b/api/ECNQueue.py index 6d588df..1042cb6 100644 --- a/api/ECNQueue.py +++ b/api/ECNQueue.py @@ -43,7 +43,7 @@ def __init__(self, queue: str, number: int) -> None: self.lastUpdated = self.__getLastUpdated() self.__rawItem = self.__getRawItem() self.headers = self.__parseHeaders() - self.content = self.__getContent() + self.content = self.__parseSections() self.isLocked = self.__isLocked() self.userEmail = self.__parseFromData(data="userEmail") self.userName = self.__parseFromData(data="userName") @@ -173,7 +173,439 @@ def __getContent(self) -> list: contentEnd = len(self.__rawItem) - 1 return self.__rawItem[ contentStart : contentEnd ] - # TODO: Implement section parsing. + + def __parseSections(self) -> list: + # List of all item events + sections = [] + + contentStart = self.__getHeaderBoundary() + 1 + contentEnd = len(self.__rawItem) - 1 + + # Find line numbers where sections start + sectionBoundaries = [ {"start": contentStart} ] + + directoryInfo = [] + initialMessageContent = [] + endInitialMessage = False + + # Parses the entire contents of the message, stores everything before any delimiter as the initial message + for lineNumber in range(contentStart, contentEnd + 1): + + line = self.__rawItem[lineNumber] + + if line.startswith("***") or line.startswith("===") and not line.startswith("===="): + + # Signifies that the inital message has been copletely parsed + endInitialMessage = True + + # Stores what line every delimeter starts/ends + sectionBoundaries.append({"start": lineNumber}) + + elif endInitialMessage == False: + + # Delimiter not encountered yet, so append line to initial message list + initialMessageContent.append(line) + + # All possible Directory Items + directoryInfoPattern = [ + "\tName: ", + " Login: ", + " Computer: ", + " Location: ", + " Email: ", + " Phone: ", + " Office: ", + " UNIX Dir: ", + " Zero Dir: ", + " User ECNDB: ", + " Host ECNDB: ", + " Subject: " + ] + + # Reference to Remove Directory Items from initial message + directoryLinesToRemove = [] + + # Line Counter + lineCounter = 0 + + # Parses the initial message for directory information + for lineContents in initialMessageContent: + + for itemsindirectory in directoryInfoPattern: + + # Checks if the line starts with any of the directory parameters + if lineContents.startswith(itemsindirectory): + + # Appends line number to be removed from initial message + directoryLinesToRemove.append(lineCounter) + + # Adds the contents of the line to the directory info + directoryInfo.append(lineContents) + + # allows to move to the next iteration of the parent for loop, no need to continue parsing directory delimiters + break + + # Increment the line counter by after each line + lineCounter = lineCounter + 1 + + # Parses the initial message to remove directory information + for lineNumber in sorted(directoryLinesToRemove, reverse=True): + + # Remove the directory line from the intital message + initialMessageContent.pop(lineNumber) + + # Removes unecessary newlines from the begining and the end of the initial message + + newLinebegining = True + newLineEnd = True + + while (newLinebegining or newLineEnd) and len(initialMessageContent) > 1: + # Initializes the Length of Message content each iteration of the loop + initialmessagecontentLength = len(initialMessageContent) + + # Checks if the last line is a newline + if initialMessageContent[initialmessagecontentLength -1] == "\n": + + # Deletes the last line if it is a newline + initialMessageContent.pop(initialmessagecontentLength - 1) + + # If the previous condition failed, then set the new line end to False if it isn't false already + elif newLineEnd == True: + + newLineEnd = False + + # Checks if the first line in message content is a newline + if initialMessageContent[0] == "\n": + + # Removes the first line in message content if it is a newline + initialMessageContent.pop(0) + + # If the previous condition failed, then set the new line begining to False if it isn't false already + elif newLinebegining == True: + + newLinebegining = False + + # Appends Directory Information into the sections array + sections.append( + {"type": "directoryInformation", + "content": directoryInfo} + ) + + # Gets the initial message date from the header + initialMessageDateStr = self.__getMostRecentHeaderByType("Date") + + # Formats the initial message date to UTC + initialMessageFormattedDate = self.__getFormattedDate(initialMessageDateStr) + + # Stores list of dictionaries for CC information + initialMessageCCSection =[] + + # Parses the header looking for CC recipients of the initial message and stores it in a list of tuples + initialMessageCCList = email.utils.getaddresses([self.__getMostRecentHeaderByType("CC")]) + + # Parses the CC list and stores the cc recipient information in a list of dictionaries + for ccRecipients in initialMessageCCList: + + initialMessageCCSection.append( + {"name": ccRecipients[0], + "email": ccRecipients[1]} + ) + + # Appends all initial message information to the sections array + sections.append( + {"type": "initialMessage", + "datetime": initialMessageFormattedDate, + "userName": self.__parseFromData(data="userName"), + "userEmail": self.__parseFromData(data="userEmail"), + "ccRecipients": initialMessageCCSection, + "content": initialMessageContent} + ) + + # Assignment Information + assignedBy = "" + assignedDateTime = "" + assignedTo = "" + + # Parses the header looking for assignment delimeters and stores info into their respective variables + for headerContent in range(0, contentStart): + + line = self.__rawItem[headerContent] + + # Gets who the Item was assigned to + if line.startswith("Assigned-To: "): + + assignedTo = (re.search("(?<=Assigned-To: )(.*)", line)).group() + + # Gets the date the Item was assigned + elif line.startswith("Assigned-To-Updated-Time: "): + + dateFromLine = (re.search("(?<=Assigned-To-Updated-Time: )(.*)", line)).group() + + assignedDateTime = self.__getFormattedDate(dateFromLine) + + # Gets who assigned the Item + elif line.startswith("Assigned-To-Updated-By: "): + + assignedBy = (re.search("(?<=Assigned-To-Updated-By: )(.*)", line)).group() + + # Appends the assignment to the sections list + sections.append( + {"type": "assign", + "by": assignedBy, + "datetime": assignedDateTime, + "to": assignedTo} + ) + + sectionBoundaries.append({"start": contentEnd + 1}) + + # Set line number where section end + for boundaryIndex in range(0, len(sectionBoundaries) - 1): + + sectionBoundaries[boundaryIndex]["end"] = sectionBoundaries[boundaryIndex + 1]["start"] + + # Remove End of File boundary + del sectionBoundaries[-1] + + # Different delimiters for different message events + delimiters = [ + {"name": "edit", "pattern": "*** Edited"}, + {"name": "status", "pattern": "*** Status"}, + {"name": "replyToUser", "pattern": "*** Replied"}, + {"name": "replyFromUser", "pattern": "=== "}, + ] + + # Parses through all the boundaries in section boundaries + for boundary in sectionBoundaries: + + # Sets line to the first line of the boundary (which is always the delimiter) + line = self.__rawItem[boundary["start"]] + + sectionType = None + + # Looks at the begining of line and determines if it starts with any of the delimiters, + # if so, name it accordingly + for delimiter in delimiters: + + if line.startswith(delimiter["pattern"]): + + sectionType = delimiter["name"] + + break + + # Returns all of the lines within the current section + sectionContent = self.__rawItem[boundary["start"] : boundary["end"]] + + # Checks for each section type + if sectionType == "edit": + + # Returns a dictionary with edit information + editInfo = self.__editParsing(line) + + # Appends content of the edit message to the dictionary + editInfo["content"] = sectionContent + + # Appends the edit dictionary to sections + sections.append(editInfo) + + continue + + elif sectionType == "replyToUser": + + # Returns a dictionary with reply-to information + replyToInfo = self.__replyToParsing(line) + + # Appends content of the reply-to message to the dicionary + replyToInfo['content'] = sectionContent + + # Appends the reply-to to sections + sections.append(replyToInfo) + + continue + + elif sectionType == "status": + + # Returns a dictionary with status information + statusInfo = self.__statusParsing(line) + + # Appends content to empty content key to avoid passing large amounts of info that isnt used within the function + statusInfo['content'] = sectionContent + + # Appends the status to sections + sections.append(statusInfo) + + continue + + elif sectionType == "replyFromUser": + + # Returns a dictionary with userReply information + replyFromInfo = self.__userReplyParsing(sectionContent) + + # Appends content to empty content key to avoid passing large amounts of info that isnt used within the function + replyFromInfo['content'] = sectionContent + + # Appends the replyFrom to sections + sections.append(replyFromInfo) + + + return sections + + def __editParsing(self, line: str) -> dict: + """Returns a dictionary with edit information + + Returns: + dictionary: "type": "edit", by, datetime and content + """ + + formattedDateTime = "" + editedBy = "" + + # Parses for the author of the edit, which is located between the "*** Edited by: " and " at:" substrings + editedBy = (re.search("(?<=\*{3} Edited by: )(.*)(?= at:)", line)).group() + + # Parses for the date and time of the edit, which is located between the " at: " and "***\n" substrings + dateTimeString = (re.search("(?<= at: )(.*)(?= \*\*\*\n)", line)).group() + + # Attempts to format the date and time into utc format + formattedDateTime = self.__getFormattedDate(dateTimeString) + + editInfo = { + "type": "edit", + "by": editedBy, + "datetime": formattedDateTime, + "content": "" + } + + return editInfo + + def __replyToParsing(self, line: str) -> dict: + """Returns a dictionary with reply to user information + + Returns: + dictionary: "type": "replyToUser", by, datetime and content + """ + + formattedDateTime = "" + repliedBy = "" + + # Parses for the author of the reply, which is located between the "*** Replied by: " and " at:" substrings + repliedBy = (re.search("(?<=\*{3} Replied by: )(.*)(?= at:)", line)).group() + + # Parses for the date and time of the reply, which is located between the " at: " and "***\n" substrings + dateTimeString = (re.search("(?<= at: )(.*)(?= \*\*\*\n)", line)).group() + + # Formats date to UTC + formattedDateTime = self.__getFormattedDate(dateTimeString) + + replyInfo = { + "type": "replyToUser", + "by": repliedBy, + "datetime": formattedDateTime, + "content": "" + } + + return replyInfo + + def __statusParsing(self, line: str) -> dict: + """Returns a dictionary with status information + + Returns: + dictionary: "type": "status", by, datetime and content + """ + + formattedDateTime = "" + updatedBy = "" + + # Parses for the author of the status change, which is located between the "*** Status updated by: " and " at:" substrings + updatedBy = (re.search("(?<=\*{3} Status updated by: )(.*)(?= at:)", line)).group() + + # Parses for the date and time of the status change, which is located between the " at: " and "***\n" substrings + dateTimeString = re.search("(?<= at: )(.*)(?= \*\*\*\n)", line).group() + + # Formats the date to UTC + formattedDateTime = self.__getFormattedDate(dateTimeString) + + statusInfo = { + "type": "status", + "by": updatedBy, + "datetime": formattedDateTime, + "content": "" + } + + return statusInfo + + def __userReplyParsing(self, replyContent: list) -> dict: + """Returns a dictionary with user Reply information information + + Returns: + dictionary: "type": "replyFromUser", datetime, subject, userName, userEmail, content, ccRecipients + """ + formattedDateTime = "" + repliedByName = "" + repliedByEmail = "" + subject = "" + ccRecipientsList = [] + newLineCounter = 0 + + #Parses the section content looking for any line that starts with a metadata + for line in replyContent: + + #Checks for a newline and breaks for loop on second occurance of a newline + if line == "\n": + newLineCounter = newLineCounter + 1 + + if newLineCounter == 2: + break + + # Checks for lines starting with Subject, From, Date and CC + if line.startswith("Subject: "): + + # Matches everything after "Subject: " in the line + subject = (re.search("(?<=Subject: )(.*)", line)).group() + + elif line.startswith("From: "): + + # Returns a list of tuples with name and email information + emailList = email.utils.getaddresses([line]) + + # The name in stored in the first index of the tuple + repliedByName = emailList[0][0] + + # The email is stored in the second index of the tuple + repliedByEmail = emailList[0][1] + + elif line.startswith("Date: "): + + # Matches everything after "Date: " + dateStr = (re.search("(?<=Date: )(.*)", line)).group() + + # Formatts the date to UTC + formattedDateTime = self.__getFormattedDate(dateStr) + + elif line.startswith("Cc: "): + + # Returns a list of tuples with email information + recipientsList = email.utils.getaddresses([line]) + + # Parses through the cc tuple list + for cc in recipientsList: + + # Stores the cc information in a dictionary and appends it to the ccRecipientsList + ccRecipientsList.append( + {"name":cc[0], + "email":cc[1]} + ) + + replyFromInfo = { + "type": "replyFromUser", + "datetime": formattedDateTime, + "subject": subject, + "userName": repliedByName, + "userEmail": repliedByEmail, + "content": "", + "ccRecipients": ccRecipientsList + } + + return replyFromInfo def __isLocked(self) -> Union[str, bool]: """Returns a string info about the lock if true and a bool False if false diff --git a/api/requirements.txt b/api/requirements.txt index 4554304..8a009e1 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -11,6 +11,7 @@ lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 pylint==2.5.3 +python-dateutil==2.8.1 pytz==2020.1 six==1.15.0 toml==0.10.1 diff --git a/docs/parseSectionsFormat.jsonc b/docs/parseSectionsFormat.jsonc new file mode 100644 index 0000000..3752cd5 --- /dev/null +++ b/docs/parseSectionsFormat.jsonc @@ -0,0 +1,217 @@ +[ + { + "type": "directoryInformation", + + // An array of lines with non-printable characters. + // Example from aae 1 + "content": [ + "\n", + "\tName: Jerry L Guerrero\n", + " Login: jerry\n", + " Computer: x-ee27å0bpc1 (128.46.164.29)\n", + " Location: EE 270B\n", + " Email: jerry@purdue.edu\n", + " Phone: \n", + " Office: \n", + " UNIX Dir: /home/pier/c/jerry\n", + " Zero Dir: U=\\\\pier.ecn.purdue.edu\\jerry\n", + " User ECNDB: http://eng.purdue.edu/jump/bcafa8\n", + " Host ECNDB: http://eng.purdue.edu/jump/2dbd461 \n", + " Subject: Win7 to Win10 Migration List - kevin\n" + ] + }, + + { + "type": "initialMessage", + + // RFC 8061 formatted date string + // Maps to Item.dateReceived + "date": "2020-09-11", + + // RFC 8061 formatted time string + // Maps to Item.dateReceived + "time": "01:26:45+00:00", + + // String of user's real name. Could be blank + // Maps to Item.userName + "userName": "Justin Campbell", + + // String of user's email + // Maps to Item.userEmail + "userEmail": "campb303@purdue.edu", + + // Array of dictionaries of names and emails for cc'd recipients. Could be blank, could have multiple addresses, names could be blank + // Comes from headers + // See: https://docs.python.org/3/library/email.utils.html#email.utils.parseaddr for parsing this field + "ccRecipients": [ + { + "name": "John Doe", + "email": "johndoe@example.com" + }, + { + "name": "", + "email": "janesmith@example.com" + } + ], + + // An array of lines with non-printable characters. + "content": [ + "I need some help with something.\n" + ] + }, + + { + "type": "edit", + + // Career account alias for ECN user who added edit + // Appears after the first colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Edited by: knewell at: 04/22/20 16:39:51 *** + // (Start) ^ ^ (End) + "by": "knewell", + + // RFC 8061 formatted date string + // Appears after the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Edited by: knewell at: 04/22/20 16:39:51 *** + // (Start) ^ ^ (End) + "date": "2020-04-22T16:39:51", + + // RFC 8061 formatted time string + // Appears after the first space following the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Edited by: knewell at: 04/22/20 16:39:51 *** + // (Start) ^ ^ (End) + "time": "16:39:51", + + // An array of lines with non-printable characters. + "content": [ + "This is related to another item. I need to do X next.\n" + ] + }, + + { + "type": "status", + + // Career account alias for ECN user who added edit + // Appears after the first colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Status updated by: knewell at: 4/23/2020 10:35:47 *** + // (Start) ^ ^ (End) + "by": "knewell", + + // RFC 8061 formatted date string + // Appears after the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Status updated by: knewell at: 4/23/2020 10:35:47 *** + // (Start) ^ ^ (End) + "date": "2020-04-23T10:35:47", + + // RFC 8061 formatted time string + // Appears after the first space following the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Status updated by: knewell at: 4/23/2020 10:35:47 *** + // (Start) ^ ^ (End) + "time": "10:35:47", + + // An array of lines with non-printable characters. + "content": [ + "Doing X thing." + ] + }, + + { + "type": "assign", + + // Career account alias for ECN user changed the assignment + // Appears in the item headers and the most recent entry can be accessed by: + // >>> self.__getMostRecentHeaderByType("Assigned-To-Updated-By") + "by": "campb303", + + // RFC 8061 formatted date string + // Appears in the item headers and the most recent entry can be accessed by: + // >>> self.__getMostRecentHeaderByType("Assigned-To-Updated-Time") + "date": "2020-06-23T13:27:00", + + // RFC 8061 formatted time string + // Appears in the item headers and the most recent entry can be accessed by: + // >>> self.__getMostRecentHeaderByType("Assigned-To-Updated-Time") + "time": "13:27:00", + + // Career account alias for ECN user the item was assigned to + // Appears in the item headers and the most recent entry can be accessed by: + // >>> self.__getMostRecentHeaderByType("Assigned-To") + "to": "campb303", + }, + + { + "type": "replyFromECN", + + // Career account alias for ECN user who added edit + // Appears after the first colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Replied by: ewhile at: 05/08/20 09:21:43 *** + // (Start) ^ ^ (End) + "by": "ewhile", + + // RFC 8061 formatted date string + // Appears after the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Replied by: ewhile at: 05/08/20 09:21:43 *** + // (Start) ^ ^ (End) + "date": "2020-05-08", + + // RFC 8061 formatted time string + // Appears after the first space following the second colon followed by a space `: ` in the delimiter string + // Ends just before the next space + // Ex: *** Replied by: ewhile at: 05/08/20 09:21:43 *** + // (Start) ^ ^ (End) + "time": "09:21:43", + + // An array of lines with non-printable characters. + "content": [ + "Sascha,\n", + "\n", + "Chicken kevin biltong, flank jowl prosciutto shoulder meatball meatloaf sirloin.\n", + "\n", + "Ethan White\n", + "ECN\n" + ] + }, + + { + "type": "replyFromUser", + + // RFC 8061 formatted date string + // Appears in the headers of a merged message + // Might be possible to create submessage and parse the headers using the email library + "date": "2020-05-08", + + // RFC 8061 formatted date string + // Appears in the headers of a merged message + // Might be possible to create submessage and parse the headers using the email library + "time": "13:57:18+00:00", + + // String of user's real name. Could be blank + // Maps to Item.userName + "userName": "Reckowsky, Michael J.", + + // String of user's email + // Maps to Item.userEmail + "userEmail": "mreckowsky@purdue.edu", + + // Array of dictionaries of names and emails for cc'd recipients. Could be blank, could have multiple addresses, names could be blank + // Comes from headers + // See: https://docs.python.org/3/library/email.utils.html#email.utils.parseaddr for parsing this field + "ccRecipients": [], + + // An array of lines with non-printable characters. + "content": [ + "Ethan,\n", + "\n", + "Biltong beef ribs doner chuck, pork chop jowl salami cow filet mignon pork.\n", + "\n", + "Mike\n", + ] + }, +] \ No newline at end of file diff --git a/src/App.js b/src/App.js index 66b6dc8..d573c3f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,19 +1,17 @@ import React, { useState } from "react"; import { ThemeProvider } from "@material-ui/core/styles"; import webqueueTheme from "./theme"; -import { Box, makeStyles } from "@material-ui/core"; -import CustomAppBar from "./CustomAppBar"; +import { Box, makeStyles, Paper } from "@material-ui/core"; +import ItemTableAppBar from "./components/ItemTableAppBar/"; import ItemTable from "./components/ItemTable/"; -import ItemViewAppBar from "./ItemViewAppBar"; -import ItemView from "./ItemView"; +import ItemViewAppBar from "./components/ItemViewAppBar/"; +import ItemView from "./components/ItemView/"; import clsx from "clsx"; -const testItem = {"queue": "ce", "number": 100, "lastUpdated": "07-23-20 10:11 PM", "headers": [{"type": "Merged-Time", "content": "Tue, 23 Jun 2020 13:31:53 -0400"}, {"type": "Merged-By", "content": "campb303"}, {"type": "QTime", "content": "1"}, {"type": "QTime-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "QTime-Updated-By", "content": "campb303"}, {"type": "Time", "content": "1"}, {"type": "Time-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "Time-Updated-By", "content": "campb303"}, {"type": "Replied-Time", "content": "Tue, 23 Jun 2020 13:28:48 -0400"}, {"type": "Replied-By", "content": "campb303"}, {"type": "Edited-Time", "content": "Tue, 23 Jun 2020 13:27:56 -0400"}, {"type": "Edited-By", "content": "campb303"}, {"type": "QAssigned-To", "content": "campb303"}, {"type": "QAssigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "QAssigned-To-Updated-By", "content": "campb303"}, {"type": "Assigned-To", "content": "campb303"}, {"type": "Assigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "Assigned-To-Updated-By", "content": "campb303"}, {"type": "QStatus", "content": "Dont Delete"}, {"type": "QStatus-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "QStatus-Updated-By", "content": "campb303"}, {"type": "Status", "content": "Dont Delete"}, {"type": "Status-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "Status-Updated-By", "content": "campb303"}, {"type": "Date", "content": "Tue, 23 Jun 2020 13:25:51 -0400"}, {"type": "From", "content": "Justin Campbell "}, {"type": "Message-ID", "content": "<911CE050-3240-4980-91DD-9C3EBB8DBCF8@purdue.edu>"}, {"type": "Subject", "content": "Beepboop"}, {"type": "To", "content": "cesite@ecn.purdue.edu"}, {"type": "Content-Type", "content": "text/plain; charset=\"utf-8\""}, {"type": "X-ECN-Queue-Original-Path", "content": "/home/pier/e/queue/Attachments/inbox/2020-06-23/208-original.txt"}], "content": ["Testtest\n", "\n", "*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\n", "Dont Delete\n", "*** Edited by: campb303 at: 06/23/20 13:27:56 ***\n", "\n", "This be an edit my boy\n", "\n", "\n", "\n", "*** Replied by: campb303 at: 06/23/20 13:28:18 ***\n", "\n", "This be a reply my son\n", "\n", "Justin\n", "ECN\n", "\n", "=== Additional information supplied by user ===\n", "\n", "Subject: Re: Beepboop\n", "From: Justin Campbell \n", "Date: Tue, 23 Jun 2020 13:30:45 -0400\n", "X-ECN-Queue-Original-Path: /home/pier/e/queue/Attachments/inbox/2020-06-23/212-original.txt\n", "X-ECN-Queue-Original-URL: https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/212-original.txt\n", "\n", "Huzzah!\n", "\n", "===============================================\n"], "isLocked": "ce 100 is locked by knewell using qvi", "userEmail": "campb303@purdue.edu", "userName": "Justin Campbell", "userAlias": "campb303", "assignedTo": "campb303", "subject": "Beepboop", "status": "Dont Delete", "priority": "", "department": "", "building": "", "dateReceived": "Tue, 23 Jun 2020 13:25:51 -0400"} - function App(){ const [darkMode, setDarkMode] = useState(false); - const [activeItem, setActiveItem] = useState(testItem); - const [sidebarOpen, setSidebarOpen] = useState(true); + const [activeItem, setActiveItem] = useState({}); + const [sidebarOpen, setSidebarOpen] = useState(false); const theme = webqueueTheme(darkMode); @@ -53,16 +51,18 @@ function App(){ return ( - - - - - - - - + + + + + + + + + + - + ); } diff --git a/src/ItemMetadataView.js b/src/ItemMetadataView.js deleted file mode 100644 index 9a08889..0000000 --- a/src/ItemMetadataView.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { makeStyles, Grid, Paper } from '@material-ui/core'; -import { Alert } from '@material-ui/lab' - - -export default function ItemMetadataView({item}){ - const LockedAlert = () => { - return ( - - {item.isLocked} - - ); - } - const useStyles = makeStyles({ - "gridContainer": { - paddingTop: ".75em", - } - }); - - const classes = useStyles(); - - const metadataFields = ["userEmail", "userAlias", "subject", "dateReceived", - "assignedTo", "status", "priority", "department", "building"]; - - return( - <> - {item.isLocked ? LockedAlert() : ""} - - {metadataFields.map((field) => { - const title = field.toUpperCase(); - const subtitle = item[field] === undefined ? "Unknown" : item[field]; - return ( - - - {title}: {subtitle} - - - ); - })} - - - ); -} \ No newline at end of file diff --git a/src/ItemView.js b/src/ItemView.js deleted file mode 100644 index c340f51..0000000 --- a/src/ItemView.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { Paper, Typography, makeStyles } from '@material-ui/core'; -import ItemMetadataView from "./ItemMetadataView" - -function ItemView(props){ - - const useStyles = makeStyles((theme) => ({ - "paperPadding": { - padding: theme.spacing(2), - } - })); - - const classes = useStyles(); - -return( - - - - - {props.activeItem["content"] ? props.activeItem["content"].map(line => ( - - {line} - - )): "" } - - ); -} - -export default ItemView; \ No newline at end of file diff --git a/src/components/EmailHeader/EmailHeader.js b/src/components/EmailHeader/EmailHeader.js new file mode 100644 index 0000000..3af9c0c --- /dev/null +++ b/src/components/EmailHeader/EmailHeader.js @@ -0,0 +1,73 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Card, CardHeader, makeStyles } from "@material-ui/core"; +import UserAvatar from "../UserAvatar/"; +import webqueue2Theme from "../../theme"; + +export default function EmailHeader({name, date, email}){ + + const theme = webqueue2Theme(false); + const useStyles = makeStyles({ + "verticalPadding": { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + "removeCardHeaderPadding": { + padding: "0" + } + }); + const classes = useStyles(); + + /** + * Returns the title for the user. + * @param {string} name Name of the user. + * @param {string} email Email address of the user. + * @example + * // Has alias and name + * return "campb303 (Justin Campbell)" + * // Has alias but no name + * return "campb303" + * // Has no alias and no name + * return "" + * @returns {string} + */ + function buildTitle(name, email){ + let title = ""; + const isNotEmpty = (value) => { + return value === "" || value === null ? false : true; + }; + if (isNotEmpty(name)){ + title += `${name}`; + }; + if (isNotEmpty(email)){ + title += ` <${email}>` + } + return title; + }; + + return( + + } + title={buildTitle(name, email)} + subheader={Date(date)} + classes={{root: classes.removeCardHeaderPadding}} + /> + + ); +}; + +EmailHeader.propTypes = { + /** Name of the user. */ + "name": PropTypes.string, + /** Date of the message. */ + "date": PropTypes.string, + /** Email address of the user. */ + "email": PropTypes.string +}; + +EmailHeader.defaultProps = { + "name": "", + "date": "", + "email": "" +}; \ No newline at end of file diff --git a/src/components/EmailHeader/EmailHeader.md b/src/components/EmailHeader/EmailHeader.md new file mode 100644 index 0000000..e163f38 --- /dev/null +++ b/src/components/EmailHeader/EmailHeader.md @@ -0,0 +1,9 @@ +Description of EmailHeader. + +```jsx +import EmailHeader from "./EmailHeader"; + +``` +```jsx static + +``` \ No newline at end of file diff --git a/src/components/EmailHeader/index.js b/src/components/EmailHeader/index.js new file mode 100644 index 0000000..47c222d --- /dev/null +++ b/src/components/EmailHeader/index.js @@ -0,0 +1 @@ +export { default } from "./EmailHeader"; \ No newline at end of file diff --git a/src/components/ItemBodyView/ItemBodyView.js b/src/components/ItemBodyView/ItemBodyView.js new file mode 100644 index 0000000..bed15c1 --- /dev/null +++ b/src/components/ItemBodyView/ItemBodyView.js @@ -0,0 +1,105 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Timeline, TimelineItem, TimelineSeparator, TimelineConnector, TimelineContent, TimelineDot } from '@material-ui/lab'; +import { Typography, makeStyles } from "@material-ui/core"; +import webqueue2Theme from "../../theme"; +import EmailHeader from "../EmailHeader/"; +import { objectIsEmpty } from "../../utilities"; + +export default function ItemBodyView({ item }) { + + const theme = webqueue2Theme(false); + const useStyles = makeStyles((theme) => ({ + missingOppositeContent: { + '&:before': { + content: '""', + flex: 0, + padding: '0', + }, + }, + })); + const classes = useStyles(theme); + + const generateTimelineItem = (section) => { + switch(section.type) { + case "directoryInformation": + if (section.content.length === 0){ + return "No Directory Information"; + } else { + return "Directory Information Present" + } + case "initialMessage": + return( + <> + + {section.content.map((line, index) => {line})} + + ); + case "edit": + return( + <> + + {`${section.by} assigned thisat ${Date(section.datetime)}`} + + {section.content.map((line) => {line})} + + ); + case "status": + return( + <> + + {`${section.by} update the status to at ${Date(section.datetime)}`} + + {section.content.map((line) => {line})} + + ); + case "assign": + return ( + + {`${section.by} assigned this to ${section.to} at ${Date(section.datetime)}`} + + ); + case "replyToUser": + return( + <> + + {`${section.by} replied ${Date(section.datetime)}`} + + {section.content.map((line) => {line})} + + ); + case "replyFromUser": + return( + <> + + {section.content.map((line, index) => {line})} + + ); + default: + return "No Match Found"; + }; + }; + + return ( + + {objectIsEmpty(item) ? "" : item.content.map((section, index) => { + return ( + + + + + + + {generateTimelineItem(section)} + + + ); + })} + + ); +}; + +ItemBodyView.propTypes = { + /** The item to diplay. */ + "item": PropTypes.object.isRequired +}; \ No newline at end of file diff --git a/src/components/ItemBodyView/ItemBodyView.md b/src/components/ItemBodyView/ItemBodyView.md new file mode 100644 index 0000000..2c224d1 --- /dev/null +++ b/src/components/ItemBodyView/ItemBodyView.md @@ -0,0 +1,23 @@ +The ItemBodyView displays the seven actions possible in an item: + +- **Directory information**: present when a user submits a ticket from the Contact Us page +- **Initial Message**: the first message a user sends. +- **Edit:** an internal note to and from ECN staff. +- **Status:** an internal summary of the current/next task for the item. +- **Assignment:** the career account alias for the ECN employee the item is assigned to. +- **Reply To User:** a message sent from an ECN employee to a user. +- **Reply To ECN:** a message sent from the user to ECN that has been merged into an existing item. + +```jsx +import ItemBodyView from "./ItemBodyView"; + +const demoItem = {"queue": "ce", "number": 100, "lastUpdated": "08-27-20 09:49 PM", "headers": [{"type": "Merged-Time", "content": "Tue, 23 Jun 2020 13:31:53 -0400"}, {"type": "Merged-By", "content": "campb303"}, {"type": "QTime", "content": "1"}, {"type": "QTime-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "QTime-Updated-By", "content": "campb303"}, {"type": "Time", "content": "1"}, {"type": "Time-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "Time-Updated-By", "content": "campb303"}, {"type": "Replied-Time", "content": "Tue, 23 Jun 2020 13:28:48 -0400"}, {"type": "Replied-By", "content": "campb303"}, {"type": "Edited-Time", "content": "Tue, 23 Jun 2020 13:27:56 -0400"}, {"type": "Edited-By", "content": "campb303"}, {"type": "QAssigned-To", "content": "campb303"}, {"type": "QAssigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "QAssigned-To-Updated-By", "content": "campb303"}, {"type": "Assigned-To", "content": "campb303"}, {"type": "Assigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "Assigned-To-Updated-By", "content": "campb303"}, {"type": "QStatus", "content": "Dont Delete"}, {"type": "QStatus-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "QStatus-Updated-By", "content": "campb303"}, {"type": "Status", "content": "Dont Delete"}, {"type": "Status-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "Status-Updated-By", "content": "campb303"}, {"type": "Date", "content": "Tue, 23 Jun 2020 13:25:51 -0400"}, {"type": "From", "content": "Justin Campbell "}, {"type": "Message-ID", "content": "<911CE050-3240-4980-91DD-9C3EBB8DBCF8@purdue.edu>"}, {"type": "Subject", "content": "Beepboop"}, {"type": "To", "content": "cesite@ecn.purdue.edu"}, {"type": "Content-Type", "content": "text/plain; charset=\"utf-8\""}, {"type": "X-ECN-Queue-Original-Path", "content": "/home/pier/e/queue/Attachments/inbox/2020-06-23/208-original.txt"}, {"type": "X-ECN-Queue-Original-URL", "content": "https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/208-original.txt"}], "content": [{"type": "directoryInformation", "content": []}, {"type": "initialMessage", "datetime": "2020-06-23T13:25:51-0400", "userName": "Justin Campbell", "userEmail": "campb303@purdue.edu", "ccRecipients": [], "content": ["Testtest\n"]}, {"type": "assign", "by": "campb303", "datetime": "2020-06-23T13:27:00-0400", "to": "campb303"}, {"type": "status", "by": "campb303", "datetime": "2020-06-23T13:26:55", "content": ["*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\n", "Dont Delete\n"]}, {"type": "edit", "by": "campb303", "datetime": "2020-06-23T13:27:56", "content": ["*** Edited by: campb303 at: 06/23/20 13:27:56 ***\n", "\n", "This be an edit my boy\n", "\n", "\n", "\n"]}, {"type": "replyToUser", "by": "campb303", "datetime": "2020-06-23T13:28:18", "content": ["*** Replied by: campb303 at: 06/23/20 13:28:18 ***\n", "\n", "This be a reply my son\n", "\n", "Justin\n", "ECN\n", "\n"]}, {"type": "replyFromUser", "datetime": "2020-06-23T13:30:45-0400", "subject": "Re: Beepboop", "userName": "Justin Campbell", "userEmail": "campb303@purdue.edu", "content": ["=== Additional information supplied by user ===\n", "\n", "Subject: Re: Beepboop\n", "From: Justin Campbell \n", "Date: Tue, 23 Jun 2020 13:30:45 -0400\n", "X-ECN-Queue-Original-Path: /home/pier/e/queue/Attachments/inbox/2020-06-23/212-original.txt\n", "X-ECN-Queue-Original-URL: https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/212-original.txt\n", "\n", "Huzzah!\n", "\n", "===============================================\n", "\n"], "ccRecipients": []}], "isLocked": "ce 100 is locked by knewell using qvi", "userEmail": "campb303@purdue.edu", "userName": "Justin Campbell", "userAlias": "campb303", "assignedTo": "campb303", "subject": "Beepboop", "status": "Dont Delete", "priority": "", "department": "", "building": "", "dateReceived": "2020-06-23T13:25:51-0400"}; + +
+ +
+``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/ItemBodyView/index.js b/src/components/ItemBodyView/index.js new file mode 100644 index 0000000..076a727 --- /dev/null +++ b/src/components/ItemBodyView/index.js @@ -0,0 +1,3 @@ +import ItemBodyView from "./ItemBodyView"; + +export default ItemBodyView; \ No newline at end of file diff --git a/src/components/ItemMetadataView/ItemMetadataView.js b/src/components/ItemMetadataView/ItemMetadataView.js new file mode 100644 index 0000000..e0f27bc --- /dev/null +++ b/src/components/ItemMetadataView/ItemMetadataView.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles, Typography } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import webqueue2Theme from "../../theme"; +import EmailHeader from '../EmailHeader/EmailHeader'; + + +export default function ItemMetadataView({item}){ + + const theme = webqueue2Theme(false); + const useStyles = makeStyles({ + "verticalSpacing": { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }, + "removeCardHeaderPadding": { + padding: "0" + } + }); + const classes = useStyles(); + + const LockedAlert = () => { + return ( + + {item.isLocked} + + ); + }; + + return( + <> + {item.isLocked ? LockedAlert() : ""} + + + {item.subject} + + + + + + Status, assignments, priority, refile, archive and delete controls coming soon to a theater near you. + + + + ); +} + +ItemMetadataView.propTypes = { + /** The item to be displayed. */ + "item": PropTypes.object.isRequired +}; \ No newline at end of file diff --git a/src/components/ItemMetadataView/ItemMetadataView.md b/src/components/ItemMetadataView/ItemMetadataView.md new file mode 100644 index 0000000..641eff7 --- /dev/null +++ b/src/components/ItemMetadataView/ItemMetadataView.md @@ -0,0 +1,16 @@ +The ItemMetadataView displays the metadata for an item as part of the [ItemView](/#/Components/ItemView). + +```jsx +import Paper from "@material-ui/core"; +import ItemMetadataView from "./ItemMetadataView"; + +const demoItem = {"queue": "ce", "number": 100, "lastUpdated": "07-23-20 10:11 PM", "headers": [{"type": "Merged-Time", "content": "Tue, 23 Jun 2020 13:31:53 -0400"}, {"type": "Merged-By", "content": "campb303"}, {"type": "QTime", "content": "1"}, {"type": "QTime-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "QTime-Updated-By", "content": "campb303"}, {"type": "Time", "content": "1"}, {"type": "Time-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "Time-Updated-By", "content": "campb303"}, {"type": "Replied-Time", "content": "Tue, 23 Jun 2020 13:28:48 -0400"}, {"type": "Replied-By", "content": "campb303"}, {"type": "Edited-Time", "content": "Tue, 23 Jun 2020 13:27:56 -0400"}, {"type": "Edited-By", "content": "campb303"}, {"type": "QAssigned-To", "content": "campb303"}, {"type": "QAssigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "QAssigned-To-Updated-By", "content": "campb303"}, {"type": "Assigned-To", "content": "campb303"}, {"type": "Assigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "Assigned-To-Updated-By", "content": "campb303"}, {"type": "QStatus", "content": "Dont Delete"}, {"type": "QStatus-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "QStatus-Updated-By", "content": "campb303"}, {"type": "Status", "content": "Dont Delete"}, {"type": "Status-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "Status-Updated-By", "content": "campb303"}, {"type": "Date", "content": "Tue, 23 Jun 2020 13:25:51 -0400"}, {"type": "From", "content": "Justin Campbell "}, {"type": "Message-ID", "content": "<911CE050-3240-4980-91DD-9C3EBB8DBCF8@purdue.edu>"}, {"type": "Subject", "content": "Beepboop"}, {"type": "To", "content": "cesite@ecn.purdue.edu"}, {"type": "Content-Type", "content": "text/plain; charset=\"utf-8\""}, {"type": "X-ECN-Queue-Original-Path", "content": "/home/pier/e/queue/Attachments/inbox/2020-06-23/208-original.txt"}], "content": ["Testtest\n", "\n", "*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\n", "Dont Delete\n", "*** Edited by: campb303 at: 06/23/20 13:27:56 ***\n", "\n", "This be an edit my boy\n", "\n", "\n", "\n", "*** Replied by: campb303 at: 06/23/20 13:28:18 ***\n", "\n", "This be a reply my son\n", "\n", "Justin\n", "ECN\n", "\n", "=== Additional information supplied by user ===\n", "\n", "Subject: Re: Beepboop\n", "From: Justin Campbell \n", "Date: Tue, 23 Jun 2020 13:30:45 -0400\n", "X-ECN-Queue-Original-Path: /home/pier/e/queue/Attachments/inbox/2020-06-23/212-original.txt\n", "X-ECN-Queue-Original-URL: https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/212-original.txt\n", "\n", "Huzzah!\n", "\n", "===============================================\n"], "isLocked": "ce 100 is locked by knewell using qvi", "userEmail": "campb303@purdue.edu", "userName": "Justin Campbell", "userAlias": "campb303", "assignedTo": "campb303", "subject": "Beepboop", "status": "Dont Delete", "priority": "", "department": "", "building": "", "dateReceived": "Tue, 23 Jun 2020 13:25:51 -0400"}; + +
+ +
+``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/ItemMetadataView/index.js b/src/components/ItemMetadataView/index.js new file mode 100644 index 0000000..7de396d --- /dev/null +++ b/src/components/ItemMetadataView/index.js @@ -0,0 +1,3 @@ +import ItemMetadataView from "./ItemMetadataView"; + +export default ItemMetadataView; \ No newline at end of file diff --git a/src/components/ItemTable/ItemTable.md b/src/components/ItemTable/ItemTable.md index 0631361..335eee0 100644 --- a/src/components/ItemTable/ItemTable.md +++ b/src/components/ItemTable/ItemTable.md @@ -1,4 +1,4 @@ -The ItemTable is the primary view for webqueue2. It displays item metadata for items of selected queues and allows for filtering by field and opening an item by clicking. By default, it is pre-configured to fetch data from the webqueue2 API. +The ItemTable is the primary view for webqueue2. It displays item metadata for items of selected queues and allows for filtering by field and opening an item by clicking. By default, it is pre-configured to fetch data from the webqueue2 API. It is to be used with the [ItemTableAppBar](/#/Components/ItemTableAppBar). It is based on [material-table](https://material-table.com/). ```jsx diff --git a/src/CustomAppBar.js b/src/components/ItemTableAppBar/ItemTableAppBar.js similarity index 68% rename from src/CustomAppBar.js rename to src/components/ItemTableAppBar/ItemTableAppBar.js index c5c8f63..e6f032a 100644 --- a/src/CustomAppBar.js +++ b/src/components/ItemTableAppBar/ItemTableAppBar.js @@ -1,11 +1,11 @@ import React from "react"; +import PropTypes from 'prop-types'; import {makeStyles, Tooltip, Typography, AppBar, Toolbar, IconButton, Zoom} from "@material-ui/core" -import MenuIcon from "@material-ui/icons/Menu"; import DarkModeIcon from '@material-ui/icons/Brightness4'; import LightModeIcon from '@material-ui/icons/Brightness7'; -export default function CustomAppBar(props){ +export default function ItemTableAppBar(props){ const useStyles = makeStyles((theme) => ({ menuButton: { marginLeft: theme.spacing(2), @@ -36,12 +36,23 @@ export default function CustomAppBar(props){ {props.darkMode ? : } - - - - ); -} \ No newline at end of file +}; + +ItemTableAppBar.propTypes = { + /** The title of the app bar. */ + "title": PropTypes.string, + /** Function to toggle darkMode. */ + "setDarkMode": PropTypes.func.isRequired, + /** State variable for darkMode */ + "darkMode": PropTypes.bool.isRequired, + /** The webqueue2 MUI theme. */ + "theme": PropTypes.object.isRequired +}; + +ItemTableAppBar.defaultProps = { + "title": "" +}; \ No newline at end of file diff --git a/src/components/ItemTableAppBar/ItemTableAppBar.md b/src/components/ItemTableAppBar/ItemTableAppBar.md new file mode 100644 index 0000000..e870d28 --- /dev/null +++ b/src/components/ItemTableAppBar/ItemTableAppBar.md @@ -0,0 +1,16 @@ +The ItemTableAppBar is the primary toolbar for the [ItemTable](/#/Components/ItemTable). It displays the application title and application wide actions. + +```jsx +import React, { useState } from "react"; +import webqueue2Theme from "../../theme.js"; + +const [darkMode, setDarkMode] = useState(false); + +const theme = webqueue2Theme(darkMode); + + +``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/ItemTableAppBar/index.js b/src/components/ItemTableAppBar/index.js new file mode 100644 index 0000000..474e0bc --- /dev/null +++ b/src/components/ItemTableAppBar/index.js @@ -0,0 +1,3 @@ +import ItemTableAppBar from "./ItemTableAppBar"; + +export default ItemTableAppBar; \ No newline at end of file diff --git a/src/components/ItemView/ItemView.js b/src/components/ItemView/ItemView.js new file mode 100644 index 0000000..e584207 --- /dev/null +++ b/src/components/ItemView/ItemView.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from "prop-types"; +import { Paper, makeStyles } from '@material-ui/core'; +import ItemMetadataView from "../ItemMetadataView/" +import ItemBodyView from "../ItemBodyView"; + +export default function ItemView(props){ + + const useStyles = makeStyles((theme) => ({ + "paperPadding": { + padding: theme.spacing(2), + } + })); + + const classes = useStyles(); + +return( + + + + + ); +}; + +ItemView.propTypes = { + /** The item to be viewed. */ + "activeItem": PropTypes.object.isRequired +}; \ No newline at end of file diff --git a/src/components/ItemView/ItemView.md b/src/components/ItemView/ItemView.md new file mode 100644 index 0000000..0546b58 --- /dev/null +++ b/src/components/ItemView/ItemView.md @@ -0,0 +1,18 @@ +The ItemView is the primary view for an iten. It displays the messages and actions in a timeline view. + +```jsx +import React, { useState } from "react"; + +const demoItem = {"queue": "ce", "number": 100, "lastUpdated": "07-23-20 10:11 PM", "headers": [{"type": "Merged-Time", "content": "Tue, 23 Jun 2020 13:31:53 -0400"}, {"type": "Merged-By", "content": "campb303"}, {"type": "QTime", "content": "1"}, {"type": "QTime-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "QTime-Updated-By", "content": "campb303"}, {"type": "Time", "content": "1"}, {"type": "Time-Updated-Time", "content": "Tue, 23 Jun 2020 13:28:50 EDT"}, {"type": "Time-Updated-By", "content": "campb303"}, {"type": "Replied-Time", "content": "Tue, 23 Jun 2020 13:28:48 -0400"}, {"type": "Replied-By", "content": "campb303"}, {"type": "Edited-Time", "content": "Tue, 23 Jun 2020 13:27:56 -0400"}, {"type": "Edited-By", "content": "campb303"}, {"type": "QAssigned-To", "content": "campb303"}, {"type": "QAssigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "QAssigned-To-Updated-By", "content": "campb303"}, {"type": "Assigned-To", "content": "campb303"}, {"type": "Assigned-To-Updated-Time", "content": "Tue, 23 Jun 2020 13:27:00 EDT"}, {"type": "Assigned-To-Updated-By", "content": "campb303"}, {"type": "QStatus", "content": "Dont Delete"}, {"type": "QStatus-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "QStatus-Updated-By", "content": "campb303"}, {"type": "Status", "content": "Dont Delete"}, {"type": "Status-Updated-Time", "content": "Tue, 23 Jun 2020 13:26:55 EDT"}, {"type": "Status-Updated-By", "content": "campb303"}, {"type": "Date", "content": "Tue, 23 Jun 2020 13:25:51 -0400"}, {"type": "From", "content": "Justin Campbell "}, {"type": "Message-ID", "content": "<911CE050-3240-4980-91DD-9C3EBB8DBCF8@purdue.edu>"}, {"type": "Subject", "content": "Beepboop"}, {"type": "To", "content": "cesite@ecn.purdue.edu"}, {"type": "Content-Type", "content": "text/plain; charset=\"utf-8\""}, {"type": "X-ECN-Queue-Original-Path", "content": "/home/pier/e/queue/Attachments/inbox/2020-06-23/208-original.txt"}], "content": ["Testtest\n", "\n", "*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\n", "Dont Delete\n", "*** Edited by: campb303 at: 06/23/20 13:27:56 ***\n", "\n", "This be an edit my boy\n", "\n", "\n", "\n", "*** Replied by: campb303 at: 06/23/20 13:28:18 ***\n", "\n", "This be a reply my son\n", "\n", "Justin\n", "ECN\n", "\n", "=== Additional information supplied by user ===\n", "\n", "Subject: Re: Beepboop\n", "From: Justin Campbell \n", "Date: Tue, 23 Jun 2020 13:30:45 -0400\n", "X-ECN-Queue-Original-Path: /home/pier/e/queue/Attachments/inbox/2020-06-23/212-original.txt\n", "X-ECN-Queue-Original-URL: https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/212-original.txt\n", "\n", "Huzzah!\n", "\n", "===============================================\n"], "isLocked": "ce 100 is locked by knewell using qvi", "userEmail": "campb303@purdue.edu", "userName": "Justin Campbell", "userAlias": "campb303", "assignedTo": "campb303", "subject": "Beepboop", "status": "Dont Delete", "priority": "", "department": "", "building": "", "dateReceived": "Tue, 23 Jun 2020 13:25:51 -0400"}; + + +const [activeItem, setActiveItem] = useState({}); +const [sidebarOpen, setSidebarOpen] = useState(false); + + + +``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/ItemView/index.js b/src/components/ItemView/index.js new file mode 100644 index 0000000..44014ba --- /dev/null +++ b/src/components/ItemView/index.js @@ -0,0 +1,3 @@ +import ItemView from "./ItemView"; + +export default ItemView; \ No newline at end of file diff --git a/src/ItemViewAppBar.js b/src/components/ItemViewAppBar/ItemViewAppBar.js similarity index 73% rename from src/ItemViewAppBar.js rename to src/components/ItemViewAppBar/ItemViewAppBar.js index 45dec54..a150cd6 100644 --- a/src/ItemViewAppBar.js +++ b/src/components/ItemViewAppBar/ItemViewAppBar.js @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import {makeStyles, Tooltip, Typography, AppBar, Toolbar, IconButton, Zoom} from "@material-ui/core" import CloseItemViewIcon from '@material-ui/icons/ChevronRight'; @@ -36,4 +37,17 @@ export default function ItemViewAppBar(props){ ); -} \ No newline at end of file +} + +ItemViewAppBar.propTypes = { + /** The webqueue2 MUI theme. */ + "theme": PropTypes.object.isRequired, + /** Function to toggle sidebar open. */ + "setSidebarOpen": PropTypes.func.isRequired, + /** The title of the app bar. */ + "title": PropTypes.string +}; + +ItemViewAppBar.defaultProps = { + "title": "" +}; \ No newline at end of file diff --git a/src/components/ItemViewAppBar/ItemViewAppBar.md b/src/components/ItemViewAppBar/ItemViewAppBar.md new file mode 100644 index 0000000..acf98df --- /dev/null +++ b/src/components/ItemViewAppBar/ItemViewAppBar.md @@ -0,0 +1,17 @@ +The ItemViewAppBar is the primary toolbar for the [ItemView](/#/Components/ItemView). It displays the item title and action for closing the sidebar. + +```jsx +import React, { useState } from "react"; +import ItemViewAppBar from "./ItemViewAppBar"; +import webqueue2Theme from "../../theme.js"; + +const theme = webqueue2Theme(false); + +const [sidebarOpen, setSidebarOpen] = useState(false); + + +``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/ItemViewAppBar/index.js b/src/components/ItemViewAppBar/index.js new file mode 100644 index 0000000..4c009ea --- /dev/null +++ b/src/components/ItemViewAppBar/index.js @@ -0,0 +1,3 @@ +import ItemViewAppBar from "./ItemViewAppBar"; + +export default ItemViewAppBar; \ No newline at end of file diff --git a/src/components/TeamMemberCard/TeamMemberCard.js b/src/components/TeamMemberCard/TeamMemberCard.js index 832ac6b..febfa5e 100644 --- a/src/components/TeamMemberCard/TeamMemberCard.js +++ b/src/components/TeamMemberCard/TeamMemberCard.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types' import { Card, CardHeader, Avatar, IconButton, CardContent, - CardActions, Typography, Link, makeStyles } from '@material-ui/core'; + CardActions, Link, makeStyles } from '@material-ui/core'; import WebsiteIcon from '@material-ui/icons/Language'; import webqueue2Theme from "../../theme"; diff --git a/src/components/UserAvatar/UserAvatar.js b/src/components/UserAvatar/UserAvatar.js new file mode 100644 index 0000000..9a56370 --- /dev/null +++ b/src/components/UserAvatar/UserAvatar.js @@ -0,0 +1,21 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Avatar } from "@material-ui/core"; + +export default function UserAvatar({userName, userAlias}){ + + return( + + {userName === "" ? null : userName.charAt(0)} + + ); +}; + +UserAvatar.propTypes = { + /** The name of the user. */ + "userName": PropTypes.string +}; + +UserAvatar.defaultProps = { + "userName": "" +}; \ No newline at end of file diff --git a/src/components/UserAvatar/UserAvatar.md b/src/components/UserAvatar/UserAvatar.md new file mode 100644 index 0000000..40c9a48 --- /dev/null +++ b/src/components/UserAvatar/UserAvatar.md @@ -0,0 +1,23 @@ +The UserAvatar displays a graphical representation of a person and is meant to be used in conjunction with other identifiers like names or emails. + +--- + +The UserAvatar will the first letter of the `userName` prop. +```jsx +import UserAvatar from "./UserAvatar"; + +``` + +```jsx static + +``` + +If no value, a null value, or an empty string is passed to the UserAvatar, it will fall back to a generic icon. +```jsx +import UserAvatar from "./UserAvatar"; + +``` + +```jsx static + +``` \ No newline at end of file diff --git a/src/components/UserAvatar/index.js b/src/components/UserAvatar/index.js new file mode 100644 index 0000000..3f9a167 --- /dev/null +++ b/src/components/UserAvatar/index.js @@ -0,0 +1,3 @@ +import UserAvatar from "./UserAvatar"; + +export default UserAvatar; \ No newline at end of file diff --git a/styleguidist/styleguide.config.js b/styleguidist/styleguide.config.js index f08f9c1..de44a92 100644 --- a/styleguidist/styleguide.config.js +++ b/styleguidist/styleguide.config.js @@ -119,8 +119,7 @@ module.exports = { */ getComponentPathLine(componentPath) { const name = path.basename(componentPath, '.js') - const dir = path.dirname(componentPath) - return `import ${name} from '${dir}/';` + return `import ${name} from './components/${name}/';` }, /**