diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000..ce9fe0f82ab --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,5 @@ +ignore: + - "src/main/java/seedu/address/ui" + - "src/main/java/seedu/address/model/util" + - "src/main/java/seedu/address/Main.java" + - "src/main/java/seedu/address/MainApp.java" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..1e1862b0e82 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: MarkBind Action + +on: + push: + branches: + - master + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - name: Install Graphviz + run: sudo apt-get install graphviz + - name: Install Java + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build & Deploy MarkBind site + uses: MarkBind/markbind-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + rootDirectory: './docs' + baseUrl: '/tp' # replace with your repo name + version: '^5.1.0' diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..eab4c7db6a5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +docs/_markbind/logs/ diff --git a/README.md b/README.md index 13f5c77403f..edd2b07dfa2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - -![Ui](docs/images/Ui.png) - -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +[![Java CI](https://github.com/AY2324S1-CS2103-F13-4/tp/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/AY2324S1-CS2103-F13-4/tp/actions/workflows/gradle.yml) + +![Ui](docs/images/unimateScreenshot.png) + + +**UniMate** is a powerful desktop application designed to streamline contact and events management. +Users can effortlessly manage their contacts, schedule events, and keep track of important information, all in one place. + +* For the detailed documentation of this project, see the **[UniMate Product Website](https://ay2324s1-cs2103-f13-4.github.io/tp/)**. +* If you are interested about developing UniMate, the **[Developer Guide](https://ay2324s1-cs2103-f13-4.github.io/tp/DeveloperGuide.html)** is a good place to start. + +### Acknowledgements +- This project is forked from the AddressBook-Level3 project created by [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index a2951cc709e..89f3fc53dd3 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = '[CS2103-F13-4][UniMate].jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..1748e487fbd --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +_markbind/logs/ + +# Dependency directories +node_modules/ + +# Production build files (change if you output the build to a different directory) +_site/ + +# Env +.env +.env.local + +# IDE configs +.vscode/ +.idea/* +*.iml diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..0cc7444bb28 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,59 +1,58 @@ --- -layout: page -title: About Us + layout: default.md + title: "About Us" --- +# About Us + We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +This project was done by a team of 4 members. ## Project team -### John Doe - - - -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Project Advisor - -### Jane Doe +### Li Hongguang - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/lihongguang00)] +[[portfolio](team/lihongguang00.md)] -* Role: Team Lead -* Responsibilities: UI +- Role: Developer +- Responsibilities: + - Calendar System Developer -### Johnny Doe +### Andre Sim - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/fallman2)] +[[portfolio](team/fallman2.md)] -* Role: Developer -* Responsibilities: Data +- Role: Developer +- Responsibilities: + - Task List Developer -### Jean Doe +### Low Jun Hong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/junhonglow)] +[[portfolio](team/junhonglow.md)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +- Role: Developer +- Responsibilities: + - Event List Developer + - Storage Developer -### James Doe +### Nicholas Lee - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/nicrandomlee)] +[[portfolio](team/nicrandomlee.md)] -* Role: Developer -* Responsibilities: UI +- Role: Developer +- Responsibilities: + - Product Feature Enhancements + - Product Tester diff --git a/docs/Configuration.md b/docs/Configuration.md index 13cf0faea16..32f6255f3b9 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,6 +1,8 @@ --- -layout: page -title: Configuration guide + layout: default.md + title: "Configuration guide" --- +# Configuration guide + Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`). diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..8228c845e86 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -1,12 +1,15 @@ --- -layout: page -title: DevOps guide + layout: default.md + title: "DevOps guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# DevOps guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Build automation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..3bdc5c25749 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,15 +1,25 @@ --- -layout: page -title: Developer Guide +layout: default.md +title: "Developer Guide" +pageNav: 3 --- -* Table of Contents -{:toc} + +# UniMate Developer Guide + + + -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +UniMate is based off the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project created by the [SE-EDU initiative](https://se-education.org/). + +-------------------------------------------------------------------------------------------------------------------- + +## **Note about terms used in DG** + +In this document, `UniMateCalendar` and `Calendar` are used synonymously. -------------------------------------------------------------------------------------------------------------------- @@ -21,14 +31,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). ## **Design** -
- -:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. -
- ### Architecture - + The ***Architecture Diagram*** given above explains the high-level design of the App. @@ -53,7 +58,7 @@ The bulk of the app's work is done by the following four components: The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. - + Each of the four main components (also shown in the diagram above), @@ -62,7 +67,7 @@ Each of the four main components (also shown in the diagram above), For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. - + The sections below give more details of each component. @@ -70,9 +75,11 @@ The sections below give more details of each component. The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) -![Structure of the UI Component](images/UiClassDiagram.png) + + -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BottomListPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) @@ -89,34 +96,36 @@ The `UI` component, Here's a (partial) class diagram of the `Logic` component: - + The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) + + + -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. -
+**Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. +1. When `Logic` is called upon to execute a command, it is passed to an `UniMateParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. 1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: - + How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. +* When called upon to parse a user command, the `UniMateParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `UniMateParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - + The `Model` component, @@ -126,22 +135,24 @@ The `Model` component, * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+ - +**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
-
+ + + ### Storage component **API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) - + The `Storage` component, -* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). +* can save address book data, calendar data, task manager data and user preference data in JSON format, and read them back into corresponding objects. +* inherits from `AddressBookStorage`,`CalendarStorage`, `TaskManagerStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) ### Common classes @@ -154,6 +165,409 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +//@@author lihongguang00 +### Adding Events + +#### Implementation + +The addEvent feature is facilitated by the `Calendar` class. It allows the users to block out some time +in their personal `Calendar` with some `Event` that has the following attributes: + +* `DESCRIPTION`  —  Brief description of the `Event` +* `START_DATE_TIME`  —  Starting date and time of the `Event` +* `END_DATE_TIME`  —  End date and time of the `Event` + +The syntax used to call this command is as follows: `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME`, +with the `START_DATE_TIME` and `END_DATE_TIME` in the `yyyy-MM-dd HH:mm` format. If any of the fields are missing +or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct +formatting. + +Given below is an example usage scenario and how the addEvent feature behaves at each step. + +Step 1. The user launches the application for the first time. The user's personal `Calendar` will be initialized +as an empty calendar. + +Step 2. The user executes `addEvent d/Go to school ts/2023-10-26 08:00 te/2023-10-26 16:00` to add the `Event` +of `Go to school` from `2023-10-26 8am` to `2023-10-26 4pm`. This will call the `UniMateParser#execute()`, +passing in the user input from the user. + +Step 3. Since the command is an `addEvent` command, it passes the user input to `AddEventCommandParser#parse()` +for parsing. + +Step 4. The `AddEventCommandParser#parse` command will parse the command into 3 argument fields  —  +`DESCRIPTION`, `START_DATE_TIME` and `END_DATE_TIME`. The `DESCRIPTION` is passed into +`ParserUtil#parseEventDescription()` to produce a `EventDescription` object, while the `START_DATE_TIME` +and `END_DATE_TIME` are passed into `ParserUtil#parseEventPeriod()` to produce a `EventPeriod` object. + +**Note**: If the `DESCRIPTION` is empty, or the `START_DATE_TIME` or `END_DATE_TIME` are of invalid +format, a `ParseException` will be thrown, displaying the appropriate command usage format. + +Step 5. The `EventDescription` and `EventPeriod` objects produced in Step 4 are then passed into +the constructor for `Event`, creating an `Event` object with the respective user-defined attributes. This +`Event` object is then passed into the constructor of the `AddEventCommand` object. + +Step 6. `AddEventCommand#execute()` is then called, and the calendar will check if there is an existing +`Event` that has a conflicting timing with the new `Event` to be added. Since the calendar is empty, +no errors will be raised and the user will see his new `Event` displayed in the UI in the `My Calendar` +region. + +**Note**: Suppose there is a conflicting `Event` that already exists in the `Calendar` +with the `Event` to be added, the new `Event` will not be added, and a message that states that there +is a timing conflict will be reflected in the UI status bar. + +#### Design considerations: + +**Aspect: Appropriate time period of an event:** + +* **Alternative 1 (Current choice): No restriction on time period** + * Pros: Allows users to add multi-day `Event` such as multi-day business trips. + * Cons: For users that rarely have multi-day `Event`, having to type the date twice when calling the command might be troublesome. +* **Alternative 2: Restrict events to a single day** + * Pros: User only has to type date once when calling command. + * Cons: For multi-day `Event`, user has to call the command multiple times for all the relevant days + + +//@@author Fallman2 +### Deleting Events + +#### Implementation + +The deletion of events is facilitated by the `model::deleteEventAt` method and the `model::findEventAt` method. The +former method deletes the event stored in the `Calendar` object which itself is an attribute of the `Model` object by +calling a similar method `Calendar::deleteEventAt`. These methods take in a `LocalDateTime` object and finds the method +within the `Calendar` object, then in the case of `model::deleteEventAt`, deletes the event. Given below is an example +usage scenario of the command. + +Step 1. The user launches the application and creates an event. + +Step 2. The user executes `deleteEvent 2023-12-09 12:00` command to delete the event at that time. The `deleteEvent` +command calls `Model#findEventAt(LocalDateTime)` to find an event at the specified date and time. This event is then stored as a +variable `toDelete`. + +**Note**: If no event is found at the specified date and time at any point of the command execution, an +`EventNotFoundException` is thrown which causes an error message to be displayed. + +Step 3. The command then calls `Model#deleteEventAt(LocalDateTime)`. This method calls similar methods of +`Calendar#deleteEventAt(LocalDateTime)` which calls `AllDaysEventListManager#deleteEventAt(LocalDateTime)`. + +Step 4. The `AllDaysEventListManager` checks for an event at the specified date and time again, then checks for all days +for which the event lasts for. Then, for each day, the event is removed from the `SingleDayEventList`. + +Step 5. The deleted event which was previously stored in as a variable is displayed in the `CommandResult` to show the +user which command was deleted. + +**Design considerations** + +**Aspect: Appropriate input:** + +* **Alternative 1: Use Event start time as input:** + * Pros: Allows the user to more specifically select which event is deleted. + * Cons: The exact start time of the event has to be used to delete the event. +* **Alternative 2 (Current Choice): Use any time within the event as input:** + * Pros: Allows for more flexibility in the time given for the event to be deleted. + * Cons: User may be confused on which event is being deleted. + +### Clearing all events within a time frame + +All events within a time frame can be cleared using the `ClearEvents` command. +The command requires the start and end time of the time frame to be cleared as inputs. +All events that fall within or intersect with the time frame (start time inclusive, end time exclusive) are selected to be deleted. +This command also has an optional confirmation as input. If the confirmation is not present in the input, the deletion will not be executed and instead, the events +that would have been executed are displayed in the result. The command can then be reentered with the confirmation to confirm the deletion. + +#### Implementation + +Given below is an example usage scenario of the command. + +Step 1. The user launches the application and creates multiple events. + +Step 2. The user executes `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00` to view all events within the time range. + +Step 3. The `ClearEventsCommandParser` creates an `EventPeriod` representing the indicated time frame and creates a new `ClearEventsCommand` with this `EventPeriod`. + +**Note**: At this step, if no events are found within the `EventPeriod` given, a `CommandException` is thrown and an error message is displayed. + +Step 4. The `ClearEventsCommand` calls the `Model#eventsInRange(EventPeriod)` method to find all events in the specified time range. + +Step 5. All events within the time range are returned and displayed in the command result. + +Step 6. The user views all events that would be deleted. + +Step 7. The user executes `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00 c/CONFIRMED` to confirm the deletion. + +Step 8. The `ClearEventsCommand` calls the `Model#eventsInRange(EventPeriod)` method to find all events in the specified time range. + +Step 9. For each event in range, the command calls the `Model#deleteEventsInRange(EventPeriod)` which deletes all events within the range. + +Step 10. All deleted events are displayed in the command result. + + + +### Calendar Comparison (compareCalendars) + +#### Implementation + +When the command `compareCalendars` is called, the arguments passed by the users are converted into a list of `Index` in `CompareCalendarByIndexCommandParser#parse()`. +The list of `Index` is then passed as an argument into the constructor `CompareCalendarByIndexCommand#new()`. Subsequently, when `CompareCalendarByIndexCommand#execute()` is called, +the list of `Person` stored in the AddressBook is retrieved using `ModelManger#getFilteredPersonList()`, and the `Person` associated with each `Index` is extracted +with `List#get()`. Their respective `UniMateCalendar` is then extracted with `Person#getCalendar()`, and all the `UniMateCalendar` of the user and relevant contacts are +merged and reduce into a single `UniMateCalendar` with `UniMateCalendar#combineCalendar()`. This combined `UniMateCalendar` is then used to produce the grey event cards +seen in the pop-up comparison calendar window displayed to the user with `CalendarEventSpace#addSolidEventCards()`. + +Step 1. The user launches the application and creates an event in the current week. + +Step 2. The user creates an event in the current week for one of their contact. For this example, let's assume the contact is `Alex Yeoh` at `Index` 1 of the AddressBook. + +Step 3. The user executed `compareCalendars 1`. The `CompareCalendarByIndexCommandParser#parse()` creates a list of `Index` containing a single `Index` object from + created from the user's argument `1`, and passed it into `CompareCalendarByIndexCommand#new()`. + +Step 4. The `CompareCalendarByIndexCommand#execute()` will turn the `Index` list into a `Stream` with `Arrays#stream()`, and each `Index` in the stream will be +converted into the `Person` with the given `Index` with `Stream#map()` and `List#get()` as the `map()` function input. + +Step 5. The stream is further converted into a `Stream` of `UniMateCalendar` with `Stream#map()` and `Person#getCalendar()` as the `map()` function input. + +Step 6. Eventually, we call `Stream#reduce()` with the user's `UniMateCalendar` as seed, merging all of the `UniMateCalendar` in the `Stream` into a single one with `UniMateCalendar#combineCalendar()`. + +Step 7. The resultant `UniMateCalendar` then has the `Event` stored in it converted into grey event cards with `CalendarEventSpace#addSolidEventCards()`, which +reflects in the resultant pop-up comparison calendar window. + + + +**Design Consideration** + +**Aspect: Whether to show calendar on a new pop-up or in the main GUI:** + +* **Alternative 1 (Current choice): Pop-up window** + * Pros: Looks cleaner since the comparison calendar is in an isolated window. + * Cons: User needs to close the pop-up, which might be inconvenient. +* **Alternative 2: Display the calendar on the main application GUI** + * Pros: Less application tabs for the user to manage. + * Cons: More clutter on the main GUI. + + +### Contact Filtering + +#### Implementation + +The filtering function executed by `FilterCommand` is facilitated by the `PersonFilter` class. +which itself is similar to the `EditPersonDescriptor` class found in `EditCommand.java`. It stores the fields by which +the contacts are to be filtered and creates a predicate to facilitate the filtering. Notably, it implements the +following operation: + +* `PersonFilter#matchesFilter(Person)` - Compares the values of the attributes of the `Person` to the strings stored as + attributes in the `PersonFilter` object. This method is later used as a lambda method to filter the contact list. + +Given below is an example of how the filter function works at each step. + +Step 1. The user executes `filter n/Bob t/CS` to filter contacts to see only people with "Bob" in their name and have at +least 1 tag with "CS" in it. The input is passed to `UniMateParser` which then parses it with the `FilterCommandParser`. + +Step 2. The `FilterCommandParser` parses the input and creates a corresponding `PersonFilter` object with null for all +parameters. It then sets all specified attributes of the created `PersonFilter` while leaving unspecified fields as +null. The `FilterCommandParser` finally returns a newly created `FilterCommand` with the PersonFilter used in the +constructor. + +Step 3. `FilterCommand#execute` is called. In this method, `model#updateFilteredPersonList` is called with +`PersonFilter#matchesFilter` being used as the predicate. This updates the GUI and populates the filtered list with +only `Person` objects that match the filter. + +Step 4. The number of people displayed is returned as a `CommandResult`. + +//@@author nicrandomlee +### Contact Sorting + +#### Implementation +The sort function executed by `SortCommand`. + +The sort function allows users to sort all persons in UniMate based on a given criteria, and has the following attributes: + +* `COMPARATOR`  —  AddressBook sorting criteria +* `reverse`  —  Determines if sorting is by descending order + +The syntax used to call this command is as follows: `sort /COMPARATOR [/reverse]`, +with the `COMPARATOR` being one of `/byname`, `/byemail`, `/byphone`, `/byaddress` to sort by name, email, phone and address respectively. If any of the fields are missing or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct formatting. The `/reverse` parameter is optional to sort in descending order instead. + +Given below is an example of how the sort function works at each step. We will simulate a user using the sort function to sort UniMate contacts by name in descending order. + +Step 1. The user executes `sort /byname /reverse` to find his friend's contact. The input is passed into `UniMateParser` which then parses it with the `SortCommandParser`. + +Step 2. The `SortCommandParser` parses the input and first checks for arguments provided. If the arguments are empty, invalid or in the wrong format, a helper message will appear to allow the user to reference the sample run case. The arguments are then matched by the keywords provided to determine the basis for sorting using a `SortComparator`. All the comparators are added into an ArrayList of `SortComparator` for `SortCommand` to parse. + +Step 3. `SortCommand` is initialized parses the array from step 2 to determine the basis of comparison when the command is executed. The `SortCommandParser` finally returns a newly created `SortCommand` consisting of a Person Comparator that decides the method of sorting for the UniMate address book. + +Step 4. `SortCommand#execute` is called. In this method, `model#sortPersonList` is called with the Person Comparator created in step 3. This in turn calls `AddressBook#sortPersons` which calls the storage function to save the contacts in the json file based on the sorted order. + +Step 5. The GUI then reads in the json file to obtain the order of addresses and populates the sorted list with the sorting criteria provided. + +Step 6. The success message is returned as a `CommandResult` and displayed on the GUI result display panel. + +Here's a sequence diagram to summarise the steps above: + + + +**Design considerations** + +* The design of the `sort` command is dependent on the structure of the `AddressBookStorage` object. Should the structure + of how the AddressBook objects are stored change, a new implementation will be required for the command. + +//@@author + +### TaskList Feature + +The task list feature is facilitated by 'TaskManager'. It extends a ReadOnlyTaskManager that will be used for +saving users' tasks. The data of the TaskManager is contained in a `TaskList` object. Additionally, it implements the following operations: + +* `TaskManager#addTask(Task)` -- Adds a task to the current task list and saves it to memory. +* `TaskManager#deleteTask(int)` -- Delete an existing task from the current task list as indicated by its index and saves the change to memory. +* `TaskManager#sortTasksBy(String)` -- Sets the comparator by which the internal TaskList is sorted to one of two preset options. +This method only accepts the strings `"Description"` or `"Deadline"` as input and throws an error otherwise. +* `TaskManager#getTaskList()` -- Returns and exposes the internal `TaskList` as an unmodifiable `ObservableList` that can be 'observed'. + +These operations are exposed in the `Model` interface as `Model#addTask(Task)`, `Model#deleteTask(int)`, `Model#sortTasksBy` and `Model#getTaskList` respectively. + +A `Task` object consists of a `TaskDescription` and can have up to 1 `Deadline`. + +#### Adding Tasks + +The adding of tasks is facilitated by the `TaskManager#addTask(Task)` method. +The method adds a `Task` to the `TaskList` object which itself is an attribute of the `TaskManager` object by +calling a similar method `TaskList#addTask(Task)`. + +#### Implementation + +These methods take in a `Task` object and adds the `Task` object to the `TaskList`. +Given below is an example usage scenario of the command. + +Step 1. The user launches the application. + +Step 2. The user executes `addTask d/CS2105 Assignment te/2023-12-12 12:00 ` command to add the task. + +**Note**: While `Tasks` can have the same `TaskDescription` or `Deadline`, they cannot have both the same `TaskDescription` and `Deadline`. + +Step 3. The `addTask` method in the model is called, which calls the `addTask` method in the TaskManager, adding the task to the `TaskList`. + +Step 4. The `TaskList` is saved to the memory. + +**Design considerations** + +* The design of the `addTask` command is such that a deadline is made optional since some tasks are recurring or do not have a specific deadline. + +### Viewing Tasks + +Tasks can only be viewed in the bottom panel of the GUI which, by default, shows the events list instead. +The viewing of tasks is facilitated by the `SwitchListCommand` which interacts with the UI to swap the bottom list between the event list and the task list. + +#### Implementation + +Given below is an example usage scenario of the command. + +Step 1: The user launches the application. + +Step 2: The user executes `switchList` command to view the task list. + +**Note**: If the task list is already displayed, the command switches the list back to the event list instead. + +Step 3: The `MainWindow` class of the UI switches the bottom list to the task list. The task list will now display the task list, allowing the user to view all tasks. + +**Design considerations** + +**Aspect: Mode of display:** + +* **Alternative 1 (Current Choice): Display the task list in the same area as another UI component:** + * Pros: The UI will be less cluttered and can have smaller minimum space. + * Cons: Both the event list and the task list cannot be viewed at the same time. +* **Alternative 2: Display the task list in a separate area of the UI:** + * Pros: Both the event list and the task list can be viewed at the same time. + * Cons: The UI may be more cluttered. + +#### Deleting Tasks + +The deletion of tasks is facilitated by the `TaskManager#deleteTask(int)` method. +The method deletes a `Task` from the `TaskList` object which is an attribute of the `TaskManager` object by calling a similar method `TaskList#deleteTask(int)`. +This in turn deletes the task in `TaskList` that is indicated by its index within the `TaskList`. + +#### Implementation + +Given below is an example usage scenario of the command. + +Step 1. The user launches the application. + +Step 2. The user executes `switchList` command to view the task list on the GUI. + +Step 3. The user executes `deleteTask 1` command to delete the first task shown in the task list as displayed in the GUI. + +Step 4. The `deleteTask` method in the model is called, which calls the `deleteTask` method in the TaskManager, deleting the task from the `TaskList`. + +Step 5. The `TaskList` is saved to the memory. + +**Design considerations** + +**Aspect: Appropriate Input:** + +* **Alternative 1: Use Task description and deadline as input:** + * Pros: User does not need to refer to the GUI to confirm which task is being deleted. + * Cons: The exact description and deadlines have to be provided since tasks can have the same description or deadline. +* **Alternative 2 (Current Choice): Use displayed index as input:** + * Pros: Much less input is required from the user. + * Cons: The user has to view the task list displayed in the GUI to confirm which task is being deleted. + +### Sorting Tasks + +#### Implementation + +Tasks in the TaskList can be sorted either alphabetically by description or by deadlines. The accepted inputs for these are `sortTasks Description` and `sortTasks Deadline` respectively. +The sorting of tasks is facilitated by the `sortTasks` command. +Given below is an example usage scenario of the command. + +Step 1. The user launches the application. + +Step 2. The user switches to the task list with the `switchList` command. + +Step 3. The user executes the `sortTasks Description` command to sort tasks by description alphabetically. + +Step 4. A `SortTasksCommand` is created after the command is parsed by the `SortTasksCommandParser`. + +Step 5. Upon execution, the `SortTasksCommand` calls `Model#sortTasks(String)` and passes the sorting method of `Description` represented by a String to it. + +Step 6. The `TaskManager#sortTasks(String)` method is called and the `sortingOrder` attribute of type `Comparator` of the `TaskManager` is set to the appropriate type. + +Step 7. The internal `TaskList` is sorted by the `sortingOrder`. + +//@@author nicrandomlee +### Edit Contact Event + +#### Implementation +The Edit Contact Event function executed by `editContactEvent` allows users to edit all person's calendar events in UniMate, and has the following attributes: + +* `PERSON_INDEX`  —  Index of the target person +* `EVENT_INDEX`  —  Index of the target person's calendar event +* `DESCRIPTION`  —  Brief description of the edited `Event` +* `NEW_START_DATE_TIME`  —  Starting date and time of the edited `Event` +* `NEW_END_DATE_TIME`  —  End date and time of the edited `Event` + +The syntax used to call this command is as follows: `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME][te/NEW_END_DATE_TIME]`, with the `START_DATE_TIME` and `END_DATE_TIME` in the `yyyy-MM-dd HH:mm` format. If any of the fields are missing or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct formatting. + +Given below is an example of how the editContactEvent function works at each step. We will simulate a user using the editContactEvent function to sort UniMate contacts by name in descending order. + +Step 1. The user executes `editContactEvent 1 1 d/CS2103 meeting ts/2023-11-11 10:00 te/2023-11-11 12:00` to reschedule the meeting with his CS2103 module group mate. The input is passed into `UniMateParser` which then parses it with the `EditContactEventCommandParser`. + +Step 2. The `EditContactEventCommandParser` parses the input and first checks for arguments provided. If the arguments are empty, invalid or in the wrong format, a helper message will appear to allow the user to reference the sample run case. The arguments are then matched by delimiters `d/`, `ts/` amd `te/` to determine the fields to be edited. If certain fields are empty (for example, the user just wants to change the time start and time end of the meeting), the event description from the edited event will be retained when `EditContactEventCommand#execute` is executed in step 4. + +Step 3. `EditContactEventCommandParser` creates a temporary event `EditEventDescriptor` as well as an array consisting of two elements, that is the PERSON_INDEX and EVENT_INDEX to be parsed. The `EditContactEventCommandParser` finally returns a newly created `EditContactEventCommand` consisting of the array and the temporary event. + +Step 4. `EditContactEventCommand#execute` is called. In this method, a new person object is created with the same attributes except an updated calendar with the updated event instead. `model#setPerson` is called to replace the target person with the new person. The observable list is then refreshed to show the new calendar event. + +Step 5. The success message is returned as a `CommandResult` and displayed on the GUI result display panel. + +Here's a sequence diagram to summarise the steps above: + + + +**Design considerations** + +* The design of the `EditContactEvent` command is dependent on the structure of the `CalendarStorage` object. Should the structure of how the Calendar objects are stored change, a new implementation will be required for the command. +//@@author + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -170,67 +584,76 @@ Given below is an example usage scenario and how the undo/redo mechanism behaves Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. -![UndoRedoState0](images/UndoRedoState0.png) + Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. -![UndoRedoState1](images/UndoRedoState1.png) + Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. -![UndoRedoState2](images/UndoRedoState2.png) + + + -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +**Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. -
+
Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. -![UndoRedoState3](images/UndoRedoState3.png) + -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather + + + +**Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. -
+ The following sequence diagram shows how the undo operation works: -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) + + + -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +**Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. -
+
The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. + -
+**Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. + + Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. -![UndoRedoState4](images/UndoRedoState4.png) + Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. -![UndoRedoState5](images/UndoRedoState5.png) + The following activity diagram summarizes what happens when a user executes a new command: - + #### Design considerations: **Aspect: How undo & redo executes:** * **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. * **Alternative 2:** Individual command knows how to undo/redo by itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. + * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). + * Cons: We must ensure that the implementation of each individual command are correct. _{more aspects and alternatives to be added}_ @@ -257,71 +680,493 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: + * has a need to manage a significant number of contacts * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps +* is an NUS student +* has a need to organize and coordinate schedules + **Value proposition**: manage contacts faster than a typical mouse/GUI driven app +//@@author lihongguang00 +| Priority | As a … | I want to … | So that I can… | +| -------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `* * *` | NUS student | search for the contacts of other students within my university | contact them for group projects | +| `* * *` | NUS student | search for a name in the contact | easily find the person’s contact | +| `* * *` | NUS student | add contacts into my address book easily | retrieve the saved contact | +| `* * *` | NUS student living on campus | save the contacts of my neighbors | contact them in case of any emergencies | +| `* * *` | NUS student living on campus | save the addresses of my neighbours | locate them easily when necessary | +| `* * *` | NUS student in multiple CCAs | filter my contacts by tags to identify all people in a group | find the relevant contacts in a certain group quickly | +| `* * *` | student staying on campus | label multiple tags to my contacts | locate my friends taking the same module and staying in the same campus residence as me | +| `* * *` | user that is familiar with the keyboard | use the keyboard to type commands in the applications | access the features of the application | +| `* * *` | user with bad memory | save a short description of my contact | identify my contacts better | +| `* * *` | visual-reliant user | save a photo of the person into my contacts | quickly recognise and find them | +| `* * *` | non-tech-savvy user | use the help feature of the app | navigate about the app easily | +| `* *` | NUS student | sort all contacts by name | easily find a person's contact | +| `* *` | NUS student | sort all contacts by name in reverse | easily find a person's contact | +| `* *` | NUS student | sort all contacts by email | easily find a person's contact | +| `* *` | NUS student | sort all contacts by email in reverse | easily find a person's contact | +| `* *` | NUS student | sort all contacts by address | easily find a person's contact | +| `* *` | NUS student | sort all contacts by address in reverse | easily find a person's contact | +| `* *` | NUS student | sort all contacts by phone | easily find a person's contact | +| `* *` | NUS student | sort all contacts by phone in reverse | easily find a person's contact | +| `* *` | NUS student | import the NUS calendar into the application | view all academic commitments more conveniently | +| `* *` | NUS Student | compare timetables/calendars with my peers easily | plan meetings more conveniently | +| `* *` | NUS student | allocate tasks and responsibilities within a project or CCA group | tasks can be done efficiently | +| `* *` | NUS Student in multiple CCAs | group my contacts | identify which group my contacts belong to | +| `* *` | NUS student doing a group project | export my calendar in my application | send it to my teammates to coordinate meeting times | +| `* *` | forgetful user | view a password hint | I can recall my password when I forget it | +| `* *` | forgetful student | be reminded of upcoming assignments | prioritize which assignment to work on first | +| `* *` | forgetful student | be reminded of upcoming examinations | prioritize which exam module to study on first | +| `* *` | forgetful student | be reminded of upcoming projects | prioritize which project to work on first | +| `* *` | forgetful student | be reminded of upcoming tutorials | so that I can prioritize which tutorial to complete first | +| `* *` | security conscious user | password-protect my appliation | prevent others from accessing my application profile easily | +| `* *` | data conscious user | backup my data | so that I can recover it in the case my data is corrupted | +| `* *` | user who cares about user experience | change the color of my user interface | modify the interface to my liking | +| `* *` | user with a strong urge for aesthetics | interact with a clean user interface | feel at ease when using the application | +| `* *` | non-tech-savvy user that does not know how to use terminal commands | use buttons around the application to navigate around the application easily | +| `*` | student who prefers using a tablet | I can use the application on my tablet | access contacts easily on my tablet | +| `*` | student who prefers mobile devices | I can use the application on my mobile phone | sync my contacts with those in the application | +| `*` | student in a group project | send a QR code of my application contact details | have my teammates add my contact details on the application much faster | +| `*` | user who prefers cloud storage | sync the contacts in my application with Google Contacts | save it in cloud-based storage | +| `*` | user accustomed to a PC | view all commands at once | explore features quickly | +| `*` | user with a tendency to open many applications at once | have the application time out and exit | ensure my computer would not be cluttered by too many applications | + +_{More to be added}_ +//@@author nicrandomlee +### Use cases -### User stories -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +(For all use cases below, the **System** is the `UniMate` and the **Actor** is the `user`, unless specified otherwise) -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +**Use case: UC1 List persons** -*{More to be added}* -### Use cases +**MSS** + -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +1. User requests to list persons +2. UniMate shows a list of persons + +Use case ends. + + +**Extensions** + + +* 2a. The list is empty. + + +* 2b. UniMate displays a message to the user. + + +Use case ends. + + +**Use case: UC2 Delete a person** -**Use case: Delete a person** **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person + +1. User requests to list persons (UC1) +2. User requests to delete a specific person in the list +3. UniMate deletes the person + + +Use case ends. + + +* 3a. The given index is invalid. + + +* 3a1. UniMate shows an error message. + + + Use case resumes at step 2. + + +**Use case: UC3 Add a person** + + + + +**MSS** + + + + +1. User requests to add in a person into the UniMate +2. UniMate adds the person +3. UniMate displays a success message + + +Use case ends. + +**Extensions** + + +* 1a. The given format is wrong + + * 1a1. UniMate shows an error message + + +Use case ends. + + +* 1b. Person is already in UniMate + + + Use case ends. + +//@@author Fallman2 + +**Use case: UC4 Clear All Entries** + + +**MSS** + + +1. User requests to clear all entries in UniMate +2. UniMate deletes all entries + + + Use Case Ends. + +**Extensions** + + +* 1a. UniMate has no data + + + Use Case Ends + + +**Use case: UC5 Edit persons** + + +**MSS** + + +1. Use requests to list all persons(UC1). +2. User requests to edit person at a specified index with new parameters. +3. UniMate updates the person at specified index with new parameters. + Use case ends. + + +**Extensions** + + +* 2a. The given index is invalid. + * 2a1. UniMate displays an error message. + Use case ends. +* 2b. The given parameters are invalid. + * 2b1. UniMate displays an error message. + Use case ends. + + +**Use case: UC6 Exit application** + + +**MSS** + + +1. User requests to exit the application. +2. UniMate closes the application. + Use case ends. + + +**Use case: UC7 Search by name** + + +**MSS** + + +1. User requests to search persons by specified keywords +2. UniMate displays persons with names that match keywords + Use case ends. + + +//@@author junhonglow
+**Use case: UC8 View events** + + +**MSS** + + +1. User requests to view events. +2. UniMate displays all events. + + +Use Case ends. + + +**Extensions** +* 2a. There are no events recorded. + * 2a1. UniMate displays a message indicating that there are no events. Use case ends. + +**Use case: UC9 Add an event** + + +**MSS** + + +1. User requests to add an event. +2. UniMate adds the event. + + +Use case ends. + + **Extensions** +* 2a. UniMate detects a conflict with an existing event. + * 2a1. UniMate shows conflicted timings and requests to modify one of the timings. + * 2a2. User modifies the timing and submits the new timings. + * Steps 2a1 and 2a2 are repeated until there are no conflicts in timings. -* 2a. The list is empty. + Use case ends. + + +**Use case: UC10 Delete an event** + + +**MSS** + + +1. User requests to view events.(UC 8) +2. User requests to delete an event. +3. UniMate deletes the event. + + +Use case ends. + + +**Extensions** +* 2a. Event does not exist. + * 2a1. UniMate shows an error message. + + + Use case ends. + + +**Use case: UC11 Edit an event** + + +**MSS** + + +1. User requests to view events. (UC 8) +2. User requests to edit an event. +3. Unimate edits the event. + + +**Extensions** +* 2a. Event does not exist. + * 2a1. UniMate shows an error message. + + + Use case ends. + + +* 3a. UniMate detects a conflict with an existing event. + * 3a1. UniMate shows conflicted timings and requests to modify one of the timings. + * 3a2. User modifies the timing and submits the new timings. + * Steps 3a1 and 3a2 are repeated until there are no conflicts in timings. + + + Use case ends. + +//@@author Fallman2 + +**Use case: UC12 Filter by fields.** + + +**MSS** + + +1. User requests to list persons with specific words in specific fields. +2. UniMate displays persons matching all provided keywords in specified fields and the number of people matching all + specified fields. + + Use case ends. + + +**Extensions** + + +* 2a. There are no persons matching the specified keywords. + * UniMate displays a message that 0 people have been displayed. + + Use case ends. +* 2b. All indicated fields are left empty instead. + * UniMate displays all available contacts. + + Use case ends. + +//@@author nicrandomlee + +**Use case: UC13 Sort by fields.** + +**MSS** + +1. User requests to sort persons +2. UniMate displays persons in specified order and by specified parameter + + Use case ends. + +**Extensions** + +- 1a. User uses the wrong delimiter or makes a spelling mistake, or provides incorrect number of arguments + - UniMate displays a message to show a helper message outlining the correct syntax and available sort options + +//@@author + +//@@author lihongguang00 + +**Use case: UC14 Compare calendars** + +**MSS** +1. User requests to compare calendars +2. User inputs and confirms the indices of the contacts they want to compare their calendars with +3. UniMate displays the calendar showing the time periods where both the contacts and the user are free + +Use case ends. + +**Extensions** + +- 2a. User inputs and confirms the tags of the contacts to identify who they want to compare their calendars with. + + Use case resumes from step 3 + +- 2b. User inputs the invalid arguments for the command. + - UniMate displays the user's calendar only, ignoring all the invalid arguments + - Use case ends. + - +//@@author + +//@@author Fallman2 + +**Use case: UC15 Adding a Task.** + +**MSS** + +1. User requests to add a Task. +2. UniMate adds the task. Use case ends. -* 3a. The given index is invalid. +**Extensions** + +* 1a. User leaves the description field empty. + * UniMate displays a message to indicate that the description cannot be empty. + + Use case ends. +* 1b. User attempts to create a Task that is identical to another Task in the Task Manager. + * UniMate displays a message to indicate that there is already a duplicate task present. + + Use case ends. + +**Use case: UC16 Viewing the task list.** + +**MSS** + +1. User requests to switch the list from the event list to the task list. +2. UniMate switches the list. + + Use case ends. + +**Extensions** + +* 1a. The list displayed is already the task list. + * UniMate displays the event list instead. + + Use case ends. + +**Use case: UC17 Deleting a task.** + +**MSS** + +1. User switches the event list to the task list to view it (UC17). +2. User requests to delete a task. +3. UniMate deletes the task from the TaskManager. + + Use case ends. + +**Extensions** + +* 2a. There are no tasks to delete . + * UniMate displays an error message indicating that the index indicated is invalid. + + Use case ends. + +**Use case: UC18 Sorting Tasks.** + +**MSS** + +1. User switches the event list to the task list to view it (UC17). +2. User requests to sort the tasks by description. +3. UniMate sorts the tasks displayed by description alphabetically. + + Use case ends. + +**Extensions** + +* 2a. User requests to sort the tasks by deadline instead. + * UniMate sorts the tasks displayed by deadline from earliest to latest. + + Use case ends. + +**Use case: UC19 Clearing Events within a Time Range.** + +**MSS** + +1. User requests to clear all events within a time range. +2. UniMate deletes all events that overlap with the time range. + + Use case ends. - * 3a1. AddressBook shows an error message. +**Extensions** + +* 1a. Time range provided has a start time after the end time. + * UniMate displays an error message indicating that the start time must be before the end time. + + Use case ends. +* 1b. There are no events within the time range. + * UniMate displays an error message indicating that there are no events within the indicated times. - Use case resumes at step 2. + Use case ends. -*{More to be added}* +//@@author ### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +Project Constraints: +1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. The product is offered as an offline application. +3. System should be beginner-friendly and easily picked up by a freshman at NUS with just the application's website. +4. Should be able to hold up to 200 events without a noticeable sluggishness in performance for typical usage. +5. Calendar should have the current week's event schedule displayed. +6. Calendar should be able to check if there are any event timing conflicts within 1 second. +7. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse, except viewing contact events as a pop-up. +8. System should be beginner-friendly and easily picked up by a freshman at NUS with just the application's website. + -*{More to be added}* +Process Requirements: +1. The product is expected to adhere to the set Milestones. +2. User can only execute actions using at most 1 command at a time. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Event**: An activity to be marked on the calendar with a specified time frame, with a description, some start date and time, and some end date and time. +* **Task**: An activity that has a description, and an optional deadline date and time. -------------------------------------------------------------------------------------------------------------------- @@ -329,49 +1174,398 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli Given below are instructions to test the app manually. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; + + +**Note:** These instructions only provide a starting point for testers to work on; testers are expected to do more *exploratory* testing. -
+ ### Launch and shutdown 1. Initial launch + 1. Download the jar file and copy into an empty folder - 1. Download the jar file and copy into an empty folder - - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. 1. Saving window preferences - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. + 2. Re-launch the app by double-clicking the jar file.
+ Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +3. Shutting down via command + +1. Launch the app. + +2. Type `exit` into the input box and press `ENTER`. + +4. Shutting down via `file` + +1. Launch the app. + +2. Click on `file` in the top menu bar. + +3. Click on `exit` in the dropdown menu. ### Deleting a person 1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 3. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Filtering all persons while all persons are being shown. -### Saving data +Prerequisites: List all persons using the `list` command. Multiple persons in the list. + +1Test case: `filter n/ p/ e/`
+Expected: All persons displayed. + +1. Test case: `filter n/o`
+ Expected: All persons with an "o" in their name displayed. + +2. Test case: `filter n/o p/8 e/@ t/CS t/10`
+ Expected: All persons with an "o" in their name, an "8" in their phone number, a tag that contains "CS" and a tag + that contains "10" displayed. + +3. Test case: `filter`
+ Expected: Error message displayed that states that the command format is invalid. all persons shown. + +### Sorting all persons while all persons are shown. + +Prerequisites: List all persons using the `list` command. Multiple persons in the list. + +1. Test case: `sort /byname`
+ Expected: All persons sorted alphabetically by name. + +2. Test case: `sort /byname /reverse`
+ Expected: All persons sorted in reverse alphabetical order by name. + +3. Test case: `sort /byemail`
+ Expected: All persons sorted by email. + +4. Test case: `sort /byphone`
+ Expected: All persons sorted by phone number. + +5. Test case: `sort /byaddress`
+ Expected: All persons sorted by address. + +6. Test case: `sort`
+ Expected: Error message displayed indicating invalid command format. + +### Adding an event + +Prerequisite: Empty event list. -1. Dealing with missing/corrupted data files +1. Test case: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: New event added with description "Cry about deadlines", start date and time "2023-01-01 00:01" and end date + and time "2023-12-31 23:59". +2. Test case: `addEvent d/Laugh about deadlines ts/2023-01-02 00:01 te/2023-01-01 23:59`
+ Expected: Error message displayed indicating invalid event period. +3. Test case: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then, re-enter: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: Error message displayed indicating conflicting event timing. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +### Deleting an event -1. _{ more test cases …​ }_ +Prerequisite: Empty event list. + +1. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `deleteEvent 2023-02-02 00:01`
+ Expected: Event is created and then successfully deleted +2. Test case: `deleteEvent 2023-02-02 00:01`
+ Expected: Error message displayed indicating no event found at specified time. + +### Clearing events within time range + +Prerequisite: Empty event list. + +1. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-01-02 23:59`
+ Enter `addEvent d/Cry about deadlines ts/2023-01-03 00:01 te/2023-01-04 23:59`
+ Enter `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: All events in Calendar deleted. +2. Test case: `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: Error message displayed indicating no events found in time range. +3. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-01-02 23:59`
+ Enter `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: All events in Calendar deleted. + +### Switching the bottom list display + +Prerequisite: Event list currently being displayed. + +1. Test case: `switchList`
+ Expected: Task list is displayed. +2. Test case: `switchList 123`
+ Expected: Task list is displayed. + +### Adding an event for another person + +Prerequisite: 1 person in AddressBook, person currently has no events. + +1. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: New event added with description "Cry about deadlines", start date and time "2023-01-01 00:01" and end date + and time "2023-12-31 23:59". +2. Test case: `addContactEvent 1 d/Laugh about deadlines ts/2023-01-02 00:01 te/2023-01-01 23:59`
+ Expected: Error message displayed indicating invalid event period. +3. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then, re-enter: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: Error message displayed indicating conflicting event timing. + +### Deleting an event for another person + +Prerequisite: 1 person in AddressBook, person currently has no events. + +1. Test case: Enter `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `deleteContactEvent 1 2023-02-02 00:01`
+ Expected: Event is created and then successfully deleted +2. Test case: `deleteContactEvent 1 2023-02-02 00:01`
+ Expected: Error message displayed indicating no event found at specified time. + +### Editing an event for another person + +Prerequisite: 1 person in AddressBook, person currently has no events. + +1. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 d/Edited Description`
+ Expected: A new event is added for the person in the AddressBook, then the description of that contact is edited to be "Edited Description". +2. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 ts/2023-01-01 00:20`
+ Expected: A new event is added for the person in the AddressBook, then the start time is edited to be 2023-01-01 00:20 +3. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 ts/2023-01-03 00:20`
+ Expected: Error message is displayed that indicates that the start date should be before the end date. +4. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 te/2023-01-04 00:20`
+ Expected: A new event is added for the person in the AddressBook, then the end time is edited to be 2023-01-04 00:20. +5. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-02 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 t3/2023-01-01 00:20`
+ Expected: Error message is displayed that indicates that the start date should be before the end date. +6. Test case: `addContactEvent`
+ Expected: Error message displayed indicating that the command format is invalid. + +### Viewing events of another person + +Prerequisite: 1 person in AddressBook, person currently has no events. + +1. Test case: `viewContactEvents 1`
+ Expected: Empty event list is displayed in a pop-up +2. Test case: Enter `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `viewContactEvents 1`
+ Expected: Event list displayed in a pop-up with 1 event listed. +3. Test case: `viewContactEvents 2`
+ Expected: Error message displayed indicating that the index indicated is invalid. +4. Test case: `viewContactEvents`
+ Expected: Error message displayed indicating that the command format is invalid. + +### Comparing Calendars of multiple people + +Prerequisite: 3 persons in AddressBook. + +1. Test case: `compareCalendars`
+ Expected: User's calendar is shown. +2. Test case: `compareCalendars 1 3`
+ Expected: Calendar comparison is shown in a pop-up with the comparison being between the user, the person of index 1 and the person of index 3. +3. Test case: `compareCalendars 4`
+ Expected: Error message displayed indicating that the index indicated is invalid. + +### Comparing Calendars of multiple people via tags + +Prerequisite: 3 persons in AddressBook, 2 persons have the tag "CS2103". + +1. Test case: `compareGroupCalendars`
+ Expected: User's calendar is shown. +2. Test case: `compareGroupCalendars CS2103`
+ Expected: Calendar comparison is shown in a pop-up with the comparison being between the user and the two people with the "CS2103" tag. + +### Adding a task + +Prerequisite: No tasks in the task list. + +1. Test case: `addTask d/Hydrate te/2023-11-13 22:30`
+ Expected: A new task is added with description "Hydrate" and deadline of 2023-11-13 22:30. +2. Test case: `addTask d/Hydrate`
+ Expected: A new task is added with description "Hydrate" and no deadline. +3. Test case: Enter `addTask d/Hydrate te/2023-11-13 22:30`
+ Then enter: `addTask d/Hydrate te/2023-11-13 22:30`
+ Expected: Error message displayed indicating that there is a conflicting task. + +### Deleting a task + +Prerequisite: No tasks in the task list. + +1. Test case: Enter `addTask d/Hydrate te/2023-11-13 22:30`
+ Then enter: `deleteTask 1`
+ Expected: Task added and then deleted successfully. +2. Test case: `deleteTask 1`
+ Expected: Error message displayed indicating that the index indicated is invalid. + +### Sorting Tasks + +Prerequisite: Multiple tasks in the task list. + +1. Test case: `sortTasks DESCRIPTION` + Expected: Tasks are sorted alphabetically by description. +2. Test case: `sortTasks DEADLINE` + Expected: Tasks are sorted by deadline. +3. Test case: `sortTasks 1askdj` + Expected: Error message displayed indicating that the sorting order indicated is invalid. + +### Saving data +Prerequisites: Have all three json data files present. + * addressbook.json + * calendar.json + * taskmanager.json +

+ +Some sample storage json files for the tests can be found in the link [here](https://github.com/AY2324S1-CS2103-F13-4/tp/tree/master/src/test/data/ManualTestingSampleStorageFiles).
+Rename the files to their respective data files and replace the original files (if any) found in the original directory.
+1. Test case: Navigate to the addressbook.json file and delete the file.
+ Expected: The AddressBook (Contact List) in UniMate should be populated with sample data. + +2. Test case: Single or multiple deletion of the different data files.
+ Expected: Similar to previous. + +3. Test case: Navigate to the addressbook.json file and edit any of the fields to become an invalid field.
+ Example: Modify the events field of a contact to the following.
+ {
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "events" : [ {
+ "description" : "Nap",
+ "eventPeriod" : "Invalid Period - 2024-01-03 18:00"
+ } ]
+ }
+ Expected: The AddressBook (Contact List) in UniMate should be blank as UniMate will discard all data.
+ Note: In this scenario, the event period is invalid. + +4. Test case: Single or multiple corruption of the different data files.
+ Expected: Similar to previous. + +5. Test case: Navigate to the addressbook.json file and duplicate a person in the file.
+ Example:
+ {
+ "persons" : [ {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "events" : [ ]
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "events" : [ ]
+ } ]
+ }
+ Expected: The AddressBook (Contact List) in UniMate should be blank as UniMate will discard all data.
+ +6. Test case: Duplication of the fields in the different data files.
+ Expected: Similar to previous. + +7. Test case: Other invalid storage formats: Not a JSON format.
+ Expected: Similar to previous. + +## **Appendix: Effort** +### Main Difficulty: Building User Calendar +This part was especially difficult because we wanted to conform to the structure of the base `AB3` code, knowing it is a brownfield project. +Thus, we wanted to ensure the abstraction granularity of the classes matched that of `AB3` (e.g. Separate classes for every single attribute of `Person`), +which explains why the `UniMateCalendar` class is so granular, as seen in the [model class diagram](#model-component). + +Furthermore, we knew from the beginning that we wanted to allow users to compare their `Calendar` with that of their contacts. +Therefore, from the start, we had to write code that would make this feature possible. + +The UI for the calendar was also challenging because nobody in the group had experience in desktop application UI development and JavaFx. +Understanding JavaFx, before even implementing it in the creation of new GUI elements, required a few hours and much trial and error. +It was also challenging to implement GUI components while ensuring that all elements that were already previously implemented worked as well. + +Lastly, the feature where the calendar would "grow" and "shrink" according to the events that the user have in real time +took a lot of careful problem-solving to avoid breaking abstraction barrier and introducing cyclic dependencies, since +we had to establish a line of communication between the `UniMateCalendar` of the `Model` interface and the `UI` interface, +and the `Observable` and `Observer` interfaces were only introduced later into the module, by which we had already implemented +this feature. + +### Other Difficulty: Contact Calendar +Integrating the calendar into the contacts in the AddressBook was challenging, but was slightly easier as we could just reuse the +code that we have written for the `UniMateCalendar`. However, we still had to learn how to make the calendar of the contacts show up +in a new window rather than in the main application GUI. + +### Other Difficulty: Task Manager/Event List +For the Task Manager, the main difficulty lies in following the structure of the `AB3` code. +While the implementation of the TaskManager was not complicated, we wanted to properly abstract the different classes from each other similarly to the +structure of AB3. Thus, the implementation required a large number of lines of code and methods, all of which required even more JUnit tests, multiplying +the number of hours spent on implementing the TaskManager. However, the implementation of the GUI for the TaskManager did not take as long as that +of the Calendar as the TaskManager used the same area of the GUI as the event list. That being said, given that no one in our team had experience working +on a GUI, especially with JavaFx, a few hours had to be spent in researching what would be the best way to carry out the switching of the lists from one to the other. + +Additionally, for both `Task List` and `Event List`, making the list update in real time when new `Task`/`Event` are added was also difficult. When implementing +the GUI, we had wanted to ensure that commands executed in the main window would reflect appropriately in both the user's own task and event lists but also +for the pop-up event list of individual persons in the AddressBook. This proved to be another challenge as we would encounter several bugs while implementing +this feature. However, we managed to work together as a group to fix most of them. + +### Achievements: +- Calendar UI that will update in real time when user adds/deletes event +- Working calendar comparison feature +- We were able to integrate 2 separate and very different components, `Task manager` and `Calendar`, successfully into the base `AB3` code +- Live updates for the `Task List` and `Event List` + +## **Appendix: Planned Enhancements** + +### Better Confirmation for ClearEventsCommand +Currently, for the ClearEventsCommand, the user has to re-enter the command entirely with the confirmation text in order +to confirm the deletion of the events. This implementation has an advantage of being able to bypass the confirmation +check if the user wishes to delete the events more quickly but is otherwise clumsier to use. One way we tried to +circumvent this is by displaying the message with the confirmation in the result shown but this would not be as convenient +for users who do not wish to use their mouse. A better implementation would be to have the confirmation only require the +user to key in Y or N to confirm or deny the deletion, allowing the events to still be deleted quickly for users who wish +to do so but also allowing for less text to be keyed in to confirm. + +### More Ways to Sort Tasks +Currently, there are only two ways to sort tasks. This can cause some tasks to be buried under others if they are not +prioritized in either sorting method. Being able to sort tasks by proximity to the current date or being able to reverse +the order of sorting of the tasks may help in making all tasks visible to the user so as to ensure that all tasks are +completed on time. + +### Allow user to view beyond the current week restriction for the calendar +Currently, the calendar for the user and contacts is restricted to viewing the current week's events. Therefore, the +user has no means of navigating looking at their events for other weeks other than the current one. This might impede +the user experience as the user might want to check their timetable for some timeframe in the near future (e.g. proceeding week). +Therefore, allowing the user to view their events for any week beyond just the current one might improve user experience, +reducing the dependency the user has on the `Event List` to view events beyond this chronological restriction. + +### Allow users to compare calendars with contacts beyond the current week +Currently, the user can only compare calendars with their contacts for the current week. Therefore, if the user wants to +schedule a meeting in a week beyond the current one, they might have difficulty as they might not be able to compare +their calendars with their contacts for that specific week. Therefore, allowing the user to navigate beyond the current +week restriction for the compare calendar feature will be hugely beneficial to the user. + +### Allow users to add single day events without typing the date twice +Currently, when the user wants to add an `Event` into their `Calendar`, they have to type the date of the event twice +(once in start date and time and another in end date and time). For single day events, this can be inconvenient. Hence, +we hope to introduce a command that will allow users to add a single day event without typing the date twice. + +### Allow users to add recurring events +Currently, our `Calendar` only supports adding of singular events. We hope to allow users to add recurring events in the future +to more accurately simulate the events that the user might have in a University context, where they might have recurring classes +every week. + +### Allow users to import .ics format files +Currently, UniMate has no support for importing of external files. Allowing users to import .ics files, which is a common export +format of other calendar platforms, will allow UniMate to integrate more seamlessly with other calendar applications. diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..082e652d947 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1,29 +1,21 @@ --- -layout: page -title: Documentation guide + layout: default.md + title: "Documentation guide" + pageNav: 3 --- -**Setting up and maintaining the project website:** - -* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation. -* The `docs/` folder is used for documentation. -* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html). -* Note these points when adapting the documentation to a different project/product: - * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar. - * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format). -* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping) +# Documentation Guide +* We use [**MarkBind**](https://markbind.org/) to manage documentation. +* The `docs/` folder contains the source files for the documentation website. +* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation. **Style guidance:** * Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style). +* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html). -* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html) - -**Diagrams:** - -* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html) -**Converting a document to the PDF format:** +**Converting to PDF** -* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html) +* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html). diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index c8385d85874..00000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -gem 'jekyll' -gem 'github-pages', group: :jekyll_plugins -gem 'wdm', '~> 0.1.0' if Gem.win_platform? -gem 'webrick' diff --git a/docs/Logging.md b/docs/Logging.md index 5e4fb9bc217..589644ad5c6 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,8 +1,10 @@ --- -layout: page -title: Logging guide + layout: default.md + title: "Logging guide" --- +# Logging guide + * We are using `java.util.logging` package for logging. * The `LogsCenter` class is used to manage the logging levels and logging destinations. * The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..03df0295bd2 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -1,27 +1,32 @@ --- -layout: page -title: Setting up and getting started + layout: default.md + title: "Setting up and getting started" + pageNav: 3 --- -* Table of Contents -{:toc} +# Setting up and getting started + + -------------------------------------------------------------------------------------------------------------------- ## Setting up the project in your computer -
:exclamation: **Caution:** + +**Caution:** Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps. -
+ First, **fork** this repo, and **clone** the fork into your computer. If you plan to use Intellij IDEA (highly recommended): 1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to to ensure Intellij is configured to use **JDK 11**. -1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. +1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA. + + Note: Importing a Gradle project is slightly different from importing a normal Java project. + 1. **Verify the setup**: 1. Run the `seedu.address.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. @@ -34,10 +39,11 @@ If you plan to use Intellij IDEA (highly recommended): If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours. -
:bulb: **Tip:** + + **Tip:** Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code. -
+ 1. **Set up CI** diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..78ddc57e670 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -1,12 +1,15 @@ --- -layout: page -title: Testing guide + layout: default.md + title: "Testing guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# Testing guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Running tests @@ -19,8 +22,10 @@ There are two ways to run tests. * **Method 2: Using Gradle** * Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`) -
:link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. -
+ + +**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. + -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 57437026c7b..c7aa9bf39c6 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,33 +1,35 @@ --- -layout: page -title: User Guide + layout: default.md + title: "User Guide" + pageNav: 3 --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +UniMate is a desktop app for students to **manage contacts** and **manage schedules** optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, UniMate can get your contact management tasks done faster than traditional GUI apps. -* Table of Contents -{:toc} + + -------------------------------------------------------------------------------------------------------------------- -## Quick start +# Quick start 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `UniMate.jar` from [here](https://github.com/AY2324S1-CS2103-F13-4/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for UniMate. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar UniMate.jar` command to run the application.

+ A GUI similar to the below should appear in a few seconds. Note that the app contains some sample data, +which may differ from that of a fresh download.
+ ![Ui](images/unimateScreenshot.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.

Some example commands you can try: * `list` : Lists all contacts. - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the AddressBook. * `delete 3` : Deletes the 3rd contact shown in the current list. @@ -35,21 +37,109 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +# Features Overview -## Features +UniMate has 3 core features, [**AddressBook**](#main-feature-1-addressbook), [**Calendar System**](#main-feature-2-calendar-system) +and [**Task Management System**](#main-feature-3-task-management-system). This section serves to +provide you with an overview of what each component entails and the functionalities supporting them. -
+![uniMateFeatureOverview](images/uniMateFeatures.png) -**:information_source: Notes about the command format:**
+The screenshot above shows a cropped view of UniMate's GUI. **AddressBook** belongs to the [**AddressBook** feature](#main-feature-1-addressbook), +**CALENDAR** belongs to the [**Calendar System** feature](#main-feature-2-calendar-system) and the **EVENT LIST/TASK LIST** +portion is shared by both the [**Calendar System** feature](#main-feature-2-calendar-system) and [**Task Management System** feature](#main-feature-3-task-management-system). + +>To switch between the [Event List](#sub-feature-2-event-list) and [Task List](#main-feature-3-task-management-system), +we have a [CLI-based command](#viewing-tasks-switchlist) for that. + +## Main feature 1: AddressBook + +The AddressBook serves to ease contact management. + +Some major CLI-based functionalities to support this feature include: +- [**Adding**](#adding-a-person-add), [**deleting**](#deleting-a-person-delete) and [**editing**](#editing-a-person-edit) operations to manipulate contacts in the AddressBook +- [**Finding**](#locating-persons-by-name-find) and [**filtering**](#filtering-persons-by-attribute-filter) operations to search for and isolate contacts of interest +- [**Sorting**](#sort-persons-sort) operation to reorganise the view of the AddressBook when needed + +Other functionalities can be found in the [AddressBook Management subsection found in the Features section](#addressbook-management). + +Additionally, each contact in the AddressBook has their own personal calendar, +which can be accessed simply by double-clicking with the left mouse button on the contact's card in the AddressBook. +This will create a pop-up displaying the contact's calendar as shown below. + +![contactCalendar](images/contactCalendar.png) + +**Note:** There is currently no CLI-based command to access this feature, but is planned for a future release. + +## Main feature 2: Calendar System +The calendar system seeks to improve the user's schedule planning experience, as well as simplify +the synchronization of schedules among multiple people. + +To achieve this, we have 2 supporting sub-features: +1. [Calendar](#sub-feature-1-calendar) +2. [Event List](#sub-feature-2-event-list) + +For the calendar system, we work solely with `events`, which differs from `tasks` used in the [task list feature](#main-feature-3-task-management-system). +- `event` has a `description`, `start date and time` and `end date and time` +- `task` only has a `description` and an **optional** `end date and time` + +### Sub-feature 1: Calendar + +The calendar allows the user to plan their timetable and compare it against their contacts' schedules. +This will facilitate scheduling of meetings and events involving the user and their contacts. + +>By default, the calendar's GUI will only display the time period of 8am to 6pm, but will automatically +extend when events that cannot be fit within this time period are added. +> +>Additionally, the GUI for this sub-feature currently only supports viewing of events within the current week. +Upcoming updates will allow the user to navigate beyond this chronological restriction. In the meantime, users can +consider using the [Event List](#sub-feature-2-event-list) to view events outside the current week. + +For the **user**'s calendar, some major CLI-based functionalities supporting this sub-feature includes: +- [Adding](#user-calendar-adding-an-event-addevent), [deleting](#user-calendar-deleting-an-event-deleteevent) and [clearing](#user-calendar-deleting-multiple-events-clearevents) operations to manipulate the events in the user's calendar + +For the **contact**'s calendar, some major CLI-based functionalities supporting this sub-feature includes: +- [Adding](#contact-calendar-adding-an-event-to-a-contact-addcontactevent), [deleting](#contact-calendar-deleting-an-event-from-a-contact-deletecontactevent) and [editing](#contact-calendar-edit-contact-calendar-event-editcontactevent) operations to manipulate the events in the contact's calendar + +Other useful CLI-based functionalities not exclusive to the user/contact supporting this sub-feature includes: +- Comparison operations to compare the user's calendar with their contacts' calendars either by [index](#1-comparison-by-index) or [tag](#2-comparison-by-tag) + +Other functionalities can be found in the [Calendar System subsection of the Features section](#calendar-system) + +### Sub-feature 2: Event List + +The event list displays all the events for the user/contact. It serves as an overview of the collective +events that the user or their contact have. + +The event list shown on the main UniMate GUI is the user's event list. To view a specific contact's +event list, we have a [CLI-based command](#viewing-contact-event-list-viewcontactevents) that will +display the contact's event list in a popup window. + +## Main-feature 3: Task Management System + +The task list stores a collection of tasks that the user might have. The primary difference +between event and task is that a task does not have a starting date and time. + +Some useful CLI-based functionalities supporting this feature includes: +- [Adding](#adding-tasks-addtask) and [deleting](#deleting-tasks-deletetask) operations to manipulate the tasks present in the task list +- [Sorting](#sorting-tasks-sorttasks) operation to reorganise the view of the task list when needed + +-------------------------------------------------------------------------------------------------------------------- + +# Features + + + +**Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g. `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. * Items with `…`​ after them can be used multiple times including zero times.
e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. @@ -60,66 +150,113 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Note that if the parameters are too lengthy, they may be truncated in the UI. + * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. -
+ +## General Commands ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) Format: `help` +Alternatively, the message can also be accessed by using the keyboard shortcut `F1` or +through the menu bar (`Help` > `Help`). + +### Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +Alternatively, the user can also exit the application through the menu bar (`File` > `Exit`) + +## AddressBook Management ### Adding a person: `add` -Adds a person to the address book. +Adds a person to the AddressBook. Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+ + +**Tip:** A person can have any number of tags (including 0) + Examples: * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` * `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` -### Listing all persons : `list` +> **Note:** Currently UniMate does not support the use of forward-slash in the name of contacts (e.g. s/o). The developers + understand this, and we hope to add support for accepting forward-slashes in the contact's name in a future release. -Shows a list of all persons in the address book. +![addCommand](images/addCommand.png) -Format: `list` +In the example, after executing +`add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`, we see `John Doe`'s +contact information stored in the AddressBook as the 7th contact on the left-hand side of the GUI. + +### Deleting a person: `delete` + +Deletes the specified person from the AddressBook. + +Format: `delete INDEX` + +* Deletes the person at the specified `INDEX` +* The index refers to the index number shown in the displayed person list +* The index must be a **positive integer** (i.e. 1, 2, 3, …​) + +Examples: +* `list` followed by `delete 2` deletes the 2nd person in the AddressBook +* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command -### Editing a person : `edit` +![deleteCommand](images/deleteCommand.png) -Edits an existing person in the address book. +In this example, after executing `list`, followed by `delete 7`, `John Doe`, the person with index 7 +is removed from the displayed AddressBook. + +### Editing a person: `edit` + +Edits an existing person in the AddressBook. Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` * Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. +* At least one of the optional fields must be provided +* Existing values will be updated to the input values +* When editing tags, the existing tags of the person will be removed i.e. adding of tags is not cumulative. * You can remove all the person’s tags by typing `t/` without - specifying any tags after it. + specifying any tags after it Examples: * `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. * `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +![editCommand](images/editCommand.png) + +In this example, after executing `edit 1 p/12345678`, `Alex Yeoh` the person with index 1 has his +phone number edited to `12345678`. + ### Locating persons by name: `find` -Finds persons whose names contain any of the given keywords. +Finds persons whose names contain **any** of the given keywords. + +> **Tip:** Multiple arguments passed into the `find` command will be delimited by whitespaces, which means +the arguments `John Doe` will be parsed into 2 separate arguments `John` and `Doe`. To circumvent +this delimitation, consider the [filter command](#filtering-persons-by-attribute-filter). Format: `find KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` +* The search is case-insensitive. e.g. `hans` will match `Hans` * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. +* Only the name is searched * Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). +* Persons matching at least one keyword will be returned (i.e. `OR` search) e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` Examples: @@ -127,43 +264,411 @@ Examples: * `find alex david` returns `Alex Yeoh`, `David Li`
![result for 'find alex david'](images/findAlexDavidResult.png) -### Deleting a person : `delete` +### Filtering persons by attribute: `filter` -Deletes the specified person from the address book. +Filters out persons whose fields contain any of the given keywords. The keywords contained in each field will be treated as a single argument. -Format: `delete INDEX` +Format: `filter [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` + +* Filter is case-insensitive. e.g. `cs2103` will match `CS2103` +* The order of the fields does not matter +* All provided fields are searched +* All tags containing the words or part-thereof will be matched (e.g. `Ba` will return `Badminton` or `Basketball` or `Football` or `Backgammon`) +* Only persons matching all specified fields will be returned (i.e. logical `AND` search) +* Arguments in a specific field are not delimited (i.e. the argument `John Doe` in `filter n/John Doe` will be treated as a single argument) +* Indicating a field but leaving it blank e.g. `filter n/` will show all contacts. + +Examples: +* `filter t/CS2103` - Displays all contacts with the CS2103 tag or tags containing ``CS2103`` e.g. CS2103T +* `filter t/MA2116 t/CS1010S n/John e/@u.nus.edu` - Displays all contacts with the ``MA2116`` and ``CS1010S`` tags as well as +with a name containing ``John`` and an email address with the domain ``u.nus.edu`` + +![filterCommand](images/filterCommand.png) + +In this example, after executing `filter t/colleagues`, the AddressBook list displays all contacts +that have the `colleagues` tag. + +### Listing all persons : `list` + +Shows a list of all persons in the AddressBook. This command is particularly useful to +display all the contacts after using the [find](#locating-persons-by-name-find) and [filter](#filtering-persons-by-attribute-filter) commands. + +Format: `list` + +![listCommand](images/listCommand.png) + +In this example, after executing the `list` command, we see the entire AddressBook again. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +### Sort persons: `sort` + +Format: `sort /COMPARATOR [/reverse]` + +* Sorts all persons by specified `COMPARATOR` +* Currently supported `COMPARATOR` include: + - `byname` + - `byemail` + - `byphone` + - `byaddress` +* The comparator refers to the attribute(s) provided for the basis to sort +* Only sorting by one attribute is allowed, due to the nature of most fields being unique (with the unlikely cases of name and address) +* The sorting is done according to [ASCII](https://www.ibm.com/docs/en/cobol-zos/6.1?topic=sequences-us-english-ascii-code-page) sequence, but is case-insensitive (i.e. `adam` has precedence over `Beatrice` when sorted in ascending order) +* Sorting is done in ascending order by default. To sort by descending order, the field `/reverse` can be added to the command (see Examples below) Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `sort /byname` sorts all contacts in UniMate AddressBook by their full name +* `sort /byaddress /reverse` sorts all contacts in UniMate AddressBook by their address + +![sortCommand](images/sortCommand.png) + +In this example, after executing `sort /byphone`, the AddressBook is now sorted according to the +contact's phone numbers, in ascending numerical order. ### Clearing all entries : `clear` -Clears all entries from the address book. +Clears all entries from the AddressBook. Format: `clear` -### Exiting the program : `exit` +![clearCommand](images/clearCommand.png) -Exits the program. +In this example, after executing `clear`, the AddressBook is now cleared of all contacts. -Format: `exit` +## Calendar System + +### General Definitions + +> **Date Time Format** +>* When inputting a date and time into a command, the following format is used: +> +> `yyyy-MM-dd HH:mm` where +> * `yyyy` represents the year, +> * `MM` represents the month, +> * `dd` represents the day, +> * `HH` represents the hours and +> * `mm` represents the minutes. +> +>**Event span** +>* The chronological span of the event is **inclusive** of the start date and time, but **exclusive** of the end date and time. + This means that for 2 consecutive events, the later event can start at the same time the earlier event ends. + +### \[USER CALENDAR\] Adding an event: `addEvent` + +Adds an event to the user's calendar. + +Format: `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME` + +* Adds the event starting from `START_DATE_TIME` and ending at `END_DATE_TIME` +* `DESCRIPTION` must be non-empty +* `START_DATE_TIME` and `END_DATE_TIME` must be in `yyyy-MM-dd HH:mm` format +* `START_DATE_TIME` must be before `END_DATE_TIME` for the command to be valid + +Example: +* `addEvent d/Cry about deadlines ts/2023-01-01 13:00 te/2023-01-01 15:00` + +![addEventCommand](images/addEventCommand.png) + +In this example, after executing the command `addEvent d/myEvent ts/2023-11-08 14:00 te/2023-11-08 16:00`, +the event `myEvent` is now visible in both the calendar on the right and the event list at the bottom, as +the date of `myEvent` coincides with the current week when the screenshot was taken. + +In the case `myEvent` happens before/after the current week, it will not be displayed in the calendar, but will still +show in the event list. + +### \[USER CALENDAR\] Deleting an event: `deleteEvent` + +Deletes an event from the user's calendar. + +Format `deleteEvent DATE_TIME_DURING_EVENT` + +* Deletes an event at the specified date and time. +* An event is considered to be at that date and time if the date time lies between the start time (inclusive) and the +end time (exclusive). +* If there is no event during `DATE_TIME_DURING_EVENT`, an error will be thrown. + +Example: +`deleteEvent 2023-11-01 12:00` + +![deleteEventCommand](images/deleteEventCommand.png) + +In this example, after executing the command `deleteEvent 2023-11-08 15:00`, `myEvent`(added in +the [addEvent command example](#user-calendar-adding-an-event-addevent)) that is happening during the +specified time was deleted, leaving an empty calendar and event list. + +### \[USER CALENDAR\] Deleting multiple events: `clearEvents` + +Clears all events within a specified time range. + +> **Note:** In order to ensure the user does not make the mistake of deleting more events than intended, +this command requires additional **confirmation** (refer to command format below) from the user to fully execute. +Without the confirmation, the result box will instead display the list of events that will be deleted if the +command were to fully execute, for the user's verification. + +Format: `clearEvents ts/START_DATE_TIME te/END_DATE_TIME c/CONFIRMATION` + +* Deletes all events from the specified start date and time to the specified end date and time +* An event is considered to be within the time range if overlaps with the time range for any period of time, inclusive of `START_DATE_TIME` but exclusive of `END_DATE_TIME` +* When the `CONFIRMATION` is absent, the command shows all events within the time range but does not delete them. The +same command is then shown with the confirmation included that can be copied and pasted to execute the command +* The `START_DATE_TIME` must be before `END_DATE_TIME` +* If there is no `START_DATE_TIME` or `END_DATE_TIME`, an error will be thrown + +Example: +`clearEvents ts/2023-01-01 00:00 te/2023-12-31 23:59 c/CONFIRMED` + +![clearEventsCommand](images/clearEventsCommand.png) + +In this example, after executing `clearEvents ts/2023-11-08 14:00 te/2023-11-08 16:00 c/CONFIRMED`, `myEvent`(added in +the [addEvent command example](#user-calendar-adding-an-event-addevent)) that is happening during the duration of the +specified time was deleted, leaving an empty calendar and event list. + +### Event List/Task List switch: `switchList` + +Events can be viewed from the calendar GUI that appears on the right. + +Additionally, a list of all events are displayed by default at the bottom. This list at the bottom can be switched to a +task list with the `switchList` command. More information can be found under at [Viewing all Tasks](#task-management-system). + +### \[CONTACT CALENDAR\] Adding an event to a contact: `addContactEvent` + +Adds an event to a contact's calendar at the specified index. + +Format: `addContactEvent INDEX d/DESCRIPTION ts/START_DATE_TIME ts/END_DATE_TIME` + +* Adds the event starting from `START_DATE_TIME` and ending at `END_DATE_TIME` +* `START_DATE_TIME` and `END_DATE_TIME` must be in `yyyy-MM-dd HH:mm` format +* `START_DATE_TIME` must be before `END_DATE_TIME` for the command to be valid + +Example: +* `addContactEvent 1 d/Team Meeting ts/2024-01-01 09:00 te/2024-01-01 11:00` + +![addContactEventCommand](images/addContactEventCommand.png) + +In this example, after executing `addContactEvent 1 d/Alex's Event ts/2023-11-08 14:00 te/2023-11-08 18:00` and +[double-clicking on the person card](#main-feature-1-addressbook) of index 1 in the AddressBook, the +contact's calendar pops up, revealing `Alex's Event`. In the case that the event happens before/after +the current week, it will not be shown in the calendar, but will still be displayed in the +[contact's event list](#viewing-contact-event-list-viewcontactevents). + +### \[CONTACT CALENDAR\] Deleting an event from a contact: `deleteContactEvent` + +Deletes an event from a contact's calendar at the specified index. + +Format `deleteContactEvent INDEX ts/DATE_TIME` + +* Deletes an event that contains the `DATE_TIME` from the contact + +Example: +* `deleteContactEvent 1 ts/2024-01-01 09:00` + +![deleteContactEvent](images/deleteContactEventCommand.png) + +In this example, after executing `deleteContactEvent 1 ts/2023-11-08 14:00` and +[double-clicking on the person card](#main-feature-1-addressbook) of index 1 in the AddressBook, +the contact's calendar pops up, revealing an empty calendar as `Alex's Event` (added in the +[example in addContactEvent](#contact-calendar-adding-an-event-to-a-contact-addcontactevent)), +occurring at the specified time, has been removed. + +### \[CONTACT CALENDAR\] Edit Contact Calendar Event: `editContactEvent` + +Edits the details of an event in a contact's calendar. + +> **Note:** If this command is executed while the event list of the contact of interest is open, +the event list will not be updated until the tab is closed and open again. + +Format: `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME][te/NEW_END_DATE_TIME]` + +* Edits `EVENT_INDEX` event of the `PERSON_INDEX` person in the AddressBook with the given fields. + +Example: `editContactEvent 1 1 d/Nap`, `editContactEvent 2 3 ts/2023-10-10 10:00 te/2023-10-12 15:00` + +* Note that all edited fields are optional, but there must be at least 1 edited field. + +![editContactEventBefore](images/editContactEventCommandBefore.png) + +In this example, we see this is the state of the calendar of `Alex Yeoh`, the person with index 1 +in the AddressBook. + +![editContactEventMessage](images/editContactEventCommandMessage.png) + +After executing `editContactEvent 1 1 d/Edited Description`, we get this confirmation message + +![editContactEventAfter](images/editContactEventCommandAfter.png) + +We can see that the event `Nap` has its description changed to `Edited Description`. + +### Viewing contact event list: `viewContactEvents` + +Creates a pop-up that displays a list of all events of a calendar belonging to a person in the AddressBook. + +Format: `viewContactEvents INDEX` + +* Views the event list of the person at `INDEX` as displayed. + +Example: +* `viewContactEvents 1` + +![viewContactEvents](images/viewContactEventsCommand.png) + +In this example, after execute `viewContactEvents 1`, a pop-up with the list of events of the person +with index 1 in the AddressBook, which in this case is `Alex Yeoh` shows up. + +### Comparing calendars with AddressBook Contacts +There are 2 ways for the user to compare calendars with their AddressBook contacts: +- [Index](#1-comparison-by-index) +- [Tag](#2-comparison-by-tag) + +The resulting pop-up calendar will display the time periods where all parties +are not available as greyed out sections. The pop-up has to be closed in order for the user to access +the main application again. + +>**Note:** Arguments for the commands are optional, hence `compareCalendars` and +`compareGroupCalendars` are valid commands, but the resulting pop-up will just display the +user's calendar. + +#### 1. Comparison by index + +Format `compareCalendars [INDEX]...` + +* Compare calendar with the contacts at the respective `INDEX` +* `INDEX` must be a positive non-zero integer that is smaller or equals to the size of the AddressBook +* If the `INDEX` number provided is invalid, an error will be returned +* If no `INDEX` is supplied, the resulting pop-up will just display the user's calendar + +Example: +`compareCalendars 1 3 5` + +![compareCalendars](images/compareCalendarsCommand.png) + +In this example, after executing `compareCalendars 1`, we see the timings when both the user and +the person with index 1 (`Alex Yeoh`) are not free is blocked out. + +#### 2. Comparison by tag + +Format `compareGroupCalendars [TAG]...` + +* Compare calendar with the contacts with the specified `TAG` +* If some of the `TAG` provided are invalid, the resulting pop-up will ignore the invalid `TAG` +* If all the `TAG` provided are invalid, the resulting pop-up will just display the user's calendar +* If no `TAG` is supplied, the resulting pop-up will just display the user's calendar + +Example: +`compareGroupCalendars school friends` + +![compareGroupCalendars](images/compareGroupCalendarsCommand.png) + +In this example, after executing `compareGroupCalendars friends`, we see the timings when both the +user and the contacts that have the `friends` tag are unavailable are blocked out. + +### Import *.ics files (Coming Soon) + +User can import *.ics files, which will automatically be integrated into +their UniMate calendars. + +Format: `import FILE_PATH` + +### View other weeks of Calendar (Coming Soon) + +User can view other weeks of their calendar schedule beyond just the +current week. + +Format: `viewWeek DATE` + +## Task Management System + +Tasks consist of a `DESCRIPTION` and an **optional** `DEADLINE`. + +> **Note:** Tasks can have the same `DESCRIPTION` or `DEADLINE`, but not both. + +### Viewing tasks: `switchList` + +Switches the bottom list between the event list and the task list. + +Format: `switchList` + +* All input after `switchList` will be ignored. +* The bottom list displays the event list by default. + +![switchList](images/switchListCommand.png) + +In this example, after executing `switchList`, we see that the bottom of the GUI has switched from +[`event list`](#sub-feature-2-event-list) to the [`task list`](#main-feature-3-task-management-system). + +### Adding tasks: `addTask` + +Adds a task to the Task Manager. + +Format: `addTask d/DESCRIPTION [te/DEADLINE]` + +* `DESCRIPTION` cannot be empty. +* `DEADLINE` must be in the same format given [above](#general-definitions) for date and time. +* `DEADLINE` can also be omitted to create a task with no specified deadline. +* When adding a task with a deadline with a date that is invalid but with a day of 31 or less, instead adds a task with +a deadline on the last day of the month. +* While there is no character limit for Task descriptions, descriptions will be truncated if they are too long to +display. + +Examples: +* `addTask d/Go for a run te/2023-02-14 19:00` +* `addTask d/Hydrate regularly!` +* `addTask d/Fix CS2103 Project bug te/2023-02-31 12:00` creates the task with deadline `2023-02-28 12:00` instead. + +![addTask](images/addTaskCommand.png) + +In this example, after executing `addTask d/Hydrate regularly!`, we see the new `Hydrate regularly!` +task appearing at the bottom of the task list. + +### Deleting tasks: `deleteTask` + +Deletes a task from the Task Manager according to the index of the task displayed in the task list. + +Format: `deleteTask INDEX` + +* Throws an error if there is no `INDEX` present or if it exceeds the length of the task list. + +Examples: +* `deleteTask 1` + +![deleteTask](images/deleteTaskCommand.png) + +In this example, after executing `deleteTask 4`, the `Hydrate regularly!` task added in the +[previous example](#adding-tasks-addtask) was removed. + +### Sorting tasks: `sortTasks` + +Changes the way tasks in the Task Manager are displayed in the task list. + +Format: `sortTasks PARAMETER` + +* `PARAMETER` can only be `DESCRIPTION` or `DEADLINE`. + +Examples: +* `sortTasks DESCRIPTION` sorts tasks by their `DESCRIPTION` alphabetically. +* `sortTasks DEADLINE` sorts tasks by their `DEADLINE`. Tasks with deadlines are prioritised above tasks with no +deadline. + +![sortTasks](images/sortTasksCommand.png) + +In this example, after executing `sortTasks DESCRIPTION`, the tasks are now sorted in ascending +alphanumeric order. + +## Miscellaneous ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +All data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +AddressBook data is saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. +Calendar data is saved automatically as a JSON file `[JAR file location]/data/calendar.json`. +Task-list data is saved automatically as a JSON file `[JAR file location]/data/taskmanager.json`. +Advanced users are welcome to update data directly by editing the data files. + + -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. -
+**Caution:** +If your changes to the data file makes its format invalid, UniMate will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. +
### Archiving data files `[coming in v2.0]` @@ -171,27 +676,44 @@ _Details coming soon ..._ -------------------------------------------------------------------------------------------------------------------- -## FAQ +# FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous UniMate home folder. -------------------------------------------------------------------------------------------------------------------- -## Known issues +# Known issues 1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +2. If the zoom setting of the operating system is not set to 100%, it might cause inconsistencies in the UI which may cause the event cards displayed in the [**Calendar GUI**](#features-overview) to be lined up incorrectly. -------------------------------------------------------------------------------------------------------------------- -## Command summary +# Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` +Action | Format, Examples +-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Help** | `help` +**Exit** | `exit` +**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +**Delete** | `delete INDEX`
e.g. `delete 3` +**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.`edit 2 n/James Lee e/jameslee@example.com` +**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g. `find James Jake` +**Filter** | `filter [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
e.g. `filter n/John Doe` +**List** | `list` +**Sort** | `sort /COMPARATOR`
e.g. `sort /byname` +**Clear** | `clear` +**addEvent** | `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME`
e.g. `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59` +**deleteEvent** | `deleteEvent DATE_TIME`
e.g. `deleteEvent 2023-02-03 12:00` +**clearEvents** | `clearEvents ts/START_DATE_TIME te/END_DATE_TIME c/CONFIRMATION`
e.g. `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00 c/CONFIRMED` +**switchList** | `switchList` +**addContactEvent** | `addContactEvent INDEX d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME`
e.g. `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59` +**deleteContactEvent** | `deleteContactEvent INDEX ts/DATE_TIME`
e.g. `deleteContactEvent 1 ts/2023-02-03 12:00` +**editContactEvent** | `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME] [te/NEW_END_DATE_TIME]`
e.g. `editContactEvent 1 1 d/Edited Description` +**viewContactEvents** | `viewContactEvents INDEX`
e.g. `viewContactEvents 1` +**compareCalendars** | `compareCalendars [INDEX]...`
e.g. `compareCalendars 1 3 5` +**compareGroupCalendars** | `compareGroupCalendars [TAG]...`
e.g. `compareGroupCalendars school friend` +**addTask** | `addTask d/DESCRIPTION [te/DEADLINE]`
e.g. `addTask d/Go for a run te/2023-02-14 19:00` +**deleteTask** | `deleteTask INDEX`
e.g. `deleteTask 1` +**sortTasks** | `sortTasks PARAMETER`
e.g. `sortTasks DESCRIPTION`
e.g. `sortTasks DEADLINE` diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 6bd245d8f4e..00000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,15 +0,0 @@ -title: "AB-3" -theme: minima - -header_pages: - - UserGuide.md - - DeveloperGuide.md - - AboutUs.md - -markdown: kramdown - -repository: "se-edu/addressbook-level3" -github_icon: "images/github-icon.png" - -plugins: - - jemoji diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml deleted file mode 100644 index 8f3e50cb601..00000000000 --- a/docs/_data/projects.yml +++ /dev/null @@ -1,23 +0,0 @@ -- name: "AB-1" - url: https://se-edu.github.io/addressbook-level1 - -- name: "AB-2" - url: https://se-edu.github.io/addressbook-level2 - -- name: "AB-3" - url: https://se-edu.github.io/addressbook-level3 - -- name: "AB-4" - url: https://se-edu.github.io/addressbook-level4 - -- name: "Duke" - url: https://se-edu.github.io/duke - -- name: "Collate" - url: https://se-edu.github.io/collate - -- name: "Book" - url: https://se-edu.github.io/se-book - -- name: "Resources" - url: https://se-edu.github.io/resources diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html deleted file mode 100644 index 8559a67ffad..00000000000 --- a/docs/_includes/custom-head.html +++ /dev/null @@ -1,6 +0,0 @@ -{% comment %} - Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons: - - 1. Head over to https://realfavicongenerator.net/ to add your own favicons. - 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet. -{% endcomment %} diff --git a/docs/_includes/head.html b/docs/_includes/head.html deleted file mode 100644 index 83ac5326933..00000000000 --- a/docs/_includes/head.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - {%- include custom-head.html -%} - - {{page.title}} - - diff --git a/docs/_includes/header.html b/docs/_includes/header.html deleted file mode 100644 index 33badcd4f99..00000000000 --- a/docs/_includes/header.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/docs/_layouts/alt-page.html b/docs/_layouts/alt-page.html deleted file mode 100644 index 5dbc6ef245f..00000000000 --- a/docs/_layouts/alt-page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
- -
-

{{ page.alt_title | escape }}

-
- -
- {{ content }} -
- -
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html deleted file mode 100644 index e092cd572e0..00000000000 --- a/docs/_layouts/default.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - {%- include head.html -%} - - - - {%- include header.html -%} - -
-
- {{ content }} -
-
- - - - diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html deleted file mode 100644 index 01e4b2a93b8..00000000000 --- a/docs/_layouts/page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
- -
-

{{ page.title | escape }}

-
- -
- {{ content }} -
- -
diff --git a/docs/_markbind/layouts/default.md b/docs/_markbind/layouts/default.md new file mode 100644 index 00000000000..b907e951747 --- /dev/null +++ b/docs/_markbind/layouts/default.md @@ -0,0 +1,66 @@ + + + + +
+ + UniMate +
  • User Guide
  • +
  • Developer Guide
  • +
  • About Us
  • +
  • :fab-github: +
  • +
  • + +
  • +
    +
    + +
    + +
    + {{ content }} +
    + + +
    + +
    + +
    + [**Powered by** {{MarkBind}}, generated on {{timestamp}}] +
    +
    diff --git a/docs/_markbind/variables.json b/docs/_markbind/variables.json new file mode 100644 index 00000000000..9d89eb0358b --- /dev/null +++ b/docs/_markbind/variables.json @@ -0,0 +1,3 @@ +{ + "jsonVariableExample": "Your variables can be defined here as well" +} diff --git a/docs/_markbind/variables.md b/docs/_markbind/variables.md new file mode 100644 index 00000000000..89ae5318fa4 --- /dev/null +++ b/docs/_markbind/variables.md @@ -0,0 +1,4 @@ + +To inject this HTML segment in your markbind files, use {{ example }} where you want to place it. +More generally, surround the segment's id with double curly braces. + diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss deleted file mode 100644 index 0d3f6e80ced..00000000000 --- a/docs/_sass/minima/_base.scss +++ /dev/null @@ -1,295 +0,0 @@ -html { - font-size: $base-font-size; -} - -/** - * Reset some basic elements - */ -body, h1, h2, h3, h4, h5, h6, -p, blockquote, pre, hr, -dl, dd, ol, ul, figure { - margin: 0; - padding: 0; - -} - - - -/** - * Basic styling - */ -body { - font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; - color: $text-color; - background-color: $background-color; - -webkit-text-size-adjust: 100%; - -webkit-font-feature-settings: "kern" 1; - -moz-font-feature-settings: "kern" 1; - -o-font-feature-settings: "kern" 1; - font-feature-settings: "kern" 1; - font-kerning: normal; - display: flex; - min-height: 100vh; - flex-direction: column; - overflow-wrap: break-word; -} - - - -/** - * Set `margin-bottom` to maintain vertical rhythm - */ -h1, h2, h3, h4, h5, h6, -p, blockquote, pre, -ul, ol, dl, figure, -%vertical-rhythm { - margin-bottom: $spacing-unit / 2; -} - -hr { - margin-top: $spacing-unit; - margin-bottom: $spacing-unit; -} - -/** - * `main` element - */ -main { - display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */ -} - - - -/** - * Images - */ -img { - max-width: 100%; - vertical-align: middle; -} - - - -/** - * Figures - */ -figure > img { - display: block; -} - -figcaption { - font-size: $small-font-size; -} - - - -/** - * Lists - */ -ul, ol { - margin-left: $spacing-unit; -} - -li { - > ul, - > ol { - margin-bottom: 0; - } -} - - - -/** - * Headings - */ -h1, h2, h3, h4, h5, h6 { - font-weight: $base-font-weight; -} - - - -/** - * Links - */ -a { - color: $link-base-color; - text-decoration: none; - - &:visited { - color: $link-visited-color; - } - - &:hover { - color: $text-color; - text-decoration: underline; - } - - .social-media-list &:hover { - text-decoration: none; - - .username { - text-decoration: underline; - } - } -} - - -/** - * Blockquotes - */ -blockquote { - color: $brand-color; - border-left: 4px solid $brand-color-light; - padding-left: $spacing-unit / 2; - @include relative-font-size(1.125); - font-style: italic; - - > :last-child { - margin-bottom: 0; - } - - i, em { - font-style: normal; - } -} - - - -/** - * Code formatting - */ -pre, -code { - font-family: $code-font-family; - font-size: 0.9375em; - border: 1px solid $brand-color-light; - border-radius: 3px; - background-color: $code-background-color; -} - -code { - padding: 1px 5px; -} - -pre { - padding: 8px 12px; - overflow-x: auto; - - > code { - border: 0; - padding-right: 0; - padding-left: 0; - } -} - -.highlight { - border-radius: 3px; - background: $code-background-color; - @extend %vertical-rhythm; - - .highlighter-rouge & { - background: $code-background-color; - } -} - - - -/** - * Wrapper - */ -.wrapper { - max-width: calc(#{$content-width} - (#{$spacing-unit})); - margin-right: auto; - margin-left: auto; - padding-right: $spacing-unit / 2; - padding-left: $spacing-unit / 2; - @extend %clearfix; - - @media screen and (min-width: $on-large) { - max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); - padding-right: $spacing-unit; - padding-left: $spacing-unit; - } -} - - - -/** - * Clearfix - */ -%clearfix:after { - content: ""; - display: table; - clear: both; -} - - - -/** - * Icons - */ - -.orange { - color: #f66a0a; -} - -.grey { - color: #828282; -} - -/** - * Tables - */ -table { - margin-bottom: $spacing-unit; - width: 100%; - text-align: $table-text-align; - color: $table-text-color; - border-collapse: collapse; - border: 1px solid $table-border-color; - tr { - &:nth-child(even) { - background-color: $table-zebra-color; - } - } - th, td { - padding: ($spacing-unit / 3) ($spacing-unit / 2); - } - th { - background-color: $table-header-bg-color; - border: 1px solid $table-header-border; - } - td { - border: 1px solid $table-border-color; - } - - @include media-query($on-laptop) { - display: block; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - } -} - -@media print { - /** - * Prevents page break from cutting through content when printing - */ - body { - display: block; - } - /** - * Replaces the top navigation menu with the project name when printing - */ - .site-header .wrapper { - display: none; - } - .site-header { - text-align: center; - } - .site-header:before { - content: "AB-3"; - font-size: 32px; - } -} - diff --git a/docs/_sass/minima/_layout.scss b/docs/_sass/minima/_layout.scss deleted file mode 100644 index ca99f981701..00000000000 --- a/docs/_sass/minima/_layout.scss +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Site header - */ -.site-header { - border-top: 5px solid $brand-color-dark; - border-bottom: 1px solid $brand-color-light; - min-height: $spacing-unit * 1.865; - line-height: $base-line-height * $base-font-size * 2.25; - - // Positioning context for the mobile navigation icon - position: relative; -} - -.site-title { - @include relative-font-size(1.625); - font-weight: 300; - letter-spacing: -1px; - margin-bottom: 0; - float: left; - - @include media-query($on-palm) { - padding-right: 45px; - } - - &, - &:visited { - color: $brand-color-dark; - } -} - -.site-nav { - position: absolute; - top: 9px; - right: $spacing-unit / 2; - background-color: $background-color; - border: 1px solid $brand-color-light; - border-radius: 5px; - text-align: right; - - .nav-trigger { - display: none; - } - - .menu-icon { - float: right; - width: 36px; - height: 26px; - line-height: 0; - padding-top: 10px; - text-align: center; - - > svg path { - fill: $brand-color-dark; - } - } - - label[for="nav-trigger"] { - display: block; - float: right; - width: 36px; - height: 36px; - z-index: 2; - cursor: pointer; - } - - input ~ .trigger { - clear: both; - display: none; - } - - input:checked ~ .trigger { - display: block; - padding-bottom: 5px; - } - - .page-link { - color: $text-color; - line-height: $base-line-height; - display: block; - padding: 5px 10px; - - // Gaps between nav items, but not on the last one - &:not(:last-child) { - margin-right: 0; - } - margin-left: 20px; - } - - @media screen and (min-width: $on-medium) { - position: static; - float: right; - border: none; - background-color: inherit; - - label[for="nav-trigger"] { - display: none; - } - - .menu-icon { - display: none; - } - - input ~ .trigger { - display: block; - } - - .page-link { - display: inline; - padding: 0; - - &:not(:last-child) { - margin-right: 20px; - } - margin-left: auto; - } - } -} - - - -/** - * Page content - */ -.page-content { - padding: $spacing-unit 0; - flex: 1 0 auto; -} - -.page-heading { - @include relative-font-size(2); -} - -.post-list-heading { - @include relative-font-size(1.75); -} - -.post-list { - margin-left: 0; - list-style: none; - - > li { - margin-bottom: $spacing-unit; - } -} - -.post-meta { - font-size: $small-font-size; - color: $brand-color; -} - -.post-link { - display: block; - @include relative-font-size(1.5); -} - - - -/** - * Posts - */ -.post-header { - margin-bottom: $spacing-unit; -} - -.post-title, -.post-content h1 { - @include relative-font-size(2.625); - letter-spacing: -1px; - line-height: 1.15; - - @media screen and (min-width: $on-large) { - @include relative-font-size(2.625); - } -} - -.post-content { - margin-bottom: $spacing-unit; - - h1, h2, h3 { margin-top: $spacing-unit * 2 } - h4, h5, h6 { margin-top: $spacing-unit } - - h2 { - @include relative-font-size(1.75); - - @media screen and (min-width: $on-large) { - @include relative-font-size(2); - } - } - - h3 { - @include relative-font-size(1.375); - - @media screen and (min-width: $on-large) { - @include relative-font-size(1.625); - } - } - - h4 { - @include relative-font-size(1.25); - } - - h5 { - @include relative-font-size(1.125); - } - h6 { - @include relative-font-size(1.0625); - } -} - - -.social-media-list { - display: table; - margin: 0 auto; - li { - float: left; - margin: 5px 10px 5px 0; - &:last-of-type { margin-right: 0 } - a { - display: block; - padding: $spacing-unit / 4; - border: 1px solid $brand-color-light; - &:hover { border-color: darken($brand-color-light, 10%) } - } - } -} - - - -/** - * Pagination navbar - */ -.pagination { - margin-bottom: $spacing-unit; - @extend .social-media-list; - li { - a, div { - min-width: 41px; - text-align: center; - box-sizing: border-box; - } - div { - display: block; - padding: $spacing-unit / 4; - border: 1px solid transparent; - - &.pager-edge { - color: darken($brand-color-light, 5%); - border: 1px dashed; - } - } - } -} - - - -/** - * Grid helpers - */ -@media screen and (min-width: $on-large) { - .one-half { - width: calc(50% - (#{$spacing-unit} / 2)); - } -} diff --git a/docs/_sass/minima/custom-mixins.scss b/docs/_sass/minima/custom-mixins.scss deleted file mode 100644 index 9d4bedc1c67..00000000000 --- a/docs/_sass/minima/custom-mixins.scss +++ /dev/null @@ -1,21 +0,0 @@ -@mixin alert-variant($background, $border, $color) { - color: $color; - @include gradient-bg($background); - border-color: $border; - - .alert-link { - color: darken($color, 10%); - } -} - -@mixin gradient-bg($color, $foreground: null) { - @if $enable-gradients { - @if $foreground { - background-image: $foreground, linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } @else { - background-image: linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } - } @else { - background-color: $color; - } -} diff --git a/docs/_sass/minima/custom-styles.scss b/docs/_sass/minima/custom-styles.scss deleted file mode 100644 index 56b5d56b430..00000000000 --- a/docs/_sass/minima/custom-styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Placeholder to allow defining custom styles that override everything else. -// (Use `_sass/minima/custom-variables.scss` to override variable defaults) -h2, h3, h4, h5, h6 { - color: #e46c0a; -} - -// Bootstrap style alerts -.alert { - position: relative; - padding: $alert-padding-y $alert-padding-x; - margin-bottom: $alert-margin-bottom; - border: $alert-border-width solid transparent; - border-radius : $alert-border-radius; -} - -// Headings for larger alerts -.alert-heading { - // Specified to prevent conflicts of changing $headings-color - color: inherit; -} - -// Provide class for links that match alerts -.alert-link { - font-weight: $alert-link-font-weight; -} - -// Generate contextual modifier classes for colorizing the alert. - -@each $color, $value in $theme-colors { - .alert-#{$color} { - @include alert-variant(color-level($value, $alert-bg-level), color-level($value, $alert-border-level), color-level($value, $alert-color-level)); - } -} - diff --git a/docs/_sass/minima/custom-variables.scss b/docs/_sass/minima/custom-variables.scss deleted file mode 100644 index a128970cbe7..00000000000 --- a/docs/_sass/minima/custom-variables.scss +++ /dev/null @@ -1,76 +0,0 @@ -// Placeholder to allow overriding predefined variables smoothly. - -//Bootstrap's default -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #6c757d !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; -$blue: #0d6efd !default; -$indigo: #6610f2 !default; -$purple: #6f42c1 !default; -$pink: #d63384 !default; -$red: #dc3545 !default; -$orange: #fd7e14 !default; -$yellow: #ffc107 !default; -$green: #28a745 !default; -$teal: #20c997 !default; -$cyan: #17a2b8 !default; - -$primary: $blue !default; -$secondary: $gray-600 !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-100 !default; -$dark: $gray-800 !default; - -$theme-colors: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark -) !default; - -$theme-color-interval: 8% !default; - -$body-bg: $white !default; -$body-color: $gray-900 !default; -$body-text-align: null !default; - -$enable-gradients: true; - -// Define alert colors, border radius, and padding. -$border-radius: .25rem !default; -$border-width: 1px !default; -$font-weight-bold: 700 !default; - -$alert-padding-y: .75rem !default; -$alert-padding-x: 1.25rem !default; -$alert-margin-bottom: 1rem !default; -$alert-border-radius: $border-radius !default; -$alert-link-font-weight: $font-weight-bold !default; -$alert-border-width: $border-width !default; - -$alert-bg-level: -10 !default; -$alert-border-level: -9 !default; -$alert-color-level: 6 !default; - -// Request a color level -// scss-docs-start color-level -@function color-level($color: $primary, $level: 0) { - $color-base: if($level > 0, $black, $white); - $level: abs($level); - @return mix($color-base, $color, $level * $theme-color-interval); -} diff --git a/docs/_sass/minima/initialize.scss b/docs/_sass/minima/initialize.scss deleted file mode 100644 index 30288811151..00000000000 --- a/docs/_sass/minima/initialize.scss +++ /dev/null @@ -1,51 +0,0 @@ -@charset "utf-8"; - -// Define defaults for each variable. - -$base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Apple Color Emoji", Roboto, Helvetica, Arial, sans-serif !default; -$code-font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; -$base-font-size: 16px !default; -$base-font-weight: 400 !default; -$small-font-size: $base-font-size * 0.875 !default; -$base-line-height: 1.5 !default; - -$spacing-unit: 30px !default; - -$table-text-align: left !default; - -// Width of the content area -$content-width: 800px !default; - -$on-palm: 600px !default; -$on-laptop: 800px !default; - -$on-medium: $on-palm !default; -$on-large: $on-laptop !default; - -// Use media queries like this: -// @include media-query($on-palm) { -// .wrapper { -// padding-right: $spacing-unit / 2; -// padding-left: $spacing-unit / 2; -// } -// } -// Notice the following mixin uses max-width, in a deprecated, desktop-first -// approach, whereas media queries used elsewhere now use min-width. -@mixin media-query($device) { - @media screen and (max-width: $device) { - @content; - } -} - -@mixin relative-font-size($ratio) { - font-size: #{$ratio}rem; -} - -// Import pre-styling-overrides hook and style-partials. -@import - "minima/custom-variables", // Hook to override predefined variables. - "minima/custom-mixins", // Hook to add custom mixins. - "minima/base", // Defines element resets. - "minima/layout", // Defines structure and style based on CSS selectors. - "minima/custom-styles" // Hook to override existing styles. -; diff --git a/docs/_sass/minima/skins/classic.scss b/docs/_sass/minima/skins/classic.scss deleted file mode 100644 index 37ea9c5244c..00000000000 --- a/docs/_sass/minima/skins/classic.scss +++ /dev/null @@ -1,84 +0,0 @@ -@charset "utf-8"; - -$brand-color: #828282 !default; -$brand-color-light: lighten($brand-color, 40%) !default; -$brand-color-dark: darken($brand-color, 25%) !default; - -$text-color: #111 !default; -$background-color: #fdfdfd !default; -$code-background-color: #eef !default; - -$link-base-color: #2a7ae2 !default; -$link-visited-color: darken($link-base-color, 15%) !default; - -$table-text-color: lighten($text-color, 18%) !default; -$table-zebra-color: lighten($brand-color, 46%) !default; -$table-header-bg-color: lighten($brand-color, 43%) !default; -$table-header-border: lighten($brand-color, 36%) !default; -$table-border-color: $brand-color-light !default; - - -// Syntax highlighting styles should be adjusted appropriately for every "skin" -// ---------------------------------------------------------------------------- - -.highlight { - .c { color: #998; font-style: italic } // Comment - .err { color: #a61717; background-color: #e3d2d2 } // Error - .k { font-weight: bold } // Keyword - .o { font-weight: bold } // Operator - .cm { color: #998; font-style: italic } // Comment.Multiline - .cp { color: #999; font-weight: bold } // Comment.Preproc - .c1 { color: #998; font-style: italic } // Comment.Single - .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: #000; background-color: #fdd } // Generic.Deleted - .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific - .ge { font-style: italic } // Generic.Emph - .gr { color: #a00 } // Generic.Error - .gh { color: #999 } // Generic.Heading - .gi { color: #000; background-color: #dfd } // Generic.Inserted - .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific - .go { color: #888 } // Generic.Output - .gp { color: #555 } // Generic.Prompt - .gs { font-weight: bold } // Generic.Strong - .gu { color: #aaa } // Generic.Subheading - .gt { color: #a00 } // Generic.Traceback - .kc { font-weight: bold } // Keyword.Constant - .kd { font-weight: bold } // Keyword.Declaration - .kp { font-weight: bold } // Keyword.Pseudo - .kr { font-weight: bold } // Keyword.Reserved - .kt { color: #458; font-weight: bold } // Keyword.Type - .m { color: #099 } // Literal.Number - .s { color: #d14 } // Literal.String - .na { color: #008080 } // Name.Attribute - .nb { color: #0086B3 } // Name.Builtin - .nc { color: #458; font-weight: bold } // Name.Class - .no { color: #008080 } // Name.Constant - .ni { color: #800080 } // Name.Entity - .ne { color: #900; font-weight: bold } // Name.Exception - .nf { color: #900; font-weight: bold } // Name.Function - .nn { color: #555 } // Name.Namespace - .nt { color: #000080 } // Name.Tag - .nv { color: #008080 } // Name.Variable - .ow { font-weight: bold } // Operator.Word - .w { color: #bbb } // Text.Whitespace - .mf { color: #099 } // Literal.Number.Float - .mh { color: #099 } // Literal.Number.Hex - .mi { color: #099 } // Literal.Number.Integer - .mo { color: #099 } // Literal.Number.Oct - .sb { color: #d14 } // Literal.String.Backtick - .sc { color: #d14 } // Literal.String.Char - .sd { color: #d14 } // Literal.String.Doc - .s2 { color: #d14 } // Literal.String.Double - .se { color: #d14 } // Literal.String.Escape - .sh { color: #d14 } // Literal.String.Heredoc - .si { color: #d14 } // Literal.String.Interpol - .sx { color: #d14 } // Literal.String.Other - .sr { color: #009926 } // Literal.String.Regex - .s1 { color: #d14 } // Literal.String.Single - .ss { color: #990073 } // Literal.String.Symbol - .bp { color: #999 } // Name.Builtin.Pseudo - .vc { color: #008080 } // Name.Variable.Class - .vg { color: #008080 } // Name.Variable.Global - .vi { color: #008080 } // Name.Variable.Instance - .il { color: #099 } // Literal.Number.Integer.Long -} diff --git a/docs/_sass/minima/skins/solarized-dark.scss b/docs/_sass/minima/skins/solarized-dark.scss deleted file mode 100644 index f3b1f387de0..00000000000 --- a/docs/_sass/minima/skins/solarized-dark.scss +++ /dev/null @@ -1,4 +0,0 @@ -@charset "utf-8"; - -$sol-is-dark: true; -@import "minima/skins/solarized"; diff --git a/docs/_sass/minima/skins/solarized.scss b/docs/_sass/minima/skins/solarized.scss deleted file mode 100644 index 982bd7f2990..00000000000 --- a/docs/_sass/minima/skins/solarized.scss +++ /dev/null @@ -1,133 +0,0 @@ -@charset "utf-8"; - -// Solarized skin -// ============== -// Created by Sander Voerman using the Solarized -// color scheme by Ethan Schoonover . - -// This style sheet implements two options for the minima.skin setting: -// "solarized" for light mode and "solarized-dark" for dark mode. -$sol-is-dark: false !default; - - -// Color scheme -// ------------ -// The inline comments show the canonical L*a*b values for each color. - -$sol-base03: #002b36; // 15 -12 -12 -$sol-base02: #073642; // 20 -12 -12 -$sol-base01: #586e75; // 45 -07 -07 -$sol-base00: #657b83; // 50 -07 -07 -$sol-base0: #839496; // 60 -06 -03 -$sol-base1: #93a1a1; // 65 -05 -02 -$sol-base2: #eee8d5; // 92 -00 10 -$sol-base3: #fdf6e3; // 97 00 10 -$sol-yellow: #b58900; // 60 10 65 -$sol-orange: #cb4b16; // 50 50 55 -$sol-red: #dc322f; // 50 65 45 -$sol-magenta: #d33682; // 50 65 -05 -$sol-violet: #6c71c4; // 50 15 -45 -$sol-blue: #268bd2; // 55 -10 -45 -$sol-cyan: #2aa198; // 60 -35 -05 -$sol-green: #859900; // 60 -20 65 - -$sol-mono3: $sol-base3; -$sol-mono2: $sol-base2; -$sol-mono1: $sol-base1; -$sol-mono00: $sol-base00; -$sol-mono01: $sol-base01; - -@if $sol-is-dark { - $sol-mono3: $sol-base03; - $sol-mono2: $sol-base02; - $sol-mono1: $sol-base01; - $sol-mono00: $sol-base0; - $sol-mono01: $sol-base1; -} - - -// Minima color variables -// ---------------------- - -$brand-color: $sol-mono1 !default; -$brand-color-light: mix($sol-mono1, $sol-mono3) !default; -$brand-color-dark: $sol-mono00 !default; - -$text-color: $sol-mono01 !default; -$background-color: $sol-mono3 !default; -$code-background-color: $sol-mono2 !default; - -$link-base-color: $sol-blue !default; -$link-visited-color: mix($sol-blue, $sol-mono00) !default; - -$table-text-color: $sol-mono00 !default; -$table-zebra-color: mix($sol-mono2, $sol-mono3) !default; -$table-header-bg-color: $sol-mono2 !default; -$table-header-border: $sol-mono1 !default; -$table-border-color: $sol-mono1 !default; - - -// Syntax highlighting styles -// -------------------------- - -.highlight { - .c { color: $sol-mono1; font-style: italic } // Comment - .err { color: $sol-red } // Error - .k { color: $sol-mono01; font-weight: bold } // Keyword - .o { color: $sol-mono01; font-weight: bold } // Operator - .cm { color: $sol-mono1; font-style: italic } // Comment.Multiline - .cp { color: $sol-mono1; font-weight: bold } // Comment.Preproc - .c1 { color: $sol-mono1; font-style: italic } // Comment.Single - .cs { color: $sol-mono1; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: $sol-red } // Generic.Deleted - .gd .x { color: $sol-red } // Generic.Deleted.Specific - .ge { color: $sol-mono00; font-style: italic } // Generic.Emph - .gr { color: $sol-red } // Generic.Error - .gh { color: $sol-mono1 } // Generic.Heading - .gi { color: $sol-green } // Generic.Inserted - .gi .x { color: $sol-green } // Generic.Inserted.Specific - .go { color: $sol-mono00 } // Generic.Output - .gp { color: $sol-mono00 } // Generic.Prompt - .gs { color: $sol-mono01; font-weight: bold } // Generic.Strong - .gu { color: $sol-mono1 } // Generic.Subheading - .gt { color: $sol-red } // Generic.Traceback - .kc { color: $sol-mono01; font-weight: bold } // Keyword.Constant - .kd { color: $sol-mono01; font-weight: bold } // Keyword.Declaration - .kp { color: $sol-mono01; font-weight: bold } // Keyword.Pseudo - .kr { color: $sol-mono01; font-weight: bold } // Keyword.Reserved - .kt { color: $sol-violet; font-weight: bold } // Keyword.Type - .m { color: $sol-cyan } // Literal.Number - .s { color: $sol-magenta } // Literal.String - .na { color: $sol-cyan } // Name.Attribute - .nb { color: $sol-blue } // Name.Builtin - .nc { color: $sol-violet; font-weight: bold } // Name.Class - .no { color: $sol-cyan } // Name.Constant - .ni { color: $sol-violet } // Name.Entity - .ne { color: $sol-violet; font-weight: bold } // Name.Exception - .nf { color: $sol-blue; font-weight: bold } // Name.Function - .nn { color: $sol-mono00 } // Name.Namespace - .nt { color: $sol-blue } // Name.Tag - .nv { color: $sol-cyan } // Name.Variable - .ow { color: $sol-mono01; font-weight: bold } // Operator.Word - .w { color: $sol-mono1 } // Text.Whitespace - .mf { color: $sol-cyan } // Literal.Number.Float - .mh { color: $sol-cyan } // Literal.Number.Hex - .mi { color: $sol-cyan } // Literal.Number.Integer - .mo { color: $sol-cyan } // Literal.Number.Oct - .sb { color: $sol-magenta } // Literal.String.Backtick - .sc { color: $sol-magenta } // Literal.String.Char - .sd { color: $sol-magenta } // Literal.String.Doc - .s2 { color: $sol-magenta } // Literal.String.Double - .se { color: $sol-magenta } // Literal.String.Escape - .sh { color: $sol-magenta } // Literal.String.Heredoc - .si { color: $sol-magenta } // Literal.String.Interpol - .sx { color: $sol-magenta } // Literal.String.Other - .sr { color: $sol-green } // Literal.String.Regex - .s1 { color: $sol-magenta } // Literal.String.Single - .ss { color: $sol-magenta } // Literal.String.Symbol - .bp { color: $sol-mono1 } // Name.Builtin.Pseudo - .vc { color: $sol-cyan } // Name.Variable.Class - .vg { color: $sol-cyan } // Name.Variable.Global - .vi { color: $sol-cyan } // Name.Variable.Instance - .il { color: $sol-cyan } // Literal.Number.Integer.Long -} diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss deleted file mode 100644 index b5ec6976efa..00000000000 --- a/docs/assets/css/style.scss +++ /dev/null @@ -1,12 +0,0 @@ ---- -# Only the main Sass file needs front matter (the dashes are enough) ---- - -@import - "minima/skins/{{ site.minima.skin | default: 'classic' }}", - "minima/initialize"; - -.icon { - height: 21px; - width: 21px -} diff --git a/docs/diagrams/BottomListPanelClassDiagram.puml b/docs/diagrams/BottomListPanelClassDiagram.puml new file mode 100644 index 00000000000..5f796247882 --- /dev/null +++ b/docs/diagrams/BottomListPanelClassDiagram.puml @@ -0,0 +1,59 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +package UI <>{ +Class "<>\nUi" as Ui +Class "{abstract}\nUiPart" as UiPart +Class UiManager +Class MainWindow +Class BottomListPanel +Class EventListPanel +Class TaskListPanel +Class EventCard +Class TaskCard +} + +package Model <> { +Class HiddenModel #FFFFFF +} + +package Logic <> { +Class HiddenLogic #FFFFFF +} + +Class HiddenOutside #FFFFFF +HiddenOutside ..> Ui + +UiManager .left.|> Ui +UiManager -down-> "1" MainWindow + +MainWindow *-down-> "1" BottomListPanel + +BottomListPanel -down-> "0..1" TaskListPanel +BottomListPanel -down-> "0..1" EventListPanel +note left on link +{XOR} +end note + +EventListPanel -down-> "*" EventCard +TaskListPanel -down-> "*" TaskCard + + +MainWindow -left-|> UiPart + +TaskListPanel --|> UiPart +EventListPanel --|> UiPart +TaskCard --|> UiPart +EventCard --|> UiPart +BottomListPanel --|> UiPart + +EventCard ..> Model +TaskCard ..> Model +UiManager -right-> Logic +MainWindow -left-> Logic + +MainWindow -[hidden]-|> UiPart +@enduml diff --git a/docs/diagrams/ClearEventsSequenceDiagram.puml b/docs/diagrams/ClearEventsSequenceDiagram.puml new file mode 100644 index 00000000000..74797225c1e --- /dev/null +++ b/docs/diagrams/ClearEventsSequenceDiagram.puml @@ -0,0 +1,77 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":UniMateParser" as UniMateParser LOGIC_COLOR +participant ":ClearEventsCommandParser" as ClearEventsCommandParser LOGIC_COLOR +participant "d:ClearEventsCommand" as ClearEventsCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Calendar" as Calendar MODEL_COLOR +end box + +[-> LogicManager : execute("clearEvents ts/2023-02-03 12:00 \nte/2023-02-03 14:00") +activate LogicManager + +LogicManager -> UniMateParser : parseCommand("clearEvents ts/2023-02-03 12:00 \nte/2023-02-03 14:00") +activate UniMateParser + +create ClearEventsCommandParser +UniMateParser -> ClearEventsCommandParser +activate ClearEventsCommandParser + +ClearEventsCommandParser --> UniMateParser +deactivate ClearEventsCommandParser + +UniMateParser -> ClearEventsCommandParser : parse("ts/2023-02-03 12:00 \nte/2023-02-03 14:00") +activate ClearEventsCommandParser + +create ClearEventsCommand +ClearEventsCommandParser -> ClearEventsCommand : new ClearEventsCommand(EventPeriod) +activate ClearEventsCommand + +ClearEventsCommand --> ClearEventsCommandParser : d +deactivate ClearEventsCommand + +ClearEventsCommandParser --> UniMateParser : d +deactivate ClearEventsCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ClearEventsCommandParser -[hidden]-> UniMateParser +destroy ClearEventsCommandParser + +UniMateParser --> LogicManager : d +deactivate UniMateParser + +LogicManager -> ClearEventsCommand : execute() +activate ClearEventsCommand + +ClearEventsCommand -> Model : findEventsInRange(EventPeriod) +activate Model + +Model -> Calendar : getEventsInRange((EventPeriod) +activate Calendar + +Calendar --> Model +deactivate Calendar + +Model --> ClearEventsCommand +deactivate Model + +create CommandResult +ClearEventsCommand -> CommandResult +activate CommandResult + +CommandResult --> ClearEventsCommand +deactivate CommandResult + +ClearEventsCommand --> LogicManager : result +deactivate ClearEventsCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/CompareCalendarsSequenceDiagram.puml b/docs/diagrams/CompareCalendarsSequenceDiagram.puml new file mode 100644 index 00000000000..6ecc81d179c --- /dev/null +++ b/docs/diagrams/CompareCalendarsSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":UniMateParser" as UniMateParser LOGIC_COLOR +participant ":CompareCalendarByIndexCommandParser" as CompareCalendarByIndexCommandParser LOGIC_COLOR +participant "c:CompareCalendarByIndexCommand" as CompareCalendarByIndexCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("compareCalendars 1") +activate LogicManager + +LogicManager -> UniMateParser : parseCommand("compareCalendars 1") +activate UniMateParser + +create CompareCalendarByIndexCommandParser +UniMateParser -> CompareCalendarByIndexCommandParser +activate CompareCalendarByIndexCommandParser + +CompareCalendarByIndexCommandParser --> UniMateParser +deactivate CompareCalendarByIndexCommandParser + +UniMateParser -> CompareCalendarByIndexCommandParser : parse("1") +activate CompareCalendarByIndexCommandParser + +create CompareCalendarByIndexCommand +CompareCalendarByIndexCommandParser -> CompareCalendarByIndexCommand +activate CompareCalendarByIndexCommand + +CompareCalendarByIndexCommand --> CompareCalendarByIndexCommandParser : c +deactivate CompareCalendarByIndexCommand + +CompareCalendarByIndexCommandParser --> UniMateParser : c +deactivate CompareCalendarByIndexCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +CompareCalendarByIndexCommandParser -[hidden]-> UniMateParser +destroy CompareCalendarByIndexCommandParser + +UniMateParser --> LogicManager : c +deactivate UniMateParser + +LogicManager -> CompareCalendarByIndexCommand : execute() +activate CompareCalendarByIndexCommand + +CompareCalendarByIndexCommand -> CompareCalendarByIndexCommand : create comparison calendar cal:ReadOnlyCalendar + +CompareCalendarByIndexCommand -> Model : setComparisonCalendar(cal) +activate Model + +Model --> CompareCalendarByIndexCommand +deactivate Model + +create CommandResult +CompareCalendarByIndexCommand -> CommandResult +activate CommandResult + +CommandResult --> CompareCalendarByIndexCommand +deactivate CommandResult + +CompareCalendarByIndexCommand --> LogicManager : result +deactivate CompareCalendarByIndexCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 40ea6c9dc4c..2acfadbb4bc 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -4,7 +4,7 @@ skinparam ArrowFontStyle plain box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":UniMateParser" as UniMateParser LOGIC_COLOR participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR @@ -17,17 +17,17 @@ end box [-> LogicManager : execute("delete 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") -activate AddressBookParser +LogicManager -> UniMateParser : parseCommand("delete 1") +activate UniMateParser create DeleteCommandParser -AddressBookParser -> DeleteCommandParser +UniMateParser -> DeleteCommandParser activate DeleteCommandParser -DeleteCommandParser --> AddressBookParser +DeleteCommandParser --> UniMateParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +UniMateParser -> DeleteCommandParser : parse("1") activate DeleteCommandParser create DeleteCommand @@ -37,14 +37,14 @@ activate DeleteCommand DeleteCommand --> DeleteCommandParser : d deactivate DeleteCommand -DeleteCommandParser --> AddressBookParser : d +DeleteCommandParser --> UniMateParser : d deactivate DeleteCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser +DeleteCommandParser -[hidden]-> UniMateParser destroy DeleteCommandParser -AddressBookParser --> LogicManager : d -deactivate AddressBookParser +UniMateParser --> LogicManager : d +deactivate UniMateParser LogicManager -> DeleteCommand : execute() activate DeleteCommand diff --git a/docs/diagrams/EditContactEventSequenceDiagram.puml b/docs/diagrams/EditContactEventSequenceDiagram.puml new file mode 100644 index 00000000000..c5b315b478e --- /dev/null +++ b/docs/diagrams/EditContactEventSequenceDiagram.puml @@ -0,0 +1,96 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditContactEventParser" as EditContactEventParser LOGIC_COLOR +participant ":EditContactEvent" as EditContactEvent LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +end box + +box Calendar CALENDAR_COLOR_T1 +participant ":Calendar" as Calendar CALENDAR_COLOR_T1 +participant ":allDaysEventListManager" as AllDaysEventListManager CALENDAR_COLOR_T1 +end box + +[-> LogicManager : execute(editContactEvent) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(editContactEvent) +activate AddressBookParser + +create EditContactEventParser +AddressBookParser -> EditContactEventParser +activate EditContactEventParser + +EditContactEventParser --> AddressBookParser +deactivate EditContactEventParser + +AddressBookParser -> EditContactEventParser : parse("1 1 d/CS2103 meeting ts/2023-11-11 10:00 te/2023-11-11 12:00") +activate EditContactEventParser + +create EditContactEvent +EditContactEventParser -> EditContactEvent +activate EditContactEvent + +EditContactEvent --> EditContactEventParser +deactivate EditContactEvent + +EditContactEventParser --> AddressBookParser +deactivate EditContactEventParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditContactEventParser -[hidden]-> AddressBookParser +destroy EditContactEventParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> EditContactEvent : execute() +activate EditContactEvent + +EditContactEvent -> Calendar +activate Calendar + +Calendar -> AllDaysEventListManager +activate AllDaysEventListManager + +AllDaysEventListManager -> Calendar +deactivate AllDaysEventListManager + +Calendar -> EditContactEvent +deactivate Calendar + +EditContactEvent -> Model : setPerson() +activate Model + +Model -> VersionedAddressBook +activate VersionedAddressBook + +VersionedAddressBook --> Model : +deactivate VersionedAddressBook + +Model --> EditContactEvent +deactivate Model + +create CommandResult +EditContactEvent -> CommandResult +activate CommandResult + +CommandResult --> EditContactEvent +deactivate CommandResult + +EditContactEvent --> LogicManager : result +deactivate EditContactEvent +EditContactEvent -[hidden]-> LogicManager : result +destroy EditContactEvent + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index a57720890ee..ce4b9175e3e 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -6,7 +6,7 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic as LogicPackage { -Class AddressBookParser +Class UniMateParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command @@ -27,8 +27,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager -right->"1" UniMateParser +UniMateParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..5a7a43cc8a9 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -7,11 +7,19 @@ skinparam classBackgroundColor MODEL_COLOR Package Model as ModelPackage <>{ Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs +Class "<>\nReadOnlyCalendar" as ReadOnlyCalendar +Class "<>\nReadOnlyTaskManager" as ReadOnlyTaskManager Class "<>\nModel" as Model Class AddressBook Class ModelManager Class UserPrefs - +Class UniMateCalendar +Class TaskManager +Class AllDaysEventListManager +Class SingleDayEventList +Class Event +Class EventDescription +Class EventPeriod Class UniquePersonList Class Person Class Address @@ -19,6 +27,10 @@ Class Email Class Name Class Phone Class Tag +Class TaskList +Class Task +Class TaskDescription +Class Deadline Class I #FFFFFF } @@ -26,22 +38,36 @@ Class I #FFFFFF Class HiddenOutside #FFFFFF HiddenOutside ..> Model +TaskManager .up.|> ReadOnlyTaskManager + +UniMateCalendar .up.|> ReadOnlyCalendar +UniMateCalendar --> "1" AllDaysEventListManager +AllDaysEventListManager --> "*" SingleDayEventList +SingleDayEventList --> "1..*" Event +Event --> "1" EventDescription +Event --> "1" EventPeriod + AddressBook .up.|> ReadOnlyAddressBook ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs +Model .right.> ReadOnlyCalendar Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook -ModelManager -right-> "1" UserPrefs +Model .left.> ReadOnlyTaskManager +ModelManager --> "1" AddressBook +ModelManager --> "1" UniMateCalendar +ModelManager --> "1" UserPrefs +ModelManager --> "1" TaskManager UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1" Name +Person *--> "1" Phone +Person *--> "1" Email +Person *--> "1" Address Person *--> "*" Tag +Person *-up-> "1" UniMateCalendar Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -50,5 +76,10 @@ Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +TaskManager --> "1" TaskList +TaskList --> "*" Task +Task *--> "1" TaskDescription +Task *--> "1"Deadline + ModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..dbea9352b07 --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,80 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":SortCommandParser" as SortCommandParser LOGIC_COLOR +participant ":SortCommand" as SortCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +end box +[-> LogicManager : execute(sort) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(sort) +activate AddressBookParser + +create SortCommandParser +AddressBookParser -> SortCommandParser +activate SortCommandParser + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser + +AddressBookParser -> SortCommandParser : parse("/byname /reverse") +activate SortCommandParser + +create SortCommand +SortCommandParser -> SortCommand +activate SortCommand + +SortCommand --> SortCommandParser +deactivate SortCommand + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommandParser + + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> SortCommand : execute() +activate SortCommand + +SortCommand -> Model : sortPersonList() +activate Model + +Model -> VersionedAddressBook : sortPersons() +activate VersionedAddressBook + +VersionedAddressBook -> VersionedAddressBook :saveAddressBook(ReadOnlyAddressBook) +VersionedAddressBook --> Model : +deactivate VersionedAddressBook + +Model --> SortCommand +deactivate Model + +create CommandResult +SortCommand -> CommandResult +activate CommandResult + +CommandResult --> SortCommand +deactivate CommandResult + +SortCommand --> LogicManager : result +deactivate SortCommand +SortCommand -[hidden]-> LogicManager : result +destroy SortCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..5691b232090 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -22,22 +22,49 @@ Class JsonAdaptedPerson Class JsonAdaptedTag } +package "Calendar Storage" #F4F6F6{ +Class "<>\nCalendarStorage" as CalendarStorage +Class JsonCalendarStorage +Class JsonSerializableCalendar +Class JsonAdaptedEvent +} + +package "Task Manager Storage" #F4F6F6{ +Class "<>\nTaskManagerStorage" as TaskManagerStorage +Class JsonTaskManagerStorage +Class JsonSerializableTaskManager +Class JsonAdaptedTask +} + } Class HiddenOutside #FFFFFF HiddenOutside ..> Storage + StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" CalendarStorage +StorageManager -up-> "1" TaskManagerStorage + -Storage -left-|> UserPrefsStorage +Storage -right-|> UserPrefsStorage Storage -right-|> AddressBookStorage +Storage -left-|> CalendarStorage +Storage -left-|> TaskManagerStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage +JsonCalendarStorage .up.|> CalendarStorage +JsonTaskManagerStorage .up.|> TaskManagerStorage JsonAddressBookStorage ..> JsonSerializableAddressBook +JsonCalendarStorage ..> JsonSerializableCalendar +JsonTaskManagerStorage ..> JsonSerializableTaskManager JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "*" JsonAdaptedEvent +JsonSerializableCalendar --> "*" JsonAdaptedEvent +JsonSerializableTaskManager --> "*" JsonAdaptedTask @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..892e0040d54 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,6 +15,8 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +Class BottomListPanel +Class CalendarContainer } package Model <> { @@ -34,6 +36,8 @@ MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter +MainWindow *-down-> "1" CalendarContainer +MainWindow *-down-> "1" BottomListPanel MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard @@ -46,8 +50,16 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +CalendarContainer --|> UiPart +BottomListPanel --|> UiPart + +note right of BottomListPanel +refer to BottomListPanel class +diagram for more detail +end note PersonCard ..> Model +CalendarContainer ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..0aff1805974 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -29,7 +29,13 @@ !define STORAGE_COLOR_T1 #FFE374 !define STORAGE_COLOR_T2 #EDC520 !define STORAGE_COLOR_T3 #806600 -!define STORAGE_COLOR_T2 #544400 +!define STORAGE_COLOR_T4 #544400 + +!define CALENDAR_COLOR #CFA500 +!define CALENDAR_COLOR_T1 #CFA500 +!define CALENDAR_COLOR_T2 #EDC520 +!define CALENDAR_COLOR_T3 #806600 +!define CALENDAR_COLOR_T4 #544400 !define USER_COLOR #000000 diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png deleted file mode 100644 index cd540665053..00000000000 Binary files a/docs/images/ArchitectureDiagram.png and /dev/null differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png deleted file mode 100644 index 37ad06a2803..00000000000 Binary files a/docs/images/ArchitectureSequenceDiagram.png and /dev/null differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png deleted file mode 100644 index 02a42e35e76..00000000000 Binary files a/docs/images/BetterModelClassDiagram.png and /dev/null differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png deleted file mode 100644 index 5b464126b35..00000000000 Binary files a/docs/images/CommitActivityDiagram.png and /dev/null differ diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png deleted file mode 100644 index ae52a35718a..00000000000 Binary files a/docs/images/ComponentManagers.png and /dev/null differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png deleted file mode 100644 index e186f7ba096..00000000000 Binary files a/docs/images/DeleteSequenceDiagram.png and /dev/null differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png deleted file mode 100644 index e3b784310fe..00000000000 Binary files a/docs/images/LogicClassDiagram.png and /dev/null differ diff --git a/docs/images/LogicStorageDIP.png b/docs/images/LogicStorageDIP.png deleted file mode 100644 index 871157f5a9c..00000000000 Binary files a/docs/images/LogicStorageDIP.png and /dev/null differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png deleted file mode 100644 index a19fb1b4ac8..00000000000 Binary files a/docs/images/ModelClassDiagram.png and /dev/null differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png deleted file mode 100644 index edfd1ff7897..00000000000 Binary files a/docs/images/ParserClasses.png and /dev/null differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png deleted file mode 100644 index 18fa4d0d51f..00000000000 Binary files a/docs/images/StorageClassDiagram.png and /dev/null differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..abf93ad6000 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png deleted file mode 100644 index 11f06d68671..00000000000 Binary files a/docs/images/UiClassDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index c5f91b58533..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index 2d3ad09c047..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 20853694e03..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 1a9551b31be..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 46dfae78c94..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index f45889b5fdf..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index c7a7e637266..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/addCommand.png b/docs/images/addCommand.png new file mode 100644 index 00000000000..de0963a538e Binary files /dev/null and b/docs/images/addCommand.png differ diff --git a/docs/images/addContactEventCommand.png b/docs/images/addContactEventCommand.png new file mode 100644 index 00000000000..e031727872e Binary files /dev/null and b/docs/images/addContactEventCommand.png differ diff --git a/docs/images/addEventCommand.png b/docs/images/addEventCommand.png new file mode 100644 index 00000000000..68e8418a82a Binary files /dev/null and b/docs/images/addEventCommand.png differ diff --git a/docs/images/addTaskCommand.png b/docs/images/addTaskCommand.png new file mode 100644 index 00000000000..26c339a487f Binary files /dev/null and b/docs/images/addTaskCommand.png differ diff --git a/docs/images/calendarUI.png b/docs/images/calendarUI.png new file mode 100644 index 00000000000..762e21efb27 Binary files /dev/null and b/docs/images/calendarUI.png differ diff --git a/docs/images/clearCommand.png b/docs/images/clearCommand.png new file mode 100644 index 00000000000..ec013deacf9 Binary files /dev/null and b/docs/images/clearCommand.png differ diff --git a/docs/images/clearEventsCommand.png b/docs/images/clearEventsCommand.png new file mode 100644 index 00000000000..7265dfdb856 Binary files /dev/null and b/docs/images/clearEventsCommand.png differ diff --git a/docs/images/compareCalendarsCommand.png b/docs/images/compareCalendarsCommand.png new file mode 100644 index 00000000000..98b2fc3bbcb Binary files /dev/null and b/docs/images/compareCalendarsCommand.png differ diff --git a/docs/images/compareGroupCalendarsCommand.png b/docs/images/compareGroupCalendarsCommand.png new file mode 100644 index 00000000000..f9b44fd4bd8 Binary files /dev/null and b/docs/images/compareGroupCalendarsCommand.png differ diff --git a/docs/images/contactCalendar.png b/docs/images/contactCalendar.png new file mode 100644 index 00000000000..60a4064d9a6 Binary files /dev/null and b/docs/images/contactCalendar.png differ diff --git a/docs/images/deleteCommand.png b/docs/images/deleteCommand.png new file mode 100644 index 00000000000..ce0b36e36cc Binary files /dev/null and b/docs/images/deleteCommand.png differ diff --git a/docs/images/deleteContactEventCommand.png b/docs/images/deleteContactEventCommand.png new file mode 100644 index 00000000000..d7f944fc154 Binary files /dev/null and b/docs/images/deleteContactEventCommand.png differ diff --git a/docs/images/deleteEventCommand.png b/docs/images/deleteEventCommand.png new file mode 100644 index 00000000000..58a1ce9c84b Binary files /dev/null and b/docs/images/deleteEventCommand.png differ diff --git a/docs/images/deleteTaskCommand.png b/docs/images/deleteTaskCommand.png new file mode 100644 index 00000000000..5b3670c0785 Binary files /dev/null and b/docs/images/deleteTaskCommand.png differ diff --git a/docs/images/editCommand.png b/docs/images/editCommand.png new file mode 100644 index 00000000000..1497b17761b Binary files /dev/null and b/docs/images/editCommand.png differ diff --git a/docs/images/editContactEventCommandAfter.png b/docs/images/editContactEventCommandAfter.png new file mode 100644 index 00000000000..71da4b8df8d Binary files /dev/null and b/docs/images/editContactEventCommandAfter.png differ diff --git a/docs/images/editContactEventCommandBefore.png b/docs/images/editContactEventCommandBefore.png new file mode 100644 index 00000000000..73900024ec2 Binary files /dev/null and b/docs/images/editContactEventCommandBefore.png differ diff --git a/docs/images/editContactEventCommandMessage.png b/docs/images/editContactEventCommandMessage.png new file mode 100644 index 00000000000..be11c71c078 Binary files /dev/null and b/docs/images/editContactEventCommandMessage.png differ diff --git a/docs/images/eventList.png b/docs/images/eventList.png new file mode 100644 index 00000000000..46fb6604a98 Binary files /dev/null and b/docs/images/eventList.png differ diff --git a/docs/images/fallman2.png b/docs/images/fallman2.png new file mode 100644 index 00000000000..68125468a90 Binary files /dev/null and b/docs/images/fallman2.png differ diff --git a/docs/images/filterCommand.png b/docs/images/filterCommand.png new file mode 100644 index 00000000000..8cbde273f77 Binary files /dev/null and b/docs/images/filterCommand.png differ diff --git a/docs/images/findCommand.png b/docs/images/findCommand.png new file mode 100644 index 00000000000..64df6b062b3 Binary files /dev/null and b/docs/images/findCommand.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..0fb92b18954 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/junhonglow.png b/docs/images/junhonglow.png new file mode 100644 index 00000000000..f599d963863 Binary files /dev/null and b/docs/images/junhonglow.png differ diff --git a/docs/images/lihongguang00.png b/docs/images/lihongguang00.png new file mode 100644 index 00000000000..2d4be6f93bb Binary files /dev/null and b/docs/images/lihongguang00.png differ diff --git a/docs/images/listCommand.png b/docs/images/listCommand.png new file mode 100644 index 00000000000..892f2c564ed Binary files /dev/null and b/docs/images/listCommand.png differ diff --git a/docs/images/nicrandomlee.png b/docs/images/nicrandomlee.png new file mode 100644 index 00000000000..30b3f1e3bc4 Binary files /dev/null and b/docs/images/nicrandomlee.png differ diff --git a/docs/images/sortCommand.png b/docs/images/sortCommand.png new file mode 100644 index 00000000000..6b89e290b9e Binary files /dev/null and b/docs/images/sortCommand.png differ diff --git a/docs/images/sortTasksCommand.png b/docs/images/sortTasksCommand.png new file mode 100644 index 00000000000..7025a6b71ce Binary files /dev/null and b/docs/images/sortTasksCommand.png differ diff --git a/docs/images/switchListCommand.png b/docs/images/switchListCommand.png new file mode 100644 index 00000000000..5b92c6fef6d Binary files /dev/null and b/docs/images/switchListCommand.png differ diff --git a/docs/images/tracing/LogicSequenceDiagram.png b/docs/images/tracing/LogicSequenceDiagram.png deleted file mode 100644 index 25c8b66b9f1..00000000000 Binary files a/docs/images/tracing/LogicSequenceDiagram.png and /dev/null differ diff --git a/docs/images/uniMateFeatures.png b/docs/images/uniMateFeatures.png new file mode 100644 index 00000000000..1a413455ccf Binary files /dev/null and b/docs/images/uniMateFeatures.png differ diff --git a/docs/images/unimateScreenshot.png b/docs/images/unimateScreenshot.png new file mode 100644 index 00000000000..c203ce10a01 Binary files /dev/null and b/docs/images/unimateScreenshot.png differ diff --git a/docs/images/viewContactEventsCommand.png b/docs/images/viewContactEventsCommand.png new file mode 100644 index 00000000000..2086b8ac946 Binary files /dev/null and b/docs/images/viewContactEventsCommand.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..da8c90323fc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,16 @@ --- -layout: page -title: AddressBook Level-3 +layout: default.md +title: UniMate --- - -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![Java CI](https://github.com/AY2324S1-CS2103-F13-4/tp/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/AY2324S1-CS2103-F13-4/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103-F13-4/tp/graph/badge.svg?token=4OY8ODQQBU)](https://codecov.io/gh/AY2324S1-CS2103-F13-4/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**UniMate is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using UniMate, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing UniMate, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000000..63a232e05dc --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,8587 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "devDependencies": { + "markbind-cli": "^5.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "node_modules/@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "node_modules/@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "dependencies": { + "unix-crypt-td-js": "^1.1.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "dependencies": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/event-stream/node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/event-stream/node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "node_modules/figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "dependencies": { + "moment": "^2.11.2" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "dependencies": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "dependencies": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/gh-pages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/gh-pages/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/gh-pages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "dependencies": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">=4.6.1" + } + }, + "node_modules/http-auth/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "dependencies": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "dependencies": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "bin": { + "live-server": "live-server.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/live-server/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/live-server/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/live-server/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/live-server/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/live-server/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/live-server/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/live-server/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "dependencies": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + }, + "bin": { + "markbind": "index.js" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "markdown-it": ">= 9.0.0" + } + }, + "node_modules/markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "node_modules/markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "dependencies": { + "markdown-it": "^13.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it-linkify-images/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "node_modules/markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "node_modules/markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "node_modules/markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "node_modules/markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true, + "engines": { + "node": ">6.4.0" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "node_modules/markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "node_modules/markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "optionalDependencies": { + "chokidar": "^3.3.0" + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "deprecated": "The package has been renamed to `open`", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "node_modules/simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/steveukx/" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "node_modules/vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + } + }, + "node_modules/vue-server-renderer/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "dependencies": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "dependencies": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "winston": "^2 || ^3" + } + }, + "node_modules/winston-daily-rotate-file/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "node_modules/winston-transport/node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/winston-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/winston/node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + }, + "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true + }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "requires": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "requires": { + "object-assign": "^4.1.1" + } + }, + "@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + } + }, + "@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "requires": { + "unix-crypt-td-js": "^1.1.4" + } + }, + "apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "requires": { + "through": "2" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "requires": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true + }, + "file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "requires": { + "moment": "^2.11.2" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true + }, + "filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "requires": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, + "gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "requires": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "requires": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "requires": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "requires": { + "commander": "^8.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "requires": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "requires": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + } + } + }, + "markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "requires": {} + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "requires": { + "markdown-it": "^13.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + } + } + }, + "markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + } + }, + "material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "requires": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "chokidar": "^3.3.0", + "commander": "^5.1.0" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + }, + "stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + } + } + }, + "winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "requires": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + } + }, + "winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "requires": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000..aa7083fd8a7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "AB-3 docs", + "scripts": { + "init": "markbind init", + "build": "markbind build", + "serve": "markbind serve", + "deploy": "markbind deploy" + }, + "devDependencies": { + "markbind-cli": "^5.1.0" + } +} diff --git a/docs/site.json b/docs/site.json new file mode 100644 index 00000000000..c19110b19e3 --- /dev/null +++ b/docs/site.json @@ -0,0 +1,29 @@ +{ + "baseUrl": "", + "titlePrefix": "UniMate", + "titleSuffix": "", + "faviconPath": "images/SeEduLogo.png", + "style": { + "codeTheme": "light" + }, + "ignore": [ + "_markbind/layouts/*", + "_markbind/logs/*", + "_site/*", + "site.json", + "*.md", + "*.njk", + ".git/*", + "node_modules/*" + ], + "pagesExclude": ["node_modules/*"], + "pages": [ + { + "glob": ["**/index.md", "**/*.md"] + } + ], + "deploy": { + "message": "Site Update." + }, + "timeZone": "Asia/Singapore" +} diff --git a/docs/stylesheets/main.css b/docs/stylesheets/main.css new file mode 100644 index 00000000000..1074ade42dd --- /dev/null +++ b/docs/stylesheets/main.css @@ -0,0 +1,144 @@ +mark { + background-color: #ff0; + border-radius: 5px; + padding-top: 0; + padding-bottom: 0; +} + +.indented { + padding-left: 20px; +} + +.theme-card img { + width: 100%; +} + +/* Scrollbar */ + +.slim-scroll::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll::-webkit-scrollbar-thumb { + background: #808080; + border-radius: 20px; +} + +.slim-scroll::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll-blue::-webkit-scrollbar-thumb { + background: #00b0ef; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +/* Layout containers */ + +#flex-body { + display: flex; + flex: 1; + align-items: start; +} + +#content-wrapper { + flex: 1; + margin: 0 auto; + min-width: 0; + max-width: 1000px; + overflow-x: auto; + padding: 0.8rem 20px 0 20px; + transition: 0.4s; + -webkit-transition: 0.4s; +} + +#site-nav, +#page-nav { + display: flex; + flex-direction: column; + position: sticky; + top: var(--sticky-header-height); + flex: 0 0 auto; + max-width: 300px; + max-height: calc(100vh - var(--sticky-header-height)); + width: 300px; +} + +#site-nav { + border-right: 1px solid lightgrey; + padding-bottom: 20px; + z-index: 999; +} + +.site-nav-top { + margin: 0.8rem 0; + padding: 0 12px 12px 12px; +} + +.nav-component { + overflow-y: auto; +} + +#page-nav { + border-left: 1px solid lightgrey; +} + +@media screen and (max-width: 1299.98px) { + #page-nav { + display: none; + } +} + +/* Bootstrap medium(md) responsive breakpoint */ +@media screen and (max-width: 991.98px) { + #site-nav { + display: none; + } +} + +/* Bootstrap small(sm) responsive breakpoint */ +@media (max-width: 767.98px) { + .indented { + padding-left: 10px; + } + + #content-wrapper { + padding: 0 10px; + } +} + +/* Bootstrap extra small(xs) responsive breakpoint */ +@media screen and (max-width: 575.98px) { + #site-nav { + display: none; + } +} + +/* Hide site navigation when printing */ +@media print { + #site-nav { + display: none; + } + + #page-nav { + display: none; + } +} + +h2, +h3, +h4, +h5, +h6 { + color: #e46c0a; +} diff --git a/docs/team/fallman2.md b/docs/team/fallman2.md new file mode 100644 index 00000000000..d566c21b602 --- /dev/null +++ b/docs/team/fallman2.md @@ -0,0 +1,115 @@ +--- +layout: default.md +title: Andre Sim's Project Portfolio Page +--- + +### Project: UniMate + +UniMate is a desktop address book and calendar infused in one application intended for National University of Singapore +(NUS) students to save group mate's contacts and sync calendars to schedule classes and group project meetings. The user +interacts with it using primarily the Command Line Interface (CLI), but can choose to interact with the GUI using a +mouse. Its GUI created with JavaFX, and the entire project is written in Java, and has about 25 kLoC. + +Given below are my contributions to the project. + + +* **Filter Command**: `filter [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` + * What it does: Filters contacts by the given fields. Contacts that match all fields are displayed. + * Justification: Allows students to search for all students in their contacts that meet the specified fields, e.g. All + students that study a specific module (and have the tag for that module) or all students that attend a particular CCA + (and have the tag for that CCA) or all students that have a school email address (filter by email "@u.nus.edu"). + * Highlights: This command is meant to be different enough from the find command such that both commands fulfil + different purposes. The filter command allows for finding groups of people while the find command allows for finding + of one specific person given that you know their name specifically. + * Credits: The structure of the execute method of this command is made to be similar to that of the edit command which + similarly uses multiple fields that may or may not be present. Apart from this, all code for this command was original + by me. + +* **Delete Event Command**: `deleteEvent DATE_TIME` + * What it does: Deletes an Event from the Calendar that is present at the specified Date and time. + * Justification: For the Calendar to be functional, events have to be able to be added and deleted. + * Highlights: Consists of methods that interact between different layers of abstraction to delete an event from the + Calendar. Also takes in a Date and time that the event is during so that the specific start or end time of the event + does not need to be used. The event is deleted as long as the time indicated is between the start and end time of the + event. Also shows the deleted event when the command is executed so that it is more easily re added if the wrong event + is deleted. + * Credits: The command uses some methods implemented by lihongguang00 for the Calendar class. Apart from that, all + code for this command was written by me. + +* **Clear all events within a time range**: `clearEvent ts/START_DATE_TIME te/END_DATE_TIME [c/CONFIRMATION]` + * What it does: Deletes all events from the Calendar that lie within a time range. + * Justification: After comparing different Calendars to look for free time, users may have to delete events within a + time range to make time for a new event. + * Highlights: Entering the command without any confirmation shows the user all events that are within the time range. + The command can then be re-entered with the confirmation to confirm the deletion. + * Credits: Code for this command was written by me. + +* **Task List Feature** + * What it does: Allows users to keep track of tasks. Tasks consist of a description and a deadline which is optional. + * Justification: A task list allows students to keep track of activities that have to be completed and do not have a + duration, thus not being appropriate for the Calendar. Examples of these include assignment submission deadlines. + * Highlights: Allows of storage of tasks. Uses the same portion of the UI as the Event List. + * Credits: The code for storing the data for the task list was written by Jun Hong and the rest of the code for the + task list functions was written by me. + +* **Adding tasks to the task list**: `addTask d/DESCRIPTION [te/DEADLINE]` + * What it does: Allows users to add tasks from the task list. + * Justification: Users have to be able to add tasks to the task list to use it. + * Highlights: Tasks with duplicate descriptions and deadlines are allowed but not both. + * Credits: All code for this command was written by me. + +* **Deleting tasks from the task list**: `deleteTask INDEX` + * What it does: Allows users to delete tasks from the task list. + * Justification: Users should be able to delete tasks from the task list when they are completed. + * Highlights: Uses the index displayed in the task list to indicate which task should be deleted. + * Credits: All code for this command was written by me. + +* **Sorting tasks in the task list**: `sortTasks PARAMETER` + * What it does: Allows users to sort tasks in the task list. + * Justification: Tasks should be able to be sorted so that the user can see which tasks have to be completed by an + earlier time or be able to find specific tasks. + * Highlights: Allows for 2 preset options for ways to sort tasks, by deadline or by description. + * Credits: All code for this command was written by me. + +* **Switch the bottom list section of the UI between the event list and the task list**: `switchList` + * What it does: Switches between the event list and the task list on the GUI. + * Justification: Users should be able to view the task list. + * Highlights: Swaps between the two lists on the bottom of the GUI so that the GUI does not become too cluttered. + * Credits: All code for this command was written by me. + +* **View the event list of someone in the address book**: `viewContactEvents INDEX` + * What it does: Creates a pop-up to view the event list of someone in the address book. + * Justification: Users should be able to view the event list of someone in the address book. + * Credits: Code for this command was written by me, with assistance from lihongguang00. + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=Fallman2&tabRepo=AY2324S1-CS2103-F13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (3 releases) on GitHub + +* **Documentation**: + * User Guide: + * Added UG documentation for the features `filter`, `addEvent` and `deleteEvent` [\#86](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/86) + * Added UG documentation for the features `clearEvents`, `addTasks`, `deleteTasks`, `sortTasks`, `switchList` + and `viewContactEvents` [\#86](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/86) + * Developer Guide: + * Added DG documentation of Use Cases 4 to 7 and UC 12. + * Added DG documentation of Use Cases 15 to 19 [\#188](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/188). + * Added DG documentation on the implementation of `TaskManager`, Adding Tasks, Deleting Tasks, Sorting Tasks [\#168](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/168/files), Filtering Persons and Deleting events [\#52](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/52). + * Added DG sequence diagram for ClearEventsCommand and edited the class diagram for Model [\#176](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/176). + * Wrote DG documentation on Manual Testing Instructions for all newly implemented features, except Storage related features [\#179](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/179). + * Wrote 2 Planned Enhancements for DG [\#182](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/182). + * Wrote on the implementation of TaskList and EventList in the Effort section of the DG [\#182](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/182/files). + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#2](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/2), + [\#26](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/26), [\#42](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/42) + , [\#44](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/44), [\#51](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/51) + , [\#63](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/63), [\#70](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/70) + , [\#75](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/75), [\#149](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/149), [\#167](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/167). + * Reported bugs and suggestions for other teams in the class (examples: [\#1](https://github.com/Fallman2/ped/issues/1), + [\#2](https://github.com/Fallman2/ped/issues/2), [\#3](https://github.com/Fallman2/ped/issues/3), [\#4](https://github.com/Fallman2/ped/issues/4) + , [\#5](https://github.com/Fallman2/ped/issues/5), [\#6](https://github.com/Fallman2/ped/issues/6), [\#7](https://github.com/Fallman2/ped/issues/7) + , [\#8](https://github.com/Fallman2/ped/issues/8), [\#9](https://github.com/Fallman2/ped/issues/9)). + diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -Given below are my contributions to the project. - -* **New Feature**: Added the ability to undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/junhonglow.md b/docs/team/junhonglow.md new file mode 100644 index 00000000000..7c77b68a302 --- /dev/null +++ b/docs/team/junhonglow.md @@ -0,0 +1,56 @@ +--- +layout: default.md +title: Low Jun Hong's Project Portfolio Page +--- + +### Project: UniMate + +UniMate is a desktop address book and calendar infused in one application intended for National University of Singapore +(NUS) students to save group mate's contacts and sync calendars to schedule classes and group project meetings. The user +interacts with it using primarily the Command Line Interface (CLI), but can choose to interact with the GUI using a +mouse. Its GUI created with JavaFX, and the entire project is written in Java, and has about 25 kLoC. + +Given below are my contributions to the project. + +* **Add Contact Event Command** + * What it does: Facilitates the overall functionality of the calendar by adding events. + * Justification: Facilitates the overall functionality of comparing calendars between user and contact. + * Highlights: Maintains syntactic consistency with the "Add Event" command, ensuring that users can confidently engage with the application in a seamless and intuitive manner. + * Credits: The command uses some methods and modified code implemented by lihongguang00 for the Calendar class. + +* **Delete Contact Event Command** + * What it does: Adds an event to the personal calendar of a contact. + * Justification: Facilitates the overall functionality of the calendar by deleting events. + * Highlights: Maintains syntactic consistency with the "Delete Event" command, ensuring that users can confidently engage with the application in a seamless and intuitive manner. + * Credits: The command uses some methods and modified code implemented by fallman2 and lihongguang00. + +* **Storage classes** + * What it does: Manages the storage for the TaskList, Calendar and contact's Calendars. + * Justification: Facilitates the overall functionality of the application by maintaining proper storage of data. + * Highlights: This feature integrates with the current storage class, ensuring compatibility without disrupting the existing implementation or future iterations. The design prioritizes a non-intrusive collaboration, allowing the new functionality to coexist harmoniously with the established storage infrastructure. + * Credits: Adapted code from AB3 addressbook's storage. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=junhonglow&tabRepo=AY2324S1-CS2103-F13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (3 releases) on GitHub + +* **Documentation**: + * User Guide: + * Added UG documentation for the features `addContactEvent`, `deleteContactEvent` and all `Storage` functionality. + * Developer Guide: + * Added use cases 8 - 11. + * Updated PUML diagram for the storage class. + * Added necessary test instructions and test cases needed for storage testing. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#36](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/36), + [\#40](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/40), [\#53](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/53), + [\#63](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/63), [\#64](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/64), + [\#70](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/70), [\#74](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/74), + [\#76](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/76), [\#78](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/78). + * Reported bugs and suggestions for other teams in the class (examples: + [\#160](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/160), [\#172](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/172), + [\#175](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/175), [\#178](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/178), + [\#185](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/185), [\#186](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/186), + [\#187](https://github.com/AY2324S1-CS2103T-T11-1/tp/issues/187). diff --git a/docs/team/lihongguang00.md b/docs/team/lihongguang00.md new file mode 100644 index 00000000000..8a8f695a319 --- /dev/null +++ b/docs/team/lihongguang00.md @@ -0,0 +1,54 @@ +--- + layout: default.md + title: "Hongguang's Project Portfolio Page" +--- + +### Project: UniMate + +UniMate is a desktop address book and calendar infused in one application intended for National University of Singapore (NUS) students to save group mate's contacts and sync calendars to schedule classes and group project meetings. The user interacts with it using primarily the Command Line Interface (CLI), but can choose to interact with the GUI using a mouse. Its GUI created with JavaFX, and the entire project is written in Java, and has about 25 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Calendar System (Model) + * What it does: A back-end model of the calendar system that supports the calendar UI used in UniMate + * Justification: Skeleton for the calendar system in UniMate + * Highlights: The model was highly reusable, being able to be utilised to create both user calendars and contact calendars. + +* **New Feature**: AddEvent command + * What it does: Allows the user to add events to the calendar system through CLI. + +* **New Feature**: CompareCalendars/CompareGroupCalendars command + * What it does: Allows the user to compare calendars with contacts of interest. + * Highlights: Creates a pop-up of the comparison calendar. + +* **New Feature**: Calendar System GUI (User Calendar, Contact Calendar, Comparison Calendar) + * What it does: Allows the user to view their personal events/their contact's events for the week + * Justification: Able to see and visualize the timetable would improve the user experience, as well as help the user to identify any mistakes in the events that they have added for the week. + * Highlights: Was able to make the GUI "adaptive", which means that by default, it would only show 8am to 6pm in the calendar, unless events exceeding the default time period were added, which the GUI will then display a longer time period all in real time. This decreased the amount of clutter in the GUI as unnecessary time periods were not shown. + +* **New Feature**: Calendar Import (UNSUCCESSFUL ATTEMPT) + * What it does: Allows the user to import .ics format files into the application + * Justification: NUSMODS allows users to export their timetable as .ics format file, so we wanted to allow our application to import it into our application + * Highlights: Using an external open-source library [ical4j](https://www.ical4j.org/) for parsing .ics files, the documentation of the ical4j library was messy and many methods deprecated. Eventually was unable to integrate into our application effectively, because the format that the library expects was different from the NUSMODS exported format. + * Credits: [ical4j](https://www.ical4j.org/) + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=lihongguang00&tabRepo=AY2324S1-CS2103-F13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Assigning of suitable group member to resolve issues raised during PE-D + * Tagging of PRs with suitable assignees, labels and milestone + +* **Documentation**: + * User Guide: + * Added documentation for the features `addEvent`, `compareCalendars` and `compareGroupCalendars` [#84](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/84) + * Reformatted UG and added screenshots for all commands: [#148](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/148) and [#152](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/152) + * Developer Guide: + * Wrote use case (UC14) + * Consolidated user stories into table format + * Adding Events implementation section + * Calendar Comparison (compareCalendars) implementation section + +* **Community**: + * PRs reviewed (with non-trivial review comments): [#7](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/7), [#9](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/9), [#11](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/11), [#25](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/25), [#28](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/28), [#31](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/31), [#36](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/36), [#65](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/65), [#66](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/66), [#72](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/72), [#86](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/86), [#87](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/87) + * Contributed to forum discussions (examples: [#267](https://github.com/nus-cs2103-AY2324S1/forum/issues/267)) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/lihongguang00/ped/issues/1), [2](https://github.com/lihongguang00/ped/issues/2), [3](https://github.com/lihongguang00/ped/issues/3), [4](https://github.com/lihongguang00/ped/issues/4), [5](https://github.com/lihongguang00/ped/issues/5), [6](https://github.com/lihongguang00/ped/issues/6), [7](https://github.com/lihongguang00/ped/issues/7)) diff --git a/docs/team/nicrandomlee.md b/docs/team/nicrandomlee.md new file mode 100644 index 00000000000..543e15c0c9a --- /dev/null +++ b/docs/team/nicrandomlee.md @@ -0,0 +1,46 @@ +--- +layout: default.md +title: Nicholas Lee's Project Portfolio Page +--- + +### Project: UniMate + +UniMate is a desktop address book and calendar infused in one application intended for National University of Singapore (NUS) students to save group mate's contacts and sync calendars to schedule classes and group project meetings. The user interacts with it using primarily the Command Line Interface (CLI), but can choose to interact with the GUI using a mouse. Its GUI created with JavaFX, and the entire project is written in Java, and has about 25 kLoC. + +Given below are my contributions to the project. + +- **New Feature**: Added the ability to sort address book contacts by a specified attribute + + - What it does: allows the user to sort address book contacts by a specified attribute in a certain order + - Justification: This feature improves the product significantly because a user may have many contacts in the address book and sorting it by either ascending or descending order will enable the user to find the contact more conveniently. + - Highlights: This enhancement does not affect existing commands and commands to be added to the future. However, the function inherently changes the way the address book data is being stored, and design alternatives may be required should the storage system change. The implementation was challenging as working around the way the address book is designed and we cannot alter the observable list directly. + +- **New Feature**: Added the ability to edit contact's calendar events + + - What it does: allows the user to sort address book contacts by a specified attribute in either + - Justification: This feature improves the product significantly because a user may want to reschedule a meeting with a contact and the edit function serves as a convenient way for the user to change the details of the event without having to delete and add a new event. + - Highlights: This enhancement affects existing attributes to be added to contact's events in the future. It requires an in-depth analysis of design alternatives should the design of contact's events be altered. The implementation too was challenging as it required an additional temporary class to store all the edited changes. + +- **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=nicrandomlee&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) + +- **Project management**: + + - Contributed to the releases `v1.2` - `v1.4` (3 releases) on GitHub + +- **Enhancements to existing features**: + + - Added two new features (Pull requests [\#40](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/40), [\#51](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/51), [\#78](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/78)) + +- **Documentation**: + + - User Guide: + - Added documentation for the features `sort` and `editContactEvents` [\#66](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/66) + - Developer Guide: + - Added implementation details and sequence diagrams for `sort` and `editContactEvents` [\#167](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/167) + +- **Community**: + + - PRs reviewed (with non-trivial review comments): [\#152](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/152), [\#161](https://github.com/AY2324S1-CS2103-F13-4/tp/pull/161) + - Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/nicrandomlee/ped/issues/1), [2](https://github.com/nicrandomlee/ped/issues/2), [3](https://github.com/nicrandomlee/ped/issues/3), [4](https://github.com/nicrandomlee/ped/issues/4), [5](https://github.com/nicrandomlee/ped/issues/5), [6](https://github.com/nicrandomlee/ped/issues/6), [7](https://github.com/nicrandomlee/ped/issues/7), [8](https://github.com/nicrandomlee/ped/issues/8), [9](https://github.com/nicrandomlee/ped/issues/9), [10](https://github.com/nicrandomlee/ped/issues/10)) + - Posted in forum discussion ([1](https://github.com/nus-cs2103-AY2324S1/forum/issues/217)) + diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index d98f38982e7..8b18f27946b 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Adding a command" + layout: default.md + title: "Tutorial: Adding a command" + pageNav: 3 --- +# Tutorial: Adding a command + Let's walk you through the implementation of a new command — `remark`. This command allows users of the AddressBook application to add optional remarks to people in their address book and edit it if required. The command should have the following format: @@ -22,7 +25,7 @@ For now, let’s keep `RemarkCommand` as simple as possible and print some outpu **`RemarkCommand.java`:** -``` java +```java package seedu.address.logic.commands; import seedu.address.model.Model; @@ -57,13 +60,13 @@ Run `Main#main` and try out your new `RemarkCommand`. If everything went well, y While we have successfully printed a message to `ResultDisplay`, the command does not do what it is supposed to do. Let’s change the command to throw a `CommandException` to accurately reflect that our command is still a work in progress. -![The relationship between RemarkCommand and Command](../images/add-remark/RemarkCommandClass.png) + Following the convention in other commands, we add relevant messages as constants and use them. **`RemarkCommand.java`:** -``` java +```java public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the remark of the person identified " + "by the index number used in the last person listing. " @@ -90,7 +93,7 @@ Let’s change `RemarkCommand` to parse input from the user. We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if our code is functioning as intended. -``` java +```java import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; //... public class RemarkCommand extends Command { @@ -142,13 +145,13 @@ Now let’s move on to writing a parser that will extract the index and remark f Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. -![The relationship between Parser and RemarkCommandParser](../images/add-remark/RemarkCommandParserClass.png) + Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. Let’s take a look at the JavaDoc provided for the function to understand what it does. **`ArgumentTokenizer.java`:** -``` java +```java /** * Tokenizes an arguments string and returns an {@code ArgumentMultimap} * object that maps prefixes to their respective argument values. Only the @@ -166,7 +169,7 @@ We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` a **`ArgumentMultimap.java`:** -``` java +```java /** * Returns the last value of {@code prefix}. */ @@ -181,7 +184,7 @@ This appears to be what we need to get a String of the remark. But what about th **`DeleteCommandParser.java`:** -``` java +```java Index index = ParserUtil.parseIndex(args); return new DeleteCommand(index); ``` @@ -192,7 +195,7 @@ Now that we have the know-how to extract the data that we need from the user’s **`RemarkCommandParser.java`:** -``` java +```java public RemarkCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, @@ -212,11 +215,11 @@ public RemarkCommand parse(String args) throws ParseException { } ``` -
    + -:information_source: Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! +Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! -
    + If you are stuck, check out the sample [here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a). @@ -244,7 +247,7 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s **`PersonCard.java`:** -``` java +```java @FXML private Label remark; ``` @@ -276,11 +279,11 @@ We change the constructor of `Person` to take a `Remark`. We will also need to d Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`! -
    + -:bulb: Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. +Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. -
    + Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order! @@ -291,11 +294,11 @@ AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the While the changes to code may be minimal, the test data will have to be updated as well. -
    + -:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! -
    + Check out [this commit](https://github.com/se-edu/addressbook-level3/commit/556cbd0e03ff224d7a68afba171ad2eb0ce56bbf) to see what the changes entail. @@ -308,7 +311,7 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c **`PersonCard.java`:** -``` java +```java public PersonCard(Person person, int displayedIndex) { //... remark.setText(person.getRemark().value); @@ -328,7 +331,7 @@ save it with `Model#setPerson()`. **`RemarkCommand.java`:** -``` java +```java //... public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..c73bd379e5e 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Removing Fields" + layout: default.md + title: "Tutorial: Removing Fields" + pageNav: 3 --- +# Tutorial: Removing Fields + > Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. > > — Antoine de Saint-Exupery @@ -10,17 +13,17 @@ title: "Tutorial: Removing Fields" When working on an existing code base, you will most likely find that some features that are no longer necessary. This tutorial aims to give you some practice on such a code 'removal' activity by removing the `address` field from `Person` class. -
    + **If you have done the [Add `remark` command tutorial](AddRemark.html) already**, you should know where the code had to be updated to add the field `remark`. From that experience, you can deduce where the code needs to be changed to _remove_ that field too. The removing of the `address` field can be done similarly.

    However, if you have no such prior knowledge, removing a field can take a quite a bit of detective work. This tutorial takes you through that process. **At least have a read even if you don't actually do the steps yourself.** -
    + -* Table of Contents -{:toc} + + ## Safely deleting `Address` @@ -50,10 +53,10 @@ Let’s try removing references to `Address` in `EditPersonDescriptor`. 1. Remove the usages of `address` and select `Do refactor` when you are done. -
    + - :bulb: **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. -
    + **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. + 1. Repeat the steps for the remaining usages of `Address` @@ -71,7 +74,7 @@ A quick look at the `PersonCard` class and its `fxml` file quickly reveals why i **`PersonCard.java`** -``` java +```java ... @FXML private Label address; diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..2b1b0f2d6b7 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -1,26 +1,30 @@ --- -layout: page -title: "Tutorial: Tracing code" + layout: default.md + title: "Tutorial: Tracing code" + pageNav: 3 --- +# Tutorial: Tracing code + + > Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …​\[Therefore,\] making it easy to read makes it easier to write. > > — Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command. -* Table of Contents -{:toc} + + ## Before we start Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components. -![ArchitectureDiagram](../images/ArchitectureDiagram.png) + It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App. - + Note how the diagram shows only the execution flows _between_ the main components. That is, it does not show details of the execution path *inside* each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works. @@ -37,16 +41,16 @@ As you know, the first step of debugging is to put in a breakpoint where you wan In our case, we would want to begin the tracing at the very point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. However, the execution path through a GUI is often somewhat obscure due to various *event-driven mechanisms* used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the `UI` transfers control to the `Logic` component. - + According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. -
    +**Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. + A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. @@ -67,14 +71,14 @@ public interface Logic { But apparently, this is an interface, not a concrete implementation. That should be fine because the [Architecture section of the Developer Guide](../DeveloperGuide.html#architecture) tells us that components interact through interfaces. Here's the relevant diagram: - + Next, let's find out which statement(s) in the `UI` code is calling this method, thus transferring control from the `UI` to the `Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. -
    +**Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. + ![`Find Usages` tool window. `Edit` \> `Find` \> `Find Usages`.](../images/tracing/FindUsages.png) @@ -87,10 +91,10 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres Recall from the User Guide that the `edit` command has the format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` For this tutorial we will be issuing the command `edit 1 n/Alice Yeoh`. -
    + -:bulb: **Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. -
    +**Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. + 1. To start the debugging session, simply `Run` \> `Debug Main` @@ -110,7 +114,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ **LogicManager\#execute().** - ``` java + ```java @Override public CommandResult execute(String commandText) throws CommandException, ParseException { @@ -142,7 +146,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![StepOver](../images/tracing/StepOver.png) 1. _Step into_ the line where user input in parsed from a String to a Command, which should bring you to the `AddressBookParser#parseCommand()` method (partial code given below): - ``` java + ```java public Command parseCommand(String userInput) throws ParseException { ... final String commandWord = matcher.group("commandWord"); @@ -157,7 +161,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping through the `switch` block, we end up at a call to `EditCommandParser().parse()` as expected (because the command we typed is an edit command). - ``` java + ```java ... case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); @@ -166,8 +170,10 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Let’s see what `EditCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`. -
    :bulb: **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! -
    + + + **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! + 1. Stepping through the method shows that it calls `ArgumentTokenizer#tokenize()` and `ParserUtil#parseIndex()` to obtain the arguments and index required. @@ -175,17 +181,17 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![EditCommand](../images/tracing/EditCommand.png) 1. As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the `Logic` component. - + 1. Let’s continue stepping through until we return to `LogicManager#execute()`. The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
    - ![Tracing an `edit` command through the Logic component](../images/tracing/LogicSequenceDiagram.png) + 1. Now, step over until you read the statement that calls the `execute()` method of the `EditCommand` object received, and step into that `execute()` method (partial code given below): **`EditCommand#execute()`:** - ``` java + ```java @Override public CommandResult execute(Model model) throws CommandException { ... @@ -205,25 +211,28 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
    FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
    To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked. -
    +
    * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) 1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
    Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
    1. Given that you have already seen quite a few classes in the `Logic` component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the `Logic` component: - + + * :bulb: This may be a good time to read through the [`Logic` component section of the DG](../DeveloperGuide.html#logic-component) 1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component. -
    :bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. -
    + + + **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. + -1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): +1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): **`JsonSerializableAddressBook` constructor:** - ``` java + ```java /** * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. * @@ -243,7 +252,8 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ This is because regular Java objects need to go through an _adaptation_ for them to be suitable to be saved in JSON format. 1. While you are stepping through the classes in the `Storage` component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
    - + + * :bulb: This may be a good time to read through the [`Storage` component section of the DG](../DeveloperGuide.html#storage-component) 1. We can continue to step through until you reach the end of the `LogicManager#execute()` method and return to the `MainWindow#executeCommand()` method (the place where we put the original breakpoint). @@ -251,7 +261,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping into `resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());`, we end up in: **`ResultDisplay#setFeedbackToUser()`** - ``` java + ```java public void setFeedbackToUser(String feedbackToUser) { requireNonNull(feedbackToUser); resultDisplay.setText(feedbackToUser); diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java index ec1b7958746..51e41d70f4d 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/address/Main.java @@ -25,7 +25,6 @@ public class Main { private static Logger logger = LogsCenter.getLogger(Main.class); public static void main(String[] args) { - // As per https://github.com/openjdk/jfx/blob/master/doc-files/release-notes-16.md // JavaFX 16 (or later) runtime logs a warning at startup if JavaFX classes are loaded from // the classpath instead of a module. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..8778affaeea 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -21,12 +21,22 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.TaskManager; +import seedu.address.model.util.SampleCalendarUtil; import seedu.address.model.util.SampleDataUtil; +import seedu.address.model.util.SampleTaskManagerUtil; import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.CalendarStorage; import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonCalendarStorage; +import seedu.address.storage.JsonTaskManagerStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; +import seedu.address.storage.TaskManagerStorage; import seedu.address.storage.UserPrefsStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -36,7 +46,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -58,7 +68,9 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + CalendarStorage calendarStorage = new JsonCalendarStorage(userPrefs.getCalendarFilePath()); + TaskManagerStorage taskManagerStorage = new JsonTaskManagerStorage(userPrefs.getTaskManagerFilePath()); + storage = new StorageManager(addressBookStorage, calendarStorage, taskManagerStorage, userPrefsStorage); model = initModelManager(storage, userPrefs); @@ -76,21 +88,52 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { logger.info("Using data file : " + storage.getAddressBookFilePath()); Optional addressBookOptional; - ReadOnlyAddressBook initialData; + ReadOnlyAddressBook addressBookInitialData; + Optional calendarOptional; + ReadOnlyCalendar calendarInitialData; + Optional taskManagerOptional; + ReadOnlyTaskManager taskManagerInitialData; + try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + addressBookInitialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); - initialData = new AddressBook(); + addressBookInitialData = new AddressBook(); + } + + try { + calendarOptional = storage.readCalendar(); + if (!calendarOptional.isPresent()) { + logger.info("Creating a new data file " + storage.getCalendarFilePath() + + " populated with a sample Calendar."); + } + calendarInitialData = calendarOptional.orElseGet(SampleCalendarUtil::getSampleCalendar); + } catch (DataLoadingException e) { + logger.warning("Data file at " + storage.getCalendarFilePath() + " could not be loaded." + + " Will be starting with an empty Calendar."); + calendarInitialData = new UniMateCalendar(); + } + + try { + taskManagerOptional = storage.readTaskManager(); + if (!taskManagerOptional.isPresent()) { + logger.info("Creating a new data file " + storage.getTaskManagerFilePath() + + " populated with a sample Task Manager."); + } + taskManagerInitialData = taskManagerOptional.orElseGet(SampleTaskManagerUtil::getSampleTaskManager); + } catch (DataLoadingException e) { + logger.warning("Data file at " + storage.getTaskManagerFilePath() + " could not be loaded." + + " Will be starting with an empty Task Manager."); + taskManagerInitialData = new TaskManager(); } - return new ModelManager(initialData, userPrefs); + return new ModelManager(addressBookInitialData, calendarInitialData, taskManagerInitialData, userPrefs); } private void initLogging(Config config) { diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..7226c15f97b 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,7 +8,11 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.Task; /** * API of the Logic component @@ -30,6 +34,32 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); + /** + * Returns the Calendar. + * + * @see seedu.address.model.Model#getCalendar() + */ + ReadOnlyCalendar getCalendar(); + + /** + * Returns the TaskManager. + * + * @see seedu.address.model.Model#getTaskManager() + */ + ReadOnlyTaskManager getTaskManager(); + + /** Returns an unmodifiable view of the filtered list of events */ + ObservableList getEventList(); + + /** Returns an unmodifiable view of the list of tasks */ + ObservableList getTaskList(); + + /** Returns an unmodifiable view of the filtered list of events for the week */ + ObservableList getCurrentWeekEventList(); + + /** Returns the resulting calendar from comparing calendars */ + ReadOnlyCalendar getComparisonCalendar(); + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..c217f06370b 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -11,11 +11,15 @@ import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.UniMateParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.Task; import seedu.address.storage.Storage; /** @@ -26,12 +30,15 @@ public class LogicManager implements Logic { public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; - + public static final String FILE_NOT_FOUND_EXCEPTION_MESSAGE = "File was moved/modified"; + public static final String FILE_PARSE_EXCEPTION_MESSAGE = "Data in the file could not be parsed."; + public static final String FEATURE_NOT_IMPLEMENTED_YET = "Feature coming soon..."; + private static final String TIMEZONE_STRING = "Asia/Singapore"; private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; private final Storage storage; - private final AddressBookParser addressBookParser; + private final UniMateParser uniMateParser; /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. @@ -39,7 +46,7 @@ public class LogicManager implements Logic { public LogicManager(Model model, Storage storage) { this.model = model; this.storage = storage; - addressBookParser = new AddressBookParser(); + uniMateParser = new UniMateParser(); } @Override @@ -47,11 +54,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE logger.info("----------------[USER COMMAND][" + commandText + "]"); CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); + Command command = uniMateParser.parseCommand(commandText); commandResult = command.execute(model); try { storage.saveAddressBook(model.getAddressBook()); + storage.saveCalendar(model.getCalendar()); + storage.saveTaskManager(model.getTaskManager()); } catch (AccessDeniedException e) { throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); } catch (IOException ioe) { @@ -66,6 +75,36 @@ public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); } + @Override + public ReadOnlyCalendar getCalendar() { + return model.getCalendar(); + } + + @Override + public ReadOnlyTaskManager getTaskManager() { + return model.getTaskManager(); + } + + @Override + public ObservableList getEventList() { + return model.getEventList(); + } + + @Override + public ObservableList getTaskList() { + return model.getTaskList(); + } + + @Override + public ObservableList getCurrentWeekEventList() { + return model.getCurrentWeekEventList(); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + return model.getComparisonCalendar(); + } + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..3eb0ab0cab3 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -1,11 +1,15 @@ package seedu.address.logic; +import java.time.format.DateTimeFormatter; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import javafx.collections.ObservableList; import seedu.address.logic.parser.Prefix; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.task.Task; /** * Container for user visible messages. @@ -18,6 +22,12 @@ public class Messages { public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_INTEGER_OVERFLOW = + "Provided index is greater than 2147483647. Please use a smaller integer."; + public static final String MESSAGE_INDEX_OUT_OF_BOUNDS = "Event index is out of bounds!"; + public static final String MESSAGE_DATE_CHANGE_SUCCESSFUL = "Successfully changed!"; + private static final String MESSAGE_EVENT_ADDED_SUCCESSFULLY = "Event added successfully!"; + private static final String MESSAGE_EVENT_TIMING_CONFLICT = "Conflicting timing!"; /** * Returns an error message indicating the duplicate prefixes. @@ -48,4 +58,54 @@ public static String format(Person person) { return builder.toString(); } + /** + * Formats the event for display to the user. + * + * @param event event to be displayed. + * @return String of appropriate format. + */ + public static String format(Event event) { + final StringBuilder builder = new StringBuilder(); + builder.append(event.getDescription()) + .append("; ") + .append(event.getEventPeriod()); + return builder.toString(); + } + + /** + * Formats a task for display to the user. + * + * @param task task to be displayed + * @return String of appropriate format. + */ + public static String format(Task task) { + final StringBuilder builder = new StringBuilder(); + builder.append(task.getDescriptionString()) + .append("\nDeadline: ") + .append(task.getDeadlineString()); + return builder.toString(); + } + + /** + * Formats the {@code person} for display to the user. + */ + public static String formatCalendar(Person person) { + final StringBuilder builder = new StringBuilder(); + ObservableList eventList = person.getCalendar().getEventList(); + builder.append(person.getName()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + for (int i = 0; i < eventList.size(); i++) { + Event event = eventList.get(i); + String eventDescription = event.getDescription().getDescription(); + String startTime = formatter.format(event.getEventPeriod().getStart()); + String endTime = formatter.format(event.getEventPeriod().getEnd()); + builder.append(";\n") + .append(eventDescription) + .append(" ") + .append(startTime) + .append(" ") + .append(endTime); + } + return builder.toString(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java b/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java new file mode 100644 index 00000000000..eba985579fe --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java @@ -0,0 +1,98 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +/** + * Adds an event to the calendar of an existing person in the address book. + */ +public class AddContactEventCommand extends Command { + + public static final String COMMAND_WORD = "addContactEvent"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to the calendar of the person " + + "identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION " + + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME " + + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_EVENT_DESCRIPTION + "Nap " + + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 " + + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00"; + + public static final String MESSAGE_ADD_EVENT_TO_PERSON_SUCCESS = "New event added to %s: %2$s"; + public static final String MESSAGE_EVENT_CONFLICT = "This event is conflicting with another event"; + + private final Index index; + private final Event event; + + /** + * @param index of the person in the filtered person list to add an event to + * @param event event to be added to the person + */ + public AddContactEventCommand(Index index, Event event) { + requireAllNonNull(index, event); + + this.index = index; + this.event = event; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + // Gets the person which the event should be added to + Person personToEdit = lastShownList.get(index.getZeroBased()); + if (!personToEdit.canAddEvent(event)) { + throw new CommandException(MESSAGE_EVENT_CONFLICT); + } + + personToEdit.addEvent(event); + + return new CommandResult(String.format(MESSAGE_ADD_EVENT_TO_PERSON_SUCCESS, + personToEdit.getName(), Messages.format(event))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddContactEventCommand)) { + return false; + } + + AddContactEventCommand otherAddContactEventCommand = (AddContactEventCommand) other; + return index.equals(otherAddContactEventCommand.index) + && event.equals(otherAddContactEventCommand.event); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("event", event) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddEventCommand.java b/src/main/java/seedu/address/logic/commands/AddEventCommand.java new file mode 100644 index 00000000000..68b9865d3b4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddEventCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; + +/** + * Adds an event to the calendar. + */ +public class AddEventCommand extends Command { + + public static final String COMMAND_WORD = "addEvent"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to the calendar. " + + "Parameters: " + + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION " + + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME " + + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_EVENT_DESCRIPTION + "Nap " + + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 " + + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00"; + + public static final String MESSAGE_SUCCESS = "New event added: %1$s"; + + public static final String MESSAGE_EVENT_CONFLICT = "This event is conflicting with another event"; + + private final Event toAdd; + + /** + * Creates an AddEventCommand to add the event into the calendar. + * + * @param event event to be added. + */ + public AddEventCommand(Event event) { + requireNonNull(event); + toAdd = event; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.canAddEvent(toAdd)) { + throw new CommandException(MESSAGE_EVENT_CONFLICT); + } + + model.addEvent(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddEventCommand)) { + return false; + } + + AddEventCommand otherAddCommand = (AddEventCommand) other; + return toAdd.equals(otherAddCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddTaskCommand.java b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java new file mode 100644 index 00000000000..e97b218633f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; +import seedu.address.model.task.exceptions.DuplicateTaskException; + +/** + * Adds a Task to the TaskList + */ +public class AddTaskCommand extends Command { + + public static final String COMMAND_WORD = "addTask"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a task to the task list. " + + "Parameters: " + + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION " + + "[" + PREFIX_EVENT_END_DATE_TIME + "DEADLINE]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_EVENT_DESCRIPTION + "Caffeinate " + + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 08:00"; + + public static final String MESSAGE_SUCCESS = "New task added: %1$s"; + public static final String MESSAGE_DUPLICATE_TASK = "This task already exists!\n" + + "Duplicate task: %s"; + public static final String MESSAGE_INVALID_DESCRIPTION = "Description cannot be empty!\n%s"; + + private final Task toAdd; + + /** + * Constructs an AddTaskCommand to add a task into the task list. + * @param task the task to add. + */ + public AddTaskCommand(Task task) { + requireNonNull(task); + + toAdd = task; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.addTask(toAdd); + } catch (DuplicateTaskException e) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_TASK, Messages.format(toAdd))); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddTaskCommand)) { + return false; + } + + AddTaskCommand otherAddTaskCommand = (AddTaskCommand) other; + return toAdd.equals(otherAddTaskCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("taskToAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java b/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java new file mode 100644 index 00000000000..b0523fc6ad3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java @@ -0,0 +1,112 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONFIRMATION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import java.util.List; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; + +/** + * Deletes all events within a time range in the calendar. + * Also uses a confirmation check to ensure that events are not accidentally deleted + */ +public class ClearEventsCommand extends Command { + public static final String COMMAND_WORD = "clearEvents"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes events within a time range from the calendar. " + + "Shows all events within the time range instead if confirmation is not present.\n" + + "Parameters: " + + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME " + + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME [" + + PREFIX_CONFIRMATION + "CONFIRMATION]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 " + + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00"; + + public static final String MESSAGE_SUCCESS = "The following events within the time period have been deleted:\n"; + public static final String MESSAGE_NO_EVENTS = "There are no events within this time period."; + public static final String MESSAGE_WITHIN_RANGE = "The following events are within the time period:\n"; + public static final String MESSAGE_ADD_CONFIRMATION = "Reenter the command with [c/CONFIRMED] at the end to " + + "confirm deletion.\n"; + public static final String COMMAND_RESEND_FORMAT = "clearEvents ts/%s te/%s c/CONFIRMED"; + + private final EventPeriod clearPeriod; + private final boolean isConfirmed; + + /** + * Creates a ClearEventsCommand to delete all events within a time range from the calendar. + * + * @param clearPeriod The time period for which to delete all events. + * @param isConfirmed A boolean to check if the deletion is confirmed by the user. + */ + public ClearEventsCommand(EventPeriod clearPeriod, boolean isConfirmed) { + requireAllNonNull(clearPeriod, isConfirmed); + this.clearPeriod = clearPeriod; + this.isConfirmed = isConfirmed; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List eventsInRange = model.eventsInRange(clearPeriod); + if (eventsInRange.isEmpty()) { + throw new CommandException(MESSAGE_NO_EVENTS); + } + StringBuilder sb; + + if (isConfirmed) { + sb = new StringBuilder(MESSAGE_SUCCESS); + } else { + sb = new StringBuilder(MESSAGE_WITHIN_RANGE); + } + + int i = 1; + for (Event event:eventsInRange) { + sb.append(String.format("%d. %s\n", i, Messages.format(event))); + i++; + } + if (isConfirmed) { + model.deleteEventsInRange(clearPeriod); + } else { + sb.append(MESSAGE_ADD_CONFIRMATION); + sb.append(String.format(COMMAND_RESEND_FORMAT, + clearPeriod.getFormattedStart(), + clearPeriod.getFormattedEnd())); + } + return new CommandResult(sb.toString()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ClearEventsCommand)) { + return false; + } + + ClearEventsCommand otherClearEventsCommand = (ClearEventsCommand) other; + boolean periodIsEqual = clearPeriod.equals(otherClearEventsCommand.clearPeriod); + boolean confirmationIsEqual = isConfirmed == otherClearEventsCommand.isConfirmed; + return periodIsEqual && confirmationIsEqual; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toClearWithin", clearPeriod) + .add("confirmed", isConfirmed) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..ab929d41ae8 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -4,12 +4,14 @@ import java.util.Objects; +import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; /** * Represents the result of a command execution. */ public class CommandResult { + private static final int INVALID_INDEX = -2; private final String feedbackToUser; @@ -19,13 +21,25 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + /** The application should show the task list in the bottom list slot. */ + private final boolean switchBottomList; + + private final ViewEventsIndicator eventViewIndex; + + /** Application should display calendar comparison result */ + private final boolean showCalendarComparison; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showCalendarComparison, + boolean switchBottomList) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.showCalendarComparison = showCalendarComparison; + this.switchBottomList = switchBottomList; + this.eventViewIndex = new ViewEventsIndicator(INVALID_INDEX); } /** @@ -33,7 +47,20 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, false, false); + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * {@code eventViewIndex}, and other fields set to their default value + */ + public CommandResult(String feedbackToUser, Index eventViewIndex) { + this.feedbackToUser = feedbackToUser; + this.showHelp = false; + this.exit = false; + this.showCalendarComparison = false; + this.switchBottomList = false; + this.eventViewIndex = new ViewEventsIndicator(eventViewIndex.getOneBased()); } public String getFeedbackToUser() { @@ -48,6 +75,22 @@ public boolean isExit() { return exit; } + public boolean isSwitchBottomList() { + return switchBottomList; + } + + public Index getEventViewIndex() { + return eventViewIndex.getIndex(); + } + + public boolean isViewEvents() { + return eventViewIndex.isViewEvents(); + } + + public boolean isShowCalendarComparison() { + return showCalendarComparison; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +105,13 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && switchBottomList == otherCommandResult.switchBottomList; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, switchBottomList); } @Override @@ -76,7 +120,41 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("switchBottomList", switchBottomList) .toString(); } + /** + * An indicator class to indicate if an event list is being viewed. + */ + public static class ViewEventsIndicator { + private static final int MINIMUM_INDEX = 1; + private final Index index; + private final boolean isViewEvents; + + /** + * Constructs a valid indicator if the index is above 1 and an invalid one otherwise. + */ + public ViewEventsIndicator(int index) { + if (index >= MINIMUM_INDEX) { + this.index = Index.fromOneBased(index); + isViewEvents = true; + } else { + this.index = Index.fromOneBased(MINIMUM_INDEX); + isViewEvents = false; + } + } + + /** + * Checks if the result requires events to be viewed. + */ + public boolean isViewEvents() { + return isViewEvents; + } + + public Index getIndex() { + return index; + } + } + } diff --git a/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java b/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java new file mode 100644 index 00000000000..c00277bcfec --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.person.Person; + +/** + * Command for comparing calendars of user and persons in address book. + */ +public class CompareCalendarByIndexCommand extends Command { + public static final String COMMAND_WORD = "compareCalendars"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": compare calendars with specified people index. " + + "Parameters: " + + "NONE/ONE/MULTIPLE VALID PERSON INDEX \n" + + "Example: " + COMMAND_WORD + " " + + "1 2 3"; + + public static final String MESSAGE_SUCCESS = "Displaying available time periods"; + private final List indexList; + + /** + * Create a new CompareCalendarCommand with a list of index of persons to compare with. + * + * @param indexList list of index of persons to compare with. + */ + public CompareCalendarByIndexCommand(List indexList) { + this.indexList = indexList; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (indexList.stream().map(Index::getZeroBased) + .anyMatch(zeroBasedIndex -> zeroBasedIndex >= lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + ReadOnlyCalendar combinedCalendar = indexList.stream().map(Index::getZeroBased).map(lastShownList::get) + .map(Person::getReadOnlyCalendar) + .reduce(model.getCalendar(), ReadOnlyCalendar::getCombinedCalendar); + + model.setComparisonCalendar(combinedCalendar); + + return new CommandResult(MESSAGE_SUCCESS, false, false, true, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java b/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java new file mode 100644 index 00000000000..7aee4efa053 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java @@ -0,0 +1,45 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Command to compare calendars of user with people in the calendar with some collection of tags. + */ +public class CompareCalendarByTagCommand extends Command { + public static final String COMMAND_WORD = "compareGroupCalendars"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": compare calendars within people with specified tags. " + + "Parameters: " + + "NONE/ONE/MULTIPLE VALID TAGS \n" + + "Example: " + COMMAND_WORD + " " + + "class friend"; + + public static final String MESSAGE_SUCCESS = "Displaying available time periods"; + + private final List tagList; + + public CompareCalendarByTagCommand(List tagList) { + this.tagList = tagList; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + ReadOnlyCalendar comparisonCalendar = lastShownList.stream().filter(person -> { + return tagList.stream().anyMatch(person::hasTag); + }).map(Person::getReadOnlyCalendar).reduce(model.getCalendar(), ReadOnlyCalendar::getCombinedCalendar); + + model.setComparisonCalendar(comparisonCalendar); + + return new CommandResult(MESSAGE_SUCCESS, false, false, true, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..f72d7081313 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -12,7 +12,7 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person identified using its displayed index from the address book. */ public class DeleteCommand extends Command { diff --git a/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java b/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java new file mode 100644 index 00000000000..671ef726831 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import java.time.LocalDateTime; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +/** + * Delete an event from the calendar of an existing person in the address book. + */ +public class DeleteContactEventCommand extends Command { + + public static final String COMMAND_WORD = "deleteContactEvent"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an event from the calendar of the person " + + "identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_EVENT_START_DATE_TIME + "ANY TIME WITHIN EVENT DURATION \n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 "; + + public static final String MESSAGE_DELETE_EVENT_FROM_PERSON_SUCCESS = "Event deleted from %s: %2$s"; + public static final String MESSAGE_NO_EVENT = "There is no valid existing event at this timing."; + + private final Index index; + private final LocalDateTime eventTime; + + /** + * @param index of the person in the filtered person list to delete an event from. + * @param eventTime time of the event to delete. + */ + public DeleteContactEventCommand(Index index, LocalDateTime eventTime) { + requireAllNonNull(index, eventTime); + + this.index = index; + this.eventTime = eventTime; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + //Obtain the calendar of the person and tries to delete an event from the person's calendar + Person personToEdit; + try { + personToEdit = lastShownList.get(index.getZeroBased()); + } catch (IndexOutOfBoundsException e) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Event toDelete; + + try { + toDelete = personToEdit.findEvent(eventTime); + personToEdit.deleteEvent(eventTime); + } catch (Exception e) { + throw new CommandException(MESSAGE_NO_EVENT); + } + return new CommandResult(String.format(MESSAGE_DELETE_EVENT_FROM_PERSON_SUCCESS, + personToEdit.getName(), Messages.format(toDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteContactEventCommand)) { + return false; + } + + DeleteContactEventCommand otherDeleteContactEventCommand = (DeleteContactEventCommand) other; + return index.equals(otherDeleteContactEventCommand.index) + && eventTime.equals(otherDeleteContactEventCommand.eventTime); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("eventTime", eventTime) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java b/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java new file mode 100644 index 00000000000..c2cfed3f1c4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDateTime; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.exceptions.EventNotFoundException; + +/** + * Deletes an event from the calendar. + */ +public class DeleteEventCommand extends Command { + public static final String COMMAND_WORD = "deleteEvent"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an event from the calendar. " + + "Parameters: " + + "ANY TIME WITHIN EVENT DURATION \n" + + "Example: " + COMMAND_WORD + " " + + "2024-01-01 12:00 "; + + public static final String MESSAGE_SUCCESS = "Event deleted: %1$s"; + public static final String MESSAGE_NO_EVENT = "There is no valid existing event at this timing."; + private final LocalDateTime eventTime; + + /** + * Creates a DeleteEventCommand to delete an event from the calendar. + * + * @param eventTime time of the event to delete. + */ + public DeleteEventCommand(LocalDateTime eventTime) { + requireNonNull(eventTime); + this.eventTime = eventTime; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Event toDelete; + try { + toDelete = model.findEventAt(eventTime); + model.deleteEventAt(eventTime); + } catch (EventNotFoundException e) { + throw new CommandException(MESSAGE_NO_EVENT); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteEventCommand)) { + return false; + } + + DeleteEventCommand otherDeleteCommand = (DeleteEventCommand) other; + return eventTime.equals(otherDeleteCommand.eventTime); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toDeleteAt", eventTime) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java new file mode 100644 index 00000000000..4b656880a7d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; + +/** + * Deletes a task identified using it's displayed index from the task list. + */ +public class DeleteTaskCommand extends Command { + + public static final String COMMAND_WORD = "deleteTask"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the task identified by the index number used in the displayed task list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_TASK_SUCCESS = "Deleted Task: %1$s"; + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid!"; + + private final Index targetIndex; + + public DeleteTaskCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Task toDelete; + + try { + toDelete = model.deleteTask(targetIndex.getZeroBased()); + return new CommandResult(String.format(MESSAGE_DELETE_TASK_SUCCESS, Messages.format(toDelete))); + } catch (IndexOutOfBoundsException e) { + throw new CommandException(MESSAGE_INVALID_INDEX); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteTaskCommand)) { + return false; + } + + DeleteTaskCommand otherDeleteTaskCommand = (DeleteTaskCommand) other; + return targetIndex.equals(otherDeleteTaskCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..c50c8eb5d11 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -21,6 +21,7 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.calendar.UniMateCalendar; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -100,8 +101,9 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + UniMateCalendar calendar = personToEdit.getCalendar(); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, calendar); } @Override diff --git a/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java b/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java new file mode 100644 index 00000000000..79ae7d57458 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java @@ -0,0 +1,271 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; +import static seedu.address.logic.parser.ParserUtil.parseDateTimeNonNull; +import static seedu.address.logic.parser.ParserUtil.parseDateTimeToString; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + + +/** + * Edits the details of an existing person's calendar in the address book. + */ +public class EditContactEventCommand extends Command { + + public static final String COMMAND_WORD = "editContactEvent"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the identified person's calendar " + + "by the index number used in the displayed person list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: PERSON_INDEX (must be a positive integer) " + + "EVENT_INDEX (must be a positive integer)" + + "[" + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_EVENT_START_DATE_TIME + "START_DATE] " + + "[" + PREFIX_EVENT_END_DATE_TIME + "END_DATE] \n" + + "Example: " + COMMAND_WORD + " 1 " + "1 " + + PREFIX_EVENT_DESCRIPTION + "Nap " + + PREFIX_EVENT_START_DATE_TIME + "2023-10-10 10:00 " + + PREFIX_EVENT_END_DATE_TIME + "2023-10-10 15:00 "; + + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_WRONG_TIME = "Both the start and end time must be edited!"; + public static final String INVALID_EVENT_INDEX = "Invalid Event Index Provided"; + private final Index personIndex; + private final Index eventIndex; + private final EditEventDescriptor editEventDescriptor; + + /** + * @param indexArrayList an ArrayList consisting of two elements, that is the Index of target person and + * the index of the target event. + * @param editEventDescriptor details to edit the calendar with + */ + public EditContactEventCommand(ArrayList indexArrayList, EditEventDescriptor editEventDescriptor) { + requireNonNull(indexArrayList.get(0)); + requireNonNull(indexArrayList.get(1)); + requireNonNull(editEventDescriptor); + this.personIndex = indexArrayList.get(0); + this.eventIndex = indexArrayList.get(1); + this.editEventDescriptor = editEventDescriptor; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (personIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(personIndex.getZeroBased()); + Person editedPerson = createPersonsEditedEvent(eventIndex, personToEdit, editEventDescriptor); + + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.formatCalendar(editedPerson))); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit}, editing the event with index + * {@code eventIndex} with {@code editEventDescriptor}. + */ + private static Person createPersonsEditedEvent(Index eventIndex, + Person personToEdit, EditEventDescriptor editEventDescriptor) + throws CommandException { + assert personToEdit != null; + + Name updatedName = personToEdit.getName(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Address updatedAddress = personToEdit.getAddress(); + Set updatedTags = personToEdit.getTags(); + UniMateCalendar calendar = personToEdit.getCalendar(); + List eventList = updateEventList(calendar, eventIndex, editEventDescriptor); + UniMateCalendar updatedCalendar = new UniMateCalendar(); + for (Event e: eventList) { + updatedCalendar.addEvent(e); + } + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, updatedCalendar); + } + + /** + * Updates an event by its {@code eventIndex} in the given {@code calendar} with the new information provided in + * the {@code editEventDescriptor}. + * + * @return The updated event list. + */ + public static List updateEventList(UniMateCalendar calendar, Index eventIndex, + EditEventDescriptor editEventDescriptor) throws CommandException { + ObservableList eventList = calendar.getEventManager().asUnmodifiableObservableList(); + Event updateEvent; + try { + updateEvent = eventList.get(eventIndex.getZeroBased()); + } catch (Exception e) { + throw new CommandException(INVALID_EVENT_INDEX); + } + EventPeriod updatePeriod = updateEvent.getEventPeriod(); + LocalDateTime newStartDateTime = editEventDescriptor.getStart().orElse(updatePeriod.getStart()); + LocalDateTime newEndDateTime = editEventDescriptor.getEnd().orElse(updatePeriod.getEnd()); + EventDescription newEventDescription = editEventDescriptor.getEventDescription() + .orElse(updateEvent.getDescription()); + String stringStartDateTime = parseDateTimeToString(newStartDateTime); + String stringEndDateTime = parseDateTimeToString(newEndDateTime); + EventPeriod newEventPeriod = new EventPeriod(stringStartDateTime, stringEndDateTime); + Event updatedEvent = new Event(newEventDescription, newEventPeriod); + eventList.set(eventIndex.getZeroBased(), updatedEvent); + return eventList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditContactEventCommand)) { + return false; + } + + EditContactEventCommand otherEditCommand = (EditContactEventCommand) other; + return personIndex.equals(otherEditCommand.personIndex) + && eventIndex.equals(otherEditCommand.eventIndex) + && editEventDescriptor.equals(otherEditCommand.editEventDescriptor); + } + + /** + * Stores the details to edit the person with. Each non-empty field value will replace the + * corresponding field value of the calendar of the person. + */ + public static class EditEventDescriptor { + private UniMateCalendar calendar; + private Event event; + private EventDescription eventDescription; + private EventPeriod eventPeriod; + private LocalDateTime start; + private LocalDateTime end; + + public EditEventDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditEventDescriptor(EditEventDescriptor toCopy) { + setEventPeriod(toCopy.eventPeriod); + setEventDescription(toCopy.eventDescription); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(start, end, eventDescription); + } + + public void setEventDescription(EventDescription eventDescription) { + this.eventDescription = eventDescription; + } + + public Optional getEventDescription() { + return Optional.ofNullable(eventDescription); + } + + public void setEventPeriod(EventPeriod eventPeriod) { + this.eventPeriod = eventPeriod; + setStart(eventPeriod.getStart()); + setEnd(eventPeriod.getEnd()); + } + + public Optional getEventPeriod() { + return Optional.ofNullable(eventPeriod); + } + public void setStart(String startString) { + LocalDateTime start = parseDateTimeNonNull(startString); + this.start = start; + } + + public void setStart(LocalDateTime start) { + this.start = start; + } + + public Optional getStart() { + return Optional.ofNullable(start); + } + + public void setEnd(String endString) { + LocalDateTime end = parseDateTimeNonNull(endString); + this.end = end; + } + + public void setEnd(LocalDateTime end) { + this.end = end; + } + + public Optional getEnd() { + return Optional.ofNullable(end); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditEventDescriptor)) { + return false; + } + + EditEventDescriptor otherEditEventDescriptor = (EditEventDescriptor) other; + return Objects.equals(eventDescription, otherEditEventDescriptor.eventDescription) + && Objects.equals(start, otherEditEventDescriptor.start) + && Objects.equals(end, otherEditEventDescriptor.end); + } + + @Override + public String toString() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return new ToStringBuilder("") + .add("eventDescription", eventDescription) + .add("start", parseDateTimeToString(eventPeriod.getStart())) + .add("end", parseDateTimeToString(eventPeriod.getEnd())) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..7874f9ccfc1 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..e586687e293 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,237 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Filters contact list to display contacts matching specified criteria. + */ +public class FilterCommand extends Command { + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose attributes contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + PREFIX_PHONE + "PHONE " + + PREFIX_EMAIL + "EMAIL " + + PREFIX_ADDRESS + "ADDRESS " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Hugh " + + PREFIX_PHONE + "90 " + + PREFIX_EMAIL + "@u.nus.edu " + + PREFIX_ADDRESS + "123, Tengah Ave 6, #69-420 " + + PREFIX_TAG + "CS2103 " + + PREFIX_TAG + "CSGod"; + public static final String CONTACTS_NOT_FILTERED = String.format("%s \n%s", + "At least one parameter is required to filter contacts.", MESSAGE_USAGE); + private final PersonFilter personFilter; + + /** + * Filters out all persons that do not match the filter criteria specified. + * Field matching is case-insensitive + * + * @param personFilter Details to filter people with + */ + public FilterCommand(PersonFilter personFilter) { + requireNonNull(personFilter); + this.personFilter = personFilter; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(personFilter::matchesFilter); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return personFilter.equals(otherFilterCommand.personFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("personFilter", personFilter) + .toString(); + } + + /** + * Stores the details to filter the contacts by. Contacts will be filtered by each non-empty field . + */ + public static class PersonFilter { + private String name; + private String phone; + private String email; + private String address; + private Set tags; + + public PersonFilter() {} + + /** + * Creates a copy of a filter with the same parameters as another filter. + * + * @param toCopy The other filter to create a copy of. + */ + public PersonFilter(PersonFilter toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAddress(toCopy.address); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getPhone() { + return phone; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Set getTags() { + return (tags != null) ? Collections.unmodifiableSet(tags) : null; + } + + /** + * Predicate to test if a person passes the filter by matching all parameters. + * + * @param person The person to be tested. + * @return a boolean representing whether the person passed the filter. + */ + public boolean matchesFilter(Person person) { + if (nonNull(name) && !person.getName().fullName.toLowerCase().contains(name.toLowerCase())) { + return false; + } + if (nonNull(phone) && !person.getPhone().value.contains(phone)) { + return false; + } + if (nonNull(email) && !person.getEmail().value.toLowerCase().contains(email.toLowerCase())) { + return false; + } + if (nonNull(address) && !person.getAddress().value.toLowerCase().contains(address.toLowerCase())) { + return false; + } + if (nonNull(tags) && !tags.isEmpty()) { + Stream tagsToCheck = tags.stream(); + Stream currentTags = person.getTags().stream(); + return tagsToCheck.allMatch(x -> currentTags.anyMatch(y -> y.tagName.contains(x.tagName))); + } + return true; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonFilter)) { + return false; + } + + PersonFilter otherPersonFilter = (PersonFilter) other; + return isEqual(name, otherPersonFilter.getName()) + && isEqual(phone, otherPersonFilter.getPhone()) + && isEqual(email, otherPersonFilter.getEmail()) + && isEqual(address, otherPersonFilter.getAddress()) + && isEqual(tags, otherPersonFilter.getTags()); + } + + private boolean isEqual(Object first, Object second) { + if (Objects.isNull(first)) { + return Objects.isNull(second); + } else if (Objects.isNull(second)) { + return false; + } else { + return first.equals(second); + } + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("email", email) + .add("address", address) + .add("tags", tags) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..492a3633fbd 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..6ad1dde88f6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,47 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Comparator; + +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.comparer.SortComparator; + +/** + * Sorts the persons in the address book by the given field. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + public static final String SORTBY_KEYWORD1 = "/byaddress"; + public static final String SORTBY_KEYWORD2 = "/byemail"; + public static final String SORTBY_KEYWORD3 = "/byname"; + public static final String SORTBY_KEYWORD4 = "/byphone"; + public static final String REVERSE_KEYWORD = "/reverse"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts all persons in UniMate " + + "by the ascending or descending order of specified attribute. \n" + + "Example: " + COMMAND_WORD + " " + SORTBY_KEYWORD1 + " " + REVERSE_KEYWORD; + + public static final String MESSAGE_SUCCESS = "Sorted all persons by specified order"; + private Comparator personComparator; + + /** + * Constructor for SortCommand, creates a comparator to sort by the specified field and order. + */ + public SortCommand(ArrayList sortComparatorList) { + SortComparator sortComparator = sortComparatorList.get(0); + boolean isReverse = sortComparator.getIsReverse(); + this.personComparator = isReverse + ? sortComparator.reversed() + : sortComparator; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.sortPersonList(personComparator); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortTasksCommand.java b/src/main/java/seedu/address/logic/commands/SortTasksCommand.java new file mode 100644 index 00000000000..f75012a9f7b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortTasksCommand.java @@ -0,0 +1,64 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.exceptions.InvalidSortingOrderException; + +/** + * Changes the sorting order of tasks in the task manager. + */ +public class SortTasksCommand extends Command { + public static final String COMMAND_WORD = "sortTasks"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Changes the way tasks in the task list are sorted." + + "Parameters: Description OR Deadline\n" + + "Example: " + COMMAND_WORD + " Description"; + + public static final String MESSAGE_SUCCESS = "Sorting order changed to %s."; + + public static final String MESSAGE_INVALID_TYPE = "Sorting order provided is invalid!\n" + + "Sorting orders available: DESCRIPTION/DEADLINE."; + + private final String comparatorType; + + public SortTasksCommand(String comparatorType) { + this.comparatorType = comparatorType; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.sortTasksBy(comparatorType); + } catch (InvalidSortingOrderException e) { + throw new CommandException(MESSAGE_INVALID_TYPE); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, comparatorType)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortTasksCommand)) { + return false; + } + + SortTasksCommand otherSortTasksCommand = (SortTasksCommand) other; + return comparatorType.equals((otherSortTasksCommand).comparatorType); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("comparatorType", comparatorType) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SwitchListCommand.java b/src/main/java/seedu/address/logic/commands/SwitchListCommand.java new file mode 100644 index 00000000000..ef805170d25 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SwitchListCommand.java @@ -0,0 +1,18 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Switches the bottom list between the task list and the event list. + */ +public class SwitchListCommand extends Command { + + public static final String COMMAND_WORD = "switchList"; + + public static final String MESSAGE_SWITCH_ACKNOWLEDGEMENT = "Switching list."; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(MESSAGE_SWITCH_ACKNOWLEDGEMENT, false, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.java b/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.java new file mode 100644 index 00000000000..c7e0017bed3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Creates a pop-up window to view the event list of a person using its displayed index from the address book. + */ +public class ViewContactEventsCommand extends Command { + public static final String COMMAND_WORD = "viewContactEvents"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Creates a window containing the event list of a person identified by their index number.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_VIEW_EVENTS_SUCCESS = "Viewing event list."; + + private final Index targetIndex; + + public ViewContactEventsCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + return new CommandResult(MESSAGE_VIEW_EVENTS_SUCCESS, targetIndex); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ViewContactEventsCommand)) { + return false; + } + + ViewContactEventsCommand otherViewContactEventsCommand = (ViewContactEventsCommand) other; + return targetIndex.equals(otherViewContactEventsCommand.targetIndex); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java new file mode 100644 index 00000000000..7835c800288 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java @@ -0,0 +1,72 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddContactEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +/** + * Parses input arguments and creates a new AddContactEventCommand object + */ +public class AddContactEventCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddContactEventCommand + * and returns an AddContactEventCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddContactEventCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddContactEventCommand.MESSAGE_USAGE), pe); + } + } + + if (!arePrefixesPresent(argMultimap, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddContactEventCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME); + EventDescription description = ParserUtil.parseEventDescription(argMultimap + .getValue(PREFIX_EVENT_DESCRIPTION).get()); + EventPeriod eventPeriod = ParserUtil.parseEventPeriod(argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(), + argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get()); + + return new AddContactEventCommand(index, new Event(description, eventPeriod)); + } + + /** + * Checks if all the given prefix fields are non-empty. + * + * @param argumentMultimap argumentMultimap managing arguments for this command. + * @param prefixes prefixes to be tested. + * @return true if all fields are non-empty, false if any field contains empty value. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java b/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java new file mode 100644 index 00000000000..766aa3bef3d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java @@ -0,0 +1,51 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +/** + * Parses input arguments and creates a new AddEventCommand object. + */ +public class AddEventCommandParser implements Parser { + @Override + public AddEventCommand parse(String args) throws ParseException { + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME); + + if (!arePrefixesPresent(argMultiMap, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME) || !argMultiMap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddEventCommand.MESSAGE_USAGE)); + } + + argMultiMap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, + PREFIX_EVENT_END_DATE_TIME); + EventDescription description = ParserUtil.parseEventDescription(argMultiMap + .getValue(PREFIX_EVENT_DESCRIPTION).get()); + EventPeriod eventPeriod = ParserUtil.parseEventPeriod(argMultiMap.getValue(PREFIX_EVENT_START_DATE_TIME).get(), + argMultiMap.getValue(PREFIX_EVENT_END_DATE_TIME).get()); + + return new AddEventCommand(new Event(description, eventPeriod)); + } + + /** + * Checks if all the given prefix fields are non-empty. + * + * @param argumentMultimap argumentMultimap managing arguments for this command. + * @param prefixes prefixes to be tested. + * @return true if all fields are non-empty, false if any field contains empty value. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java new file mode 100644 index 00000000000..621d8f321a3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java @@ -0,0 +1,58 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.AddTaskCommand.MESSAGE_INVALID_DESCRIPTION; +import static seedu.address.logic.commands.AddTaskCommand.MESSAGE_USAGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.model.task.Deadline.MESSAGE_CONSTRAINTS; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.task.Task; + +/** + * Parses input arguments and creates a new addTaskCommand object + */ +public class AddTaskCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddTaskCommand + * and returns an AddTaskCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddTaskCommand parse(String args) throws ParseException { + requireNonNull(args); + + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_END_DATE_TIME); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_END_DATE_TIME); + + if (argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).isEmpty() || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTaskCommand.MESSAGE_USAGE)); + } + + String description = argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).get(); + if (description.equals("")) { + throw new ParseException(String.format(MESSAGE_INVALID_DESCRIPTION, MESSAGE_USAGE)); + } + LocalDateTime deadline = null; + + if (argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent()) { + try { + deadline = ParserUtil.parseDateTime(argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get()); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_CONSTRAINTS); + } + } + + Task toAdd = new Task(description, deadline); + return new AddTaskCommand(toAdd); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java b/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java new file mode 100644 index 00000000000..d790e38a2d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java @@ -0,0 +1,48 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CONFIRMATION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import seedu.address.logic.commands.ClearEventsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventPeriod; + +/** + * Parses the input arguments and creates a new ClearEventsCommand object. + */ +public class ClearEventsCommandParser implements Parser { + private static final String CONFIRMATION = "CONFIRMED"; + @Override + public ClearEventsCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(userInput, PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME, + PREFIX_CONFIRMATION); + + if (!isTimePresent(argMultiMap) || !argMultiMap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClearEventsCommand.MESSAGE_USAGE)); + } + + argMultiMap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME, + PREFIX_CONFIRMATION); + + boolean isConfirmed = isCommandConfirmed(argMultiMap); + EventPeriod clearPeriod = ParserUtil.parseEventPeriod(argMultiMap.getValue(PREFIX_EVENT_START_DATE_TIME).get(), + argMultiMap.getValue(PREFIX_EVENT_END_DATE_TIME).get()); + return new ClearEventsCommand(clearPeriod, isConfirmed); + } + + private static boolean isCommandConfirmed(ArgumentMultimap argumentMultimap) { + if (argumentMultimap.getValue(PREFIX_CONFIRMATION).isPresent()) { + return argumentMultimap.getValue(PREFIX_CONFIRMATION).get().equals(CONFIRMATION); + } + return false; + } + + private static boolean isTimePresent(ArgumentMultimap argumentMultimap) { + boolean isStartTimePresent = argumentMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).isPresent(); + boolean isEndTimePresent = argumentMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent(); + return isStartTimePresent && isEndTimePresent; + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..b2c3ed12b62 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -12,4 +12,10 @@ public class CliSyntax { public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_EVENT_DESCRIPTION = new Prefix("d/"); + + public static final Prefix PREFIX_EVENT_START_DATE_TIME = new Prefix("ts/"); + + public static final Prefix PREFIX_EVENT_END_DATE_TIME = new Prefix("te/"); + public static final Prefix PREFIX_CONFIRMATION = new Prefix("c/"); } diff --git a/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java b/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java new file mode 100644 index 00000000000..866b2a115ee --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CompareCalendarByIndexCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.exceptions.RuntimeParseException; + +/** + * Parses input arguments and creates a new CompareCalendarCommand object. + */ +public class CompareCalendarByIndexCommandParser implements Parser { + private static final String splitRegex = "\\s+"; + @Override + public CompareCalendarByIndexCommand parse(String userInput) throws ParseException { + try { + List indexList = new ArrayList(); + String personIndexString = userInput.trim(); + if (!personIndexString.isEmpty()) { + String[] personIndexArray = personIndexString.split(splitRegex); + indexList = Arrays.stream(personIndexArray) + .map(ParserUtil::parseIndexSafe) + .collect(Collectors.toList()); + } + return new CompareCalendarByIndexCommand(indexList); + } catch (RuntimeParseException pe) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, CompareCalendarByIndexCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java b/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java new file mode 100644 index 00000000000..0188065b2e7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import seedu.address.logic.commands.CompareCalendarByTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parser for creating a CompareCalendarByTagCommand. + */ +public class CompareCalendarByTagCommandParser implements Parser { + private static final String splitRegex = "\\s+"; + @Override + public CompareCalendarByTagCommand parse(String userInput) throws ParseException { + try { + String trimmedInput = userInput.trim(); + List tagList = new ArrayList(); + if (!trimmedInput.isEmpty()) { + String[] tagStringArray = trimmedInput.split(splitRegex); + Set tagSet = ParserUtil.parseTags(Arrays.asList(tagStringArray)); + tagList.addAll(tagSet); + } + return new CompareCalendarByTagCommand(tagList); + } catch (ParseException pe) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, CompareCalendarByTagCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..337cd47028e 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,6 +1,8 @@ package seedu.address.logic.parser; +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; @@ -21,9 +23,12 @@ public DeleteCommand parse(String args) throws ParseException { Index index = ParserUtil.parseIndex(args); return new DeleteCommand(index); } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + } } } - } diff --git a/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java new file mode 100644 index 00000000000..0a39ffe0620 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java @@ -0,0 +1,69 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; + +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteContactEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteContactEventCommand object + */ +public class DeleteContactEventCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteContactEventCommand + * and returns an DeleteContactEventCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteContactEventCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_START_DATE_TIME); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteContactEventCommand.MESSAGE_USAGE), pe); + } + } + + if (!arePrefixesPresent(argMultimap, PREFIX_EVENT_START_DATE_TIME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteContactEventCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_START_DATE_TIME); + try { + LocalDateTime eventTime = LocalDateTime.parse( + argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(), + DATE_TIME_STRING_FORMATTER); + return new DeleteContactEventCommand(index, eventTime); + } catch (Exception pe) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, DeleteContactEventCommand.MESSAGE_USAGE)); + } + } + + /** + * Checks if all the given prefix fields are non-empty. + * + * @param argumentMultimap argumentMultimap managing arguments for this command. + * @param prefixes prefixes to be tested. + * @return true if all fields are non-empty, false if any field contains empty value. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java new file mode 100644 index 00000000000..95bd2d7222e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ParserUtil.DATE_TIME_STRING_FORMATTER; + +import java.time.LocalDateTime; + +import seedu.address.logic.commands.DeleteEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteEventCommand object. + */ +public class DeleteEventCommandParser implements Parser { + @Override + public DeleteEventCommand parse(String userInput) throws ParseException { + try { + String eventString = userInput.trim(); + LocalDateTime eventTime = LocalDateTime.parse(eventString, DATE_TIME_STRING_FORMATTER); + return new DeleteEventCommand(eventTime); + } catch (Exception pe) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, DeleteEventCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java new file mode 100644 index 00000000000..3c45656ada3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteTaskCommand object + */ +public class DeleteTaskCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteTaskCommand + * and returns a DeleteTaskCommand object for execution. + * @throws ParseException if the use input does not conform to the expected format. + */ + public DeleteTaskCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteTaskCommand(index); + } catch (ParseException pe) { + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTaskCommand.MESSAGE_USAGE), pe); + } + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..2e803662c3e 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -1,23 +1,20 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; +import static seedu.address.logic.parser.ParserUtil.parseTagsForUse; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object @@ -39,7 +36,12 @@ public EditCommand parse(String args) throws ParseException { try { index = ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + } } argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); @@ -58,7 +60,7 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseTagsForUse(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -67,19 +69,4 @@ public EditCommand parse(String args) throws ParseException { return new EditCommand(index, editPersonDescriptor); } - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - } diff --git a/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java new file mode 100644 index 00000000000..db59d7cab60 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java @@ -0,0 +1,68 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; + +import java.util.ArrayList; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditContactEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditContactEventCommand object + */ +public class EditContactEventCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EditContactEventCommand + * and returns an EditContactEventCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditContactEventCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, + PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME); + + String preamble; + ArrayList indexArrayList; + + try { + preamble = argMultimap.getPreamble(); + indexArrayList = ParserUtil.parseDualIndexes(preamble); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditContactEventCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, + PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME); + + EditContactEventCommand.EditEventDescriptor editEventDescriptor = + new EditContactEventCommand.EditEventDescriptor(); + + if (argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).isPresent()) { + editEventDescriptor.setEventDescription(ParserUtil.parseEventDescription(argMultimap + .getValue(PREFIX_EVENT_DESCRIPTION).get())); + } + boolean eventStartDateTimePresent = argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).isPresent(); + boolean eventEndDateTimePresent = argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent(); + if (eventStartDateTimePresent && eventEndDateTimePresent) { + editEventDescriptor.setEventPeriod( + ParserUtil.parseEventPeriod(argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(), + argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get())); + } + if ((eventStartDateTimePresent && !eventEndDateTimePresent) + || (!eventStartDateTimePresent && eventEndDateTimePresent)) { + throw new ParseException(EditContactEventCommand.MESSAGE_WRONG_TIME); + } + if (!editEventDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditContactEventCommand.MESSAGE_NOT_EDITED); + } + + return new EditContactEventCommand(indexArrayList, editEventDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..f96342df51c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,54 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.FilterCommand.CONTACTS_NOT_FILTERED; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.ParserUtil.parseTagsForUse; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.commands.FilterCommand.PersonFilter; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + + PersonFilter personFilter = new PersonFilter(); + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + personFilter.setName(argMultimap.getValue(PREFIX_NAME).get()); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + personFilter.setPhone(argMultimap.getValue(PREFIX_PHONE).get()); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + personFilter.setEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + personFilter.setAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + } + parseTagsForUse(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personFilter::setTags); + + if (!personFilter.isAnyFieldEdited()) { + throw new ParseException(CONTACTS_NOT_FILTERED); + } + + return new FilterCommand(personFilter); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..cf2763d0c57 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,14 +1,25 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.exceptions.RuntimeParseException; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.InvalidEventPeriodException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -19,9 +30,13 @@ * Contains utility methods used for parsing strings in the various *Parser classes. */ public class ParserUtil { - + public static final String INVALID_INTEGER = "0"; public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - + public static final String MESSAGE_INDEX_TOO_LARGE = "Index given is more than maximum integer of 2147483647."; + public static final String NUMBER_INDEX_INVALID_INDEX = "Number of arguments provided is invalid."; + public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm"); + private static final String SPLIT_SPACE_DELIMITER = " "; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. @@ -29,12 +44,56 @@ public class ParserUtil { */ public static Index parseIndex(String oneBasedIndex) throws ParseException { String trimmedIndex = oneBasedIndex.trim(); + String allNumbersRegex = "\\d+"; if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); + throw trimmedIndex.matches(allNumbersRegex) && !trimmedIndex.equals(INVALID_INTEGER) + ? new ParseException(MESSAGE_INDEX_TOO_LARGE) + : new ParseException(MESSAGE_INVALID_INDEX); } return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses given string and returns an array consisting of 2 Indexes. Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static ArrayList parseDualIndexes(String oneBasedIndex) throws ParseException { + final int maxParseIndexes = 2; + + ArrayList arrayList = new ArrayList<>(); + String[] indexArray = oneBasedIndex.split(SPLIT_SPACE_DELIMITER); + + if (indexArray.length != maxParseIndexes) { + throw new ParseException(NUMBER_INDEX_INVALID_INDEX); + } + + String trimmedIndex1 = indexArray[0].trim(); + String trimmedIndex2 = indexArray[1].trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex1) + || !StringUtil.isNonZeroUnsignedInteger(trimmedIndex2)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + arrayList.add(Index.fromOneBased(Integer.parseInt(trimmedIndex1))); + arrayList.add(Index.fromOneBased(Integer.parseInt(trimmedIndex2))); + return arrayList; + } + + /** + * Wrapped version of parseIndex that throws a RuntimeException instead. + * + * @param oneBasedIndex index integer to be converted into Index object. + * @return Index object that is parsed from the input index integer. + * @throws RuntimeException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndexSafe(String oneBasedIndex) throws RuntimeParseException { + try { + return parseIndex(oneBasedIndex); + } catch (ParseException pe) { + throw new RuntimeParseException(); + } + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +180,91 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + public static Optional> parseTagsForUse(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + + /** + * Parses the event description String into a EventDescription object. + * + * @param description description String. + * @return EventDescription object with the given description String. + * @throws ParseException if the description is empty. + */ + public static EventDescription parseEventDescription(String description) throws ParseException { + requireNonNull(description); + String trimmedDescription = description.trim(); + if (!EventDescription.isValid(description)) { + throw new ParseException(EventDescription.MESSAGE_CONSTRAINTS); + } + return new EventDescription(trimmedDescription); + } + + /** + * Parses given start date string and end date string into an EventPeriod object. + * + * @param startDate start date string in 'yyyy-MM-dd HH:mm' format. + * @param endDate end date string in 'yyyy-MM-dd HH:mm' format. + * @return EventPeriod object describing the time period between startDate and endDate. + * @throws ParseException if the startDate or endDate strings are in improper format. + */ + public static EventPeriod parseEventPeriod(String startDate, String endDate) throws ParseException { + requireAllNonNull(startDate, endDate); + String trimmedStartDate = startDate.trim(); + String trimmedEndDate = endDate.trim(); + try { + EventPeriod.isValidPeriod(trimmedStartDate, trimmedEndDate); + } catch (DateTimeParseException dateTimeParseException) { + throw new ParseException(EventPeriod.MESSAGE_CONSTRAINTS); + } catch (InvalidEventPeriodException invalidEventPeriodException) { + throw new ParseException(EventPeriod.PERIOD_INVALID); + } + return new EventPeriod(trimmedStartDate, trimmedEndDate); + } + + /** + * Parses given DateTime String into a LocalDateTime object. + * + * @param dateTime a date time string in 'yyyy-MM-dd HH:mm' format. + * @return LocalDateTime object describing the time and date. + * @throws ParseException if the string is not in the stated format. + */ + public static LocalDateTime parseDateTime(String dateTime) throws ParseException { + requireNonNull(dateTime); + String trimmedDateTime = dateTime.trim(); + return LocalDateTime.parse(trimmedDateTime, DATE_TIME_STRING_FORMATTER); + } + + /** + * Parses given DateTime String into a LocalDateTime object. + * + * @param dateTime a date time string in 'yyyy-MM-dd HH:mm' format. + * @return LocalDateTime object describing the time and date. + */ + public static LocalDateTime parseDateTimeNonNull(String dateTime) { + String trimmedDateTime = dateTime.trim(); + return LocalDateTime.parse(trimmedDateTime, DATE_TIME_STRING_FORMATTER); + } + + /** + * Parses given LocalDateTime into a String object. + * + * @param dateTime a LocalDateTime object. + * @return String object describing the time and date in the format of 'yyyy-MM-dd HH:mm'. + */ + public static String parseDateTimeToString(LocalDateTime dateTime) { + return dateTime.format(DATE_TIME_STRING_FORMATTER); + } } diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..73ed9bc1158 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,103 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.comparer.AddressComparator; +import seedu.address.model.person.comparer.EmailComparator; +import seedu.address.model.person.comparer.NameComparator; +import seedu.address.model.person.comparer.PhoneComparator; +import seedu.address.model.person.comparer.SortComparator; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + + public static final String PARSE_EXCEPTION_MESSAGE = "Invalid syntax for sort. Please use the " + + "following syntax: sort {/byattribute} {/reverse (optional)}. Example: sort /byname /reverse." + + " Possible attributes: /byname, /byemail, /byaddress, /byphone "; + private static final Prefix PREFIX_DELIMITER = new Prefix("/"); + private static final String SORT_BY_ADDRESS_KEYWORD = "byaddress"; + private static final String SORT_BY_EMAIL_KEYWORD = "byemail"; + private static final String SORT_BY_NAME_KEYWORD = "byname"; + private static final String SORT_BY_PHONE_KEYWORD = "byphone"; + private static final String REVERSE_KEYWORD = "reverse"; + private static final int MAX_SECONDARY_SORT = 0; + private static final int MAX_SORTS = MAX_SECONDARY_SORT + 1; + private static final int MAX_SORT_ARGS = 2 + MAX_SECONDARY_SORT * 2; + /** + * Parses the given {@code String} of arguments in the context of the SortCommand + * and returns an SortCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SortCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DELIMITER); + argMultimap.verifyNoDuplicatePrefixesFor(); + + List sortValues = argMultimap.getAllValues(PREFIX_DELIMITER); + + ArrayList sortComparatorList = new ArrayList<>(); + boolean isEmptyArray = sortValues.isEmpty(); + boolean isTooManyArgs = sortValues.size() > MAX_SORT_ARGS; + if (isEmptyArray || isTooManyArgs) { + throw new ParseException(PARSE_EXCEPTION_MESSAGE); + } + + for (String command: sortValues) { + String commandLower = command.toLowerCase(); + SortComparator sortComparator; + + if (!commandLower.equals(REVERSE_KEYWORD) && sortComparatorList.size() > MAX_SECONDARY_SORT) { + throw new ParseException(PARSE_EXCEPTION_MESSAGE); + } + + switch (commandLower) { + case SORT_BY_ADDRESS_KEYWORD: + sortComparator = new AddressComparator(true, false, 1); + sortComparatorList.add(sortComparator); + break; + + case SORT_BY_EMAIL_KEYWORD: + sortComparator = new EmailComparator(true, false, 1); + sortComparatorList.add(sortComparator); + break; + + case SORT_BY_NAME_KEYWORD: + sortComparator = new NameComparator(true, false, 1); + sortComparatorList.add(sortComparator); + break; + + case SORT_BY_PHONE_KEYWORD: + sortComparator = new PhoneComparator(true, false, 1); + sortComparatorList.add(sortComparator); + break; + + case REVERSE_KEYWORD: + if (sortComparatorList.isEmpty()) { + throw new ParseException(PARSE_EXCEPTION_MESSAGE); + } + int lastIdx = sortComparatorList.size() - 1; + sortComparatorList.get(lastIdx).setIsReverse(true); + break; + + default: + throw new ParseException(PARSE_EXCEPTION_MESSAGE); + } + } + + return new SortCommand(sortComparatorList); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java b/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java new file mode 100644 index 00000000000..d0b188f53a3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.SortTasksCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SortTasksCommand object. + */ +public class SortTasksCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SortTasksCommand + * and returns a SortTasksCommand object for execution + * @throws ParseException if no parameters are provided. + */ + public SortTasksCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortTasksCommand.MESSAGE_USAGE)); + } + return new SortTasksCommand(trimmedArgs); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/UniMateParser.java similarity index 52% rename from src/main/java/seedu/address/logic/parser/AddressBookParser.java rename to src/main/java/seedu/address/logic/parser/UniMateParser.java index 3149ee07e0b..befac266cf8 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/UniMateParser.java @@ -9,26 +9,41 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddContactEventCommand; +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.logic.commands.AddTaskCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.ClearEventsCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CompareCalendarByIndexCommand; +import seedu.address.logic.commands.CompareCalendarByTagCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteContactEventCommand; +import seedu.address.logic.commands.DeleteEventCommand; +import seedu.address.logic.commands.DeleteTaskCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditContactEventCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.SortTasksCommand; +import seedu.address.logic.commands.SwitchListCommand; +import seedu.address.logic.commands.ViewContactEventsCommand; import seedu.address.logic.parser.exceptions.ParseException; /** - * Parses user input. + * Parses user input for UniMate. */ -public class AddressBookParser { +public class UniMateParser { /** * Used for initial separation of command word and args. */ private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - private static final Logger logger = LogsCenter.getLogger(AddressBookParser.class); + private static final Logger logger = LogsCenter.getLogger(UniMateParser.class); /** * Parses user input into command for execution. @@ -77,10 +92,54 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case AddEventCommand.COMMAND_WORD: + return new AddEventCommandParser().parse(arguments); + + case DeleteEventCommand.COMMAND_WORD: + return new DeleteEventCommandParser().parse(arguments); + + case ClearEventsCommand.COMMAND_WORD: + return new ClearEventsCommandParser().parse(arguments); + + case AddContactEventCommand.COMMAND_WORD: + return new AddContactEventCommandParser().parse(arguments); + + case EditContactEventCommand.COMMAND_WORD: + return new EditContactEventCommandParser().parse(arguments); + + case DeleteContactEventCommand.COMMAND_WORD: + return new DeleteContactEventCommandParser().parse(arguments); + + case AddTaskCommand.COMMAND_WORD: + return new AddTaskCommandParser().parse(arguments); + + case SwitchListCommand.COMMAND_WORD: + return new SwitchListCommand(); + + case SortTasksCommand.COMMAND_WORD: + return new SortTasksCommandParser().parse(arguments); + + case DeleteTaskCommand.COMMAND_WORD: + return new DeleteTaskCommandParser().parse(arguments); + + case ViewContactEventsCommand.COMMAND_WORD: + return new ViewContactEventsCommandParser().parse(arguments); + + case CompareCalendarByIndexCommand.COMMAND_WORD: + return new CompareCalendarByIndexCommandParser().parse(arguments); + + case CompareCalendarByTagCommand.COMMAND_WORD: + return new CompareCalendarByTagCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } } - } diff --git a/src/main/java/seedu/address/logic/parser/ViewContactEventsCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewContactEventsCommandParser.java new file mode 100644 index 00000000000..266a41a301f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewContactEventsCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewContactEventsCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewContactEventsCommand object. + */ +public class ViewContactEventsCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewContactEventsCommand + * and returns a ViewContactEventsCommand object for execution. + * @throws ParseException if the user input does not conform the expected format. + */ + public ViewContactEventsCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ViewContactEventsCommand(index); + } catch (ParseException pe) { + if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) { + throw new ParseException(MESSAGE_INTEGER_OVERFLOW); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewContactEventsCommand.MESSAGE_USAGE), pe); + } + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java b/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java new file mode 100644 index 00000000000..11f1159723d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java @@ -0,0 +1,7 @@ +package seedu.address.logic.parser.exceptions; + +/** + * Thrown when there is a ParseException caught at runtime. + */ +public class RuntimeParseException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..40c029c2986 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; @@ -108,6 +109,14 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + /** + * Sort persons in Person List according to comparator provided. + * @param comparator + */ + public void sortPersons(Comparator comparator) { + persons.sortPersons(comparator); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..6d29af3274d 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,20 @@ package seedu.address.model; import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; /** * The API of the Model component. @@ -52,6 +61,36 @@ public interface Model { /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + /** + * Returns the user prefs' calendar file path. + */ + Path getCalendarFilePath(); + + /** + * Sets the user prefs' calendar file path. + */ + void setCalendarFilePath(Path calendarFilePath); + + /** + * Replaces calendar data with the data in {@code calendar}. + */ + void setCalendar(ReadOnlyCalendar calendar); + + /** + * Returns the Calendar + */ + ReadOnlyCalendar getCalendar(); + + /** + * Sets the user prefs' task manager file path. + */ + void setTaskManagerFilePath(Path taskManagerFilePath); + + /** + * Returns the user prefs' task manager file path. + */ + Path getTaskManagerFilePath(); + /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -76,12 +115,111 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** + * Check if calendar can add event + * + * @param event event to be added + * @return true if possible, false otherwise + */ + boolean canAddEvent(Event event); + + /** + * Adds an event into the calendar + * + * @param event event to be added + */ + void addEvent(Event event); + + /** + * Deletes an event at the specified time. + * + * @param dateTime The specified time. + * @throws EventNotFoundException if there is no event found at the specified time. + */ + void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException; + + /** + * Checks for an event at the specified time. + * + * @param dateTime the specified time. + * @return the Event + * @throws EventNotFoundException if there is no event at the specified time. + */ + Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException; + + /** + * Looks for all events within a time range and returns them in a list. + * + * @param range an {@code EventPeriod} representing a time range. + * @return A list that contains all events within the time range. + */ + List eventsInRange(EventPeriod range); + + /** + * Looks for all events within a time range and deletes them. + * + * @param range an {@code EventPeriod} representing a time range. + */ + void deleteEventsInRange(EventPeriod range); + + + /** + * Get the calendar resulting from the calendar comparison operation. + */ + ReadOnlyCalendar getComparisonCalendar(); + + /** + * Set the event list after comparison of calendars. + * + * @param eventList event list generated from comparison of calendars. + */ + void setComparisonCalendar(ReadOnlyCalendar eventList); + + /** Returns a view of the event list */ + ObservableList getEventList(); + + /** Returns a view of the task list */ + ObservableList getTaskList(); + + /** Returns a view of the event list for the current week */ + ObservableList getCurrentWeekEventList(); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** + * Adds a task to the TaskManager. + * @param task the task to be added. + */ + void addTask(Task task); + + /** + * Deletes a task from the TaskManager. + * @param index the displayed index of the task to delete + * @return the deleted task + */ + Task deleteTask(int index); + + /** + * Returns the TaskManager. + */ + TaskManager getTaskManager(); + + /** + * Sorts the task list by the preset comparator type listed. + */ + void sortTasksBy(String comparatorType); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Sorts the filtered person list by the given {@code comparator}. + * @throws NullPointerException if {@code comparator} is null. + */ + void sortPersonList(Comparator personComparator); } + diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..9d2b9705dc6 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,10 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,7 +15,15 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; import seedu.address.model.person.Person; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; /** * Represents the in-memory model of the address book data. @@ -20,24 +32,31 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final AddressBook addressBook; + private final UniMateCalendar calendar; + private final TaskManager taskManager; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private ReadOnlyCalendar comparisonCalendar; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a ModelManager with the given addressBook, calendar and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyCalendar calendar, ReadOnlyTaskManager taskManager, + ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); + this.calendar = new UniMateCalendar(calendar); + this.taskManager = new TaskManager(taskManager); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + comparisonCalendar = new UniMateCalendar(); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new AddressBook(), new UniMateCalendar(), new TaskManager(), new UserPrefs()); } //=========== UserPrefs ================================================================================== @@ -75,6 +94,29 @@ public void setAddressBookFilePath(Path addressBookFilePath) { userPrefs.setAddressBookFilePath(addressBookFilePath); } + @Override + public Path getCalendarFilePath() { + return userPrefs.getCalendarFilePath(); + } + + @Override + public void setCalendarFilePath(Path calendarFilePath) { + requireNonNull(calendarFilePath); + userPrefs.setCalendarFilePath(calendarFilePath); + } + + @Override + public Path getTaskManagerFilePath() { + return userPrefs.getTaskManagerFilePath(); + } + + @Override + public void setTaskManagerFilePath(Path taskManagerFilePath) { + requireNonNull(taskManagerFilePath); + userPrefs.setTaskManagerFilePath(taskManagerFilePath); + } + + //=========== AddressBook ================================================================================ @Override @@ -111,6 +153,118 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + //=========== Sorted Person List Accessors =============================================================== + @Override + public void sortPersonList(Comparator personComparator) { + addressBook.sortPersons(personComparator); + } + + //=========== Calendar =================================================================================== + + @Override + public void setCalendar(ReadOnlyCalendar calendar) { + this.calendar.resetData(calendar); + } + + @Override + public ReadOnlyCalendar getCalendar() { + return calendar; + } + @Override + public ObservableList getEventList() { + return calendar.getEventList(); + } + + @Override + public ObservableList getCurrentWeekEventList() { + return calendar.getCurrentWeekEventList(); + } + + @Override + public boolean canAddEvent(Event event) { + return calendar.canAddEvent(event); + } + + @Override + public void addEvent(Event event) { + requireNonNull(event); + + calendar.addEvent(event); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + requireNonNull(dateTime); + + calendar.deleteEventAt(dateTime); + } + + @Override + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + requireNonNull(dateTime); + + try { + return calendar.findEventAt(dateTime).orElseThrow(); + } catch (NoSuchElementException e) { + throw new EventNotFoundException(); + } + } + + @Override + public List eventsInRange(EventPeriod range) { + requireNonNull(range); + + return calendar.getEventsInRange(range); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + requireNonNull(range); + + calendar.deleteEventsInRange(range); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + return comparisonCalendar; + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar comparisonCalendar) { + requireNonNull(comparisonCalendar); + + this.comparisonCalendar = comparisonCalendar; + } + + //=========== TaskManager ================================================================================ + + @Override + public TaskManager getTaskManager() { + return taskManager; + } + + @Override + public void addTask(Task task) { + requireNonNull(task); + + taskManager.addTask(task); + } + + @Override + public Task deleteTask(int index) { + return taskManager.deleteTask(index); + } + + @Override + public ObservableList getTaskList() { + return taskManager.getTaskList(); + } + + @Override + public void sortTasksBy(String comparatorType) { + taskManager.sortTasksBy(comparatorType); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -141,8 +295,9 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) + && calendar.equals(otherModelManager.calendar) + && taskManager.equals(otherModelManager.taskManager) && userPrefs.equals(otherModelManager.userPrefs) && filteredPersons.equals(otherModelManager.filteredPersons); } - } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..47cdf900702 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -15,6 +15,8 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path calendarFilePath = Paths.get("data", "calendar.json"); + private Path taskManagerFilePath = Paths.get("data", "taskmanager.json"); /** * Creates a {@code UserPrefs} with default values. @@ -56,6 +58,24 @@ public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } + public Path getCalendarFilePath() { + return calendarFilePath; + } + + public void setCalendarFilePath(Path calendarFilePath) { + requireNonNull(calendarFilePath); + this.calendarFilePath = calendarFilePath; + } + + public Path getTaskManagerFilePath() { + return taskManagerFilePath; + } + + public void setTaskManagerFilePath(Path taskManagerFilePath) { + requireNonNull(taskManagerFilePath); + this.taskManagerFilePath = taskManagerFilePath; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java b/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java new file mode 100644 index 00000000000..ecc0051d94c --- /dev/null +++ b/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java @@ -0,0 +1,48 @@ +package seedu.address.model.calendar; + +import java.time.LocalTime; +import java.util.Optional; + +import javafx.collections.ObservableList; +import seedu.address.model.event.Event; + +/** + * Unmodifiable view of the Calendar. + */ +public interface ReadOnlyCalendar { + /** + * Generates an unmodifiable view of the event list. + * + * @return unmodifiable view of event list. + */ + ObservableList getEventList(); + + /** + * Generates an unmodifiable view of the event list for the current week. + * + * @return unmodifiable view of event list for the current week. + */ + ObservableList getCurrentWeekEventList(); + + /** + * Retrieves the earliest starting time of any event for the current week in a LocalTime object. + * + * @return the earliest starting time of any event for the current week in a LocalTime object. + */ + Optional getEarliestEventStartTimeInCurrentWeek(); + + /** + * Retrieves the latest ending time of any event for the current week in a LocalTime object. + * + * @return the latest ending time of any event for the current week in a LocalTime object. + */ + Optional getLatestEventEndTimeInCurrentWeek(); + + /** + * Combine this calendar with another calendar, disregarding conflicts in events. + * + * @param other other calendar to be combined. + * @return new calendar with events from both calendars. + */ + ReadOnlyCalendar getCombinedCalendar(ReadOnlyCalendar other); +} diff --git a/src/main/java/seedu/address/model/calendar/UniMateCalendar.java b/src/main/java/seedu/address/model/calendar/UniMateCalendar.java new file mode 100644 index 00000000000..7a58d9b336b --- /dev/null +++ b/src/main/java/seedu/address/model/calendar/UniMateCalendar.java @@ -0,0 +1,236 @@ +package seedu.address.model.calendar; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.AllDaysEventListManager; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; + +/** + * Represents a calendar that stores and manages events. + */ +public class UniMateCalendar implements ReadOnlyCalendar { + private static final int DAYS_IN_WEEK = 7; + private static final LocalDate DATE_OF_START_OF_CURRENT_WEEK = LocalDate.now().minusDays( + LocalDate.now().getDayOfWeek().getValue() - 1); + private static final LocalDate DATE_OF_END_OF_CURRENT_WEEK = LocalDate.now().plusDays( + DAYS_IN_WEEK - LocalDate.now().getDayOfWeek().getValue()); + private final AllDaysEventListManager eventManager; + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalListForCurrentWeek = FXCollections.observableArrayList(); + + /** + * Constructs a Calendar object with an empty event tree. + */ + public UniMateCalendar() { + this.eventManager = new AllDaysEventListManager(); + } + + /** + * Creates a Calendar using the Events in the {@code toBeCopied} + */ + public UniMateCalendar(ReadOnlyCalendar toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// overwrite operations + + /** + * Replaces the contents of the Calendar with {@code events}. + * {@code events} must not contain duplicate events. + */ + public void setEvents(List events) { + events.forEach(this::addEvent); + } + + /** + * Resets the existing data of this {@code Calendar} with {@code newData}. + */ + public void resetData(ReadOnlyCalendar newData) { + requireNonNull(newData); + + setEvents(newData.getEventList()); + } + + /** + * Return the AllDaysEventListManager managing the events for this calendar. + * + * @return the AllDaysEventListManager managing the events. + */ + public AllDaysEventListManager getEventManager() { + return this.eventManager; + } + + /** + * Check if the event can be added to the calendar + * + * @param event event to be checked. + * @return true if it can be added, false otherwise. + */ + public boolean canAddEvent(Event event) { + return eventManager.canAddEvent(event); + } + + /** + * Adds an event to the calendar. + * + * @param event The event to be added. + */ + public void addEvent(Event event) { + requireNonNull(event); + eventManager.addEvent(event); + internalList.setAll(eventManager.asUnmodifiableObservableList()); + internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList( + DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK)); + } + + /** + * Force add the event into the event manager. + * + * @param event event to be added. + */ + private void forceAddEvent(Event event) { + requireNonNull(event); + eventManager.forceAddEvent(event); + internalList.setAll(eventManager.asUnmodifiableObservableList()); + internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList( + DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK)); + } + + /** + * Checks if there is an event at specified time and deletes it if there is. + * @param dateTime the specified time + */ + public void deleteEventAt(LocalDateTime dateTime) { + requireNonNull(dateTime); + eventManager.deleteEventAt(dateTime); + internalList.setAll(eventManager.asUnmodifiableObservableList()); + internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList( + DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK)); + } + + /** + * Looks for an event at specified time. + * + * @param dateTime the specified time. + * @return an optional containing the event if there is an event at the time, an empty optional otherwise. + */ + public Optional findEventAt(LocalDateTime dateTime) { + requireNonNull(dateTime); + return eventManager.eventAt(dateTime); + } + + /** + * Looks for all events within a specified time range and returns them in a list. + * + * @param range the specified time range represented by an {@code EventPeriod} + * @return a list containing all events within the range. + */ + public List getEventsInRange(EventPeriod range) { + requireNonNull(range); + return eventManager.eventsInRange(range); + } + + /** + * Looks for all events within a specified time range and deletes them. + * + * @param range the specified time range represented by an {@code EventPeriod} + */ + public void deleteEventsInRange(EventPeriod range) { + requireNonNull(range); + for (Event event:getEventsInRange(range)) { + deleteEventAt(event.getStartDateTime()); + } + } + + /** + * Clears the calendar + */ + public void clear() { + this.eventManager.clear(); + } + + /** + * Check if the calendar contains the event. + * + * @param event event to be checked. + * @return true if it is contained in the calendar, false otherwise. + */ + public boolean contains(Event event) { + return this.eventManager.contains(event); + } + + /** + * Checks if the calendar is empty. + * + * @return true if calendar is empty, false otherwise. + */ + public boolean isEmpty() { + return this.eventManager.isEmpty(); + } + + /** + * Checks if there are any events at all in the calendar. + * + * @return true if there are events in the calendar, false otherwise. + */ + public boolean hasEvents() { + if (!this.isEmpty()) { + return eventManager.hasEvents(); + } + return false; + } + + @Override + public ReadOnlyCalendar getCombinedCalendar(ReadOnlyCalendar other) { + requireNonNull(other); + + UniMateCalendar combinedCalendar = new UniMateCalendar(); + Stream.concat(internalList.stream(), other.getEventList().stream()).forEach(combinedCalendar::forceAddEvent); + return combinedCalendar; + } + + @Override + public ObservableList getEventList() { + return internalList; + } + + @Override + public ObservableList getCurrentWeekEventList() { + return internalListForCurrentWeek; + } + + @Override + public Optional getEarliestEventStartTimeInCurrentWeek() { + return getCurrentWeekEventList().stream().min(Event::compareStartTime).map(Event::getStartTime); + } + + @Override + public Optional getLatestEventEndTimeInCurrentWeek() { + return getCurrentWeekEventList().stream().max(Event::compareEndTime).map(Event::getEndTime); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof UniMateCalendar)) { + return false; + } + + UniMateCalendar otherCalendar = (UniMateCalendar) other; + return this.eventManager.equals(otherCalendar.eventManager); + } +} diff --git a/src/main/java/seedu/address/model/event/AllDaysEventListManager.java b/src/main/java/seedu/address/model/event/AllDaysEventListManager.java new file mode 100644 index 00000000000..04517578f4e --- /dev/null +++ b/src/main/java/seedu/address/model/event/AllDaysEventListManager.java @@ -0,0 +1,232 @@ +package seedu.address.model.event; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.NavigableMap; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.exceptions.EventNotFoundException; + +/** + * Manages the SingleDayEventList objects for every day. + */ +public class AllDaysEventListManager { + private final NavigableMap dayToEventListMap; + + /** + * Creates a AllDaysEventListManager to manage the daily events. + */ + public AllDaysEventListManager() { + this.dayToEventListMap = new TreeMap(); + } + + /** + * Check if the event is conflicting with any events stored in the manager. + * + * @param event event to be added. + * @return true if the event can be added, false otherwise. + */ + public boolean canAddEvent(Event event) { + List eventDays = event.getEventDays(); + return eventDays.stream().filter(x -> dayToEventListMap.containsKey(x.toString())) + .allMatch(x -> dayToEventListMap.get(x.toString()).isEventAddValid(event)); + } + + /** + * Stores the event to the appropriate SingleDayEventList objects corresponding to the event's dates. + * + * @param event event to be added. + */ + public void addEvent(Event event) { + List eventDays = event.getEventDays(); + for (LocalDate date : eventDays) { + if (!(dayToEventListMap.containsKey(date.toString()))) { + dayToEventListMap.put(date.toString(), new SingleDayEventList(date)); + } + dayToEventListMap.get(date.toString()).addEvent(event.boundEventByDate(date)); + } + } + + /** + * Forcibly add the event to the appropriate SingleDayEventList objects corresponding to the event's dates. + * + * @param event event to be added. + */ + public void forceAddEvent(Event event) { + List eventDays = event.getEventDays(); + for (LocalDate date : eventDays) { + if (!(dayToEventListMap.containsKey(date.toString()))) { + dayToEventListMap.put(date.toString(), new SingleDayEventList(date)); + } + dayToEventListMap.get(date.toString()).forceAddEvent(event.boundEventByDate(date)); + } + } + + /** + * Clears the manager. + */ + public void clear() { + this.dayToEventListMap.clear(); + } + + /** + * Check if the event is stored in this manager. + * + * @param event event to be checked. + * @return true if it is stored, false otherwise. + */ + public boolean contains(Event event) { + for (LocalDate date : event.getEventDays()) { + if (!(this.dayToEventListMap.getOrDefault(date.toString(), new SingleDayEventList(date)) + .containsEvent(event))) { + return false; + } + } + return true; + } + + /** + * Check if this manager has an equivalent SingleDayEventList object stored in its dayToEventListMap. + * + * @param day SingleDayEventList object to be checked. + * @return true if there is a equivalent SingleDayEventList object stored in this manager, false otherwise. + */ + private boolean contains(SingleDayEventList day) { + for (SingleDayEventList thisDay : this.dayToEventListMap.values()) { + if (thisDay.equals(day)) { + return true; + } + } + return false; + } + + /** + * Checks if there is an event at the specified time. + * + * @param dateTime the specified time. + * @return an optional containing the event at the specified time if there is an event, an empty optional otherwise. + */ + public Optional eventAt(LocalDateTime dateTime) { + String key = dateTime.toLocalDate().toString(); + if (dayToEventListMap.containsKey((key))) { + return dayToEventListMap.get(key).eventAtTime(dateTime); + } + return Optional.empty(); + } + + /** + * Looks for an event at specified time and deletes it if found. + * + * @param dateTime the specified time. + * @throws EventNotFoundException if no event is found. + */ + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + Optional optionalToDelete = eventAt(dateTime); + try { + Event toDelete = optionalToDelete.orElseThrow(); + List days = toDelete.getEventDays(); + days.stream().map(LocalDate::toString) + .forEach(x -> dayToEventListMap.get(x).remove(toDelete)); + } catch (NoSuchElementException e) { + throw new EventNotFoundException(); + } + } + + /** + * Looks for events within a specified time and returns the list of events. + * + * @param range the {@code EventPeriod} describing the time range to check. + * @return A list of events or an empty list if no events are within the range. + */ + public List eventsInRange(EventPeriod range) { + List days = range.getDates(); + return days.stream().map(LocalDate::toString) + .flatMap(x -> dayToEventListMap.containsKey(x) + ? dayToEventListMap.get(x).eventsInRange(range).stream() + : Stream.empty()) + .distinct() + .collect(Collectors.toList()); + } + + /** + * Checks if the manager is empty. + * + * @return true if the manager has no events stored, false otherwise. + */ + public boolean isEmpty() { + return this.dayToEventListMap.isEmpty(); + } + + /** + * Returns the event list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + List list = dayToEventListMap.values().stream() + .flatMap(singleDayEventList -> singleDayEventList.getDayEventList().stream()) + .map(Event::getParentEvent).distinct() + .collect(Collectors.toList()); + + return FXCollections.observableList(list); + } + + /** + * Returns the event list storing events that occur between the start and end dates as an unmodifiable + * ObservableList. + * + * @param startDate start date. + * @param endDate end date. + * @return the event list storing events that occur between the start and end dates as an unmodifiable + * ObservableList. + */ + public ObservableList asUnmodifiableObservableList(LocalDate startDate, LocalDate endDate) { + List list = dayToEventListMap.values().stream() + .flatMap(singleDayEventList -> singleDayEventList.getDayEventList().stream()) + .filter(event -> event.occursBetweenDates(startDate, endDate)) + .collect(Collectors.toList()); + + return FXCollections.observableList(list); + } + + /** + * Checks if there are any events at all in the manager. + * + * @return true if there are any events in the manager, false otherwise. + */ + public boolean hasEvents() { + if (!this.isEmpty()) { + return dayToEventListMap.values().stream().map(SingleDayEventList::isEmpty).anyMatch(x -> x.equals(false)); + } + return false; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AllDaysEventListManager)) { + return false; + } + + AllDaysEventListManager otherManager = (AllDaysEventListManager) other; + + if (!(this.dayToEventListMap.size() == otherManager.dayToEventListMap.size())) { + return false; + } + + for (SingleDayEventList day : this.dayToEventListMap.values()) { + if (!otherManager.contains(day)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/event/Event.java b/src/main/java/seedu/address/model/event/Event.java new file mode 100644 index 00000000000..dcf923ffcd1 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Event.java @@ -0,0 +1,314 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +/** + * Represents an event in the calendar + */ +public class Event { + private EventDescription description; + private EventPeriod eventPeriod; + private Optional parentEvent; + + /** + * Constructs an event with a given description occurring over the given time period. + * + * @param description The description of the event. + * @param eventPeriod The period of time when the event occurs. + */ + public Event(EventDescription description, EventPeriod eventPeriod) { + requireAllNonNull(description, eventPeriod); + + this.description = description; + this.eventPeriod = eventPeriod; + this.parentEvent = Optional.empty(); + } + + /** + * Constructs an event with a given description occurring over a given time period with a parent Event. + * + * @param description The description of the event. + * @param eventPeriod The period of the time when the event occurs. + * @param parent parent Event object this Event object was dervied from. + */ + private Event(EventDescription description, EventPeriod eventPeriod, Event parent) { + requireAllNonNull(description, eventPeriod, parent); + + this.description = description; + this.eventPeriod = eventPeriod; + this.parentEvent = Optional.of(parent); + } + + /** + * Creates and returns a new Event object that is guaranteed to not conflict with any other event. + * + * @return A new non-conflicting Event object. + */ + public static Event createNonConflictingEvent() { + return new Event(EventDescription.createUnusedDescription(), EventPeriod.createNonConflictingPeriod()); + } + + /** + * Gets the description of the event. + * + * @return The description of the event. + */ + public EventDescription getDescription() { + return this.description; + } + + /** + * Gets the time period during which the event occurs. + * + * @return The time period of the event. + */ + public EventPeriod getEventPeriod() { + return this.eventPeriod; + } + + /** + * Checks if this event overlaps with another event period + * + * @param period The other event period. + * @return True if there is an overlap, false otherwise. + */ + public boolean isPeriodConflicting(EventPeriod period) { + requireNonNull(period); + + return eventPeriod.isOverlapping(period); + } + + /** + * Checks if this event conflicts with another event. + * + * @param other The other event to check for conflicts with. + * @return True if there is a conflict, false otherwise. + */ + public boolean isConflicting(Event other) { + requireNonNull(other); + + return isPeriodConflicting(other.eventPeriod); + } + + /** + * Changes the description of the event. + * + * @param description The new description for the event. + */ + public void changeDescription(EventDescription description) { + requireNonNull(description); + + this.description = description; + } + + /** + * Changes the time period during which the event occurs. + * + * @param eventPeriod The new time period for the event. + */ + public void changeEventPeriod(EventPeriod eventPeriod) { + requireNonNull(eventPeriod); + + this.eventPeriod = eventPeriod; + } + + /** + * Get the dates the event spans, stored in a list. + * + * @return list of the dates the event spans. + */ + public List getEventDays() { + return this.eventPeriod.getDates(); + } + + /** + * Checks if the {@code @LocalDateTime} is during the event. + * + * @param dateTime the specified {@code @LocalDateTime}. + * @return true if the time is during the event, false otherwise. + */ + public boolean isDuring(LocalDateTime dateTime) { + requireNonNull(dateTime); + + return this.eventPeriod.isWithin(dateTime); + } + + /** + * Bounds the {@code @Event} such that the {@code @EventPeriod} of the formatted event happens within + * the specified {@code @LocalDate}. + * + * @param date the specified {@code @LocalDate}. + * @return new {@code @Event} with adjusted {@code @EventPeriod}. + */ + public Event boundEventByDate(LocalDate date) { + requireNonNull(date); + if (hasParent()) { + return this; + } + return new Event(description, eventPeriod.boundPeriodByDate(date), this); + } + + /** + * Get the parent Event object of this Event object. + * + * @return the parent Event object. If there is no parent, returns itself. + */ + public Event getParentEvent() { + return parentEvent.orElse(this); + } + + /** + * Checks if this Event object has a parent Event. + * + * @return true if it has a parent Event object, false otherwise. + */ + public boolean hasParent() { + return parentEvent.isPresent(); + } + + public LocalDateTime getStartDateTime() { + if (hasParent()) { + return parentEvent.get().getStartDateTime(); + } + return eventPeriod.getStart(); + } + + /** + * Checks if the Event's eventDuration spans a single day. + * + * @return true if it spans a single day, false otherwise. + */ + private boolean isEventDurationSingleDay() { + return eventPeriod.isSingleDay(); + } + + /** + * Checks if parent Event's EventPeriod overlaps with the start and end dates(inclusive). + * + * @param start start date. + * @param end end date. + * @return true if the parent Event's EventPeriod overlaps with the start and end date. + */ + public boolean occursBetweenDates(LocalDate start, LocalDate end) { + requireAllNonNull(start, end); + return eventPeriod.isOverlapping(start, end); + } + + /** + * Compare the start time (independent of date) of this Event with another. + * + * @param other other Event. + * @return a negative integer if this Event has an earlier start time than the other, 0 if they have the same start + * time and a positive integer otherwise. + */ + public int compareStartTime(Event other) { + requireNonNull(other); + + return this.eventPeriod.compareStartTime(other.eventPeriod); + } + + /** + * Compare the end time (independent of date) of this Event with another. + * + * @param other other Event. + * @return a negative integer if this Event has an earlier end time than the other, 0 if they have the same end time + * and a positive integer otherwise. + */ + public int compareEndTime(Event other) { + requireNonNull(other); + + return this.eventPeriod.compareEndTime(other.eventPeriod); + } + + /** + * Get the start time of the event as a LocalTime object, omitting the date. + * + * @return the start time of the event as a LocalTime object, omitting the date. + */ + public LocalTime getStartTime() { + return eventPeriod.getStartTime(); + } + + /** + * Get the end time of the event as a LocalTime object, omitting the date. + * + * @return the end time of the event as a LocalTime object, omitting the date. + */ + public LocalTime getEndTime() { + return eventPeriod.getEndTime(); + } + + /** + * Get the duration of the Event, stored in a LocalTime object. + * + * @return LocalTime object of the duration of the Event. + */ + public Duration getDurationOfEvent() { + return eventPeriod.getDuration(); + } + + /** + * Get the description of the Event in a String format. + * + * @return String of the description of the Event. + */ + public String getDescriptionString() { + return description.getDescription(); + } + + /** + * Gets the time period during which the event occurs in a String format. + * + * @return String of the time period of the event. + */ + public String getEventPeriodString() { + return eventPeriod.getFormattedPeriod(); + } + + /** + * Get the DayOfWeek of the Event. Event has to have a EventPeriod that spans a single day. + * + * @return DayOfWeek of the Event. + */ + public DayOfWeek getDayOfWeek() { + assert(isEventDurationSingleDay()); + + return eventPeriod.getDayOfWeek(); + } + + /** + * Get the number of minutes elapsed from the input time to the start time of the Event. + * + * @param time input time. + * @return number of minutes elapsed from the input time to the start time of the Event. + */ + public long getMinutesFromTimeToStartTime(LocalTime time) { + requireNonNull(time); + return eventPeriod.getMinutesFromTimeToStartTime(time); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Event)) { + return false; + } + + Event otherEvent = (Event) other; + return (otherEvent.description.equals(this.description) + && (otherEvent.eventPeriod.equals(this.eventPeriod) + || otherEvent.eventPeriod.isContinuous(this.eventPeriod))); + } +} diff --git a/src/main/java/seedu/address/model/event/EventDescription.java b/src/main/java/seedu/address/model/event/EventDescription.java new file mode 100644 index 00000000000..c8ede35c8d3 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventDescription.java @@ -0,0 +1,74 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.event.exceptions.InvalidDescriptionException; + +/** + * Represents the description of an event. + */ +public class EventDescription { + public static final String MESSAGE_CONSTRAINTS = "Description cannot be empty."; + private final String description; + + /** + * Constructs an EventDescription object with the given description. + * + * @param description The description of the event. + */ + public EventDescription(String description) { + requireNonNull(description); + if (description.isEmpty()) { + throw new InvalidDescriptionException(); + } + this.description = description; + } + + /** + * Creates and returns a new EventDescription object with a placeholder description. + * + * @return A new EventDescription object with a placeholder description. + */ + public static EventDescription createUnusedDescription() { + return new EventDescription("THIS IS A PLACEHOLDER"); + } + + /** + * Checks if the given string is a valid description for creating a EventDescription object. + * + * @param description description String to be checked. + * @return true if description is non-empty, false if it is empty. + */ + public static boolean isValid(String description) { + requireNonNull(description); + return !description.isEmpty(); + } + + /** + * Retrieve the underlying String of the description. + * + * @return String of the description. + */ + public String getDescription() { + return this.description; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EventDescription)) { + return false; + } + + EventDescription otherDescription = (EventDescription) other; + return otherDescription.description.equals(this.description); + } + + @Override + public String toString() { + return this.description; + } +} diff --git a/src/main/java/seedu/address/model/event/EventPeriod.java b/src/main/java/seedu/address/model/event/EventPeriod.java new file mode 100644 index 00000000000..99fcf192902 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventPeriod.java @@ -0,0 +1,344 @@ +package seedu.address.model.event; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.model.event.exceptions.DateOutOfBoundsException; +import seedu.address.model.event.exceptions.InvalidEventPeriodException; + +/** + * Represents a period in time when an event will occur. + */ +public class EventPeriod implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "The start date and time and end date and time should " + + "be in the format 'yyyy-MM-dd HH:mm' where:\n" + + " -'yyyy' is the year.\n" + + " -'MM' is the month.\n" + + " -'dd' is the day.\n" + + " -'HH:mm' is the time in 24-hour format."; + public static final String PERIOD_INVALID = "The start date has to be before the end date."; + public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm"); + public static final LocalTime MAX_TIME_OF_DAY = LocalTime.MIDNIGHT.minusMinutes(1); + + private final LocalDateTime start; + private final LocalDateTime end; + + /** + * Constructs an EventPeriod object with the given start and end date/time strings. + * + * @param startString The string representation of the start date and time. + * @param endString The string representation of the end date and time. + */ + public EventPeriod(String startString, String endString) { + this(LocalDateTime.parse(startString, DATE_TIME_STRING_FORMATTER), + LocalDateTime.parse(endString, DATE_TIME_STRING_FORMATTER)); + } + + /** + * Constructs an EventPeriod object with the given start and end date/time LocalDateTime. + * + * @param start The LocalDateTime representation of the start date and time. + * @param end The LocalDateTime representation of the end date and time. + */ + private EventPeriod(LocalDateTime start, LocalDateTime end) { + this.start = start; + this.end = end; + } + + public LocalDateTime getStart() { + return this.start; + } + + public LocalDateTime getEnd() { + return this.end; + } + + /** + * Creates and returns a new EventPeriod object with minimum date/time values. + * + * @return A new EventPeriod object with minimum date/time values. + */ + public static EventPeriod createNonConflictingPeriod() { + return new EventPeriod(LocalDateTime.MAX, LocalDateTime.MIN); + } + + /** + * Checks if the given start and end date/time strings form a valid period. + * + * @param startString The string representation of the start date and time. + * @param endString The string representation of the end date and time. + * @return True if the period is valid, false otherwise. + */ + public static boolean isValidPeriod(String startString, String endString) { + requireAllNonNull(startString, endString); + + LocalDateTime startDateTime; + LocalDateTime endDateTime; + startDateTime = LocalDateTime.parse(startString, DATE_TIME_STRING_FORMATTER); + endDateTime = LocalDateTime.parse(endString, DATE_TIME_STRING_FORMATTER); + + if (!startDateTime.isBefore(endDateTime)) { + throw new InvalidEventPeriodException(PERIOD_INVALID); + } + return true; + } + + /** + * Checks if this EventPeriod overlaps with another EventPeriod. + * + * @param other The EventPeriod to check for overlap with. + * @return True if there is an overlap, false otherwise. + */ + public boolean isOverlapping(EventPeriod other) { + requireNonNull(other); + + return this.start.isBefore(other.end) && other.start.isBefore(this.end); + } + + /** + * Checks if this EventPeriod overlaps with the given start date and end date. + * + * @param start start date. + * @param end end date. + * @return True if there is an overlap, false otherwise. + */ + public boolean isOverlapping(LocalDate start, LocalDate end) { + requireAllNonNull(start, end); + + boolean isThisEventStartingAfterOtherEventEnds = this.start.toLocalDate().isAfter(end); + boolean isThisEventEndedBeforeOtherEventStarts = this.end.toLocalDate().isBefore(start); + return !(isThisEventStartingAfterOtherEventEnds || isThisEventEndedBeforeOtherEventStarts); + } + + /** + * Checks if a specified {@code @LocalDateTime} is within the {@code @EventPeriod}. + * + * @param dateTime the specified {@code @LocalDateTime}. + * @return True if it is within the period, false otherwise. + */ + public boolean isWithin(LocalDateTime dateTime) { + requireNonNull(dateTime); + return (start.isEqual(dateTime) || start.isBefore(dateTime)) && end.isAfter(dateTime); + } + + /** + * Compares this EventPeriod with another EventPeriod. + * + * @param other The EventPeriod to compare with. + * @return 1 if this EventPeriod is after the other, -1 if it's before, 0 if they are the same. + */ + @Override + public int compareTo(EventPeriod other) { + requireNonNull(other); + if (this.start.isBefore(other.start)) { + return -1; + } else if (this.start.isEqual(other.start)) { + return 0; + } else { + return 1; + } + } + + /** + * Get the dates the eventPeriod spans, stored in an arrayList. + * + * @return list of the dates the eventPeriod spans. + */ + public List getDates() { + LocalDate startDate = start.toLocalDate(); + LocalDate endDate = end.toLocalDate(); + return Stream.iterate(startDate, date -> !date.isAfter(endDate), date -> date.plusDays(1)) + .collect(Collectors.toList()); + } + + /** + * Get the string representation of the EventPeriod. + * + * @return string representation of the period the eventPeriod spans. + */ + public String getFormattedPeriod() { + String startString = start.format(DATE_TIME_STRING_FORMATTER); + String endString = end.format(DATE_TIME_STRING_FORMATTER); + return startString + " - " + endString; + } + + /** + * Get the string representation of the EventPeriod start DateTime. + * + * @return string representation of the start DateTime. + */ + public String getFormattedStart() { + return start.format(DATE_TIME_STRING_FORMATTER); + } + + /** + * Get the string representation of the EventPeriod end DateTime. + * + * @return string representation of the end DateTime. + */ + public String getFormattedEnd() { + return end.format(DATE_TIME_STRING_FORMATTER); + } + + /** + * Checks if another {@code @EventPeriod} occurs within a minute with this {@code @EventPeriod}. + * + * @param other the other {@code @EventPeriod}. + * @return true if they are apart by a single minute. + */ + public boolean isContinuous(EventPeriod other) { + requireNonNull(other); + + return this.start.plusMinutes(1) == other.end || other.start.plusMinutes(1) == this.end; + } + + /** + * Bounds the {@code EventPeriod} such that the start and end lies within the specified {@code LocalDate}. + * + * @param date the specified {@code @LocalDate}. + * @return new {@code @EventPeriod} with adjusted start and end times. + */ + public EventPeriod boundPeriodByDate(LocalDate date) { + requireNonNull(date); + + boolean isDateOnStartDate = date.isEqual(start.toLocalDate()); + boolean isDateOnEndDate = date.isEqual(end.toLocalDate()); + boolean isDateAfterStartDate = date.isAfter(start.toLocalDate()); + boolean isDateBeforeEndDate = date.isBefore(end.toLocalDate()); + + + if (isDateOnStartDate && isDateOnEndDate) { + return this; + } else if (isDateOnStartDate) { + return new EventPeriod(start, LocalDateTime.of(date, MAX_TIME_OF_DAY)); + } else if (isDateOnEndDate) { + return new EventPeriod(LocalDateTime.of(date, LocalTime.MIDNIGHT), end); + } else if (isDateAfterStartDate && isDateBeforeEndDate) { + return new EventPeriod(LocalDateTime.of(date, LocalTime.MIDNIGHT), + LocalDateTime.of(date, MAX_TIME_OF_DAY)); + } else { + throw new DateOutOfBoundsException(); + } + } + + /** + * Compare the start time (independent of date) of this EventPeriod with another. + * + * @param other other EventPeriod object. + * @return a negative integer if this EventPeriod has an earlier start time than the other, 0 if both have the same + * start time and a positive integer otherwise. + */ + public int compareStartTime(EventPeriod other) { + requireNonNull(other); + + return this.start.toLocalTime().compareTo(other.start.toLocalTime()); + } + + /** + * Compare the end time (independent of date) of this EventPeriod with another. + * + * @param other other EventPeriod object. + * @return a negative integer if this EventPeriod has an earlier end time than the other, 0 if both have the same + * end time and a positive integer otherwise. + */ + public int compareEndTime(EventPeriod other) { + requireNonNull(other); + + return this.end.toLocalTime().compareTo(other.end.toLocalTime()); + } + + /** + * Get the start time as a LocalTime of the EventPeriod, omitting the date. + * + * @return the start time as a LocalTime of the EventPeriod. + */ + public LocalTime getStartTime() { + return start.toLocalTime(); + } + + /** + * Get the end time as a LocalTime of the EventPeriod, omitting the date. + * + * @return the end time as a LocalTime of the EventPeriod. + */ + public LocalTime getEndTime() { + return end.toLocalTime(); + } + + /** + * Get the duration of the EventPeriod stored in a LocalTime. + * + * @return duration of the EventPeriod stored in a LocalTime. + */ + public Duration getDuration() { + Duration periodDuration = Duration.between(start, end); + if (end.toLocalTime().equals(MAX_TIME_OF_DAY)) { + periodDuration = periodDuration.plusMinutes(1); + } + return periodDuration; + } + + /** + * Checks if the start and end LocalDateTime objects occur on the same date. + * + * @return true if they both occur on the same date, false otherwise. + */ + public boolean isSingleDay() { + return start.toLocalDate().isEqual(end.toLocalDate()); + } + + /** + * Get the DayOfWeek of this EventPeriod. EventPeriod has to only span a single day. + * + * @return DayOfWeek of this EventPeriod. + */ + public DayOfWeek getDayOfWeek() { + assert(isSingleDay()); + + return start.getDayOfWeek(); + } + + /** + * Get the number of minutes elapsed from the input time to the start time of this EventPeriod. + * + * @param time input time. + * @return minutes elapsed from the input time to the start time of the EventPeriod. + */ + public long getMinutesFromTimeToStartTime(LocalTime time) { + return MINUTES.between(time, getStartTime()); + } + @Override + public boolean equals(Object other) { + requireNonNull(other); + if (other == this) { + return true; + } + + if (!(other instanceof EventPeriod)) { + return false; + } + + EventPeriod otherEventPeriod = (EventPeriod) other; + return otherEventPeriod.start.isEqual(this.start) && otherEventPeriod.end.isEqual(this.end); + } + + @Override + public String toString() { + return "start: " + + this.start.format(DATE_TIME_STRING_FORMATTER) + + "; end: " + + this.end.format(DATE_TIME_STRING_FORMATTER); + } +} diff --git a/src/main/java/seedu/address/model/event/SingleDayEventList.java b/src/main/java/seedu/address/model/event/SingleDayEventList.java new file mode 100644 index 00000000000..8885371a255 --- /dev/null +++ b/src/main/java/seedu/address/model/event/SingleDayEventList.java @@ -0,0 +1,183 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.TreeMap; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.exceptions.ConflictingEventException; +import seedu.address.model.event.exceptions.EventNotFoundException; + +/** + * Stores the event for a single day. + */ +public class SingleDayEventList { + private final LocalDate date; + private final NavigableMap eventTree; + + /** + * Constructs a SingleDayEventList to store events for the day. + */ + public SingleDayEventList(LocalDate date) { + requireNonNull(date); + + this.date = date; + this.eventTree = new TreeMap(EventPeriod::compareTo); + } + + /** + * Checks if adding an event is valid without causing conflicts with existing events. + * + * @param event The event to be added. + * @return True if the event can be added without conflicts, false otherwise. + */ + public boolean isEventAddValid(Event event) { + requireNonNull(event); + + EventPeriod eventPeriod = event.getEventPeriod(); + + Event precedingEvent = Optional.>ofNullable(this.eventTree.floorEntry( + eventPeriod)).map(Map.Entry::getValue).orElse(Event.createNonConflictingEvent()); + Event proceedingEvent = Optional.>ofNullable(this.eventTree.ceilingEntry( + eventPeriod)).map(Map.Entry::getValue).orElse(Event.createNonConflictingEvent()); + return !(precedingEvent.isConflicting(event) || proceedingEvent.isConflicting(event)); + } + + /** + * Adds an event to the event tree. + * + * @param event The event to be added. + */ + public void addEvent(Event event) { + requireNonNull(event); + if (!isEventAddValid(event)) { + throw new ConflictingEventException(); + } + eventTree.put(event.getEventPeriod(), event); + } + + /** + * Forcibly add the event to the event tree, disregarding time period conflict. + * + * @param event The event to be added. + */ + public void forceAddEvent(Event event) { + requireNonNull(event); + eventTree.put(event.getEventPeriod(), event); + } + + /** + * Check if the event exists in the list. + * + * @param event event to be checked. + * @return true if it exists in the list, false otherwise. + */ + public boolean containsEvent(Event event) { + requireNonNull(event); + for (Event thisEvent : eventTree.values()) { + if (event.equals(thisEvent)) { + return true; + } + } + return false; + } + + /** + * Check if there is an event at specified time + * + * @param dateTime the specified time + * @return an {@code @Optional} containing the event if there is an event at the time, empty optional otherwise. + */ + public Optional eventAtTime(LocalDateTime dateTime) { + requireNonNull(dateTime); + for (Event event : eventTree.values()) { + Event parentEvent = event.getParentEvent(); + if (parentEvent.isDuring(dateTime)) { + return Optional.of(event.getParentEvent()); + } + } + return Optional.empty(); + } + + /** + * Looks for all events within a time range and returns a List containing these events. + * + * @param eventPeriod The specified time range represented by a {@code EventPeriod} + * @return A List object containing all events that are within the time range. + */ + public List eventsInRange(EventPeriod eventPeriod) { + ArrayList output = new ArrayList(); + for (Event thisEvent : eventTree.values()) { + if (thisEvent.isPeriodConflicting(eventPeriod)) { + output.add(thisEvent.getParentEvent()); + } + } + return output; + } + + /** + * Removes the event from the list. The event must exist in the list. + * @param toRemove event to be removed. + */ + public void remove(Event toRemove) { + requireNonNull(toRemove); + for (Event thisEvent : this.eventTree.values()) { + if (toRemove.getParentEvent().equals(thisEvent.getParentEvent())) { + this.eventTree.remove(thisEvent.getEventPeriod(), thisEvent); + return; + } + } + throw new EventNotFoundException(); + } + + /** + * Returns the events for the day as an observableList. + * + * @return the events for the day as an observableList. + */ + public ObservableList getDayEventList() { + return FXCollections.observableArrayList(this.eventTree.values() + .toArray(new Event[0])); + } + + /** + * Checks if there are no events present in this list. + * + * @return true if there are no events present, false otherwise. + */ + public boolean isEmpty() { + return eventTree.isEmpty(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof SingleDayEventList)) { + return false; + } + + SingleDayEventList otherDay = (SingleDayEventList) other; + if (!(this.eventTree.size() == otherDay.eventTree.size())) { + return false; + } + + for (Event thisEvent : this.eventTree.values()) { + if (!otherDay.containsEvent(thisEvent)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java b/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java new file mode 100644 index 00000000000..f5b736011db --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java @@ -0,0 +1,10 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the operation will result in events that overlap in their time frame. + */ +public class ConflictingEventException extends RuntimeException { + public ConflictingEventException() { + super("Operation would result in events with overlapping timeframe"); + } +} diff --git a/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java b/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java new file mode 100644 index 00000000000..8c309c2528f --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java @@ -0,0 +1,8 @@ +package seedu.address.model.event.exceptions; + +/** + * Thrown when the date input to bound the {@code @EventPeriod} is outside the original bounds of the + * {@code @EventPeriod}. + */ +public class DateOutOfBoundsException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java new file mode 100644 index 00000000000..b1da908e10e --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the event could not be found in the calendar. + */ +public class EventNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java b/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java new file mode 100644 index 00000000000..7f7dcd020ec --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java @@ -0,0 +1,7 @@ +package seedu.address.model.event.exceptions; + +/** + * This exception is thrown when an eventDescription has a description that is invalid. + */ +public class InvalidDescriptionException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java b/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java new file mode 100644 index 00000000000..9db6ad01507 --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java @@ -0,0 +1,14 @@ +package seedu.address.model.event.exceptions; + + +/** + * This exception is thrown when an event period is invalid. + */ +public class InvalidEventPeriodException extends RuntimeException { + /** + * @param message should contain relevant information on the failed constraint(s) + */ + public InvalidEventPeriodException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..3a331d22cb8 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,12 +2,17 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.time.LocalDateTime; import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; import seedu.address.model.tag.Tag; /** @@ -24,8 +29,10 @@ public class Person { // Data fields private final Address address; private final Set tags = new HashSet<>(); + private final UniMateCalendar calendar = new UniMateCalendar(); /** + * Primary constructor for a person object. * Every field must be present and not null. */ public Person(Name name, Phone phone, Email email, Address address, Set tags) { @@ -37,6 +44,22 @@ public Person(Name name, Phone phone, Email email, Address address, Set tag this.tags.addAll(tags); } + /** + * Alternative constructor to be used when replacing the calendar of a contact. + * Every field must be present and not null. + * + */ + public Person(Name name, Phone phone, Email email, Address address, Set tags, UniMateCalendar calendar) { + requireAllNonNull(name, phone, email, address, tags); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.calendar.resetData(calendar); + } + + public Name getName() { return name; } @@ -53,6 +76,21 @@ public Address getAddress() { return address; } + public UniMateCalendar getCalendar() { + return calendar; + } + + public ReadOnlyCalendar getReadOnlyCalendar() { + return calendar; + } + + /** + * Returns a view of the events belonging to this person + */ + public ObservableList getEventList() { + return calendar.getEventList(); + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -74,6 +112,44 @@ public boolean isSamePerson(Person otherPerson) { && otherPerson.getName().equals(getName()); } + /** + * Adds an event to the calendar of this Person. + */ + public void addEvent(Event toAdd) { + calendar.addEvent(toAdd); + } + + /** + * Checks if an event can be added to the calendar of this Person. + */ + public boolean canAddEvent(Event toAdd) { + return calendar.canAddEvent(toAdd); + } + + /** + * Deletes an event at the specified time from the calendar of this person. + */ + public void deleteEvent(LocalDateTime targetTime) { + calendar.deleteEventAt(targetTime); + } + + /** + * Looks for an event at the specified time and returns it. + */ + public Event findEvent(LocalDateTime targetTime) { + return calendar.findEventAt(targetTime).get(); + } + + /** + * Checks if the person is tagged with the input tag. + * + * @param tag input tag to be checked. + * @return true if this person is tagged with the input tag. + */ + public boolean hasTag(Tag tag) { + return getTags().stream().anyMatch(tag::equals); + } + /** * Returns true if both persons have the same identity and data fields. * This defines a stronger notion of equality between two persons. diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..46c58cfd95a 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,11 +3,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -97,6 +99,14 @@ public void setPersons(List persons) { internalList.setAll(persons); } + /** + * Sort persons in Person List according to comparator provided. + * @param comparator + */ + public void sortPersons(Comparator comparator) { + internalList.setAll(new SortedList(internalList, comparator)); + } + /** * Returns the backing list as an unmodifiable {@code ObservableList}. */ diff --git a/src/main/java/seedu/address/model/person/comparer/AddressComparator.java b/src/main/java/seedu/address/model/person/comparer/AddressComparator.java new file mode 100644 index 00000000000..88b9d3c6585 --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparer/AddressComparator.java @@ -0,0 +1,40 @@ +package seedu.address.model.person.comparer; + +import seedu.address.model.person.Person; + +/** + * A comparator that sorts Person objects by address. + */ +public class AddressComparator extends SortComparator { + /** + * The sort type for this comparator. + */ + public static final String SORT_TYPE = "address"; + + /** + * Creates a new AddressComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public AddressComparator(boolean isActive, boolean isReverse, int priority) { + super(isActive, isReverse, priority); + } + + /** + * Compares two Person objects by address. + * + * @param p1 The first Person to compare. + * @param p2 The second Person to compare. + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + @Override + public int compare(Person p1, Person p2) { + if (p1.equals(p2)) { + return 0; + } + return p1.getAddress().value.toLowerCase().compareTo(p2.getAddress().value.toLowerCase()); + } +} diff --git a/src/main/java/seedu/address/model/person/comparer/EmailComparator.java b/src/main/java/seedu/address/model/person/comparer/EmailComparator.java new file mode 100644 index 00000000000..314a0a0064d --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparer/EmailComparator.java @@ -0,0 +1,41 @@ +package seedu.address.model.person.comparer; + +import seedu.address.model.person.Person; + +/** + * A comparator that sorts Person objects by email. + */ +public class EmailComparator extends SortComparator { + + /** + * The sort type for this comparator. + */ + public static final String SORT_TYPE = "email"; + + /** + * Creates a new AddressComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public EmailComparator(boolean isActive, boolean isReverse, int priority) { + super(isActive, isReverse, priority); + } + + /** + * Compares two Person objects by email. + * + * @param p1 The first Person to compare. + * @param p2 The second Person to compare. + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + @Override + public int compare(Person p1, Person p2) { + if (p1.equals(p2)) { + return 0; + } + return p1.getEmail().value.compareTo(p2.getEmail().value); + } +} diff --git a/src/main/java/seedu/address/model/person/comparer/NameComparator.java b/src/main/java/seedu/address/model/person/comparer/NameComparator.java new file mode 100644 index 00000000000..87d80c3e1a0 --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparer/NameComparator.java @@ -0,0 +1,41 @@ +package seedu.address.model.person.comparer; + +import seedu.address.model.person.Person; + +/** + * A comparator that sorts Person objects by name. + */ +public class NameComparator extends SortComparator { + + /** + * The sort type for this comparator. + */ + public static final String SORT_TYPE = "name"; + + /** + * Creates a new NameComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public NameComparator(boolean isActive, boolean isReverse, int priority) { + super(isActive, isReverse, priority); + } + + /** + * Compares two Person objects by name. + * + * @param p1 The first Person to compare. + * @param p2 The second Person to compare. + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + @Override + public int compare(Person p1, Person p2) { + if (p1.equals(p2)) { + return 0; + } + return p1.getName().fullName.toLowerCase().compareTo(p2.getName().fullName.toLowerCase()); + } +} diff --git a/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java b/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java new file mode 100644 index 00000000000..ea512574cb7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java @@ -0,0 +1,40 @@ +package seedu.address.model.person.comparer; + +import seedu.address.model.person.Person; + +/** + * A comparator that sorts Person objects by phone number. + */ +public class PhoneComparator extends SortComparator { + /** + * The sort type for this comparator. + */ + public static final String SORT_TYPE = "phone"; + + /** + * Creates a new PhoneComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public PhoneComparator(boolean isActive, boolean isReverse, int priority) { + super(isActive, isReverse, priority); + } + + /** + * Compares two Person objects by phone number. + * + * @param p1 The first Person to compare. + * @param p2 The second Person to compare. + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + @Override + public int compare(Person p1, Person p2) { + if (p1.equals(p2)) { + return 0; + } + return p1.getPhone().value.compareTo(p2.getPhone().value); + } +} diff --git a/src/main/java/seedu/address/model/person/comparer/SortComparator.java b/src/main/java/seedu/address/model/person/comparer/SortComparator.java new file mode 100644 index 00000000000..2bc1894d5f6 --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparer/SortComparator.java @@ -0,0 +1,84 @@ +package seedu.address.model.person.comparer; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +/** + * An abstract class that defines a comparator for sorting Person objects. + */ +public abstract class SortComparator implements Comparator { + + /** + * Whether this comparator is currently active. + */ + protected boolean isActive; + + /** + * Whether to sort in reverse order. + */ + protected boolean isReverse; + + /** + * The priority of this comparator. + */ + protected int priority; + + /** + * Creates a new SortComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public SortComparator(boolean isActive, boolean isReverse, int priority) { + this.isActive = isActive; + this.isReverse = isReverse; + this.priority = isActive ? priority : -1; + } + + /** + * Returns whether this comparator is currently active. + * + * @return Whether this comparator is currently active. + */ + public boolean getIsActive() { + return this.isActive; + } + + /** + * Returns whether to sort in reverse order. + * + * @return Whether to sort in reverse order. + */ + public boolean getIsReverse() { + return this.isReverse; + } + + /** + * Sets whether to sort in reverse order + * + */ + public void setIsReverse(boolean isReverse) { + this.isReverse = isReverse; + } + + /** + * Returns the priority of this comparator. + * + * @return The priority of this comparator. + */ + public int getPriority() { + return this.priority; + } + + /** + * Compares two Person objects using this comparator's sorting criteria. + * + * @param p1 The first Person to compare. + * @param p2 The second Person to compare. + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + public abstract int compare(Person p1, Person p2); +} diff --git a/src/main/java/seedu/address/model/task/Deadline.java b/src/main/java/seedu/address/model/task/Deadline.java new file mode 100644 index 00000000000..a8f37fdc2c3 --- /dev/null +++ b/src/main/java/seedu/address/model/task/Deadline.java @@ -0,0 +1,127 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ParserUtil.DATE_TIME_STRING_FORMATTER; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.task.exceptions.InvalidDeadlineException; + +/** + * An object representing the deadline of a task. + */ +public class Deadline implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "The deadline should " + + "be in the format 'yyyy-MM-dd HH:mm' where:\n" + + " -'yyyy' is the year.\n" + + " -'MM' is the month.\n" + + " -'dd' is the day.\n" + + " -'HH:mm' is the time in 24-hour format."; + + public static final String ABSENT_DEADLINE = "-"; + + + private final Optional deadline; + + /** + * Constructs a Deadline object with String representation of a datetime. + * If is String is null, instead creates an empty deadline. + * + * @param deadline the String representation of the deadline datetime. + */ + public Deadline(String deadline) { + if (deadline == null) { + this.deadline = Optional.empty(); + } else { + try { + this.deadline = Optional.of(LocalDateTime.parse(deadline, DATE_TIME_STRING_FORMATTER)); + } catch (DateTimeParseException e) { + throw new InvalidDeadlineException(); + } + } + } + + /** + * Constructs a Deadline object with the provided LocalDateTime. + * If the LocalDateTime is null, instead creates an empty deadline. + * + * @param deadline the LocalDateTime. + */ + public Deadline(LocalDateTime deadline) { + this.deadline = Optional.ofNullable(deadline); + } + + /** + * Checks if a deadline is present + * + * @return true if a deadline is present, false otherwise. + */ + public boolean isPresent() { + return deadline.isPresent(); + } + + public LocalDateTime getDeadline() { + return deadline.orElse(null); + } + + /** + * Compares this deadline to another deadline. Deadlines that are present are prioritized above absent ones. + * + * @param other the object to be compared. + * @return -1 if this deadline is before, 1 if deadline is after and 0 if the deadlines are both present and equal. + */ + @Override + public int compareTo(Deadline other) { + requireNonNull(other); + + if (isPresent() && other.isPresent()) { + return deadline.get().compareTo(other.deadline.get()); + } else if (isPresent()) { + return -1; + } else if (other.isPresent()) { + return 1; + } else { + return 0; + } + } + + /** + * Gets the formatted string representation of the deadline. + * + * @return the string representation of "-" if no deadline is present. + */ + public String getFormattedDeadline() { + if (isPresent()) { + return deadline.get().format(DATE_TIME_STRING_FORMATTER); + } + return ABSENT_DEADLINE; + } + + @Override + public boolean equals(Object other) { + requireNonNull(other); + if (other == this) { + return true; + } + + if (!(other instanceof Deadline)) { + return false; + } + + Deadline otherDeadline = (Deadline) other; + if (isPresent() && otherDeadline.isPresent()) { + return deadline.get().equals(otherDeadline.deadline.get()); + } + return isPresent() == otherDeadline.isPresent(); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("deadline", deadline) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java b/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java new file mode 100644 index 00000000000..d69fc3dc991 --- /dev/null +++ b/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java @@ -0,0 +1,16 @@ +package seedu.address.model.task; + +import javafx.collections.ObservableList; + +/** + * A read only version of Task Manager. + */ +public interface ReadOnlyTaskManager { + /** + * Generates an unmodifiable view of the task list. + * + * @return unmodifiable view of the task list. + */ + ObservableList getTaskList(); + +} diff --git a/src/main/java/seedu/address/model/task/Task.java b/src/main/java/seedu/address/model/task/Task.java new file mode 100644 index 00000000000..823f241f1b2 --- /dev/null +++ b/src/main/java/seedu/address/model/task/Task.java @@ -0,0 +1,114 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a task in the task list. + */ +public class Task { + public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm"); + private final TaskDescription description; + private final Deadline deadline; + + /** + * Constructs a Task object with the given description and deadline + */ + public Task(String description, LocalDateTime deadline) { + requireNonNull(description); + + this.description = new TaskDescription(description); + this.deadline = new Deadline(deadline); + } + + /** + * Constructs a Task object with the given {@code TaskDescription} and {@code Deadline} + */ + public Task(TaskDescription description, Deadline deadline) { + requireAllNonNull(description, deadline); + + this.description = description; + this.deadline = deadline; + } + + public TaskDescription getDescription() { + return description; + } + + public String getDescriptionString() { + return description.get(); + } + + public Deadline getDeadline() { + return deadline; + } + + public String getDeadlineString() { + return deadline.getFormattedDeadline(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Task)) { + return false; + } + + Task otherTask = (Task) other; + boolean descriptionMatches = description.equals(otherTask.description); + boolean deadlineMatches = deadline.equals(otherTask.deadline); + return descriptionMatches && deadlineMatches; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("description", description) + .add("deadline", deadline) + .toString(); + } + + /** + * A comparator to sort the task list by deadline. + */ + public static class TaskDeadlineComparator implements Comparator { + @Override + public int compare(Task task1, Task task2) { + int firstResult = task1.deadline.compareTo(task2.deadline); + if (firstResult == 0) { + return new TaskDescriptorComparator().compare(task1, task2); + } + return firstResult; + } + + @Override + public boolean equals(Object other) { + return other instanceof TaskDeadlineComparator; + } + } + + /** + * A comparator to sort the task list by description, alphabetically. + */ + public static class TaskDescriptorComparator implements Comparator { + public int compare(Task task1, Task task2) { + return task1.getDescription().compareTo(task2.getDescription()); + } + + @Override + public boolean equals(Object other) { + return other instanceof TaskDescriptorComparator; + } + } +} diff --git a/src/main/java/seedu/address/model/task/TaskDescription.java b/src/main/java/seedu/address/model/task/TaskDescription.java new file mode 100644 index 00000000000..9a6b908ac48 --- /dev/null +++ b/src/main/java/seedu/address/model/task/TaskDescription.java @@ -0,0 +1,63 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.task.exceptions.InvalidTaskDescriptionException; + +/** + * Represents the description of a task. + */ +public class TaskDescription implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "Task description cannot be empty."; + private final String description; + + /** + * Constructs a TaskDescription object with the given description. + * + * @param description A String representing the description of the task. + */ + public TaskDescription(String description) { + requireNonNull(description); + if (!isValidDescription(description)) { + throw new InvalidTaskDescriptionException(); + } + this.description = description; + } + + /** + * Checks if the given string is valid as a TaskDescription. + * + * @param description the description String to be checked. + * @return true if the description is valid, false otherwise. + */ + public static boolean isValidDescription(String description) { + requireNonNull(description); + return !description.isEmpty(); + } + + /** + * Returns the string representation of the description. + */ + public String get() { + return description; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof TaskDescription)) { + return false; + } + + TaskDescription otherTaskDescription = (TaskDescription) other; + return description.equals(otherTaskDescription.description); + } + + @Override + public int compareTo(TaskDescription other) { + return description.toLowerCase().compareTo(other.description.toLowerCase()); + } +} diff --git a/src/main/java/seedu/address/model/task/TaskList.java b/src/main/java/seedu/address/model/task/TaskList.java new file mode 100644 index 00000000000..3eb2f18717c --- /dev/null +++ b/src/main/java/seedu/address/model/task/TaskList.java @@ -0,0 +1,135 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.task.exceptions.DuplicateTaskException; +import seedu.address.model.task.exceptions.TaskNotFoundException; + +/** + * A list of tasks that enforces uniqueness between its elements and does not allow nulls. + */ +public class TaskList implements Iterable { + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + + /** + * Returns true if the list contains an equivalent task as the given argument. + */ + public boolean contains(Task toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::equals); + } + + /** + * Adds a task to the list. + * An equivalent task must not already exist in the list. + */ + public void add(Task task) { + requireNonNull(task); + if (contains(task)) { + throw new DuplicateTaskException(); + } + internalList.add(task); + } + + /** + * Removes the equivalent task from the list. + * The task must exist in the list. + */ + public void remove(Task toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new TaskNotFoundException(); + } + } + + /** + * Removes a task at specified index from the list and returns the removed task. + */ + public Task remove(int index) { + requireNonNull(index); + + return internalList.remove(index); + } + + public void setTasks(TaskList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code tasks}. + * {@code tasks} must not contain duplicate tasks. + */ + public void setTasks(List tasks) { + requireAllNonNull(tasks); + if (!tasksAreUnique(tasks)) { + throw new DuplicateTaskException(); + } + internalList.setAll(tasks); + } + + /** + * Sorts tasks in the list according to the current sorting order setting of the list. + */ + public void sortTasks(Comparator sortingOrder) { + internalList.sort(sortingOrder); + } + + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableList() { + return internalUnmodifiableList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TaskList)) { + return false; + } + + TaskList otherTaskList = (TaskList) other; + return internalList.equals(otherTaskList.internalList); + } + + /** + * Returns true if {@code tasks} contains only unique tasks. + */ + private boolean tasksAreUnique(List tasks) { + for (int i = 0; i < tasks.size() - 1; i++) { + for (int j = i + 1; j < tasks.size(); j++) { + if (tasks.get(i).equals(tasks.get(j))) { + return false; + } + } + } + return true; + } + + @Override + public String toString() { + return internalList.toString(); + } + + + @Override + public Iterator iterator() { + return internalList.iterator(); + } +} diff --git a/src/main/java/seedu/address/model/task/TaskManager.java b/src/main/java/seedu/address/model/task/TaskManager.java new file mode 100644 index 00000000000..d69b3c756af --- /dev/null +++ b/src/main/java/seedu/address/model/task/TaskManager.java @@ -0,0 +1,141 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.model.task.exceptions.InvalidSortingOrderException; + +/** + * Represents a task manager that stores and manages tasks + */ +public class TaskManager implements ReadOnlyTaskManager { + private static final String COMPARATOR_TYPE_DESCRIPTION = "Description"; + private static final String COMPARATOR_TYPE_DEADLINE = "Deadline"; + + private final TaskList tasks; + private Comparator sortingOrder; + + /** + * Constructs a TaskManager object with an empty task list. + */ + public TaskManager() { + this.tasks = new TaskList(); + this.sortingOrder = new Task.TaskDeadlineComparator(); + } + + /** + * Constructs a TaskManager using the tasks in the {@code toBeCopied} + */ + public TaskManager(ReadOnlyTaskManager toBeCopied) { + this(); + requireNonNull(toBeCopied); + resetData(toBeCopied); + } + + /** + * Resets the existing data of this {@code TaskManager} with {@code newData}. + */ + public void resetData(ReadOnlyTaskManager newData) { + requireNonNull(newData); + + setTasks(newData.getTaskList()); + } + + @Override + public ObservableList getTaskList() { + return tasks.asUnmodifiableList(); + } + + /** + * Replaces the contents of the taskList with {@code taskList}. + * {@code taskList} must not contain duplicate tasks. + */ + public void setTasks(List taskList) { + requireNonNull(taskList); + + tasks.setTasks(taskList); + } + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasTask(Task task) { + requireNonNull(task); + return tasks.contains(task); + } + + /** + * Adds a task into the taskManager. + */ + public void addTask(Task task) { + requireNonNull(task); + tasks.add(task); + sort(); + } + + public Task deleteTask(int index) { + return tasks.remove(index); + } + + /** + * Sets the sorting order setting to sort by deadline. + */ + private void setSortDeadline() { + sortingOrder = new Task.TaskDeadlineComparator(); + sort(); + } + + /** + * Sets the sorting order setting to sort by description. + */ + private void setSortDescription() { + sortingOrder = new Task.TaskDescriptorComparator(); + sort(); + } + + /** + * Returns the sorting order. + */ + public Comparator getSortingOrder() { + return sortingOrder; + } + + /** + * Sets the sorting order of the task list to the specified comparator type. + */ + public void sortTasksBy(String comparatorType) { + if (comparatorType.equalsIgnoreCase(COMPARATOR_TYPE_DESCRIPTION)) { + setSortDescription(); + } else if (comparatorType.equalsIgnoreCase(COMPARATOR_TYPE_DEADLINE)) { + setSortDeadline();; + } else { + throw new InvalidSortingOrderException(); + } + } + + /** + * Sorts tasks in the task list according to the current sorting order. + */ + public void sort() { + tasks.sortTasks(sortingOrder); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof TaskManager)) { + return false; + } + + TaskManager otherTaskManager = (TaskManager) other; + boolean tasksMatch = this.tasks.equals(otherTaskManager.tasks); + boolean sortOrderMatch = this.sortingOrder.equals(otherTaskManager.sortingOrder); + return tasksMatch && sortOrderMatch; + } +} diff --git a/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java b/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java new file mode 100644 index 00000000000..fa8a65a0bc9 --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java @@ -0,0 +1,7 @@ +package seedu.address.model.task.exceptions; + +/** + * Signals that there is a duplicate task in the task list. + */ +public class DuplicateTaskException extends RuntimeException{ +} diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java new file mode 100644 index 00000000000..85e2324a8e5 --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java @@ -0,0 +1,7 @@ +package seedu.address.model.task.exceptions; + +/** + * An exception thrown when a string of improper format is used to create a Deadline. + */ +public class InvalidDeadlineException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java new file mode 100644 index 00000000000..60909f8973f --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java @@ -0,0 +1,7 @@ +package seedu.address.model.task.exceptions; + +/** + * An exception thrown when a sorting order provided is invalid. + */ +public class InvalidSortingOrderException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java new file mode 100644 index 00000000000..6daa8e53ed6 --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java @@ -0,0 +1,7 @@ +package seedu.address.model.task.exceptions; + +/** + * An exception thrown when a task description is invalid. + */ +public class InvalidTaskDescriptionException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java b/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java new file mode 100644 index 00000000000..c5216c4ae57 --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.task.exceptions; + +/** + * Signals that the task could not be found in the task list. + */ +public class TaskNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/util/SampleCalendarUtil.java b/src/main/java/seedu/address/model/util/SampleCalendarUtil.java new file mode 100644 index 00000000000..c27f99682f3 --- /dev/null +++ b/src/main/java/seedu/address/model/util/SampleCalendarUtil.java @@ -0,0 +1,32 @@ +package seedu.address.model.util; + +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +/** + * Contains utility methods for populating {@code Calendar} with sample data. + */ +public class SampleCalendarUtil { + public static Event[] getSampleEvents() { + return new Event[] { + new Event(new EventDescription("Weekly team meeting"), + new EventPeriod("2023-10-25 09:00", "2023-10-25 10:30")), + new Event(new EventDescription(" Annual health checkup"), + new EventPeriod("2023-11-10 15:15", "2023-11-10 16:00")), + new Event(new EventDescription(" Birthday celebration"), + new EventPeriod("2023-11-15 18:00", "2023-11-15 22:00")) + }; + } + + public static ReadOnlyCalendar getSampleCalendar() { + UniMateCalendar sampleCalendar = new UniMateCalendar(); + for (Event sampleEvent : getSampleEvents()) { + sampleCalendar.addEvent(sampleEvent); + } + return sampleCalendar; + } + +} diff --git a/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java b/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java new file mode 100644 index 00000000000..e360cb7d185 --- /dev/null +++ b/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java @@ -0,0 +1,32 @@ +package seedu.address.model.util; + +import seedu.address.model.task.Deadline; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskDescription; +import seedu.address.model.task.TaskManager; + +/** + * Contains utility methods for populating {@code TaskManager} with sample data. + */ +public class SampleTaskManagerUtil { + public static Task[] getSampleTasks() { + return new Task[] { + new Task(new TaskDescription("Caffeinate"), + new Deadline("2023-11-11 00:00")), + new Task(new TaskDescription("Buy a new dog"), + new Deadline((String) null)), + new Task(new TaskDescription("Sell textbooks"), + new Deadline("2023-12-31 00:00")) + }; + } + + public static ReadOnlyTaskManager getSampleTaskManager() { + TaskManager sampleTaskManager = new TaskManager(); + for (Task sampleTask : getSampleTasks()) { + sampleTaskManager.addTask(sampleTask); + } + return sampleTaskManager; + } + +} diff --git a/src/main/java/seedu/address/storage/CalendarStorage.java b/src/main/java/seedu/address/storage/CalendarStorage.java new file mode 100644 index 00000000000..2aa09aebb85 --- /dev/null +++ b/src/main/java/seedu/address/storage/CalendarStorage.java @@ -0,0 +1,45 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.calendar.ReadOnlyCalendar; + +/** + * Represents a storage for the Calendar. + */ +public interface CalendarStorage { + + /** + * Returns the file path of the data file. + */ + Path getCalendarFilePath(); + + /** + * Returns AddressBook data as a {@link ReadOnlyCalendar}. + * Returns {@code Optional.empty()} if storage file is not found. + * + * @throws DataLoadingException if loading the data from storage failed. + */ + Optional readCalendar() throws DataLoadingException; + + /** + * @see #getCalendarFilePath() + */ + Optional readCalendar(Path filePath) throws DataLoadingException; + + /** + * Saves the given {@link ReadOnlyCalendar} to the storage. + * @param calendar cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveCalendar(ReadOnlyCalendar calendar) throws IOException; + + /** + * @see #saveCalendar(ReadOnlyCalendar) + */ + void saveCalendar(ReadOnlyCalendar addressBook, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedEvent.java b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java new file mode 100644 index 00000000000..070886f84e1 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java @@ -0,0 +1,81 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +/** + * Jackson-friendly version of {@link Event}. + */ +class JsonAdaptedEvent { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Event's %s field is missing!"; + public static final String INVALID_FIELD_MESSAGE_FORMAT = EventPeriod.MESSAGE_CONSTRAINTS; + + private final String description; + private final String eventPeriod; + + /** + * Constructs a {@code JsonAdaptedEvent} with the given event details. + */ + @JsonCreator + public JsonAdaptedEvent(@JsonProperty("description") String description, + @JsonProperty("eventPeriod") String eventPeriod) { + this.description = description; + this.eventPeriod = eventPeriod; + } + + /** + * Converts a given {@code Event} into this class for Jackson use. + */ + public JsonAdaptedEvent(Event source) { + description = source.getDescriptionString(); + eventPeriod = source.getEventPeriodString(); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Event} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted event. + */ + public Event toModelType() throws IllegalValueException { + if (description == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, + EventDescription.class.getSimpleName())); + } + if (!EventDescription.isValid(description)) { + throw new IllegalValueException(EventDescription.MESSAGE_CONSTRAINTS); + } + final EventDescription modelDescription = new EventDescription(description); + + // Event Periods are expected to be saved in this format yyyy-MM-dd HH:mm - yyyy-MM-dd HH:mm + String start; + String end; + if (eventPeriod == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, + EventPeriod.class.getSimpleName())); + } + String[] parts = eventPeriod.split(" - "); + if (parts.length == 2) { + start = parts[0]; + end = parts[1]; + } else { + throw new IllegalValueException(String.format(INVALID_FIELD_MESSAGE_FORMAT, + EventPeriod.class.getSimpleName())); + } + try { + final EventPeriod modelEventPeriod = ParserUtil.parseEventPeriod(start, end); + return new Event(modelDescription, modelEventPeriod); + } catch (ParseException pe) { + throw new IllegalValueException(pe.getMessage()); + } + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..55089dbc2ec 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -23,12 +25,14 @@ class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + public static final String MESSAGE_DUPLICATE_EVENT = "Calendar contains duplicate event(s)."; private final String name; private final String phone; private final String email; private final String address; private final List tags = new ArrayList<>(); + private final List events = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,7 +40,7 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("tags") List tags, @JsonProperty("events") List events) { this.name = name; this.phone = phone; this.email = email; @@ -44,6 +48,7 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.events.addAll(events); } /** @@ -57,6 +62,8 @@ public JsonAdaptedPerson(Person source) { tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + events.addAll(source.getEventList().stream().map(Event::getParentEvent).distinct() + .map(JsonAdaptedEvent::new).collect(Collectors.toList())); } /** @@ -103,7 +110,17 @@ public Person toModelType() throws IllegalValueException { final Address modelAddress = new Address(address); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + UniMateCalendar calendar = new UniMateCalendar(); + for (JsonAdaptedEvent jsonAdaptedEvent : events) { + Event event = jsonAdaptedEvent.toModelType(); + if (calendar.contains(event)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_EVENT); + } + calendar.addEvent(event); + } + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, calendar); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTask.java b/src/main/java/seedu/address/storage/JsonAdaptedTask.java new file mode 100644 index 00000000000..3c04b082879 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedTask.java @@ -0,0 +1,71 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.task.Deadline; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskDescription; +import seedu.address.model.task.exceptions.InvalidDeadlineException; + +/** + * Jackson-friendly version of {@link Task}. + */ +class JsonAdaptedTask { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Task's %s field is missing!"; + + private final String description; + private final String deadline; + + /** + * Constructs a {@code JsonAdaptedTask} with the given task details. + */ + @JsonCreator + public JsonAdaptedTask(@JsonProperty("description") String description, + @JsonProperty("deadline") String deadline) { + this.description = description; + this.deadline = deadline; + } + + /** + * Converts a given {@code Task} into this class for Jackson use. + */ + public JsonAdaptedTask(Task source) { + description = source.getDescriptionString(); + deadline = source.getDeadlineString(); + } + + /** + * Converts this Jackson-friendly adapted task object into the model's {@code Task} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted task. + */ + public Task toModelType() throws IllegalValueException { + if (description == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, + TaskDescription.class.getSimpleName())); + } + if (!TaskDescription.isValidDescription(description)) { + throw new IllegalValueException(TaskDescription.MESSAGE_CONSTRAINTS); + } + final TaskDescription modelDescription = new TaskDescription(description); + + // Deadlines are expected to be saved in this format yyyy-MM-dd HH:mm or - if there is no deadline + if (deadline == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, + Deadline.class.getSimpleName())); + } + //Null argument provided if there is deadline is saved as "-" in Json file + try { + Deadline modelDeadline = deadline.equals(Deadline.ABSENT_DEADLINE) + ? new Deadline((String) null) : new Deadline(deadline); + return new Task(modelDescription, modelDeadline); + } catch (InvalidDeadlineException e) { + throw new IllegalValueException(Deadline.MESSAGE_CONSTRAINTS); + } + } +} diff --git a/src/main/java/seedu/address/storage/JsonCalendarStorage.java b/src/main/java/seedu/address/storage/JsonCalendarStorage.java new file mode 100644 index 00000000000..98405417432 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonCalendarStorage.java @@ -0,0 +1,80 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.calendar.ReadOnlyCalendar; + +/** + * A class to access Calendar data stored as a json file on the hard disk. + */ +public class JsonCalendarStorage implements CalendarStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonCalendarStorage.class); + + private Path filePath; + + public JsonCalendarStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getCalendarFilePath() { + return filePath; + } + + @Override + public Optional readCalendar() throws DataLoadingException { + return readCalendar(filePath); + } + + /** + * Similar to {@link #readCalendar()}. + * + * @param filePath location of the data. Cannot be null. + * @throws DataLoadingException if loading the data from storage failed. + */ + public Optional readCalendar(Path filePath) throws DataLoadingException { + requireNonNull(filePath); + + Optional jsonCalendar = JsonUtil.readJsonFile( + filePath, JsonSerializableCalendar.class); + if (!jsonCalendar.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonCalendar.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataLoadingException(ive); + } + } + + @Override + public void saveCalendar(ReadOnlyCalendar calendar) throws IOException { + saveCalendar(calendar, filePath); + } + + /** + * Similar to {@link #saveCalendar(ReadOnlyCalendar)}. + * + * @param filePath location of the data. Cannot be null. + */ + public void saveCalendar(ReadOnlyCalendar calendar, Path filePath) throws IOException { + requireNonNull(calendar); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableCalendar(calendar), filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableCalendar.java b/src/main/java/seedu/address/storage/JsonSerializableCalendar.java new file mode 100644 index 00000000000..c23ecd5fd5f --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonSerializableCalendar.java @@ -0,0 +1,60 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; + +/** + * An Immutable Calendar that is serializable to JSON format. + */ +@JsonRootName(value = "calendar") +class JsonSerializableCalendar { + public static final String MESSAGE_DUPLICATE_EVENT = "Calendar contains duplicate event(s)."; + + private final List events = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableCalendar} with the given events. + */ + @JsonCreator + public JsonSerializableCalendar(@JsonProperty("events") List events) { + this.events.addAll(events); + } + + /** + * Converts a given {@code ReadOnlyCalendar} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableCalendar}. + */ + public JsonSerializableCalendar(ReadOnlyCalendar source) { + events.addAll(source.getEventList().stream().map(Event::getParentEvent).distinct() + .map(JsonAdaptedEvent::new).collect(Collectors.toList())); + } + + /** + * Converts this address book into the model's {@code AddressBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public UniMateCalendar toModelType() throws IllegalValueException { + UniMateCalendar calendar = new UniMateCalendar(); + for (JsonAdaptedEvent jsonAdaptedEvent : events) { + Event event = jsonAdaptedEvent.toModelType(); + if (calendar.contains(event)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_EVENT); + } + calendar.addEvent(event); + } + return calendar; + } + +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java b/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java new file mode 100644 index 00000000000..dff767fd346 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java @@ -0,0 +1,60 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; + +/** + * An Immutable TaskManager that is serializable to JSON format. + */ +@JsonRootName(value = "taskmanager") +class JsonSerializableTaskManager { + + public static final String MESSAGE_DUPLICATE_TASK = "Task list contains duplicate task(s)."; + + private final List tasks = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableTaskManager} with the given tasks. + */ + @JsonCreator + public JsonSerializableTaskManager(@JsonProperty("tasks") List tasks) { + this.tasks.addAll(tasks); + } + + /** + * Converts a given {@code ReadOnlyTaskManager} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableTaskManager}. + */ + public JsonSerializableTaskManager(ReadOnlyTaskManager source) { + tasks.addAll(source.getTaskList().stream().map(JsonAdaptedTask::new).collect(Collectors.toList())); + } + + /** + * Converts this task manager into the model's {@code TaskManager} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public TaskManager toModelType() throws IllegalValueException { + TaskManager taskManager = new TaskManager(); + for (JsonAdaptedTask jsonAdaptedTask : tasks) { + Task task = jsonAdaptedTask.toModelType(); + if (taskManager.hasTask(task)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_TASK); + } + taskManager.addTask(task); + } + return taskManager; + } + +} diff --git a/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java b/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java new file mode 100644 index 00000000000..0a3b1b71ece --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java @@ -0,0 +1,80 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.task.ReadOnlyTaskManager; + +/** + * A class to access TaskManager data stored as a json file on the hard disk. + */ +public class JsonTaskManagerStorage implements TaskManagerStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonTaskManagerStorage.class); + + private Path filePath; + + public JsonTaskManagerStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getTaskManagerFilePath() { + return filePath; + } + + @Override + public Optional readTaskManager() throws DataLoadingException { + return readTaskManager(filePath); + } + + /** + * Similar to {@link #readTaskManager()}. + * + * @param filePath location of the data. Cannot be null. + * @throws DataLoadingException if loading the data from storage failed. + */ + public Optional readTaskManager(Path filePath) throws DataLoadingException { + requireNonNull(filePath); + + Optional jsonTaskManager = JsonUtil.readJsonFile( + filePath, JsonSerializableTaskManager.class); + if (!jsonTaskManager.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonTaskManager.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataLoadingException(ive); + } + } + + @Override + public void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException { + saveTaskManager(taskManager, filePath); + } + + /** + * Similar to {@link #saveTaskManager(ReadOnlyTaskManager)}. + * + * @param filePath location of the data. Cannot be null. + */ + public void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException { + requireAllNonNull(taskManager, filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableTaskManager(taskManager), filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 9fba0c7a1d6..f9a2357058a 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -8,11 +8,13 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.task.ReadOnlyTaskManager; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, CalendarStorage, TaskManagerStorage, UserPrefsStorage { @Override Optional readUserPrefs() throws DataLoadingException; @@ -29,4 +31,17 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + @Override + Optional readCalendar() throws DataLoadingException; + + @Override + void saveCalendar(ReadOnlyCalendar calendar) throws IOException; + + @Override + Optional readTaskManager() throws DataLoadingException; + + @Override + void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException; + + } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..f1fd38ee7fc 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -10,6 +10,8 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.task.ReadOnlyTaskManager; /** * Manages storage of AddressBook data in local storage. @@ -18,13 +20,21 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; + private CalendarStorage calendarStorage; + private TaskManagerStorage taskManagerStorage; private UserPrefsStorage userPrefsStorage; /** - * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. + * Creates a {@code StorageManager} with the given {@code AddressBookStorage}, + * {@code CalendarStorage}, {@code TaskManagerStorage}and {@code UserPrefStorage}. */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(AddressBookStorage addressBookStorage, + CalendarStorage calendarStorage, + TaskManagerStorage taskManagerStorage, + UserPrefsStorage userPrefsStorage) { this.addressBookStorage = addressBookStorage; + this.calendarStorage = calendarStorage; + this.taskManagerStorage = taskManagerStorage; this.userPrefsStorage = userPrefsStorage; } @@ -45,7 +55,6 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { userPrefsStorage.saveUserPrefs(userPrefs); } - // ================ AddressBook methods ============================== @Override @@ -75,4 +84,60 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + // ================ Calendar methods ================================= + @Override + public Path getCalendarFilePath() { + return calendarStorage.getCalendarFilePath(); + } + + @Override + public Optional readCalendar() throws DataLoadingException { + return readCalendar(calendarStorage.getCalendarFilePath()); + } + + @Override + public Optional readCalendar(Path filePath) throws DataLoadingException { + logger.fine("Attempting to read data from file: " + filePath); + return calendarStorage.readCalendar(filePath); + } + + @Override + public void saveCalendar(ReadOnlyCalendar calendar) throws IOException { + saveCalendar(calendar, calendarStorage.getCalendarFilePath()); + } + + @Override + public void saveCalendar(ReadOnlyCalendar calendar, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + calendarStorage.saveCalendar(calendar, filePath); + } + + // ================ TaskManager methods ================================= + @Override + public Path getTaskManagerFilePath() { + return taskManagerStorage.getTaskManagerFilePath(); + } + + @Override + public Optional readTaskManager() throws DataLoadingException { + return readTaskManager(taskManagerStorage.getTaskManagerFilePath()); + } + + @Override + public Optional readTaskManager(Path filePath) throws DataLoadingException { + logger.fine("Attempting to read data from file: " + filePath); + return taskManagerStorage.readTaskManager(filePath); + } + + @Override + public void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException { + saveTaskManager(taskManager, taskManagerStorage.getTaskManagerFilePath()); + } + + @Override + public void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + taskManagerStorage.saveTaskManager(taskManager, filePath); + } + } diff --git a/src/main/java/seedu/address/storage/TaskManagerStorage.java b/src/main/java/seedu/address/storage/TaskManagerStorage.java new file mode 100644 index 00000000000..1417be8ae20 --- /dev/null +++ b/src/main/java/seedu/address/storage/TaskManagerStorage.java @@ -0,0 +1,45 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.task.ReadOnlyTaskManager; + +/** + * Represents a storage for {@link seedu.address.model.task.TaskManager}. + */ +public interface TaskManagerStorage { + + /** + * Returns the file path of the data file. + */ + Path getTaskManagerFilePath(); + + /** + * Returns TaskManager data as a {@link ReadOnlyTaskManager}. + * Returns {@code Optional.empty()} if storage file is not found. + * + * @throws DataLoadingException if loading the data from storage failed. + */ + Optional readTaskManager() throws DataLoadingException; + + /** + * @see #getTaskManagerFilePath() + */ + Optional readTaskManager(Path filePath) throws DataLoadingException; + + /** + * Saves the given {@link ReadOnlyTaskManager} to the storage. + * @param taskManager cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException; + + /** + * @see #saveTaskManager(ReadOnlyTaskManager) + */ + void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/ui/BottomListPanel.java b/src/main/java/seedu/address/ui/BottomListPanel.java new file mode 100644 index 00000000000..7f7f686a441 --- /dev/null +++ b/src/main/java/seedu/address/ui/BottomListPanel.java @@ -0,0 +1,15 @@ +package seedu.address.ui; + +import javafx.scene.layout.Region; + +/** + * A panel class for list panel classes that should appear on the lower list. + */ +abstract class BottomListPanel extends UiPart { + /** + * Creates a {@code BottomListPanel} with the given fxmlFileName. + */ + public BottomListPanel(String fxmlFileName) { + super(fxmlFileName); + } +} diff --git a/src/main/java/seedu/address/ui/CalendarContainer.java b/src/main/java/seedu/address/ui/CalendarContainer.java new file mode 100644 index 00000000000..b7113af29fd --- /dev/null +++ b/src/main/java/seedu/address/ui/CalendarContainer.java @@ -0,0 +1,102 @@ +package seedu.address.ui; + +import static seedu.address.ui.UiConstants.POPUP_CALENDAR_HEIGHT; +import static seedu.address.ui.UiConstants.POPUP_CALENDAR_WIDTH; + +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import seedu.address.model.calendar.ReadOnlyCalendar; + +/** + * Represents the container for displaying the calendar view in the user interface. + * It contains various graphical elements such as the calendar label, event space, and background. + */ +public class CalendarContainer extends UiPart { + private static final String FXML = "CalendarContainer.fxml"; + + private ReadOnlyCalendar calendar; + @FXML + private StackPane calendarLabelPlaceholder; + + @FXML + private StackPane eventSpaceContainer; + + @FXML + private GridPane eventSpaceBackground; + @FXML + private GridPane eventSpace; + + /** + * Constructs a CalendarContainer with the specified ReadOnlyCalendar. + * + * @param calendar The ReadOnlyCalendar to be displayed in the container. + */ + public CalendarContainer(ReadOnlyCalendar calendar) { + super(FXML); + this.calendar = calendar; + } + + /** + * Creates and returns a default CalendarContainer for the specified calendar, filling it with the default + * components. + * + * @param calendar The ReadOnlyCalendar to be displayed. + * @return The default CalendarContainer. + */ + public static CalendarContainer createDefaultCalendar(ReadOnlyCalendar calendar) { + CalendarContainer defaultCalendarContainer = new CalendarContainer(calendar); + defaultCalendarContainer.fillCalendar(); + return defaultCalendarContainer; + } + + /** + * Creates and returns a CalendarContainer for the specified comparison calendar, filling it solid event cards. + * + * @param calendar The ReadOnlyCalendar generated after comparison. + * @return The CalendarContainer for the resultant comparison calendar. + */ + private static CalendarContainer createComparisonCalendar(ReadOnlyCalendar calendar) { + CalendarContainer comparisonCalendarContainer = new CalendarContainer(calendar); + comparisonCalendarContainer.fillComparisonCalendar(); + return comparisonCalendarContainer; + } + + /** + * Displays a comparison calendar in a separate modal window. + * + * @param calendar The ReadOnlyCalendar generated after comparison. + */ + public static void displayComparisonCalendar(ReadOnlyCalendar calendar) { + Stage comparisonCalendarStage = new Stage(); + comparisonCalendarStage.setResizable(false); + comparisonCalendarStage.initModality(Modality.APPLICATION_MODAL); + comparisonCalendarStage.setMinHeight(POPUP_CALENDAR_HEIGHT); + comparisonCalendarStage.setMinWidth(POPUP_CALENDAR_WIDTH); + CalendarContainer root = CalendarContainer.createComparisonCalendar(calendar); + comparisonCalendarStage.setScene(new Scene(root.getRoot())); + comparisonCalendarStage.show(); + } + + /** + * Fills the calendar container with the necessary components (calendar labels and event spaces). + */ + public void fillCalendar() { + calendarLabelPlaceholder.getChildren().add(new CalendarLabelColumn().getRoot()); + eventSpaceBackground.getChildren().add(new EventSpaceBackground(calendar).getRoot()); + eventSpace.getChildren().add(CalendarEventSpace.createDefaultEventSpace(calendar).getRoot()); + } + + /** + * Fills the calendar container with the necessary components for the comparison calendar view. + */ + public void fillComparisonCalendar() { + calendarLabelPlaceholder.getChildren().add(new CalendarLabelColumn().getRoot()); + eventSpaceBackground.getChildren().add(new EventSpaceBackground(calendar).getRoot()); + eventSpace.getChildren().add(CalendarEventSpace.createComparisonCalendarEventSpace(calendar).getRoot()); + } +} diff --git a/src/main/java/seedu/address/ui/CalendarEventSpace.java b/src/main/java/seedu/address/ui/CalendarEventSpace.java new file mode 100644 index 00000000000..123afdbc1d3 --- /dev/null +++ b/src/main/java/seedu/address/ui/CalendarEventSpace.java @@ -0,0 +1,261 @@ +package seedu.address.ui; + +import static java.time.temporal.ChronoUnit.MINUTES; + +import java.time.LocalTime; +import java.util.stream.Stream; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.StrokeType; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; + +/** + * The UI component that represents the space for displaying events in a calendar view. + */ +public class CalendarEventSpace extends UiPart { + private static final String FXML = "CalendarEventSpace.fxml"; + private static final LocalTime DEFAULT_CALENDAR_START_TIME = LocalTime.of(8, 0); + private static final LocalTime DEFAULT_CALENDAR_END_TIME = LocalTime.of(18, 0); + private static final int NUMBER_OF_ROWS = 8; + private static final int NUMBER_OF_HALF_HOURS_IN_HOUR = 2; + private static final int NUMBER_OF_MINUTES_IN_HALF_HOUR = 30; + private static final int NUMBER_OF_MINUTES_IN_AN_HOUR = 60; + private static final int NODE_WIDTH_PER_HALF_HOUR = 25; + private static final int MAXIMUM_DISPLAY_HOUR_OF_DAY = 23; + private static final int NODE_HEIGHT = 30; + + private final ReadOnlyCalendar calendar; + private final ObservableList eventList; + private final ObservableList rowList = FXCollections.observableArrayList(); + private LocalTime calendarStartTime = DEFAULT_CALENDAR_START_TIME; + private LocalTime calendarEndTime = DEFAULT_CALENDAR_END_TIME; + + @FXML + private GridPane eventSpace; + + /** + * Constructs a CalendarEventSpace with the given ReadOnlyCalendar. + * + * @param calendar The ReadOnlyCalendar to be displayed in the event space. + */ + public CalendarEventSpace(ReadOnlyCalendar calendar) { + super(FXML); + this.calendar = calendar; + eventList = calendar.getCurrentWeekEventList(); + } + + /** + * Creates the default EventSpace layout for calendar UI. + * + * @param calendar ReadOnlyCalendar to be displayed in the event space. + * @return CalendarEventSpace object with default layout. + */ + public static CalendarEventSpace createDefaultEventSpace(ReadOnlyCalendar calendar) { + CalendarEventSpace defaultEventSpace = new CalendarEventSpace(calendar); + defaultEventSpace.updateStartAndEnd(); + defaultEventSpace.initializeEventSpace(); + defaultEventSpace.addEventCards(); + defaultEventSpace.addListenerToEventList(); + return defaultEventSpace; + } + + /** + * Creates the EventSpace layout for comparison calendar UI. + * + * @param calendar ReadOnlyCalendar generated from comparison to be displayed in the event space. + * @return CalendarEventSpace object with comparison layout. + */ + public static CalendarEventSpace createComparisonCalendarEventSpace(ReadOnlyCalendar calendar) { + CalendarEventSpace comparisonEventSpace = new CalendarEventSpace(calendar); + comparisonEventSpace.updateStartAndEnd(); + comparisonEventSpace.initializeEventSpace(); + comparisonEventSpace.addSolidEventCards(); + comparisonEventSpace.addListenerToComparisonCalendarEventList(); + return comparisonEventSpace; + } + + /** + * Initializes the event space by creating and adding empty rows to the grid. + */ + private void initializeEventSpace() { + Stream.generate(AnchorPane::new).limit(NUMBER_OF_ROWS).forEachOrdered(pane -> { + pane.setPrefHeight(NODE_HEIGHT); + pane.setPrefWidth(calculateCalendarWidth()); + pane.setMaxWidth(calculateCalendarWidth()); + eventSpace.addRow(eventSpace.getRowCount(), pane); + rowList.add(pane); + }); + } + + /** + * Adds a listener to the event list to update the event space when events change. + */ + private void addListenerToEventList() { + eventList.addListener((ListChangeListener) c -> { + clear(); + updateStartAndEnd(); + initializeEventSpace(); + addEventCards(); + }); + } + + /** + * Adds a listener to the event list for the comparison calendar to update the event space when events change. + */ + private void addListenerToComparisonCalendarEventList() { + eventList.addListener((ListChangeListener) c -> { + clear(); + updateStartAndEnd(); + initializeEventSpace(); + addSolidEventCards(); + }); + } + + /** + * Clears the event space by removing all event cards. + */ + private void clear() { + eventSpace.getChildren().clear(); + rowList.clear(); + } + + /** + * Calculates the width of the calendar space based on the time range being displayed. + * + * @return The calculated width of the calendar space. + */ + private int calculateCalendarWidth() { + long minutesBetweenStartAndEnd = MINUTES.between(calendarStartTime, calendarEndTime); + int numberOfHoursToShow = calendarEndTime.getHour() - calendarStartTime.getHour(); + if (minutesBetweenStartAndEnd % NUMBER_OF_MINUTES_IN_AN_HOUR > 0) { + numberOfHoursToShow++; + } + return numberOfHoursToShow * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR; + } + + + /** + * Updates the start and end times for the displayed calendar. + */ + private void updateStartAndEnd() { + LocalTime newStartTime = calendar.getEarliestEventStartTimeInCurrentWeek() + .map(time -> { + return time.minusMinutes(time.getMinute()); + }) + .orElse(DEFAULT_CALENDAR_START_TIME); + LocalTime newEndTime = calendar.getLatestEventEndTimeInCurrentWeek() + .map(time -> { + if (time.getMinute() == 0 || time.getHour() == MAXIMUM_DISPLAY_HOUR_OF_DAY) { + return time; + } + return time.plusMinutes(NUMBER_OF_MINUTES_IN_AN_HOUR - time.getMinute()); + }) + .orElse(DEFAULT_CALENDAR_END_TIME); + + calendarStartTime = newStartTime.isBefore(DEFAULT_CALENDAR_START_TIME) + ? newStartTime : DEFAULT_CALENDAR_START_TIME; + calendarEndTime = newEndTime.isAfter(DEFAULT_CALENDAR_END_TIME) + ? newEndTime : DEFAULT_CALENDAR_END_TIME; + } + + /** + * Adds event cards to the event space for all events. + */ + private void addEventCards() { + eventList.forEach(event -> { + StackPane eventNode = generateEventCard(event); + rowList.get(findRow(event)).getChildren().add(eventNode); + AnchorPane.setLeftAnchor(eventNode, findLeftOffset(event)); + }); + } + + /** + * Adds solid event cards to the event space for comparison calendar. + */ + private void addSolidEventCards() { + eventList.forEach(event -> { + StackPane eventNode = generateSolidEventCard(event); + rowList.get(findRow(event)).getChildren().add(eventNode); + AnchorPane.setLeftAnchor(eventNode, findLeftOffset(event)); + }); + } + + /** + * Generates an event card for the given event. + * + * @param event The event for which to create a card. + * @return A StackPane containing the event card. + */ + private StackPane generateEventCard(Event event) { + double widthMultiplier = (double) event.getDurationOfEvent().toMinutes() / NUMBER_OF_MINUTES_IN_AN_HOUR; + StackPane cardHolder = new StackPane(); + + Rectangle cardRectangle = new Rectangle(); + cardRectangle.setHeight(NODE_HEIGHT); + cardRectangle.setWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR); + cardRectangle.setFill(Color.CRIMSON); + cardRectangle.setStroke(Color.BLACK); + cardRectangle.setStrokeType(StrokeType.INSIDE); + + + Label description = new Label(); + description.setMaxHeight(NODE_HEIGHT); + description.setMaxWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR); + description.setText(event.getDescriptionString()); + description.setAlignment(Pos.CENTER); + + cardHolder.getChildren().addAll(cardRectangle, description); + return cardHolder; + } + + /** + * Generates a solid event card (no description and single color) for the given event. + * + * @param event The event for which to create a card. + * @return A StackPane containing the solid event card. + */ + private StackPane generateSolidEventCard(Event event) { + double widthMultiplier = (double) event.getDurationOfEvent().toMinutes() / NUMBER_OF_MINUTES_IN_AN_HOUR; + StackPane cardHolder = new StackPane(); + Rectangle cardRectangle = new Rectangle(); + cardRectangle.setHeight(NODE_HEIGHT); + cardRectangle.setWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR); + cardRectangle.setFill(Color.GREY); + cardHolder.getChildren().add(cardRectangle); + return cardHolder; + } + + /** + * Determines the row (day) in which an event should be placed in the event space. + * + * @param event The event for which to find the row. + * @return The row (day) index. + */ + private int findRow(Event event) { + return event.getDayOfWeek().getValue(); + } + + + /** + * Calculates the left offset for positioning an event card in the event space. + * + * @param event The event for which to calculate the left offset. + * @return The left offset in pixels. + */ + private double findLeftOffset(Event event) { + return ((double) event.getMinutesFromTimeToStartTime(calendarStartTime) + / NUMBER_OF_MINUTES_IN_HALF_HOUR) * NODE_WIDTH_PER_HALF_HOUR; + } +} diff --git a/src/main/java/seedu/address/ui/CalendarLabelColumn.java b/src/main/java/seedu/address/ui/CalendarLabelColumn.java new file mode 100644 index 00000000000..689274e824d --- /dev/null +++ b/src/main/java/seedu/address/ui/CalendarLabelColumn.java @@ -0,0 +1,17 @@ +package seedu.address.ui; + +import javafx.scene.layout.Region; + +/** + * The UI component that represents the column of labels in a calendar view, typically displaying day names or headers. + */ +public class CalendarLabelColumn extends UiPart { + private static final String FXML = "CalendarLabelColumn.fxml"; + + /** + * Constructs a CalendarLabelColumn with the appropriate FXML file. + */ + public CalendarLabelColumn() { + super(FXML); + } +} diff --git a/src/main/java/seedu/address/ui/EventCard.java b/src/main/java/seedu/address/ui/EventCard.java new file mode 100644 index 00000000000..adb81ea90ab --- /dev/null +++ b/src/main/java/seedu/address/ui/EventCard.java @@ -0,0 +1,45 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.event.Event; + +/** + * A UI component that displays information of a {@code Event}. + */ +public class EventCard extends UiPart { + + private static final String FXML = "EventListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Event event; + + @FXML + private HBox cardPane; + @FXML + private Label description; + @FXML + private Label id; + @FXML + private Label eventPeriod; + + /** + * Creates a {@code EventCard} with the given {@code Event} and index to display. + */ + public EventCard(Event event, int displayedIndex) { + super(FXML); + this.event = event; + id.setText(displayedIndex + ". "); + description.setText(event.getDescription().getDescription()); + eventPeriod.setText(event.getEventPeriod().getFormattedPeriod()); + } +} diff --git a/src/main/java/seedu/address/ui/EventListContainer.java b/src/main/java/seedu/address/ui/EventListContainer.java new file mode 100644 index 00000000000..9ce8d86b476 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventListContainer.java @@ -0,0 +1,35 @@ +package seedu.address.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.address.model.event.Event; + +/** + * A pop-up window that shows an event list of a specific person in the addressbook. + */ +public class EventListContainer extends UiPart { + private static final String FXML = "EventListContainer.fxml"; + + @FXML + private StackPane eventListPlaceholder; + + private final EventListPanel eventListPanel; + + /** + * Constructs an EventListContainer with the given event list + */ + public EventListContainer(ObservableList eventList) { + super(FXML); + this.eventListPanel = new EventListPanel(eventList); + fillInnerPart(); + } + + /** + * Fills up the placeholder of this window + */ + void fillInnerPart() { + eventListPlaceholder.getChildren().add(eventListPanel.getRoot()); + } +} diff --git a/src/main/java/seedu/address/ui/EventListPanel.java b/src/main/java/seedu/address/ui/EventListPanel.java new file mode 100644 index 00000000000..5cea83c7b2b --- /dev/null +++ b/src/main/java/seedu/address/ui/EventListPanel.java @@ -0,0 +1,48 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.event.Event; + +/** + * Panel containing the list of events. + */ +public class EventListPanel extends BottomListPanel { + private static final String FXML = "EventListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EventListPanel.class); + + @FXML + private ListView eventListView; + + /** + * Creates a {@code EventListPanel} with the given {@code ObservableList}. + */ + public EventListPanel(ObservableList eventList) { + super(FXML); + eventListView.setItems(eventList); + eventListView.setCellFactory(listView -> new EventListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Event} using a {@code EventCard}. + */ + class EventListViewCell extends ListCell { + @Override + protected void updateItem(Event event, boolean empty) { + super.updateItem(event, empty); + + if (empty || event == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new EventCard(event, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/EventSpaceBackground.java b/src/main/java/seedu/address/ui/EventSpaceBackground.java new file mode 100644 index 00000000000..58aab043ab7 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventSpaceBackground.java @@ -0,0 +1,114 @@ +package seedu.address.ui; + +import java.time.LocalTime; +import java.util.stream.Stream; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; + +/** + * The UI component that represents the background grid for an event space, displaying time intervals. + */ +public class EventSpaceBackground extends UiPart { + private static final String FXML = "EventSpaceBackground.fxml"; + private static final LocalTime DEFAULT_CALENDAR_START_TIME = LocalTime.of(8, 0); + private static final LocalTime DEFAULT_CALENDAR_END_TIME = LocalTime.of(18, 0); + private static final int NUMBER_OF_HOURS_IN_ONE_DAY = 24; + private static final int MAXIMUM_DISPLAY_HOUR = 23; + private static final int NUMBER_OF_MINUTES_IN_AN_HOUR = 60; + + private LocalTime calendarStartTime = DEFAULT_CALENDAR_START_TIME; + private LocalTime calendarEndTime = DEFAULT_CALENDAR_END_TIME; + + private ReadOnlyCalendar calendar; + private ObservableList eventList; + private ObservableList columns; + + @FXML + private HBox backgroundGrid; + + /** + * Constructs an EventSpaceBackground with the given ReadOnlyCalendar. + * + * @param calendar The ReadOnlyCalendar to be displayed in the background. + */ + public EventSpaceBackground(ReadOnlyCalendar calendar) { + super(FXML); + this.calendar = calendar; + eventList = calendar.getCurrentWeekEventList(); + columns = FXCollections.observableArrayList(); + + fillBackground(); + updateStartAndEnd(); + showRelevantBackground(); + addListenerToEventList(); + } + + + /** + * Adds a listener to the event list to update the background when events change. + */ + private void addListenerToEventList() { + eventList.addListener((ListChangeListener) c -> { + updateStartAndEnd(); + showRelevantBackground(); + }); + } + + /** + * Fills the background with time intervals. + */ + public void fillBackground() { + Stream.iterate(LocalTime.MIDNIGHT, time -> time.plusHours(1)) + .limit(NUMBER_OF_HOURS_IN_ONE_DAY) + .map(EventSpaceBackgroundColumn::new) + .forEachOrdered(column -> columns.add(column)); + columns.forEach(column -> backgroundGrid.getChildren().add(column.getRoot())); + } + + /** + * Updates the start and end times based on the events in the calendar. + */ + private void updateStartAndEnd() { + LocalTime newStartTime = calendar.getEarliestEventStartTimeInCurrentWeek() + .map(time -> { + return time.minusMinutes(time.getMinute()); + }) + .orElse(DEFAULT_CALENDAR_START_TIME); + LocalTime newEndTime = calendar.getLatestEventEndTimeInCurrentWeek() + .map(time -> { + if (time.getMinute() == 0 || time.getHour() == MAXIMUM_DISPLAY_HOUR) { + return time; + } + return time.plusMinutes(NUMBER_OF_MINUTES_IN_AN_HOUR - time.getMinute()); + }) + .orElse(DEFAULT_CALENDAR_END_TIME); + + calendarStartTime = newStartTime.isBefore(DEFAULT_CALENDAR_START_TIME) + ? newStartTime : DEFAULT_CALENDAR_START_TIME; + calendarEndTime = newEndTime.isAfter(DEFAULT_CALENDAR_END_TIME) + ? newEndTime : DEFAULT_CALENDAR_END_TIME; + } + + /** + * Shows the relevant background columns based on the current time range. + */ + private void showRelevantBackground() { + columns.stream().filter(column -> !column.isWithin(calendarStartTime, calendarEndTime)) + .map(EventSpaceBackgroundColumn::getRoot).forEach(region -> { + region.setVisible(false); + region.setManaged(false); + }); + columns.stream().filter(column -> column.isWithin(calendarStartTime, calendarEndTime)) + .map(EventSpaceBackgroundColumn::getRoot).forEach(region -> { + region.setVisible(true); + region.setManaged(true); + }); + } +} diff --git a/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java b/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java new file mode 100644 index 00000000000..31e73f63871 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java @@ -0,0 +1,123 @@ +package seedu.address.ui; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Line; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.StrokeType; + +/** + * The UI component that represents a column in the event space background, displaying time intervals. + */ +public class EventSpaceBackgroundColumn extends UiPart { + private static final String FXML = "EventSpaceBackgroundColumn.fxml"; + private static final String DIVIDER_LINE_STROKE_COLOUR_STRING = "#86826a"; + private static final String RECTANGLE_STROKE_COLOUR_STRING = "#dbc3ae"; + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + private static final int NUMBER_OF_DAY_CELLS = 7; + private static final int RECTANGLE_ARC_VALUE = 5; + private static final int CELL_HEIGHT = 30; + private static final int CELL_WIDTH = 50; + private static final int RECTANGLE_WIDTH = 25; + private static final int DIVIDER_LINE_LENGTH = 28; + private static final int DIVIDER_LINE_STROKE_WIDTH = 2; + private static final double DIVIDER_LINE_OPACITY = 0.5; + private final LocalTime startTime; + + @FXML + private VBox columnContainer; + + @FXML + private Label startTimeLabel; + + /** + * Constructs an EventSpaceBackgroundColumn with the specified start time. + * + * @param startTime The start time represented by this column. + */ + public EventSpaceBackgroundColumn(LocalTime startTime) { + super(FXML); + this.startTime = startTime; + + setLabel(); + setColumnRectangles(); + } + + /** + * Checks if this column's time interval is within the specified time range. + * + * @param start The start time of the range (inclusive). + * @param end The end time of the range (exclusive). + * @return True if the column's time interval is within the specified range; otherwise, false. + */ + public boolean isWithin(LocalTime start, LocalTime end) { + return (startTime.equals(start) || (startTime.isAfter(start) && startTime.isBefore(end))) + && !startTime.equals(end); + } + + /** + * Sets the label to display the start time formatted as "HH:mm". + */ + private void setLabel() { + startTimeLabel.setText(startTime.format(TIME_FORMATTER)); + } + + /** + * Sets up the rectangles and divider line for each day cell in the column. + */ + private void setColumnRectangles() { + Stream.generate(this::constructDayColumnCell).limit(NUMBER_OF_DAY_CELLS) + .forEachOrdered(cell -> columnContainer.getChildren().add(cell)); + } + + /** + * Constructs a day column cell with rectangles and a divider line. + * + * @return StackPane representing a day column cell. + */ + private StackPane constructDayColumnCell() { + StackPane cellContainer = new StackPane(); + VBox.setVgrow(cellContainer, Priority.NEVER); + HBox.setHgrow(cellContainer, Priority.NEVER); + cellContainer.setPrefHeight(CELL_HEIGHT); + cellContainer.setPrefWidth(CELL_WIDTH); + HBox rectangleContainer = new HBox(); + Rectangle leftRectangle = new Rectangle(RECTANGLE_WIDTH, CELL_HEIGHT); + Rectangle rightRectangle = new Rectangle(RECTANGLE_WIDTH, CELL_HEIGHT); + Line dividerLine = new Line(); + + leftRectangle.setFill(Color.ANTIQUEWHITE); + rightRectangle.setFill(Color.BISQUE); + leftRectangle.setArcHeight(RECTANGLE_ARC_VALUE); + leftRectangle.setArcWidth(RECTANGLE_ARC_VALUE); + rightRectangle.setArcHeight(RECTANGLE_ARC_VALUE); + rightRectangle.setArcWidth(RECTANGLE_ARC_VALUE); + leftRectangle.setStroke(Paint.valueOf(RECTANGLE_STROKE_COLOUR_STRING)); + leftRectangle.setStrokeType(StrokeType.INSIDE); + rightRectangle.setStroke(Paint.valueOf(RECTANGLE_STROKE_COLOUR_STRING)); + rightRectangle.setStrokeType(StrokeType.INSIDE); + + dividerLine.setStroke(Paint.valueOf(DIVIDER_LINE_STROKE_COLOUR_STRING)); + dividerLine.setEndY(DIVIDER_LINE_LENGTH); + dividerLine.setStrokeWidth(DIVIDER_LINE_STROKE_WIDTH); + dividerLine.setOpacity(DIVIDER_LINE_OPACITY); + dividerLine.setFill(Color.GAINSBORO); + StackPane.setAlignment(dividerLine, Pos.CENTER); + + rectangleContainer.getChildren().addAll(leftRectangle, rightRectangle); + cellContainer.getChildren().addAll(rectangleContainer, dividerLine); + return cellContainer; + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..e58576f3b33 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s1-cs2103-f13-4.github.io/tp/UserGuide.html#quick-start"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..8a9c754a459 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -4,6 +4,7 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; @@ -12,10 +13,12 @@ import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing @@ -24,13 +27,18 @@ public class MainWindow extends UiPart { private static final String FXML = "MainWindow.fxml"; + private static final int BOTTOM_LIST_MODES = 2; + private static final int POPUP_LIST_HEIGHT = 500; + private static final int POPUP_LIST_WIDTH = 555; private final Logger logger = LogsCenter.getLogger(getClass()); private Stage primaryStage; private Logic logic; + private int bottomListIndicator; // Independent Ui parts residing in this Ui container + private BottomListPanel bottomListPanel; private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -41,6 +49,9 @@ public class MainWindow extends UiPart { @FXML private MenuItem helpMenuItem; + @FXML + private StackPane bottomListPanelPlaceholder; + @FXML private StackPane personListPanelPlaceholder; @@ -50,6 +61,13 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane calendarPlaceholder; + + private EventListPanel eventListPanel; + + private TaskListPanel taskListPanel; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -66,6 +84,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + bottomListIndicator = 0; } public Stage getPrimaryStage() { @@ -110,6 +129,12 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { + eventListPanel = new EventListPanel(logic.getEventList()); + taskListPanel = new TaskListPanel(logic.getTaskList()); + + bottomListPanel = eventListPanel; + bottomListPanelPlaceholder.getChildren().add(bottomListPanel.getRoot()); + personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); @@ -121,6 +146,8 @@ void fillInnerParts() { CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + calendarPlaceholder.getChildren().add(CalendarContainer.createDefaultCalendar(logic.getCalendar()).getRoot()); } /** @@ -163,6 +190,35 @@ private void handleExit() { primaryStage.hide(); } + private void handleComparison() { + CalendarContainer.displayComparisonCalendar(logic.getComparisonCalendar()); + } + + /** + * Switches the bottom list between the available lists. + */ + @FXML + private void handleSwitchBottomList() { + if (bottomListIndicator == 0) { + switchToTaskList(); + } else { + switchToEventList(); + } + bottomListIndicator = (bottomListIndicator + 1) % BOTTOM_LIST_MODES; + } + + private void handleViewEventList(Index index) { + Person person = logic.getFilteredPersonList().get(index.getZeroBased()); + Stage eventListStage = new Stage(); + eventListStage.setResizable(false); + eventListStage.setTitle(person.getName().toString() + "'s Event List"); + eventListStage.setMinHeight(POPUP_LIST_HEIGHT); + eventListStage.setMinWidth(POPUP_LIST_WIDTH); + EventListContainer root = new EventListContainer(person.getEventList()); + eventListStage.setScene(new Scene(root.getRoot())); + eventListStage.show(); + } + public PersonListPanel getPersonListPanel() { return personListPanel; } @@ -186,6 +242,18 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isSwitchBottomList()) { + handleSwitchBottomList(); + } + + if (commandResult.isViewEvents()) { + handleViewEventList(commandResult.getEventViewIndex()); + } + + if (commandResult.isShowCalendarComparison()) { + handleComparison(); + } + return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); @@ -193,4 +261,20 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Switches the Bottom List Panel to display the task list instead. + */ + public void switchToTaskList() { + bottomListPanel = taskListPanel; + bottomListPanelPlaceholder.getChildren().setAll(bottomListPanel.getRoot()); + } + + /** + * Switches the Bottom List Panel to display the event list instead. + */ + public void switchToEventList() { + bottomListPanel = eventListPanel; + bottomListPanelPlaceholder.getChildren().setAll(bottomListPanel.getRoot()); + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..a63b48fdcb9 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,12 +1,20 @@ package seedu.address.ui; +import static seedu.address.ui.UiConstants.POPUP_CALENDAR_HEIGHT; +import static seedu.address.ui.UiConstants.POPUP_CALENDAR_WIDTH; + import java.util.Comparator; +import javafx.event.EventHandler; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.Label; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.stage.Stage; import seedu.address.model.person.Person; /** @@ -15,6 +23,8 @@ public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; + private static final String TITLE_STRING_AFTER_NAME = "'s Calendar"; + private static final int NUMBER_OF_CLICK_TO_SHOW_CALENDAR = 2; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -55,5 +65,35 @@ public PersonCard(Person person, int displayedIndex) { person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + setClickListener(); + } + + /** + * Adds a listener to detect when the user double-clicks the PersonCard. + */ + private void setClickListener() { + cardPane.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + if (mouseEvent.getButton().equals(MouseButton.PRIMARY) + && mouseEvent.getClickCount() == NUMBER_OF_CLICK_TO_SHOW_CALENDAR) { + showCalendar(); + } + } + }); + } + + /** + * Display this person's calendar + */ + private void showCalendar() { + Stage calendarStage = new Stage(); + calendarStage.setResizable(false); + calendarStage.setTitle(person.getName().toString() + TITLE_STRING_AFTER_NAME); + calendarStage.setMinHeight(POPUP_CALENDAR_HEIGHT); + calendarStage.setMinWidth(POPUP_CALENDAR_WIDTH); + CalendarContainer root = CalendarContainer.createDefaultCalendar(person.getReadOnlyCalendar()); + calendarStage.setScene(new Scene(root.getRoot())); + calendarStage.show(); } } diff --git a/src/main/java/seedu/address/ui/TaskCard.java b/src/main/java/seedu/address/ui/TaskCard.java new file mode 100644 index 00000000000..71bdb63ae73 --- /dev/null +++ b/src/main/java/seedu/address/ui/TaskCard.java @@ -0,0 +1,36 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.task.Task; + +/** + * A UI component that displays information of a {@code Task}. + */ +public class TaskCard extends UiPart { + private static final String FXML = "TaskListCard.fxml"; + + public final Task task; + + @FXML + private HBox cardPane; + @FXML + private Label description; + @FXML + private Label id; + @FXML + private Label deadline; + + /** + * Creates a {@code TaskCard} with the given {@code Task} and index to display. + */ + public TaskCard(Task task, int displayedIndex) { + super(FXML); + this.task = task; + id.setText(displayedIndex + ". "); + description.setText(task.getDescriptionString()); + deadline.setText("Deadline: " + task.getDeadlineString()); + } +} diff --git a/src/main/java/seedu/address/ui/TaskListPanel.java b/src/main/java/seedu/address/ui/TaskListPanel.java new file mode 100644 index 00000000000..e1ee60bdc37 --- /dev/null +++ b/src/main/java/seedu/address/ui/TaskListPanel.java @@ -0,0 +1,47 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.task.Task; + +/** + * Panel containing a list of tasks. + */ +public class TaskListPanel extends BottomListPanel { + private static final String FXML = "TaskListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(TaskListPanel.class); + + @FXML + private ListView taskListView; + + /** + * Creates a {@code TaskListPanel} with the given {@code ObservableList}. + */ + public TaskListPanel(ObservableList taskList) { + super(FXML); + taskListView.setItems(taskList); + taskListView.setCellFactory(listView -> new TaskListPanel.TaskListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Task} using a {@code TaskCard}. + */ + class TaskListViewCell extends ListCell { + @Override + protected void updateItem(Task task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new TaskCard(task, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/UiConstants.java b/src/main/java/seedu/address/ui/UiConstants.java new file mode 100644 index 00000000000..e6490e5aa33 --- /dev/null +++ b/src/main/java/seedu/address/ui/UiConstants.java @@ -0,0 +1,9 @@ +package seedu.address.ui; + +/** + * Stores some constants used across multiple UI classes. + */ +public class UiConstants { + public static final int POPUP_CALENDAR_HEIGHT = 285; + public static final int POPUP_CALENDAR_WIDTH = 555; +} diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..1871afdb6ba 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -18,10 +18,8 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; - private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; - + private static final String ICON_APPLICATION = "/images/unimate_logo.png"; private Logic logic; private MainWindow mainWindow; @@ -84,5 +82,4 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { Platform.exit(); System.exit(1); } - } diff --git a/src/main/resources/images/unimate_logo.png b/src/main/resources/images/unimate_logo.png new file mode 100644 index 00000000000..22b0e26ea1c Binary files /dev/null and b/src/main/resources/images/unimate_logo.png differ diff --git a/src/main/resources/view/CalendarContainer.fxml b/src/main/resources/view/CalendarContainer.fxml new file mode 100644 index 00000000000..eaea0a4fc1f --- /dev/null +++ b/src/main/resources/view/CalendarContainer.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/CalendarEventSpace.fxml b/src/main/resources/view/CalendarEventSpace.fxml new file mode 100644 index 00000000000..39797785a6f --- /dev/null +++ b/src/main/resources/view/CalendarEventSpace.fxml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/view/CalendarLabelColumn.fxml b/src/main/resources/view/CalendarLabelColumn.fxml new file mode 100644 index 00000000000..d080b3eda06 --- /dev/null +++ b/src/main/resources/view/CalendarLabelColumn.fxml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..ed750843362 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -97,6 +97,7 @@ -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; + -fx-background-color: derive(#1d1d1d, 20%); } .list-cell:filled:even { @@ -120,6 +121,18 @@ -fx-text-fill: white; } +.calendar_header_label { + -fx-text-fill: "Segoe UI Semibold"; + -fx-font-size: 18px; + -fx-text-fill: #c21c1c; + -fx-text-alignment: center; +} + +.calendar_cell_border { + -fx-border-color: #654f4fba; + -fx-border-width: 1px; +} + .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; @@ -142,6 +155,10 @@ -fx-border-top-width: 1px; } +.pane-without-border { + -fx-background-color: derive(#1d1d1d, 20%); +} + .status-bar { -fx-background-color: derive(#1d1d1d, 30%); } diff --git a/src/main/resources/view/EventListCard.fxml b/src/main/resources/view/EventListCard.fxml new file mode 100644 index 00000000000..263ecc486f9 --- /dev/null +++ b/src/main/resources/view/EventListCard.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/EventListContainer.fxml b/src/main/resources/view/EventListContainer.fxml new file mode 100644 index 00000000000..2ba28ecf8db --- /dev/null +++ b/src/main/resources/view/EventListContainer.fxml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/view/EventListPanel.fxml b/src/main/resources/view/EventListPanel.fxml new file mode 100644 index 00000000000..7701eea5fa7 --- /dev/null +++ b/src/main/resources/view/EventListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/EventSpaceBackground.fxml b/src/main/resources/view/EventSpaceBackground.fxml new file mode 100644 index 00000000000..ce6af6366c7 --- /dev/null +++ b/src/main/resources/view/EventSpaceBackground.fxml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/view/EventSpaceBackgroundColumn.fxml b/src/main/resources/view/EventSpaceBackgroundColumn.fxml new file mode 100644 index 00000000000..07fad8a51a9 --- /dev/null +++ b/src/main/resources/view/EventSpaceBackgroundColumn.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..e8b3b9f31d7 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,16 +3,18 @@ + - + + + - + @@ -33,24 +35,46 @@ - + - + - + - + - + + + + + + + + + + + + +
    + + + + + +
    +
    +
    + + - + - + diff --git a/src/main/resources/view/TaskListCard.fxml b/src/main/resources/view/TaskListCard.fxml new file mode 100644 index 00000000000..23c49b6a37b --- /dev/null +++ b/src/main/resources/view/TaskListCard.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/TaskListPanel.fxml b/src/main/resources/view/TaskListPanel.fxml new file mode 100644 index 00000000000..e899e6f151e --- /dev/null +++ b/src/main/resources/view/TaskListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/test.txt b/src/test.txt new file mode 100644 index 00000000000..f2ffc6e25f7 --- /dev/null +++ b/src/test.txt @@ -0,0 +1,2 @@ +hello +hello 2 diff --git a/src/test/data/JsonAddressBookStorageTest/duplicatePersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/duplicatePersonAddressBook.json new file mode 100644 index 00000000000..e3d5b7e8bde --- /dev/null +++ b/src/test/data/JsonAddressBookStorageTest/duplicatePersonAddressBook.json @@ -0,0 +1,15 @@ +{ + "persons": [ { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + }, { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + } ] +} diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..2f4139c334c 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,13 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "events": [ ] }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "events": "events" } ] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json index ccd21f7d1a9..ae5ab6b0f20 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -3,6 +3,7 @@ "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "events": [ ] } ] } diff --git a/src/test/data/JsonCalendarStorageTest/invalidAndValidEventCalendar.json b/src/test/data/JsonCalendarStorageTest/invalidAndValidEventCalendar.json new file mode 100644 index 00000000000..04c0664eb31 --- /dev/null +++ b/src/test/data/JsonCalendarStorageTest/invalidAndValidEventCalendar.json @@ -0,0 +1,9 @@ +{ + "events" : [ { + "description" : "Valid", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + }, { + "description" : "", + "eventPeriod" : "2023-11-10 15:15 - 2023-11-10 16:00" + } ] +} diff --git a/src/test/data/JsonCalendarStorageTest/invalidEventCalendar.json b/src/test/data/JsonCalendarStorageTest/invalidEventCalendar.json new file mode 100644 index 00000000000..c0c2b747ab8 --- /dev/null +++ b/src/test/data/JsonCalendarStorageTest/invalidEventCalendar.json @@ -0,0 +1,6 @@ +{ + "events" : [ { + "description" : "", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] +} diff --git a/src/test/data/JsonCalendarStorageTest/notJsonFormatCalendar.json b/src/test/data/JsonCalendarStorageTest/notJsonFormatCalendar.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/JsonCalendarStorageTest/notJsonFormatCalendar.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..940050578f7 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,13 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", - "tags": [ "friends" ] + "tags": [ "friends" ], + "events" : [ ] }, { "name": "Alice Pauline", "phone": "94351253", "email": "pauline@example.com", - "address": "4th street" + "address": "4th street", + "events" : [ ] } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..ca33a2691f8 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,7 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" + "address": "4th street", + "events": [ ] } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..142889eef39 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -5,42 +5,61 @@ "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", - "tags" : [ "friends" ] + "tags" : [ "friends" ], + "events" : [ { + "description" : "Conference", + "eventPeriod" : "2023-11-15 08:30 - 2023-11-15 17:00" + } ] }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", - "tags" : [ "owesMoney", "friends" ] + "tags" : [ "owesMoney", "friends" ], + "events" : [ ] }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", - "tags" : [ ] + "tags" : [ ], + "events" : [ { + "description" : "Launch and Marketing Strategy Discussion", + "eventPeriod" : "2023-12-05 10:00 - 2023-12-05 12:00" + } ] }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", - "tags" : [ "friends" ] + "tags" : [ "friends" ], + "events" : [ ] }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", - "tags" : [ ] + "tags" : [ ], + "events" : [ ] }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", - "tags" : [ ] + "tags" : [ ], + "events" : [ { + "description" : "Team Meeting", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", - "tags" : [ ] + "tags" : [ ], + "events" : [ { + "description" : "Webinar", + "eventPeriod" : "2023-10-30 15:00 - 2023-10-30 16:30" + } ] } ] } diff --git a/src/test/data/JsonSerializableCalendarTest/duplicateEventCalendar.json b/src/test/data/JsonSerializableCalendarTest/duplicateEventCalendar.json new file mode 100644 index 00000000000..24bcb850430 --- /dev/null +++ b/src/test/data/JsonSerializableCalendarTest/duplicateEventCalendar.json @@ -0,0 +1,9 @@ +{ + "events" : [ { + "description" : "Weekly team meeting", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + }, { + "description" : "Weekly team meeting", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] +} diff --git a/src/test/data/JsonSerializableCalendarTest/invalidEventCalendar.json b/src/test/data/JsonSerializableCalendarTest/invalidEventCalendar.json new file mode 100644 index 00000000000..c0c2b747ab8 --- /dev/null +++ b/src/test/data/JsonSerializableCalendarTest/invalidEventCalendar.json @@ -0,0 +1,6 @@ +{ + "events" : [ { + "description" : "", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] +} diff --git a/src/test/data/JsonSerializableCalendarTest/typicalEventsCalendar.json b/src/test/data/JsonSerializableCalendarTest/typicalEventsCalendar.json new file mode 100644 index 00000000000..67acc329e62 --- /dev/null +++ b/src/test/data/JsonSerializableCalendarTest/typicalEventsCalendar.json @@ -0,0 +1,19 @@ +{ + "_comment": "Calendar save file which contains the same Event values as in TypicalEvents#getTypicalCalendar()", + "events" : [ { + "description" : "Conference", + "eventPeriod" : "2023-11-15 08:30 - 2023-11-15 17:00" + }, { + "description" : "Launch and Marketing Strategy Discussion", + "eventPeriod" : "2023-12-05 10:00 - 2023-12-05 12:00" + }, { + "description" : "Team Meeting", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + }, { + "description" : "Webinar", + "eventPeriod" : "2023-10-30 15:00 - 2023-10-30 16:30" + }, { + "description" : "Workshop", + "eventPeriod" : "2023-11-10 14:00 - 2023-11-10 16:30" + } ] +} diff --git a/src/test/data/JsonSerializableTaskManagerTest/duplicateTaskTaskManager.json b/src/test/data/JsonSerializableTaskManagerTest/duplicateTaskTaskManager.json new file mode 100644 index 00000000000..3decddc3893 --- /dev/null +++ b/src/test/data/JsonSerializableTaskManagerTest/duplicateTaskTaskManager.json @@ -0,0 +1,9 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "-" + }, { + "description" : "Drink Water", + "deadline" : "-" + } ] +} diff --git a/src/test/data/JsonSerializableTaskManagerTest/invalidTaskTaskManager.json b/src/test/data/JsonSerializableTaskManagerTest/invalidTaskTaskManager.json new file mode 100644 index 00000000000..174e1726608 --- /dev/null +++ b/src/test/data/JsonSerializableTaskManagerTest/invalidTaskTaskManager.json @@ -0,0 +1,6 @@ +{ + "tasks" : [ { + "description" : "", + "deadline" : "2023-10-25 09:00" + } ] +} diff --git a/src/test/data/JsonSerializableTaskManagerTest/typicalTasksTaskManager.json b/src/test/data/JsonSerializableTaskManagerTest/typicalTasksTaskManager.json new file mode 100644 index 00000000000..aef9e6bedab --- /dev/null +++ b/src/test/data/JsonSerializableTaskManagerTest/typicalTasksTaskManager.json @@ -0,0 +1,13 @@ +{ + "_comment": "TaskManager save file which contains the same task values as in TypicalTasks#getTypicalTaskManager()", + "tasks" : [ { + "description" : "Submit Assignment", + "deadline" : "2023-11-15 08:30" + }, { + "description" : "Drink more water!", + "deadline" : "-" + }, { + "description" : "Lunch", + "deadline" : "2023-11-15 15:00" + } ] +} diff --git a/src/test/data/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json b/src/test/data/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json new file mode 100644 index 00000000000..02317e3483e --- /dev/null +++ b/src/test/data/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json @@ -0,0 +1,9 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "-" + }, { + "description" : "Task with invalid deadline", + "deadline" : "Invalid Deadline" + } ] +} diff --git a/src/test/data/JsonTaskManagerStorageTest/invalidTaskTaskManager.json b/src/test/data/JsonTaskManagerStorageTest/invalidTaskTaskManager.json new file mode 100644 index 00000000000..80047b08e4e --- /dev/null +++ b/src/test/data/JsonTaskManagerStorageTest/invalidTaskTaskManager.json @@ -0,0 +1,6 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "Invalid Deadline" + } ] +} diff --git a/src/test/data/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json b/src/test/data/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/duplicatePersonAddressBook.json b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/duplicatePersonAddressBook.json new file mode 100644 index 00000000000..e3d5b7e8bde --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/duplicatePersonAddressBook.json @@ -0,0 +1,15 @@ +{ + "persons": [ { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + }, { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json new file mode 100644 index 00000000000..2f4139c334c --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -0,0 +1,15 @@ +{ + "persons": [ { + "name": "Valid Person", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + }, { + "name": "Person With Invalid Phone Field", + "phone": "948asdf2424", + "email": "hans@example.com", + "address": "4th street", + "events": "events" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidPersonAddressBook.json new file mode 100644 index 00000000000..ae5ab6b0f20 --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -0,0 +1,9 @@ +{ + "persons": [ { + "name": "Person with invalid name field: Ha!ns Mu@ster", + "phone": "9482424", + "email": "hans@example.com", + "address": "4th street", + "events": [ ] + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/notJsonFormatAddressBook.json b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/notJsonFormatAddressBook.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonAddressBookStorageTest/notJsonFormatAddressBook.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/duplicateEventsCalendar.json b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/duplicateEventsCalendar.json new file mode 100644 index 00000000000..97fbce2756b --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/duplicateEventsCalendar.json @@ -0,0 +1,9 @@ +{ + "events" : [ { + "description" : "Duplicate Event", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + }, { + "description" : "Duplicate Event", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidAndValidEventCalendar.json b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidAndValidEventCalendar.json new file mode 100644 index 00000000000..03ec05fce8f --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidAndValidEventCalendar.json @@ -0,0 +1,9 @@ +{ + "events" : [ { + "description" : "Valid Event", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + }, { + "description" : "", + "eventPeriod" : "2023-11-10 15:15 - 2023-11-10 16:00" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidEventCalendar.json b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidEventCalendar.json new file mode 100644 index 00000000000..c0c2b747ab8 --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/invalidEventCalendar.json @@ -0,0 +1,6 @@ +{ + "events" : [ { + "description" : "", + "eventPeriod" : "2023-10-25 09:00 - 2023-10-25 10:30" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/notJsonFormatCalendar.json b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/notJsonFormatCalendar.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonCalendarStorageTest/notJsonFormatCalendar.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/duplicateTaskTaskManager.json b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/duplicateTaskTaskManager.json new file mode 100644 index 00000000000..3decddc3893 --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/duplicateTaskTaskManager.json @@ -0,0 +1,9 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "-" + }, { + "description" : "Drink Water", + "deadline" : "-" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json new file mode 100644 index 00000000000..02317e3483e --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidAndValidTaskTaskManager.json @@ -0,0 +1,9 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "-" + }, { + "description" : "Task with invalid deadline", + "deadline" : "Invalid Deadline" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidTaskTaskManager.json b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidTaskTaskManager.json new file mode 100644 index 00000000000..80047b08e4e --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/invalidTaskTaskManager.json @@ -0,0 +1,6 @@ +{ + "tasks" : [ { + "description" : "Drink Water", + "deadline" : "Invalid Deadline" + } ] +} diff --git a/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json new file mode 100644 index 00000000000..a1097343b5d --- /dev/null +++ b/src/test/data/ManualTestingSampleStorageFiles/JsonTaskManagerStorageTest/notJsonFormatTaskManager.json @@ -0,0 +1 @@ +not json format! diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index baf8ce336a2..4554aab41f9 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -1,6 +1,7 @@ package seedu.address.logic; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; @@ -18,17 +19,23 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import javafx.collections.FXCollections; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.calendar.UniMateCalendar; import seedu.address.model.person.Person; +import seedu.address.model.task.TaskManager; import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonCalendarStorage; +import seedu.address.storage.JsonTaskManagerStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.StorageManager; import seedu.address.testutil.PersonBuilder; @@ -47,8 +54,12 @@ public class LogicManagerTest { public void setUp() { JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); + JsonCalendarStorage calendarStorage = new JsonCalendarStorage(temporaryFolder.resolve("calendar.json")); + JsonTaskManagerStorage taskManagerStorage = new JsonTaskManagerStorage( + temporaryFolder.resolve("taskManager.json")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); - StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); + StorageManager storage = new StorageManager(addressBookStorage, calendarStorage, taskManagerStorage, + userPrefsStorage); logic = new LogicManager(model, storage); } @@ -64,6 +75,40 @@ public void execute_commandExecutionError_throwsCommandException() { assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } + @Test + public void getEventList() { + assertEquals(logic.getEventList(), FXCollections.observableArrayList()); + } + + @Test + public void getTaskList() { + assertEquals(logic.getTaskList(), FXCollections.observableArrayList()); + } + + @Test + public void getAddressBook() { + assertEquals(logic.getAddressBook(), new AddressBook()); + } + + @Test + public void getCalendar() { + assertEquals(logic.getCalendar(), new UniMateCalendar()); + } + + @Test + public void getTaskManager() { + assertEquals(logic.getTaskManager(), new TaskManager()); + } + + @Test + public void getCurrentWeekEventList() { + assertEquals(logic.getCurrentWeekEventList(), FXCollections.observableArrayList()); + } + @Test + public void getAddressBookFilePath() { + assertNotNull(logic.getAddressBookFilePath()); + } + @Test public void execute_validCommand_success() throws Exception { String listCommand = ListCommand.COMMAND_WORD; @@ -123,7 +168,8 @@ private void assertCommandException(String inputCommand, String expectedMessage) */ private void assertCommandFailure(String inputCommand, Class expectedException, String expectedMessage) { - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), model.getTaskManager(), + new UserPrefs()); assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); } @@ -141,7 +187,8 @@ private void assertCommandFailure(String inputCommand, Class getCurrentWeekEventList() { + throw new AssertionError("This method should not be called."); + } + @Override public boolean hasPerson(Person person) { throw new AssertionError("This method should not be called."); @@ -148,15 +192,89 @@ public void setPerson(Person target, Person editedPerson) { throw new AssertionError("This method should not be called."); } + @Override + public boolean canAddEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public List eventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar eventList) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getEventList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getTaskList() { + throw new AssertionError("This method should not be called."); + } + @Override public ObservableList getFilteredPersonList() { throw new AssertionError("This method should not be called."); } + @Override + public void addTask(Task task) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Task deleteTask(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public TaskManager getTaskManager() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void sortTasksBy(String comparatorType) { + throw new AssertionError("This method should not be called."); + } + @Override public void updateFilteredPersonList(Predicate predicate) { throw new AssertionError("This method should not be called."); } + @Override + public void sortPersonList(Comparator personComparator) { + throw new AssertionError("This method should not be called."); + } } /** @@ -200,5 +318,4 @@ public ReadOnlyAddressBook getAddressBook() { return new AddressBook(); } } - } diff --git a/src/test/java/seedu/address/logic/commands/AddContactEventCommandTest.java b/src/test/java/seedu/address/logic/commands/AddContactEventCommandTest.java new file mode 100644 index 00000000000..522c0d8ec6c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddContactEventCommandTest.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.LAUNCH; +import static seedu.address.testutil.TypicalEvents.TRAINING; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +public class AddContactEventCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + + @Test + public void execute_addEvent_success() { + Person person = ALICE; + Event event = TRAINING; + AddContactEventCommand addContactEventCommand = new AddContactEventCommand(INDEX_FIRST_PERSON, event); + + String expectedMessage = String.format(AddContactEventCommand.MESSAGE_ADD_EVENT_TO_PERSON_SUCCESS, + person.getName(), Messages.format(event)); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(0), person); + + assertCommandSuccess(addContactEventCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddContactEventCommand addContactEventCommand = new AddContactEventCommand(outOfBoundIndex, TRAINING); + + assertCommandFailure(addContactEventCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Add event to filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + AddContactEventCommand addContactEventCommand = new AddContactEventCommand(outOfBoundIndex, TRAINING); + + assertCommandFailure(addContactEventCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final AddContactEventCommand standardCommand = new AddContactEventCommand(INDEX_FIRST_PERSON, TRAINING); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new AddContactEventCommand(INDEX_SECOND_PERSON, TRAINING))); + + // different event -> returns false + assertFalse(standardCommand.equals(new AddContactEventCommand(INDEX_FIRST_PERSON, LAUNCH))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + Event event = TRAINING; + AddContactEventCommand addContactEventCommand = new AddContactEventCommand(index, event); + String expected = AddContactEventCommand.class.getCanonicalName() + "{index=" + index + ", event=" + + event + "}"; + assertEquals(expected, addContactEventCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/AddEventCommandTest.java b/src/test/java/seedu/address/logic/commands/AddEventCommandTest.java new file mode 100644 index 00000000000..c33fedb2b6a --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddEventCommandTest.java @@ -0,0 +1,300 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; +import seedu.address.testutil.EventBuilder; + +public class AddEventCommandTest { + @Test + public void constructor_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddEventCommand(null)); + } + + @Test + public void constructor_eventAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingEventAdded modelStub = new ModelStubAcceptingEventAdded(); + Event validEvent = new EventBuilder().build(); + + CommandResult commandResult = new AddEventCommand(validEvent).execute(modelStub); + assertEquals(String.format(AddEventCommand.MESSAGE_SUCCESS, Messages.format(validEvent)), + commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validEvent), modelStub.eventsAdded); + } + + @Test + public void execute_conflictingEvent_throwsCommandException() { + Event validEvent = new EventBuilder().build(); + AddEventCommand addEventCommand = new AddEventCommand(validEvent); + ModelStub modelStub = new ModelStubWithEvent(validEvent); + + assertThrows(CommandException.class, AddEventCommand.MESSAGE_EVENT_CONFLICT, () -> addEventCommand + .execute(modelStub)); + } + + @Test + public void equalsTest() { + Event validEvent = new EventBuilder().build(); + EventBuilder otherValidEventBuilder = new EventBuilder(); + otherValidEventBuilder.withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER); + Event otherEvent = otherValidEventBuilder.build(); + AddEventCommand addEventCommand = new AddEventCommand(validEvent); + AddEventCommand notEqualAddEventCommand = new AddEventCommand(otherEvent); + Object nonAddEventCommandObject = new Object(); + + assertTrue(addEventCommand.equals(addEventCommand)); + + assertFalse(addEventCommand.equals(notEqualAddEventCommand)); + + assertFalse(addEventCommand.equals(nonAddEventCommandObject)); + } + + /** + * A default model stub that have all the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getCalendarFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendarFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getTaskManagerFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setTaskManagerFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendar(ReadOnlyCalendar newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getCurrentWeekEventList() { + throw new AssertionError("This method should not be called."); + } + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canAddEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public List eventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar eventList) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getEventList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getTaskList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTask(Task task) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Task deleteTask(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public TaskManager getTaskManager() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void sortTasksBy(String comparatorType) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + @Override + public void sortPersonList(Comparator personComparator) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single event. + */ + private class ModelStubWithEvent extends AddEventCommandTest.ModelStub { + private final Event event; + + ModelStubWithEvent(Event event) { + requireNonNull(event); + this.event = event; + } + + @Override + public boolean canAddEvent(Event other) { + return !event.isConflicting(other); + } + } + + /** + * A Model stub that always accept the person being added. + */ + private class ModelStubAcceptingEventAdded extends AddEventCommandTest.ModelStub { + final ArrayList eventsAdded = new ArrayList(); + + @Override + public boolean canAddEvent(Event event) { + requireNonNull(event); + return eventsAdded.stream().anyMatch(x -> !x.isConflicting(event)) || eventsAdded.isEmpty(); + } + + @Override + public void addEvent(Event event) { + requireNonNull(event); + eventsAdded.add(event); + } + } +} diff --git a/src/test/java/seedu/address/logic/commands/AddTaskCommandTest.java b/src/test/java/seedu/address/logic/commands/AddTaskCommandTest.java new file mode 100644 index 00000000000..ddce8187177 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddTaskCommandTest.java @@ -0,0 +1,301 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; +import seedu.address.model.task.exceptions.DuplicateTaskException; +import seedu.address.testutil.TaskBuilder; + +class AddTaskCommandTest { + + @Test + public void constructor_nullTask_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddTaskCommand(null)); + } + + @Test + public void constructor_taskAcceptedByModel_addSuccessful() throws CommandException { + ModelStubAcceptingTaskAdded modelStub = new ModelStubAcceptingTaskAdded(); + Task validTask = new TaskBuilder().build(); + + CommandResult commandResult = new AddTaskCommand(validTask).execute(modelStub); + assertEquals(String.format(AddTaskCommand.MESSAGE_SUCCESS, Messages.format(validTask)), + commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validTask), modelStub.tasksAdded); + } + + @Test + public void execute_conflictingEvent_throwsCommandException() { + Task validTask = new TaskBuilder().build(); + AddTaskCommand addTaskCommand = new AddTaskCommand(validTask); + AddTaskCommandTest.ModelStub modelStub = new AddTaskCommandTest.ModelStubWithTask(validTask); + + assertThrows(CommandException.class, String.format(AddTaskCommand.MESSAGE_DUPLICATE_TASK, + Messages.format(validTask)), () -> addTaskCommand.execute(modelStub)); + } + + @Test + public void equalsTest() { + Task validTask = new TaskBuilder().build(); + Task otherValidTask = new TaskBuilder().withDeadline(VALID_END_DATE_LATER).build(); + AddTaskCommand addTaskCommand = new AddTaskCommand(validTask); + AddTaskCommand notEqualAddTaskCommand = new AddTaskCommand(otherValidTask); + Object nonAddTaskCommandObject = new Object(); + + assertTrue(addTaskCommand.equals(addTaskCommand)); + + assertFalse(addTaskCommand.equals(notEqualAddTaskCommand)); + + assertFalse(addTaskCommand.equals(nonAddTaskCommandObject)); + } + + @Test + void toStringMethod() { + Task toAdd = new TaskBuilder().build(); + AddTaskCommand addTaskCommand = new AddTaskCommand(toAdd); + String expected = AddTaskCommand.class.getCanonicalName() + "{taskToAdd=" + toAdd + "}"; + assertEquals(expected, addTaskCommand.toString()); + } + + /** + * A default model stub that have all the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getCalendarFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendarFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getTaskManagerFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setTaskManagerFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendar(ReadOnlyCalendar newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getCurrentWeekEventList() { + throw new AssertionError("This method should not be called."); + } + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canAddEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public List eventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar eventList) { + + } + + @Override + public ObservableList getEventList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getTaskList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTask(Task task) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Task deleteTask(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public TaskManager getTaskManager() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void sortTasksBy(String comparatorType) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + @Override + public void sortPersonList(Comparator personComparator) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single task. + */ + private class ModelStubWithTask extends AddTaskCommandTest.ModelStub { + private final Task task; + + ModelStubWithTask(Task task) { + requireNonNull(task); + this.task = task; + } + + @Override + public void addTask(Task task) { + throw new DuplicateTaskException(); + } + } + + /** + * A Model stub that always accept the person being added. + */ + private class ModelStubAcceptingTaskAdded extends AddTaskCommandTest.ModelStub { + final ArrayList tasksAdded = new ArrayList(); + + @Override + public void addTask(Task task) { + requireNonNull(task); + tasksAdded.add(task); + } + } +} diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java index 80d9110c03a..cbaf5345e39 100644 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java @@ -1,7 +1,9 @@ package seedu.address.logic.commands; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import org.junit.jupiter.api.Test; @@ -22,8 +24,10 @@ public void execute_emptyAddressBook_success() { @Test public void execute_nonEmptyAddressBook_success() { - Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); expectedModel.setAddressBook(new AddressBook()); assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/ClearEventsCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearEventsCommandTest.java new file mode 100644 index 00000000000..6abf84f0a53 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ClearEventsCommandTest.java @@ -0,0 +1,326 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.EventPeriodBuilder; + +class ClearEventsCommandTest { + private static final EventPeriod defaultPeriod = new EventPeriodBuilder().build(); + private static final ClearEventsCommand defaultTrueCommand = + new ClearEventsCommand(defaultPeriod, true); + private static final ClearEventsCommand defaultFalseCommand = + new ClearEventsCommand(defaultPeriod, false); + + @Test + public void constructor_nullPerson_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new ClearEventsCommand(null, true)); + assertThrows(NullPointerException.class, () -> new ClearEventsCommand(null, false)); + } + + @Test + public void execute_noEvent_throwsCommandException() { + ClearEventsCommandTest.ModelStub modelStub = new ClearEventsCommandTest.ModelStubWithNoEvent(); + + assertThrows(CommandException.class, ClearEventsCommand.MESSAGE_NO_EVENTS, () -> defaultFalseCommand + .execute(modelStub)); + assertThrows(CommandException.class, ClearEventsCommand.MESSAGE_NO_EVENTS, () -> defaultTrueCommand + .execute(modelStub)); + } + + @Test + public void constructor_oneEventClearedFromModel_deleteSuccessful() throws Exception { + Event validEvent = new EventBuilder().build(); + ClearEventsCommandTest.ModelStubWithOneEvent modelStub = + new ClearEventsCommandTest.ModelStubWithOneEvent(validEvent); + + EventPeriodBuilder builder = new EventPeriodBuilder(); + builder.changeStartAndEnd(EventBuilder.DEFAULT_START_TIME_STRING, EventBuilder.DEFAULT_END_TIME_STRING); + EventPeriod eventPeriod = builder.build(); + + CommandResult commandResult = new ClearEventsCommand(eventPeriod, true).execute(modelStub); + CommandResult commandResult2 = new ClearEventsCommand(eventPeriod, false).execute(modelStub); + assertEquals(ClearEventsCommand.MESSAGE_SUCCESS + "1. " + Messages.format(validEvent) + "\n", + commandResult.getFeedbackToUser()); + assertEquals(ClearEventsCommand.MESSAGE_WITHIN_RANGE + + "1. " + Messages.format(validEvent) + "\n" + + ClearEventsCommand.MESSAGE_ADD_CONFIRMATION + + String.format(ClearEventsCommand.COMMAND_RESEND_FORMAT, EventBuilder.DEFAULT_START_TIME_STRING, + EventBuilder.DEFAULT_END_TIME_STRING), + commandResult2.getFeedbackToUser()); + } + + @Test + public void equals() { + EventPeriodBuilder nonDefaultBuilder = new EventPeriodBuilder(); + nonDefaultBuilder.changeStart("2023-01-02 08:00"); + EventPeriod nonDefaultPeriod = nonDefaultBuilder.build(); + ClearEventsCommand nonDefaultTrueCommand = new ClearEventsCommand(nonDefaultPeriod, true); + + // same object -> returns true + assertTrue(defaultTrueCommand.equals(defaultTrueCommand)); + + // same values -> returns true + ClearEventsCommand clearEventsTrueDefaultCopy = new ClearEventsCommand(defaultPeriod, true); + assertTrue(defaultTrueCommand.equals(clearEventsTrueDefaultCopy)); + + // different types -> returns false + assertFalse(defaultTrueCommand.equals(1)); + + // null -> returns false + assertFalse(defaultTrueCommand.equals(null)); + + // different timing -> returns false + assertFalse(defaultTrueCommand.equals(nonDefaultTrueCommand)); + + // different confirmation -> returns false + assertFalse(defaultTrueCommand.equals(defaultFalseCommand)); + } + + @Test + void toStringMethod() { + String expected = ClearEventsCommand.class.getCanonicalName() + "{toClearWithin=" + defaultPeriod + + ", confirmed=true}"; + assertEquals(expected, defaultTrueCommand.toString()); + } + + /** + * A default model stub that have all the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getCalendarFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendarFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getTaskManagerFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setTaskManagerFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendar(ReadOnlyCalendar newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getCurrentWeekEventList() { + throw new AssertionError("This method should not be called."); + } + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canAddEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public List eventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar eventList) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getEventList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getTaskList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTask(Task task) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Task deleteTask(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public TaskManager getTaskManager() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void sortTasksBy(String comparatorType) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + @Override + public void sortPersonList(Comparator personComparator) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single event. + */ + private class ModelStubWithNoEvent extends ClearEventsCommandTest.ModelStub { + + @Override + public List eventsInRange(EventPeriod eventPeriod) { + return List.of(); + } + } + + private class ModelStubWithOneEvent extends ClearEventsCommandTest.ModelStub { + private final Event event; + + ModelStubWithOneEvent(Event event) { + requireNonNull(event); + this.event = event; + } + + @Override + public List eventsInRange(EventPeriod eventPeriod) { + return List.of(event); + } + + @Override + public void deleteEventsInRange(EventPeriod eventPeriod) { + return; + } + } +} diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java index 7b8c7cd4546..42798c887f2 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Test; +import seedu.address.commons.core.index.Index; + public class CommandResultTest { @Test public void equals() { @@ -14,7 +16,7 @@ public void equals() { // same values -> returns true assertTrue(commandResult.equals(new CommandResult("feedback"))); - assertTrue(commandResult.equals(new CommandResult("feedback", false, false))); + assertTrue(commandResult.equals(new CommandResult("feedback", false, false, false, false))); // same object -> returns true assertTrue(commandResult.equals(commandResult)); @@ -29,10 +31,13 @@ public void equals() { assertFalse(commandResult.equals(new CommandResult("different"))); // different showHelp value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", true, false))); + assertFalse(commandResult.equals(new CommandResult("feedback", true, false, false, false))); // different exit value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + assertFalse(commandResult.equals(new CommandResult("feedback", false, true, false, false))); + + // different switchBottomList value -> returns false + assertFalse(commandResult.equals(new CommandResult("feedback", false, false, false, true))); } @Test @@ -46,10 +51,37 @@ public void hashcode() { assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode()); // different showHelp value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false, false, false).hashCode()); // different exit value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true, false, false).hashCode()); + + // different switchBottomList value -> returns different hashCode + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, false, false, true).hashCode()); + } + + @Test + public void indicatorViewEvents_validViewEventsIndicator_success() { + CommandResult.ViewEventsIndicator viewEventsIndicator = new CommandResult.ViewEventsIndicator(1); + assertTrue(viewEventsIndicator.isViewEvents()); + } + + @Test + public void indicatorGetIndex_validViewEventsIndicator_success() { + CommandResult.ViewEventsIndicator viewEventsIndicator = new CommandResult.ViewEventsIndicator(1); + assertEquals(viewEventsIndicator.getIndex(), Index.fromOneBased(1)); + } + + @Test + public void isViewEvents_validViewEventsIndicator_success() { + CommandResult commandResult = new CommandResult("feedback", Index.fromOneBased(1)); + assertTrue(commandResult.isViewEvents()); + } + + @Test + public void getIndex_validViewEventsIndicator_success() { + CommandResult commandResult = new CommandResult("feedback", Index.fromOneBased(1)); + assertEquals(commandResult.getEventViewIndex(), Index.fromOneBased(1)); } @Test @@ -57,7 +89,8 @@ public void toStringMethod() { CommandResult commandResult = new CommandResult("feedback"); String expected = CommandResult.class.getCanonicalName() + "{feedbackToUser=" + commandResult.getFeedbackToUser() + ", showHelp=" + commandResult.isShowHelp() - + ", exit=" + commandResult.isExit() + "}"; + + ", exit=" + commandResult.isExit() + + ", switchBottomList=" + commandResult.isSwitchBottomList() + "}"; assertEquals(expected, commandResult.toString()); } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..4d32e47dfea 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -4,6 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -20,6 +23,7 @@ import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.PersonFilterBuilder; /** * Contains helper methods for testing commands. @@ -28,15 +32,28 @@ public class CommandTestUtil { public static final String VALID_NAME_AMY = "Amy Bee"; public static final String VALID_NAME_BOB = "Bob Choo"; + public static final String VALID_NAME_BOB_LOWER = "bob Choo"; + public static final String VALID_NAME_CANDY = "Candy C"; + public static final String VALID_NAME_DANIEL = "Daniel Mcadoo Dunbar"; public static final String VALID_PHONE_AMY = "11111111"; public static final String VALID_PHONE_BOB = "22222222"; public static final String VALID_EMAIL_AMY = "amy@example.com"; public static final String VALID_EMAIL_BOB = "bob@example.com"; public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_ADDRESS_BOB_LOWER = "block 123, Bobby Street 3"; + public static final String VALID_ADDRESS_CANDY = "Block 133, Candy Street 6"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; - + public static final String VALID_DESCRIPTION = "sleep"; + public static final String VALID_UNUSED_DESCRIPTION = "THIS IS A PLACEHOLDER"; + public static final String VALID_START_DATE_EARLIER = "2023-01-01 08:00"; + public static final String VALID_END_DATE_EARLIER = "2023-01-01 09:00"; + public static final String VALID_START_DATE_LATER = "2024-01-01 08:00"; + public static final String VALID_END_DATE_LATER = "2024-01-01 09:00"; + public static final String EVENT_DESC_SLEEP = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION; + public static final String START_DATE_DESC_EARLIER = " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER; + public static final String END_DATE_DESC_EARLIER = " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; @@ -53,6 +70,13 @@ public class CommandTestUtil { public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags + public static final String INVALID_DESCRIPTION = ""; // empty string not allowed. + public static final String INVALID_DATE = "2023-13-35 16:80"; // No such time and date exists + public static final String INVALID_START_DATE = "2023-99-01 08:00"; + public static final String INVALID_END_DATE = "01-01-2023 08:00"; + public static final String INVALID_EVENT = " " + PREFIX_EVENT_DESCRIPTION + INVALID_DESCRIPTION + + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; @@ -60,6 +84,10 @@ public class CommandTestUtil { public static final EditCommand.EditPersonDescriptor DESC_AMY; public static final EditCommand.EditPersonDescriptor DESC_BOB; + public static final FilterCommand.PersonFilter FILTER_AMY; + + public static final FilterCommand.PersonFilter FILTER_BOB; + static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) @@ -67,6 +95,12 @@ public class CommandTestUtil { DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + FILTER_AMY = new PersonFilterBuilder().withName(VALID_NAME_AMY) + .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withTags(VALID_TAG_FRIEND).build(); + FILTER_BOB = new PersonFilterBuilder().withName(VALID_NAME_BOB) + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); } /** diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java index b6f332eabca..b9243bc94e3 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java @@ -6,9 +6,11 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import org.junit.jupiter.api.Test; @@ -25,7 +27,8 @@ */ public class DeleteCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { @@ -35,7 +38,8 @@ public void execute_validIndexUnfilteredList_success() { String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete)); - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), + model.getTaskManager(), new UserPrefs()); expectedModel.deletePerson(personToDelete); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); @@ -59,7 +63,8 @@ public void execute_validIndexFilteredList_success() { String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete)); - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), model.getTaskManager(), + new UserPrefs()); expectedModel.deletePerson(personToDelete); showNoPerson(expectedModel); @@ -79,6 +84,8 @@ public void execute_invalidIndexFilteredList_throwsCommandException() { assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } + + @Test public void equals() { DeleteCommand deleteFirstCommand = new DeleteCommand(INDEX_FIRST_PERSON); diff --git a/src/test/java/seedu/address/logic/commands/DeleteContactEventCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteContactEventCommandTest.java new file mode 100644 index 00000000000..f978466c67f --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteContactEventCommandTest.java @@ -0,0 +1,116 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.LAUNCH; +import static seedu.address.testutil.TypicalEvents.TRAINING; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +public class DeleteContactEventCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), + getTypicalTaskManager(), new UserPrefs()); + + @Test + public void execute_deleteEvent_success() { + Person person = ALICE; + Event event = LAUNCH; + LocalDateTime eventTime = event.getStartDateTime(); + DeleteContactEventCommand deleteContactEventCommand = new DeleteContactEventCommand(INDEX_FIRST_PERSON, + eventTime); + + String expectedMessage = String.format(DeleteContactEventCommand.MESSAGE_DELETE_EVENT_FROM_PERSON_SUCCESS, + person.getName(), Messages.format(event)); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(0), person); + + assertCommandSuccess(deleteContactEventCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + DeleteContactEventCommand deleteContactEventCommand = new DeleteContactEventCommand(outOfBoundIndex, + LAUNCH.getStartDateTime()); + + assertCommandFailure(deleteContactEventCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Add event to filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + DeleteContactEventCommand deleteContactEventCommand = new DeleteContactEventCommand(outOfBoundIndex, + LAUNCH.getStartDateTime()); + + assertCommandFailure(deleteContactEventCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final DeleteContactEventCommand standardCommand = new DeleteContactEventCommand( + INDEX_FIRST_PERSON, + LAUNCH.getStartDateTime()); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals( + new DeleteContactEventCommand( + INDEX_SECOND_PERSON, + LAUNCH.getStartDateTime()))); + + // different event -> returns false + assertFalse(standardCommand.equals( + new DeleteContactEventCommand( + INDEX_FIRST_PERSON, + TRAINING.getStartDateTime()))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + LocalDateTime eventTime = TRAINING.getStartDateTime(); + DeleteContactEventCommand deleteContactEventCommand = new DeleteContactEventCommand(index, eventTime); + String expected = DeleteContactEventCommand.class.getCanonicalName() + "{index=" + index + ", eventTime=" + + eventTime + "}"; + assertEquals(expected, deleteContactEventCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteEventCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteEventCommandTest.java new file mode 100644 index 00000000000..d63d6b814fa --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteEventCommandTest.java @@ -0,0 +1,305 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.EventBuilder.DEFAULT_END_TIME_STRING; +import static seedu.address.testutil.EventBuilder.DEFAULT_START_TIME_STRING; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; +import seedu.address.testutil.EventBuilder; + + +class DeleteEventCommandTest { + private static LocalDateTime firstTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + private static LocalDateTime secondTime = LocalDateTime.parse(DEFAULT_END_TIME_STRING, DATE_TIME_STRING_FORMATTER); + @Test + public void constructor_nullPerson_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DeleteEventCommand(null)); + } + + @Test + public void constructor_eventDeletedFromModel_deleteSuccessful() throws Exception { + Event validEvent = new EventBuilder().build(); + DeleteEventCommandTest.ModelStubWithEvent modelStub = new DeleteEventCommandTest.ModelStubWithEvent(validEvent); + CommandResult commandResult = new DeleteEventCommand(firstTime).execute(modelStub); + assertEquals(String.format(DeleteEventCommand.MESSAGE_SUCCESS, Messages.format(validEvent)), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_noEvent_throwsCommandException() { + DeleteEventCommand deleteEventCommand = new DeleteEventCommand(secondTime); + DeleteEventCommandTest.ModelStub modelStub = new DeleteEventCommandTest.ModelStubWithNoEvent(); + + assertThrows(CommandException.class, DeleteEventCommand.MESSAGE_NO_EVENT, () -> deleteEventCommand + .execute(modelStub)); + } + + @Test + public void equals() { + DeleteEventCommand deleteFirstEventCommand = new DeleteEventCommand(firstTime); + DeleteEventCommand deleteSecondEventCommand = new DeleteEventCommand(secondTime); + + // same object -> returns true + assertTrue(deleteFirstEventCommand.equals(deleteFirstEventCommand)); + + // same values -> returns true + DeleteEventCommand deleteFirstEventCommandCopy = new DeleteEventCommand(firstTime); + assertTrue(deleteFirstEventCommand.equals(deleteFirstEventCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstEventCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstEventCommand.equals(null)); + + // different time -> returns false + assertFalse(deleteFirstEventCommand.equals(deleteSecondEventCommand)); + } + + @Test + public void toStringMethod() { + DeleteEventCommand deleteFirstEventCommand = new DeleteEventCommand(firstTime); + String expected = DeleteEventCommand.class.getCanonicalName() + "{toDeleteAt=" + firstTime + "}"; + assertEquals(expected, deleteFirstEventCommand.toString()); + } + + /** + * A default model stub that have all the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getCalendarFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendarFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getTaskManagerFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setTaskManagerFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setCalendar(ReadOnlyCalendar newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getCurrentWeekEventList() { + throw new AssertionError("This method should not be called."); + } + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canAddEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addEvent(Event event) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + + public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public List eventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteEventsInRange(EventPeriod range) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyCalendar getComparisonCalendar() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setComparisonCalendar(ReadOnlyCalendar eventList) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getEventList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getTaskList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTask(Task task) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Task deleteTask(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public TaskManager getTaskManager() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void sortTasksBy(String comparatorType) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + @Override + public void sortPersonList(Comparator personComparator) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single event. + */ + private class ModelStubWithNoEvent extends DeleteEventCommandTest.ModelStub { + + @Override + public Event findEventAt(LocalDateTime dateTime) { + throw new EventNotFoundException(); + } + } + + private class ModelStubWithEvent extends DeleteEventCommandTest.ModelStub { + private final Event event; + + ModelStubWithEvent(Event event) { + requireNonNull(event); + this.event = event; + } + + @Override + public Event findEventAt(LocalDateTime dateTime) { + return event; + } + + @Override + public void deleteEventAt(LocalDateTime dateTime) { + return; + } + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteTaskCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteTaskCommandTest.java new file mode 100644 index 00000000000..597aa9130ab --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteTaskCommandTest.java @@ -0,0 +1,84 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.task.Task; + + + +/** + * Contains integration tests (interaction with the Model) and unit tests for {@code DeleteTaskCommand}. + */ +class DeleteTaskCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + + @Test + public void execute_validIndex_success() { + Task taskToDelete = model.getTaskList().get(INDEX_FIRST_PERSON.getZeroBased()); + DeleteTaskCommand deleteTaskCommand = new DeleteTaskCommand(INDEX_FIRST_PERSON); + + String expectedMessage = String.format(DeleteTaskCommand.MESSAGE_DELETE_TASK_SUCCESS, + Messages.format(taskToDelete)); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), + model.getTaskManager(), new UserPrefs()); + expectedModel.deleteTask(0); + + assertCommandSuccess(deleteTaskCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getTaskList().size() + 1); + DeleteTaskCommand deleteTaskCommand = new DeleteTaskCommand(outOfBoundIndex); + + assertCommandFailure(deleteTaskCommand, model, DeleteTaskCommand.MESSAGE_INVALID_INDEX); + } + + @Test + public void equals() { + DeleteTaskCommand deleteFirstCommand = new DeleteTaskCommand(INDEX_FIRST_PERSON); + DeleteTaskCommand deleteSecondCommand = new DeleteTaskCommand(INDEX_SECOND_PERSON); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + DeleteTaskCommand deleteFirstCopy = new DeleteTaskCommand(INDEX_FIRST_PERSON); + assertTrue(deleteFirstCommand.equals(deleteFirstCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different task -> returns false + assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); + } + + @Test + public void toStringMethod() { + Index targetIndex = Index.fromOneBased(1); + DeleteTaskCommand deleteTaskCommand = new DeleteTaskCommand(targetIndex); + String expected = DeleteTaskCommand.class.getCanonicalName() + "{targetIndex=" + targetIndex + "}"; + assertEquals(expected, deleteTaskCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 469dd97daa7..da79b9d502c 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -11,9 +11,11 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import org.junit.jupiter.api.Test; @@ -33,7 +35,8 @@ */ public class EditCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); @Test public void execute_allFieldsSpecifiedUnfilteredList_success() { @@ -43,7 +46,8 @@ public void execute_allFieldsSpecifiedUnfilteredList_success() { String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)); - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -64,7 +68,8 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() { String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)); - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); expectedModel.setPerson(lastPerson, editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -77,7 +82,8 @@ public void execute_noFieldSpecifiedUnfilteredList_success() { String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)); - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -93,7 +99,8 @@ public void execute_filteredList_success() { String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)); - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/EditContactEventCommandTest.java b/src/test/java/seedu/address/logic/commands/EditContactEventCommandTest.java new file mode 100644 index 00000000000..1453d11f963 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditContactEventCommandTest.java @@ -0,0 +1,187 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.parser.ParserUtil.parseDateTimeNonNull; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_EVENT; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalIndexes.INVALID_INDEX; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.person.Person; +import seedu.address.model.task.TaskManager; +import seedu.address.testutil.EditEventDescriptorBuilder; + +public class EditContactEventCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), new TaskManager(), + new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + int personIdx = 0; + Person editedPerson = model.getFilteredPersonList().get(personIdx); + EventDescription expectedEventDescription = new EventDescription("Eat Tacos"); + EditContactEventCommand.EditEventDescriptor descriptor = new EditContactEventCommand.EditEventDescriptor(); + descriptor.setEventDescription(expectedEventDescription); + String startTime = "2023-10-10 10:00"; + String endTime = "2023-10-10 12:00"; + descriptor.setStart(startTime); + descriptor.setEnd(endTime); + ArrayList indexArrayList = new ArrayList<>(); + indexArrayList.add(INDEX_FIRST_PERSON); + indexArrayList.add(INDEX_FIRST_EVENT); + EditContactEventCommand editContactEventCommand = new EditContactEventCommand(indexArrayList, descriptor); + EventPeriod newEventPeriod = new EventPeriod(startTime, endTime); + + Event editEvent = new Event(expectedEventDescription, newEventPeriod); + editedPerson.getCalendar().getEventList().set(personIdx, editEvent); + String expectedMessage = String.format(EditContactEventCommand.MESSAGE_EDIT_PERSON_SUCCESS, + Messages.formatCalendar(editedPerson)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + model.getCalendar(), model.getTaskManager(), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(personIdx), editedPerson); + + assertCommandSuccess(editContactEventCommand, model, expectedMessage, expectedModel); + } + + @Test + public void editEventDescriptor_success() { + EditContactEventCommand.EditEventDescriptor editEventDescriptor = + new EditContactEventCommand.EditEventDescriptor(); + String startDateTimeString = "2023-10-10 10:00"; + String endDateTimeString = "2023-10-10 12:00"; + String desc = "Eat taco"; + editEventDescriptor.setStart(startDateTimeString); + editEventDescriptor.setEnd(endDateTimeString); + editEventDescriptor.setEventDescription(new EventDescription(desc)); + LocalDateTime startDateTime = parseDateTimeNonNull(startDateTimeString); + LocalDateTime endDateTime = parseDateTimeNonNull(endDateTimeString); + + assertEquals(editEventDescriptor.getStart().get(), startDateTime); + assertEquals(editEventDescriptor.getEnd().get(), endDateTime); + assertEquals(editEventDescriptor.getEventDescription().get().getDescription(), desc); + } + + @Test + public void execute_eventIndexOutOfBoundsEvent_failure() { + EditContactEventCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder().build(); + ArrayList indexArrayList = new ArrayList<>(); + indexArrayList.add(INDEX_FIRST_PERSON); + indexArrayList.add(INVALID_INDEX); + EditContactEventCommand editContactEventCommand = new EditContactEventCommand(indexArrayList, descriptor); + + assertCommandFailure(editContactEventCommand, model, EditContactEventCommand.INVALID_EVENT_INDEX); + } + + @Test + public void execute_personIndexOutOfBoundsEvent_failure() { + EditContactEventCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder().build(); + ArrayList indexArrayList = new ArrayList<>(); + indexArrayList.add(INVALID_INDEX); + indexArrayList.add(INDEX_FIRST_EVENT); + EditContactEventCommand editContactEventCommand = new EditContactEventCommand(indexArrayList, descriptor); + + assertCommandFailure(editContactEventCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_editEventDescriptorEquals_success() { + String startTimeString = "2023-10-10 10:10"; + String endTimeString = "2023-11-11 11:11"; + EditContactEventCommand.EditEventDescriptor descriptor1 = new EditEventDescriptorBuilder() + .withEventDescription("Sleep").withEventStartTime(startTimeString) + .withEventEndTime(endTimeString).build(); + EditContactEventCommand.EditEventDescriptor descriptor2 = new EditEventDescriptorBuilder() + .withEventDescription("Sleep").withEventStartTime(startTimeString) + .withEventEndTime(endTimeString).build(); + assertEquals(descriptor1, descriptor2); + assertTrue(descriptor1.equals(descriptor1)); + } + + @Test + public void execute_editEventDescriptorNotEquals_failure() { + String startTimeString = "2023-10-10 10:10"; + String endTimeString = "2023-11-11 11:11"; + EditContactEventCommand.EditEventDescriptor descriptor1 = new EditEventDescriptorBuilder() + .withEventDescription("Nap").withEventStartTime(startTimeString) + .withEventEndTime(endTimeString).build(); + EditContactEventCommand.EditEventDescriptor descriptor2 = new EditEventDescriptorBuilder() + .withEventDescription("Sleep").withEventStartTime(startTimeString) + .withEventEndTime(endTimeString).build(); + assertFalse(descriptor1.equals(descriptor2)); + assertFalse(descriptor1.equals(null)); + } + + @Test + public void execute_editEventDescriptorNotEquals_failure2() { + String desc = "Nap"; + String startTimeString1 = "2023-10-10 10:10"; + String endTimeString1 = "2023-11-11 11:11"; + String startTimeString2 = "2023-10-11 10:10"; + EditContactEventCommand.EditEventDescriptor descriptor1 = new EditEventDescriptorBuilder() + .withEventDescription(desc).withEventStartTime(startTimeString1) + .withEventEndTime(endTimeString1).build(); + EditContactEventCommand.EditEventDescriptor descriptor2 = new EditEventDescriptorBuilder() + .withEventDescription(desc).withEventStartTime(startTimeString2) + .withEventEndTime(endTimeString1).build(); + assertFalse(descriptor1.equals(descriptor2)); + } + + @Test + public void execute_editEventDescriptorToString_success() { + String desc = "Nap"; + String startTimeString1 = "2023-10-10 10:10"; + String endTimeString1 = "2023-11-11 11:11"; + EditContactEventCommand.EditEventDescriptor descriptor1 = new EditContactEventCommand.EditEventDescriptor(); + descriptor1.setEventDescription(new EventDescription(desc)); + descriptor1.setEventPeriod(new EventPeriod(startTimeString1, endTimeString1)); + String expected = "{" + "eventDescription=" + desc + ", start=" + startTimeString1 + + ", end=" + endTimeString1 + "}"; + assertEquals(descriptor1.toString(), expected); + } + + @Test + public void execute_equalTo_success() { + EventDescription expectedEventDescription = new EventDescription("Eat Tacos"); + EditContactEventCommand.EditEventDescriptor descriptor = new EditContactEventCommand.EditEventDescriptor(); + descriptor.setEventDescription(expectedEventDescription); + String startTime = "2023-10-10 10:00"; + String endTime = "2023-10-10 12:00"; + descriptor.setStart(startTime); + descriptor.setEnd(endTime); + ArrayList indexArrayList = new ArrayList<>(); + indexArrayList.add(INDEX_FIRST_PERSON); + indexArrayList.add(INDEX_FIRST_EVENT); + ArrayList indexArrayList2 = new ArrayList<>(); + indexArrayList2.add(INDEX_SECOND_PERSON); + indexArrayList2.add(INDEX_FIRST_EVENT); + EditContactEventCommand editContactEventCommand1 = new EditContactEventCommand(indexArrayList, descriptor); + EditContactEventCommand editContactEventCommand2 = new EditContactEventCommand(indexArrayList2, descriptor); + + assertTrue(editContactEventCommand1.equals(editContactEventCommand1)); + assertFalse(editContactEventCommand1.equals(null)); + assertFalse(editContactEventCommand1.equals(editContactEventCommand2)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java index 9533c473875..e89532db1c4 100644 --- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java @@ -14,7 +14,8 @@ public class ExitCommandTest { @Test public void execute_exit_success() { - CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + CommandResult expectedCommandResult = new CommandResult( + MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false, false); assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/FilterCommandTest.java b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java new file mode 100644 index 00000000000..78ded938ab9 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java @@ -0,0 +1,89 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.FILTER_AMY; +import static seedu.address.logic.commands.CommandTestUtil.FILTER_BOB; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.GEORGE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.testutil.PersonFilterBuilder; + +/** + * Contains integration tests (interaction with the Model) for {@code FilterCommand}. + */ +class FilterCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), + getTypicalTaskManager(), new UserPrefs()); + + @Test + public void execute_oneField_multiplePersonsDisplayed() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + FilterCommand.PersonFilter filter = new PersonFilterBuilder().withPhone("948").build(); + FilterCommand command = new FilterCommand(filter); + expectedModel.updateFilteredPersonList(filter::matchesFilter); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ELLE, FIONA, GEORGE), model.getFilteredPersonList()); + } + @Test + public void execute_multipleKeywords_onePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1); + FilterCommand.PersonFilter filter = new PersonFilterBuilder().withName("carl").withEmail("heinz").build(); + FilterCommand command = new FilterCommand(filter); + expectedModel.updateFilteredPersonList(filter::matchesFilter); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CARL), model.getFilteredPersonList()); + } + @Test + public void toStringMethod() { + FilterCommand.PersonFilter filter = new PersonFilterBuilder().build(); + FilterCommand filterCommand = new FilterCommand(filter); + String expected = FilterCommand.class.getCanonicalName() + "{personFilter=" + filter + "}"; + assertEquals(expected, filterCommand.toString()); + } + + @Test + public void equals() { + FilterCommand.PersonFilter firstFilter = + new FilterCommand.PersonFilter(FILTER_AMY); + FilterCommand.PersonFilter secondFilter = + new FilterCommand.PersonFilter(FILTER_BOB); + + FilterCommand filterFirstCommand = new FilterCommand(firstFilter); + FilterCommand filterSecondCommand = new FilterCommand(secondFilter); + + // same object -> returns true + assertTrue(filterFirstCommand.equals(filterFirstCommand)); + + // same values -> returns true + FilterCommand filterFirstCommandCopy = new FilterCommand(firstFilter); + assertTrue(filterFirstCommand.equals(filterFirstCommandCopy)); + + // different types -> returns false + assertFalse(filterFirstCommand.equals(1)); + + // null -> returns false + assertFalse(filterFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(filterFirstCommand.equals(filterSecondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index b8b7dbba91a..7f1cabdac13 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -5,10 +5,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalPersons.CARL; import static seedu.address.testutil.TypicalPersons.ELLE; import static seedu.address.testutil.TypicalPersons.FIONA; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import java.util.Arrays; import java.util.Collections; @@ -24,8 +26,10 @@ * Contains integration tests (interaction with the Model) for {@code FindCommand}. */ public class FindCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), + getTypicalTaskManager(), new UserPrefs()); @Test public void equals() { diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java index 4904fc4352e..ab3eb4e25a7 100644 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java @@ -14,7 +14,7 @@ public class HelpCommandTest { @Test public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false, false, false); assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index 435ff1f7275..48ad83884d7 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -2,8 +2,10 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,8 +24,10 @@ public class ListCommandTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), getTypicalTaskManager(), + new UserPrefs()); } @Test diff --git a/src/test/java/seedu/address/logic/commands/PersonFilterTest.java b/src/test/java/seedu/address/logic/commands/PersonFilterTest.java new file mode 100644 index 00000000000..831b377ea9f --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/PersonFilterTest.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.FILTER_AMY; +import static seedu.address.logic.commands.CommandTestUtil.FILTER_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FilterCommand.PersonFilter; +import seedu.address.testutil.PersonFilterBuilder; + +class PersonFilterTest { + @Test + public void equals() { + // same object -> returns true + assertTrue(FILTER_AMY.equals(FILTER_AMY)); + + // null -> returns false + assertFalse(FILTER_AMY.equals(null)); + + // different types -> returns false + assertFalse(FILTER_AMY.equals(5)); + + // different values -> returns false + assertFalse(FILTER_AMY.equals(FILTER_BOB)); + + // different name -> returns false + PersonFilter editedAmy = new PersonFilterBuilder(FILTER_AMY).withName(VALID_NAME_BOB).build(); + assertFalse(FILTER_AMY.equals(editedAmy)); + + // different phone -> returns false + editedAmy = new PersonFilterBuilder(FILTER_AMY).withPhone(VALID_PHONE_BOB).build(); + assertFalse(FILTER_AMY.equals(editedAmy)); + + // different email -> returns false + editedAmy = new PersonFilterBuilder(FILTER_AMY).withEmail(VALID_EMAIL_BOB).build(); + assertFalse(FILTER_AMY.equals(editedAmy)); + + // different address -> returns false + editedAmy = new PersonFilterBuilder(FILTER_AMY).withAddress(VALID_ADDRESS_BOB).build(); + assertFalse(FILTER_AMY.equals(editedAmy)); + + // different tags -> returns false + editedAmy = new PersonFilterBuilder(FILTER_AMY).withTags(VALID_TAG_HUSBAND).build(); + assertFalse(FILTER_AMY.equals(editedAmy)); + } + + @Test + public void toStringMethod() { + PersonFilter personFilter = new PersonFilter(); + String expected = PersonFilter.class.getCanonicalName() + "{name=" + + personFilter.getName() + ", phone=" + + personFilter.getPhone() + ", email=" + + personFilter.getEmail() + ", address=" + + personFilter.getAddress() + ", tags=" + + personFilter.getTags() + "}"; + assertEquals(expected, personFilter.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java new file mode 100644 index 00000000000..cff1dadf56d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalPersons.getTypicalUnsortedAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.model.person.comparer.SortComparator; + +/** + * Contains integration tests (interaction with the Model) and unit tests for SortCommand. + */ +public class SortCommandTest { + + private Model model; + private Model expectedModel; + private Model unsortedModel; + private Model expectedSortedModel; + private ArrayList sortComparatorArrayList; + private NameComparatorStub nameComparatorStub; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + unsortedModel = new ModelManager(getTypicalUnsortedAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + expectedSortedModel = new ModelManager(model.getAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + sortComparatorArrayList = new ArrayList<>(); + nameComparatorStub = new NameComparatorStub(true, false, 1); + sortComparatorArrayList.add(nameComparatorStub); + } + + @Test + public void execute_sortByName_showsCorrectSortedList() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + assertCommandSuccess(new SortCommand(sortComparatorArrayList), unsortedModel, + SortCommand.MESSAGE_SUCCESS, expectedSortedModel); + } + public class NameComparatorStub extends SortComparator { + + /** + * Creates a new SortComparator with the given parameters. + * + * @param isActive Whether this comparator is currently active. + * @param isReverse Whether to sort in reverse order. + * @param priority The priority of this comparator. + */ + public NameComparatorStub(boolean isActive, boolean isReverse, int priority) { + super(isActive, isReverse, priority); + } + + @Override + public int compare(Person p1, Person p2) { + return p1.getName().fullName.compareTo(p2.getName().fullName); + } + } +} + diff --git a/src/test/java/seedu/address/logic/commands/SortTasksCommandTest.java b/src/test/java/seedu/address/logic/commands/SortTasksCommandTest.java new file mode 100644 index 00000000000..135434b60f8 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortTasksCommandTest.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.task.Task; + + +class SortTasksCommandTest { + private static final String COMPARATOR_TYPE_DESCRIPTION = "Description"; + private static final String COMPARATOR_TYPE_DEADLINE = "Deadline"; + private static final String COMPARATOR_TYPE_INVALID = "Invalid"; + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + + @Test + public void equals() { + SortTasksCommand sortTasksByDescriptionCommand = new SortTasksCommand(COMPARATOR_TYPE_DESCRIPTION); + SortTasksCommand sortTasksByDeadlineCommand = new SortTasksCommand(COMPARATOR_TYPE_DEADLINE); + SortTasksCommand invalidSortTasksCommand = new SortTasksCommand(COMPARATOR_TYPE_INVALID); + Object notSortTasksCommand = new Object(); + + assertTrue(sortTasksByDescriptionCommand.equals(sortTasksByDescriptionCommand)); + + assertFalse(sortTasksByDeadlineCommand.equals(sortTasksByDescriptionCommand)); + + assertFalse(invalidSortTasksCommand.equals(sortTasksByDeadlineCommand)); + + assertFalse(sortTasksByDescriptionCommand.equals(notSortTasksCommand)); + + assertFalse(sortTasksByDescriptionCommand.equals(null)); + } + + @Test + public void execute_nullInput_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> + new SortTasksCommand(COMPARATOR_TYPE_DESCRIPTION).execute(null)); + } + + @Test + public void execute_invalidType_throwsCommandException() { + assertThrows(CommandException.class, () -> + new SortTasksCommand(COMPARATOR_TYPE_INVALID).execute(model)); + } + + @Test + public void execute_validType_success() throws CommandException { + new SortTasksCommand(COMPARATOR_TYPE_DESCRIPTION).execute(model); + assertTrue(model.getTaskManager().getSortingOrder().equals(new Task.TaskDescriptorComparator())); + } + + @Test + public void toStringMethod() { + SortTasksCommand sortTasksCommand = new SortTasksCommand(COMPARATOR_TYPE_DESCRIPTION); + String expected = SortTasksCommand.class.getCanonicalName() + "{comparatorType=" + COMPARATOR_TYPE_DESCRIPTION + + "}"; + assertEquals(expected, sortTasksCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ViewContactEventsCommandTest.java b/src/test/java/seedu/address/logic/commands/ViewContactEventsCommandTest.java new file mode 100644 index 00000000000..a3184c850fb --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ViewContactEventsCommandTest.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.ViewContactEventsCommand.MESSAGE_VIEW_EVENTS_SUCCESS; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +class ViewContactEventsCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalCalendar(), getTypicalTaskManager(), + new UserPrefs()); + + @Test + public void execute_validViewContactEvents_success() { + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getCalendar(), + model.getTaskManager(), new UserPrefs()); + Index validIndex = Index.fromOneBased(1); + String expectedMessage = MESSAGE_VIEW_EVENTS_SUCCESS; + ViewContactEventsCommand viewContactEventsCommand = new ViewContactEventsCommand(validIndex); + + assertCommandSuccess(viewContactEventsCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_inValidIndex_throwsCommandException() { + Index invalidIndex = Index.fromOneBased(999); + String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; + ViewContactEventsCommand viewContactEventsCommand = new ViewContactEventsCommand(invalidIndex); + + assertCommandFailure(viewContactEventsCommand, model, expectedMessage); + } + + @Test + public void equalsMethod() { + ViewContactEventsCommand viewFirstCommand = new ViewContactEventsCommand(Index.fromOneBased(1)); + ViewContactEventsCommand viewSecondCommand = new ViewContactEventsCommand(Index.fromOneBased(2)); + + // same object -> returns true + assertTrue(viewFirstCommand.equals(viewFirstCommand)); + + // same values -> returns true + ViewContactEventsCommand viewFirstCommandCopy = new ViewContactEventsCommand(Index.fromOneBased(1)); + assertTrue(viewFirstCommand.equals(viewFirstCommandCopy)); + + // different types -> returns false + assertFalse(viewFirstCommand.equals(1)); + + // null -> returns false + assertFalse(viewFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(viewFirstCommand.equals(viewSecondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddContactEventCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddContactEventCommandParserTest.java new file mode 100644 index 00000000000..560c3c3dd12 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddContactEventCommandParserTest.java @@ -0,0 +1,79 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.END_DATE_DESC_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.EVENT_DESC_SLEEP; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_EVENT; +import static seedu.address.logic.commands.CommandTestUtil.START_DATE_DESC_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddContactEventCommand; +import seedu.address.model.event.Event; +import seedu.address.testutil.EventBuilder; + +public class AddContactEventCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddContactEventCommand.MESSAGE_USAGE); + private static final String VALID_EVENT = EVENT_DESC_SLEEP + START_DATE_DESC_EARLIER + END_DATE_DESC_EARLIER; + + private AddContactEventCommandParser parser = new AddContactEventCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_EVENT, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", MESSAGE_INVALID_FORMAT); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + VALID_EVENT, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + VALID_EVENT, MESSAGE_INVALID_FORMAT); + + // int overflow + assertParseFailure(parser, "2147483648" + VALID_EVENT, MESSAGE_INTEGER_OVERFLOW); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_EVENT, MESSAGE_INVALID_FORMAT); // invalid event + } + + @Test + public void parse_validEvent_success() { + Index targetIndex = INDEX_SECOND_PERSON; + String userInput = targetIndex.getOneBased() + VALID_EVENT; + + Event event = new EventBuilder() + .withDescription(VALID_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER) + .build(); + AddContactEventCommand expectedCommand = new AddContactEventCommand(targetIndex, event); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddEventCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddEventCommandParserTest.java new file mode 100644 index 00000000000..18092349b6d --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddEventCommandParserTest.java @@ -0,0 +1,119 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_DATE; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; +import seedu.address.testutil.EventBuilder; + +public class AddEventCommandParserTest { + private AddEventCommandParser parser = new AddEventCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Event expectedEvent = new EventBuilder().withDescription(VALID_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + String validUserInput = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseSuccess(parser, validUserInput, new AddEventCommand(expectedEvent)); + } + + @Test + public void parse_repeatedPrefix_failure() { + String userInputRepeatDescriptionPrefix = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputRepeatDescriptionPrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_DESCRIPTION)); + + String userInputRepeatEventStartDateTimePrefix = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputRepeatEventStartDateTimePrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_START_DATE_TIME)); + + String userInputRepeatEventEndDateTimePrefix = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputRepeatEventEndDateTimePrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_END_DATE_TIME)); + + String userInputRepeatMultiplePrefix = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputRepeatMultiplePrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_DESCRIPTION, + PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddEventCommand.MESSAGE_USAGE); + + String missingDescription = " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, missingDescription, expectedMessage); + + String missingStartDate = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, missingStartDate, expectedMessage); + + String missingEndDate = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER; + + assertParseFailure(parser, missingEndDate, expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + String userInputInvalidDescription = " " + PREFIX_EVENT_DESCRIPTION + INVALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputInvalidDescription, + EventDescription.MESSAGE_CONSTRAINTS); + + String userInputInvalidStartDateTime = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + INVALID_DATE + + " " + PREFIX_EVENT_END_DATE_TIME + VALID_END_DATE_EARLIER; + + assertParseFailure(parser, userInputInvalidStartDateTime, + EventPeriod.MESSAGE_CONSTRAINTS); + + String userInputInvalidEndDateTime = " " + PREFIX_EVENT_DESCRIPTION + VALID_DESCRIPTION + + " " + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + + " " + PREFIX_EVENT_END_DATE_TIME + INVALID_DATE; + + assertParseFailure(parser, userInputInvalidEndDateTime, + EventPeriod.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddTaskCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddTaskCommandParserTest.java new file mode 100644 index 00000000000..dc264a401dc --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddTaskCommandParserTest.java @@ -0,0 +1,79 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.AddTaskCommand.MESSAGE_INVALID_DESCRIPTION; +import static seedu.address.logic.commands.AddTaskCommand.MESSAGE_USAGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.task.Deadline.MESSAGE_CONSTRAINTS; +import static seedu.address.testutil.TaskBuilder.DEFAULT_DEADLINE; +import static seedu.address.testutil.TaskBuilder.DEFAULT_DESCRIPTION; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.model.task.Task; +import seedu.address.testutil.TaskBuilder; + +class AddTaskCommandParserTest { + private AddTaskCommandParser parser = new AddTaskCommandParser(); + + @Test + void parse_allFieldsPresent_success() { + Task expectedTask = new TaskBuilder().build(); + String validUserInput = " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION + + " " + PREFIX_EVENT_END_DATE_TIME + DEFAULT_DEADLINE; + + assertParseSuccess(parser, validUserInput, new AddTaskCommand(expectedTask)); + } + + @Test + void parse_descriptionPresent_success() { + Task expectedTask = new TaskBuilder().withNoDeadline().build(); + String validUserInput = " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION; + + assertParseSuccess(parser, validUserInput, new AddTaskCommand(expectedTask)); + } + + @Test + public void parse_repeatedPrefix_failure() { + String userInputRepeatDescriptionPrefix = " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION + + " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION; + + assertParseFailure(parser, userInputRepeatDescriptionPrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_DESCRIPTION)); + + String userInputRepeatEndPrefix = " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION + + " " + PREFIX_EVENT_END_DATE_TIME + DEFAULT_DEADLINE + + " " + PREFIX_EVENT_END_DATE_TIME + DEFAULT_DEADLINE; + + assertParseFailure(parser, userInputRepeatEndPrefix, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EVENT_END_DATE_TIME)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTaskCommand.MESSAGE_USAGE); + + String missingDescription = " "; + + assertParseFailure(parser, missingDescription, expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + String userInputInvalidDescription = " " + PREFIX_EVENT_DESCRIPTION; + + assertParseFailure(parser, userInputInvalidDescription, + String.format(MESSAGE_INVALID_DESCRIPTION, MESSAGE_USAGE)); + + String userInputInvalidDeadline = " " + PREFIX_EVENT_DESCRIPTION + DEFAULT_DESCRIPTION + + " " + PREFIX_EVENT_END_DATE_TIME; + + assertParseFailure(parser, userInputInvalidDeadline, + MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java deleted file mode 100644 index 5a1ab3dbc0c..00000000000 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package seedu.address.logic.parser; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; -import seedu.address.testutil.PersonBuilder; -import seedu.address.testutil.PersonUtil; - -public class AddressBookParserTest { - - private final AddressBookParser parser = new AddressBookParser(); - - @Test - public void parseCommand_add() throws Exception { - Person person = new PersonBuilder().build(); - AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); - assertEquals(new AddCommand(person), command); - } - - @Test - public void parseCommand_clear() throws Exception { - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); - } - - @Test - public void parseCommand_delete() throws Exception { - DeleteCommand command = (DeleteCommand) parser.parseCommand( - DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); - assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); - } - - @Test - public void parseCommand_edit() throws Exception { - Person person = new PersonBuilder().build(); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); - EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " - + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); - assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); - } - - @Test - public void parseCommand_exit() throws Exception { - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); - } - - @Test - public void parseCommand_find() throws Exception { - List keywords = Arrays.asList("foo", "bar", "baz"); - FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); - assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); - } - - @Test - public void parseCommand_help() throws Exception { - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); - } - - @Test - public void parseCommand_list() throws Exception { - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); - } - - @Test - public void parseCommand_unrecognisedInput_throwsParseException() { - assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); - } - - @Test - public void parseCommand_unknownCommand_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); - } -} diff --git a/src/test/java/seedu/address/logic/parser/ClearEventsCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ClearEventsCommandParserTest.java new file mode 100644 index 00000000000..c4345da893d --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ClearEventsCommandParserTest.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ClearEventsCommand; +import seedu.address.model.event.EventPeriod; +import seedu.address.testutil.EventPeriodBuilder; + +class ClearEventsCommandParserTest { + private static final ClearEventsCommandParser parser = new ClearEventsCommandParser(); + private static final String sampleString = "ts/2023-01-02 13:00 te/2023-01-02 15:00"; + private static final String sampleStringConfirmed = "ts/2023-01-02 13:00 te/2023-01-02 15:00 c/CONFIRMED"; + + @Test + public void parse_success_unconfirmed() { + EventPeriod expectedPeriod = new EventPeriodBuilder().build(); + String validUserInput = " " + sampleString; + assertParseSuccess(parser, validUserInput, new ClearEventsCommand(expectedPeriod, false)); + } + + @Test + public void parse_success_confirmed() { + EventPeriod expectedPeriod = new EventPeriodBuilder().build(); + String validUserInput = " " + sampleStringConfirmed; + assertParseSuccess(parser, validUserInput, new ClearEventsCommand(expectedPeriod, true)); + } + + @Test + public void parse_invalidValue_failure() { + String invalidUserInput = " 123"; + assertParseFailure(parser, invalidUserInput, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClearEventsCommand.MESSAGE_USAGE)); + String halfValidInput = " ts/2023-01-02 13:00"; + assertParseFailure(parser, halfValidInput, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClearEventsCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java index 6a40e14a649..aa1c940f39d 100644 --- a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java @@ -1,5 +1,6 @@ package seedu.address.logic.parser; +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; @@ -28,5 +29,7 @@ public void parse_validArgs_returnsDeleteCommand() { @Test public void parse_invalidArgs_throwsParseException() { assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + // int overflow + assertParseFailure(parser, "2147483648", MESSAGE_INTEGER_OVERFLOW); } } diff --git a/src/test/java/seedu/address/logic/parser/DeleteContactEventCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteContactEventCommandParserTest.java new file mode 100644 index 00000000000..2a5237186ea --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteContactEventCommandParserTest.java @@ -0,0 +1,78 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_DATE; +import static seedu.address.logic.commands.CommandTestUtil.START_DATE_DESC_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteContactEventCommand; + +public class DeleteContactEventCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteContactEventCommand.MESSAGE_USAGE); + private static final String VALID_DATE_TIME = START_DATE_DESC_EARLIER; + private static final String VALID_DATE_TIME_WITHOUT_PREFIX = VALID_START_DATE_EARLIER; + + + + private DeleteContactEventCommandParser parser = new DeleteContactEventCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_DATE_TIME, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", MESSAGE_INVALID_FORMAT); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + VALID_DATE_TIME, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + VALID_DATE_TIME, MESSAGE_INVALID_FORMAT); + + // int overflow + assertParseFailure(parser, "2147483648" + VALID_DATE_TIME, MESSAGE_INTEGER_OVERFLOW); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_DATE, MESSAGE_INVALID_FORMAT); // invalid date + } + + @Test + public void parse_validEvent_success() { + Index targetIndex = INDEX_SECOND_PERSON; + String userInput = targetIndex.getOneBased() + VALID_DATE_TIME; + + LocalDateTime expectedDateTime = LocalDateTime.parse( + VALID_DATE_TIME_WITHOUT_PREFIX, + DATE_TIME_STRING_FORMATTER); + DeleteContactEventCommand expectedCommand = new DeleteContactEventCommand(targetIndex, expectedDateTime); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteEventCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteEventCommandParserTest.java new file mode 100644 index 00000000000..47a3ff0a3d7 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteEventCommandParserTest.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteEventCommand; + +class DeleteEventCommandParserTest { + private static final DeleteEventCommandParser parser = new DeleteEventCommandParser(); + private static final String sampleString = "2024-01-01 12:00"; + + @Test + public void parse_success() { + LocalDateTime expectedDateTime = LocalDateTime.parse(sampleString, DATE_TIME_STRING_FORMATTER); + String validUserInput = " " + sampleString; + assertParseSuccess(parser, validUserInput, new DeleteEventCommand(expectedDateTime)); + } + + @Test + public void parse_invalidValue_failure() { + String invalidUserInput = " 123"; + assertParseFailure(parser, invalidUserInput, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteEventCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteTaskCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteTaskCommandParserTest.java new file mode 100644 index 00000000000..3ee13bfc290 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteTaskCommandParserTest.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteTaskCommand; + +class DeleteTaskCommandParserTest { + + private DeleteTaskCommandParser parser = new DeleteTaskCommandParser(); + + @Test + public void parse_validArgs_returnsDeleteTaskCommand() { + assertParseSuccess(parser, "1", new DeleteTaskCommand(INDEX_FIRST_PERSON)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTaskCommand.MESSAGE_USAGE)); + + // integer overflow + assertParseFailure(parser, "2147483648", MESSAGE_INTEGER_OVERFLOW); + + } +} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index cc7175172d4..05e3077aadf 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -1,5 +1,6 @@ package seedu.address.logic.parser; +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; @@ -74,6 +75,9 @@ public void parse_invalidPreamble_failure() { // zero index assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + // int overflow + assertParseFailure(parser, "2147483648" + NAME_DESC_AMY, MESSAGE_INTEGER_OVERFLOW); + // invalid arguments being parsed as preamble assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); diff --git a/src/test/java/seedu/address/logic/parser/EditContactEventCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditContactEventCommandParserTest.java new file mode 100644 index 00000000000..11e881ed5af --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditContactEventCommandParserTest.java @@ -0,0 +1,127 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_START_DATE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.logic.commands.EditContactEventCommand.MESSAGE_NOT_EDITED; +import static seedu.address.logic.commands.EditContactEventCommand.MESSAGE_WRONG_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_EVENT; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditContactEventCommand; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class EditContactEventCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditContactEventCommand.MESSAGE_USAGE); + + private EditContactEventCommandParser parser = new EditContactEventCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", MESSAGE_INVALID_FORMAT); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5 1 " + VALID_START_DATE_EARLIER, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0 1 " + VALID_START_DATE_EARLIER, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_missingParameters_failure() { + // Only 1 index + assertParseFailure(parser, "1 " + PREFIX_EVENT_START_DATE_TIME + + VALID_START_DATE_EARLIER, MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1 1 " + PREFIX_EVENT_START_DATE_TIME + + INVALID_START_DATE, MESSAGE_WRONG_TIME); + } + + @Test + public void parse_invalidTimeEnd_failure2() { + assertParseFailure(parser, "1 1 " + PREFIX_EVENT_START_DATE_TIME, MESSAGE_WRONG_TIME); + } + + @Test + public void parse_invalidTimeEnd_failure3() { + assertParseFailure(parser, "1 1", MESSAGE_NOT_EDITED); + } + + @Test + public void parse_allFieldsSpecified_success() { + Index personIndex = INDEX_FIRST_PERSON; + Index eventIndex = INDEX_FIRST_EVENT; + String userInput = personIndex.getOneBased() + " " + eventIndex.getOneBased() + " " + PREFIX_EVENT_DESCRIPTION + + "Webinar"; + + Person person = new PersonBuilder().withCalendar().build(); + Event event = person.getCalendar().getEventList().get(INDEX_FIRST_EVENT.getOneBased()); + EventDescription expectedEventDescription = event.getDescription(); + EditContactEventCommand.EditEventDescriptor descriptor = new EditContactEventCommand.EditEventDescriptor(); + descriptor.setEventDescription(expectedEventDescription); + ArrayList expectedIndexArray = new ArrayList<>(); + expectedIndexArray.add(INDEX_FIRST_PERSON); + expectedIndexArray.add(INDEX_FIRST_EVENT); + EditContactEventCommand expectedCommand = new EditContactEventCommand(expectedIndexArray, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_allFieldsSpecified_success2() { + Index personIndex = INDEX_FIRST_PERSON; + Index eventIndex = INDEX_FIRST_EVENT; + String userInput = personIndex.getOneBased() + " " + eventIndex.getOneBased() + " " + + PREFIX_EVENT_START_DATE_TIME + VALID_START_DATE_EARLIER + " " + + PREFIX_EVENT_END_DATE_TIME + VALID_START_DATE_LATER; + + Person person = new PersonBuilder().withCalendar().build(); + Event event = person.getCalendar().getEventList().get(INDEX_FIRST_EVENT.getOneBased()); + EventDescription expectedEventDescription = event.getDescription(); + EditContactEventCommand.EditEventDescriptor descriptor = new EditContactEventCommand.EditEventDescriptor(); + descriptor.setStart(VALID_START_DATE_EARLIER); + descriptor.setEnd(VALID_START_DATE_LATER); + ArrayList expectedIndexArray = new ArrayList<>(); + expectedIndexArray.add(INDEX_FIRST_PERSON); + expectedIndexArray.add(INDEX_FIRST_EVENT); + EditContactEventCommand expectedCommand = new EditContactEventCommand(expectedIndexArray, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java new file mode 100644 index 00000000000..9b4f25e6873 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.testutil.PersonFilterBuilder; + +public class FilterCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE); + + private FilterCommandParser parser = new FilterCommandParser(); + + @Test + public void parse_noParameters_failure() { + // no field specified + assertParseFailure(parser, "", FilterCommand.CONTACTS_NOT_FILTERED); + } + + @Test + public void parse_allFieldsSpecified_success() throws ParseException { + String userInput = PHONE_DESC_BOB + TAG_DESC_HUSBAND + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; + + FilterCommand.PersonFilter filter = new PersonFilterBuilder().withName(VALID_NAME_AMY) + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + FilterCommand expectedCommand = new FilterCommand(filter); + FilterCommand actual = parser.parse(userInput); + + assertTrue(actual.equals(expectedCommand)); + } + + @Test + public void parse_validArgs_returnsFilterCommand() throws ParseException { + // no leading and trailing whitespaces + FilterCommand expectedFilterCommand = + new FilterCommand(new PersonFilterBuilder().withName("A").withEmail("@").build()); + assertTrue(parser.parse(" n/A e/@").equals(expectedFilterCommand)); + + // multiple whitespaces between keywords + assertTrue(parser.parse(" \n n/A \n \t e/@ \t").equals(expectedFilterCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..602bc7f8ef9 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -6,6 +6,7 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -13,7 +14,10 @@ import org.junit.jupiter.api.Test; +import seedu.address.commons.core.index.Index; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -27,12 +31,18 @@ public class ParserUtilTest { private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_DESCRIPTION = ""; + private static final String INVALID_DATE = "2023-13-35 16:80"; + private static final String VALID_NAME = "Rachel Walker"; private static final String VALID_PHONE = "123456"; private static final String VALID_ADDRESS = "123 Main Street #0505"; private static final String VALID_EMAIL = "rachel@example.com"; private static final String VALID_TAG_1 = "friend"; private static final String VALID_TAG_2 = "neighbour"; + private static final String VALID_DESCRIPTION = "sleep"; + private static final String VALID_START_DATE = "2023-01-01 08:00"; + private static final String VALID_END_DATE = "2023-01-01 09:00"; private static final String WHITESPACE = " \t\r\n"; @@ -56,6 +66,41 @@ public void parseIndex_validInput_success() throws Exception { assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex(" 1 ")); } + @Test + public void parseDualIndex_invalidInput_throwsParseException() throws Exception { + assertThrows(ParseException.class, () -> ParserUtil.parseDualIndexes("10 a")); + } + + @Test + public void parseDualIndex_negativeValueInput_throwsParseException() throws Exception { + assertThrows(ParseException.class, MESSAGE_INVALID_INDEX, () + -> ParserUtil.parseDualIndexes("-1 1")); + } + + @Test + public void parseDualIndexes_validInput_success() throws Exception { + ArrayList expected = new ArrayList<>(); + Index expectedFirstItem = Index.fromOneBased(1); + Index expectedSecondItem = Index.fromOneBased(2); + expected.add(expectedFirstItem); + expected.add(expectedSecondItem); + + ArrayList actual = ParserUtil.parseDualIndexes("1 2"); + assertEquals(expected, actual); + } + + @Test + public void parseDualIndexes_validInput_success2() throws Exception { + ArrayList expected = new ArrayList<>(); + Index expectedFirstItem = Index.fromOneBased(1); + Index expectedSecondItem = Index.fromOneBased(4); + expected.add(expectedFirstItem); + expected.add(expectedSecondItem); + + ArrayList actual = ParserUtil.parseDualIndexes("1 4 "); + assertEquals(expected, actual); + } + @Test public void parseName_null_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> ParserUtil.parseName((String) null)); @@ -193,4 +238,65 @@ public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { assertEquals(expectedTagSet, actualTagSet); } + + @Test + public void parseEventDescription_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseEventDescription(null)); + } + + @Test + public void parseEventDescription_invalidDescription_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEventDescription(INVALID_DESCRIPTION)); + } + + @Test + public void parseEventDescription_validDescription_returnsDescription() throws Exception { + EventDescription actual = ParserUtil.parseEventDescription(VALID_DESCRIPTION); + EventDescription expected = new EventDescription(VALID_DESCRIPTION); + + assertEquals(actual, expected); + } + + @Test + public void parseEventPeriod_nullStart_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseEventPeriod(null, VALID_END_DATE)); + } + + @Test + public void parseEventPeriod_nullEnd_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseEventPeriod(VALID_START_DATE, null)); + } + + @Test + public void parseEventPeriod_nullStartAndEnd_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseEventPeriod(null, null)); + } + + @Test + public void parseEventPeriod_invalidStart_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEventPeriod(INVALID_DATE, VALID_END_DATE)); + } + + @Test + public void parseEventPeriod_invalidEnd_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEventPeriod(VALID_START_DATE, INVALID_DATE)); + } + + @Test + public void parseEventPeriod_invalidStartAndEnd_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEventPeriod(INVALID_DATE, INVALID_DATE)); + } + + @Test + public void parseEventPeriod_endBeforeStart_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseEventPeriod(VALID_END_DATE, VALID_START_DATE)); + } + + @Test + public void parseEventPeriod_validStartAndEnd_returnsValidEventPeriod() throws Exception { + EventPeriod actual = ParserUtil.parseEventPeriod(VALID_START_DATE, VALID_END_DATE); + EventPeriod expected = new EventPeriod(VALID_START_DATE, VALID_END_DATE); + + assertEquals(actual, expected); + } } diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java new file mode 100644 index 00000000000..adaafd270f0 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java @@ -0,0 +1,123 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.SortCommand.REVERSE_KEYWORD; +import static seedu.address.logic.commands.SortCommand.SORTBY_KEYWORD1; +import static seedu.address.logic.commands.SortCommand.SORTBY_KEYWORD2; +import static seedu.address.logic.commands.SortCommand.SORTBY_KEYWORD3; +import static seedu.address.logic.commands.SortCommand.SORTBY_KEYWORD4; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.SortCommandParser.PARSE_EXCEPTION_MESSAGE; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.SortCommand; + + +public class SortCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE); + private SortCommandParser parser = new SortCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_missingDelimiters_failure() { + assertParseFailure(parser, " 2", PARSE_EXCEPTION_MESSAGE); + + assertParseFailure(parser, " abc", PARSE_EXCEPTION_MESSAGE); + } + + @Test + public void parse_emptyInput_failure() { + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidDelimiters_failure() { + assertParseFailure(parser, " -bydescription", PARSE_EXCEPTION_MESSAGE); + assertParseFailure(parser, " -byname", PARSE_EXCEPTION_MESSAGE); + } + + @Test + public void parse_wrongSortComparator_failure() { + assertParseFailure(parser, " /name", PARSE_EXCEPTION_MESSAGE); + assertParseFailure(parser, " /phone", PARSE_EXCEPTION_MESSAGE); + } + + @Test + public void parse_tooManyArgs_failure() { + assertParseFailure(parser, " /byname /reverse /byphone", PARSE_EXCEPTION_MESSAGE); + assertParseFailure(parser, " /byname /byphone", PARSE_EXCEPTION_MESSAGE); + } + + @Test + public void parse_firstArgIsReverse_failure() { + assertParseFailure(parser, " /reverse", PARSE_EXCEPTION_MESSAGE); + } + + @Test + public void parse_sortComparator_success1() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD1); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } + + @Test + public void parse_sortComparator_success2() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD2); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } + @Test + public void parse_sortComparator_success3() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD3); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } + @Test + public void parse_sortComparator_success4() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD4); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } + + @Test + public void parse_sortComparator_success5() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD2 + " " + REVERSE_KEYWORD); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } + + @Test + public void parse_sortComparator_success6() { + try { + Command command = parser.parse(" " + SORTBY_KEYWORD4 + " " + REVERSE_KEYWORD); + } catch (Exception e) { + fail("Fail"); + } + assertTrue(true); + } +} diff --git a/src/test/java/seedu/address/logic/parser/SortTasksCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortTasksCommandParserTest.java new file mode 100644 index 00000000000..d40fa4e24d0 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SortTasksCommandParserTest.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.SortTasksCommand; + +class SortTasksCommandParserTest { + private static final String COMPARATOR_TYPE_DESCRIPTION = "Description"; + private SortTasksCommandParser parser = new SortTasksCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortTasksCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsSortTasksCommand() { + SortTasksCommand expectedSortTasksCommand = new SortTasksCommand(COMPARATOR_TYPE_DESCRIPTION); + assertParseSuccess(parser, "Description", expectedSortTasksCommand); + + assertParseSuccess(parser, " \n \t Description", expectedSortTasksCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/UniMateParserTest.java b/src/test/java/seedu/address/logic/parser/UniMateParserTest.java new file mode 100644 index 00000000000..a5ec6662e31 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/UniMateParserTest.java @@ -0,0 +1,282 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.ParserUtil.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_EVENT; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddContactEventCommand; +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.ClearEventsCommand; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteContactEventCommand; +import seedu.address.logic.commands.DeleteEventCommand; +import seedu.address.logic.commands.DeleteTaskCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditContactEventCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.SortTasksCommand; +import seedu.address.logic.commands.SwitchListCommand; +import seedu.address.logic.commands.ViewContactEventsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventPeriod; +import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.testutil.EditEventDescriptorBuilder; +import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.EventPeriodBuilder; +import seedu.address.testutil.PersonBuilder; +import seedu.address.testutil.PersonUtil; + +public class UniMateParserTest { + + private final UniMateParser parser = new UniMateParser(); + + @Test + public void parseCommand_add() throws Exception { + Person person = new PersonBuilder().build(); + AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); + assertEquals(new AddCommand(person), command); + } + + @Test + public void parseCommand_clear() throws Exception { + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); + } + + @Test + public void parseCommand_delete() throws Exception { + DeleteCommand command = (DeleteCommand) parser.parseCommand( + DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_edit() throws Exception { + Person person = new PersonBuilder().build(); + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); + EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); + assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); + } + + @Test + public void parseCommand_editContactEvent() throws Exception { + Person person = new PersonBuilder().withCalendar().build(); + Event event = person.getCalendar().getEventList().get(INDEX_FIRST_EVENT.getOneBased()); + EditContactEventCommand.EditEventDescriptor descriptor = new EditEventDescriptorBuilder(event).build(); + EditContactEventCommand command = (EditContactEventCommand) + parser.parseCommand(EditContactEventCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + INDEX_FIRST_EVENT.getOneBased() + " " + + PersonUtil.getEditPersonEventDetails(descriptor)); + ArrayList expectedIndexArray = new ArrayList<>(); + expectedIndexArray.add(INDEX_FIRST_PERSON); + expectedIndexArray.add(INDEX_FIRST_EVENT); + assertEquals(new EditContactEventCommand(expectedIndexArray, descriptor), command); + } + + @Test + public void parseCommand_editContactEvent2() throws Exception { + EditContactEventCommand command = (EditContactEventCommand) + parser.parseCommand(EditContactEventCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + INDEX_FIRST_EVENT.getOneBased() + " " + + PREFIX_EVENT_DESCRIPTION + " sleep"); + assertTrue(command instanceof EditContactEventCommand); + } + + @Test + public void parseCommand_exit() throws Exception { + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + } + + @Test + public void parseCommand_find() throws Exception { + List keywords = Arrays.asList("foo", "bar", "baz"); + FindCommand command = (FindCommand) parser.parseCommand( + FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); + } + + @Test + public void parseCommand_help() throws Exception { + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); + } + + @Test + public void parseCommand_list() throws Exception { + assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); + assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); + } + + @Test + public void parseCommand_addEvent() throws Exception { + String description = "sleep"; + String startDateTime = "2023-10-10 10:00"; + String endDateTime = "2023-10-10 12:00"; + String validArg = "d/" + description + + " ts/" + startDateTime + + " te/" + endDateTime; + validArg = AddEventCommand.COMMAND_WORD + " " + validArg; + EventBuilder eventBuilder = new EventBuilder(); + eventBuilder.withDescription(description); + eventBuilder.withStartEndDate(startDateTime, endDateTime); + + AddEventCommand addEventCommand = (AddEventCommand) parser.parseCommand(validArg); + assertEquals(addEventCommand, new AddEventCommand(eventBuilder.build())); + } + + @Test + public void parseCommand_sort() throws Exception { + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + + SortCommand.SORTBY_KEYWORD1) instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + + SortCommand.SORTBY_KEYWORD2) instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + + SortCommand.SORTBY_KEYWORD3 + " " + SortCommand.REVERSE_KEYWORD) instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + + SortCommand.SORTBY_KEYWORD4 + " " + SortCommand.REVERSE_KEYWORD) instanceof SortCommand); + } + + @Test + public void parserCommand_addContactEvent() throws Exception { + String description = "sleep"; + String startDateTime = "2023-10-10 10:00"; + String endDateTime = "2023-10-10 12:00"; + String validArg = "d/" + description + + " ts/" + startDateTime + + " te/" + endDateTime; + EventBuilder eventBuilder = new EventBuilder(); + eventBuilder.withDescription(description); + eventBuilder.withStartEndDate(startDateTime, endDateTime); + AddContactEventCommand command = + (AddContactEventCommand) parser.parseCommand(AddContactEventCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + validArg); + + assertEquals(command, new AddContactEventCommand(INDEX_FIRST_PERSON, eventBuilder.build())); + } + + @Test + public void parserCommand_deleteContactEvent() throws Exception { + String targetDateTime = "2023-10-10 10:00"; + LocalDateTime localDateTime = LocalDateTime.parse(targetDateTime, DATE_TIME_STRING_FORMATTER); + String validArg = "ts/" + targetDateTime; + DeleteContactEventCommand command = + (DeleteContactEventCommand) parser.parseCommand(DeleteContactEventCommand.COMMAND_WORD + " " + + INDEX_FIRST_PERSON.getOneBased() + " " + validArg); + + assertEquals(command, new DeleteContactEventCommand(INDEX_FIRST_PERSON, localDateTime)); + } + + @Test + public void parserCommand_deleteEvent() throws Exception { + String targetDateTime = "2023-10-10 10:00"; + String validArg = " " + targetDateTime; + DeleteEventCommand deleteEventCommand = + new DeleteEventCommand(LocalDateTime.parse("2023-10-10 10:00", DATE_TIME_STRING_FORMATTER)); + + assertEquals(deleteEventCommand, + (DeleteEventCommand) parser.parseCommand(DeleteEventCommand.COMMAND_WORD + validArg)); + } + + @Test + public void parserCommand_clearEvents() throws Exception { + String startDateTime = "2023-10-10 10:00"; + String endDateTime = "2023-10-10 12:00"; + String validArg = " ts/" + startDateTime + + " te/" + endDateTime + + " c/CONFIRMED"; + + EventPeriod period = new EventPeriodBuilder().changeStartAndEnd(startDateTime, endDateTime).build(); + + ClearEventsCommand clearEventsCommand = + new ClearEventsCommand(period, true); + + assertEquals(clearEventsCommand, + (ClearEventsCommand) parser.parseCommand(ClearEventsCommand.COMMAND_WORD + validArg)); + } + + @Test + public void parserCommand_addTask() throws Exception { + String description = "A"; + String validArg = " d/" + description; + + AddTaskCommand addTaskCommand = new AddTaskCommand(new Task(description, null)); + + assertEquals(addTaskCommand, + (AddTaskCommand) parser.parseCommand(AddTaskCommand.COMMAND_WORD + validArg)); + } + + @Test + public void parserCommand_switchList() throws Exception { + String validArg = " 1231231"; + + SwitchListCommand switchListCommand = new SwitchListCommand(); + + assertTrue(parser.parseCommand(SwitchListCommand.COMMAND_WORD + validArg) + instanceof SwitchListCommand); + } + + @Test + public void parserCommand_sortTasks() throws Exception { + String comparatorType = "Description"; + String validArg = " " + comparatorType; + + SortTasksCommand sortTasksCommand = new SortTasksCommand(comparatorType); + + assertEquals(sortTasksCommand, + (SortTasksCommand) parser.parseCommand(SortTasksCommand.COMMAND_WORD + validArg)); + } + + @Test + public void parseCommand_deleteTask() throws Exception { + DeleteTaskCommand command = (DeleteTaskCommand) parser.parseCommand( + DeleteTaskCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new DeleteTaskCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_viewContactEvents() throws Exception { + ViewContactEventsCommand command = (ViewContactEventsCommand) parser.parseCommand( + ViewContactEventsCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new ViewContactEventsCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_unrecognisedInput_throwsParseException() { + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () + -> parser.parseCommand("")); + } + + @Test + public void parseCommand_unknownCommand_throwsParseException() { + assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ViewContactEventsCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ViewContactEventsCommandParserTest.java new file mode 100644 index 00000000000..ad12aedfb51 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ViewContactEventsCommandParserTest.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.ViewContactEventsCommand.MESSAGE_USAGE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ViewContactEventsCommand; + +class ViewContactEventsCommandParserTest { + private ViewContactEventsCommandParser parser = new ViewContactEventsCommandParser(); + + @Test + public void parser_validArgs_returnsViewContactEventsCommand() { + assertParseSuccess(parser, "1", new ViewContactEventsCommand(INDEX_FIRST_PERSON)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + // int overflow + assertParseFailure(parser, "2147483648", MESSAGE_INTEGER_OVERFLOW); + assertParseFailure(parser, "-1", String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..3df109ec7dd 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -4,19 +4,33 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.EventBuilder.DEFAULT_START_TIME_STRING; +import static seedu.address.testutil.TypicalEvents.CONFERENCE; +import static seedu.address.testutil.TypicalEvents.TEST_EVENT_A; +import static seedu.address.testutil.TypicalEvents.TEST_EVENT_B; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalTasks.ASSIGNMENT; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.exceptions.EventNotFoundException; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.task.TaskManager; import seedu.address.testutil.AddressBookBuilder; +import seedu.address.testutil.CalendarBuilder; +import seedu.address.testutil.EventPeriodBuilder; +import seedu.address.testutil.TaskManagerBuilder; public class ModelManagerTest { @@ -72,6 +86,30 @@ public void setAddressBookFilePath_validPath_setsAddressBookFilePath() { assertEquals(path, modelManager.getAddressBookFilePath()); } + @Test + public void setCalendarFilePath_nullPath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.setCalendarFilePath(null)); + } + + @Test + public void setCalendarFilePath_validPath_setsCalendarFilePath() { + Path path = Paths.get("calendar/file/path"); + modelManager.setCalendarFilePath(path); + assertEquals(path, modelManager.getCalendarFilePath()); + } + + @Test + public void setTaskManagerFilePath_nullPath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.setTaskManagerFilePath(null)); + } + + @Test + public void setTaskManagerFilePath_validPath_setsTaskManagerFilePath() { + Path path = Paths.get("task/manager/file/path"); + modelManager.setTaskManagerFilePath(path); + assertEquals(path, modelManager.getTaskManagerFilePath()); + } + @Test public void hasPerson_nullPerson_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> modelManager.hasPerson(null)); @@ -93,15 +131,67 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredPersonList().remove(0)); } + @Test + public void addEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.addEvent(null)); + } + + @Test + public void deleteEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.deleteEventAt(null)); + } + + @Test + public void findEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.findEventAt(null)); + } + + @Test + public void findEvent_throwsEventNotFoundException() { + LocalDateTime eventTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + assertThrows(EventNotFoundException.class, () -> modelManager.findEventAt(eventTime)); + } + + @Test + public void eventsInRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.eventsInRange(null)); + } + + @Test + public void eventsInRange_returnsSuccessful() { + modelManager.addEvent(CONFERENCE); + EventPeriodBuilder builder = new EventPeriodBuilder(); + builder.changeStartAndEnd("2023-11-15 08:30", "2023-11-15 17:00"); + assertEquals(List.of(CONFERENCE), modelManager.eventsInRange(builder.build())); + } + + @Test + public void deleteEventsInRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.deleteEventsInRange(null)); + } + + @Test + public void deleteEventsInRange_successful() { + EventPeriodBuilder builder = new EventPeriodBuilder(); + builder.changeStartAndEnd("2023-11-15 08:30", "2023-11-15 17:00"); + modelManager.deleteEventsInRange(builder.build()); + LocalDateTime localDateTime = LocalDateTime.parse("2023-11-15 08:30", DATE_TIME_STRING_FORMATTER); + assertThrows(EventNotFoundException.class, () -> modelManager.findEventAt(localDateTime)); + } + @Test public void equals() { AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); AddressBook differentAddressBook = new AddressBook(); + UniMateCalendar calendar = new CalendarBuilder().withEvent(TEST_EVENT_A).withEvent(TEST_EVENT_B).build(); + UniMateCalendar differentCalendar = new UniMateCalendar(); + TaskManager taskManager = new TaskManagerBuilder().withTask(ASSIGNMENT).build(); + TaskManager differentTaskManager = new TaskManager(); UserPrefs userPrefs = new UserPrefs(); // same values -> returns true - modelManager = new ModelManager(addressBook, userPrefs); - ModelManager modelManagerCopy = new ModelManager(addressBook, userPrefs); + modelManager = new ModelManager(addressBook, calendar, taskManager, userPrefs); + ModelManager modelManagerCopy = new ModelManager(addressBook, calendar, taskManager, userPrefs); assertTrue(modelManager.equals(modelManagerCopy)); // same object -> returns true @@ -113,13 +203,23 @@ public void equals() { // different types -> returns false assertFalse(modelManager.equals(5)); - // different addressBook -> returns false - assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs))); + // different addressBook, same calendar, same taskManager -> returns false + assertFalse(modelManager.equals(new ModelManager(differentAddressBook, calendar, taskManager, userPrefs))); + + // same addressBook, different calendar, same taskManager -> returns false + assertFalse(modelManager.equals(new ModelManager(addressBook, differentCalendar, taskManager, userPrefs))); + + // same addressBook, same calendar, different taskManager -> returns false + assertFalse(modelManager.equals(new ModelManager(addressBook, calendar, differentTaskManager, userPrefs))); + + // different addressBook, different calendar, different taskManager -> returns false + assertFalse(modelManager.equals(new ModelManager(differentAddressBook, differentCalendar, + differentTaskManager, userPrefs))); // different filteredList -> returns false String[] keywords = ALICE.getName().fullName.split("\\s+"); modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords))); - assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); + assertFalse(modelManager.equals(new ModelManager(addressBook, calendar, taskManager, userPrefs))); // resets modelManager to initial state for upcoming tests modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); @@ -127,6 +227,6 @@ public void equals() { // different userPrefs -> returns false UserPrefs differentUserPrefs = new UserPrefs(); differentUserPrefs.setAddressBookFilePath(Paths.get("differentFilePath")); - assertFalse(modelManager.equals(new ModelManager(addressBook, differentUserPrefs))); + assertFalse(modelManager.equals(new ModelManager(addressBook, calendar, taskManager, differentUserPrefs))); } } diff --git a/src/test/java/seedu/address/model/UserPrefsTest.java b/src/test/java/seedu/address/model/UserPrefsTest.java index b1307a70d52..852ba8980c9 100644 --- a/src/test/java/seedu/address/model/UserPrefsTest.java +++ b/src/test/java/seedu/address/model/UserPrefsTest.java @@ -18,4 +18,16 @@ public void setAddressBookFilePath_nullPath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> userPrefs.setAddressBookFilePath(null)); } + @Test + public void setCalendarFilePath_nullPath_throwsNullPointerException() { + UserPrefs userPrefs = new UserPrefs(); + assertThrows(NullPointerException.class, () -> userPrefs.setCalendarFilePath(null)); + } + + @Test + public void setTaskManagerFilePath_nullPath_throwsNullPointerException() { + UserPrefs userPrefs = new UserPrefs(); + assertThrows(NullPointerException.class, () -> userPrefs.setTaskManagerFilePath(null)); + } + } diff --git a/src/test/java/seedu/address/model/calendar/CalendarTest.java b/src/test/java/seedu/address/model/calendar/CalendarTest.java new file mode 100644 index 00000000000..30ad07a17f2 --- /dev/null +++ b/src/test/java/seedu/address/model/calendar/CalendarTest.java @@ -0,0 +1,208 @@ +package seedu.address.model.calendar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.EventBuilder.DEFAULT_END_TIME_STRING; +import static seedu.address.testutil.EventBuilder.DEFAULT_START_TIME_STRING; +import static seedu.address.testutil.TypicalEvents.CONFERENCE; +import static seedu.address.testutil.TypicalEvents.TRAINING; +import static seedu.address.testutil.TypicalEvents.WORKSHOP; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.event.AllDaysEventListManager; +import seedu.address.model.event.Event; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.EventPeriodBuilder; + +public class CalendarTest { + private final UniMateCalendar calendar = new UniMateCalendar(); + + @Test + public void constructor() { + assertEquals(new AllDaysEventListManager(), calendar.getEventManager()); + } + + @Test + public void isEmptyValid_emptyCalendar_returnsTrue() { + UniMateCalendar emptyCalendar = new UniMateCalendar(); + assertTrue(emptyCalendar.isEmpty()); + } + + @Test + public void isEmptyValid_nonEmptyCalendar_returnsFalse() { + calendar.clear(); + calendar.addEvent(new EventBuilder().build()); + assertFalse(calendar.isEmpty()); + } + + @Test + public void isClearValid_emptyCalendar_returnsEmptyCalendar() { + UniMateCalendar emptyCalendar = new UniMateCalendar(); + emptyCalendar.clear(); + assertTrue(emptyCalendar.isEmpty()); + } + + @Test + public void isClearValid_nonEmptyCalendar_returnsEmptyCalendar() { + UniMateCalendar oneEventCalendar = new UniMateCalendar(); + oneEventCalendar.addEvent(new EventBuilder().build()); + assertFalse(oneEventCalendar.isEmpty()); + + oneEventCalendar.clear(); + assertTrue(oneEventCalendar.isEmpty()); + } + + @Test + public void addEvent_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> calendar.addEvent(null)); + } + + @Test + public void addEvent_validEvent_successful() { + calendar.clear(); + Event validEvent = new EventBuilder().build(); + calendar.addEvent(validEvent); + assertTrue(calendar.contains(validEvent)); + } + + @Test + public void deleteEvent_validEvent_successful() { + UniMateCalendar oneEventCalendar = new UniMateCalendar(); + oneEventCalendar.addEvent(new EventBuilder().build()); + LocalDateTime expectedDateTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + oneEventCalendar.deleteEventAt(expectedDateTime); + assertFalse(oneEventCalendar.hasEvents()); + } + + @Test + public void deleteEvent_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> calendar.deleteEventAt(null)); + } + + @Test + public void findEventAt_validEvent_successful() { + UniMateCalendar oneEventCalendar = new UniMateCalendar(); + Event sample = new EventBuilder().build(); + oneEventCalendar.addEvent(sample); + LocalDateTime expectedDateTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + assertEquals(sample, oneEventCalendar.findEventAt(expectedDateTime).get()); + } + + @Test + public void findEvent_nullEvent_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> calendar.findEventAt(null)); + } + + @Test + public void getEventsInRange_nullRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> calendar.getEventsInRange(null)); + } + + @Test + public void getEventsInRange_oneEvent_successful() { + UniMateCalendar oneEventCalendar = new UniMateCalendar(); + Event sample = new EventBuilder().build(); + oneEventCalendar.addEvent(sample); + EventPeriodBuilder builder = new EventPeriodBuilder(); + builder.changeStartAndEnd(DEFAULT_START_TIME_STRING, DEFAULT_END_TIME_STRING); + assertEquals(List.of(sample), oneEventCalendar.getEventsInRange(builder.build())); + } + + @Test + public void deleteEventsInRange_nullRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> calendar.deleteEventsInRange(null)); + } + + @Test + public void hasEvents_noEvent_false() { + UniMateCalendar noEventCalendar = new UniMateCalendar(); + assertFalse(noEventCalendar.hasEvents()); + } + + @Test + public void deleteEventsInRange_twoEvents_successful() { + UniMateCalendar eventCalendar = new UniMateCalendar(); + eventCalendar.addEvent(WORKSHOP); + eventCalendar.addEvent(CONFERENCE); + eventCalendar.addEvent(TRAINING); + EventPeriodBuilder builder = new EventPeriodBuilder(); + builder.changeStartAndEnd("2023-11-10 14:00", "2023-11-15 17:00"); + assertTrue(eventCalendar.hasEvents()); + + eventCalendar.deleteEventsInRange(builder.build()); + assertTrue(eventCalendar.hasEvents()); + + builder = new EventPeriodBuilder(); + builder.changeStartAndEnd("2023-11-15 14:00", "2023-11-20 14:01"); + eventCalendar.deleteEventsInRange(builder.build()); + assertFalse(eventCalendar.hasEvents()); + } + + @Test + public void isEqualsValid_nullValue_returnsFalse() { + calendar.clear(); + assertFalse(calendar.equals(null)); + } + + @Test + public void isEqualsValid_nonCalendarObject_returnFalse() { + calendar.clear(); + assertFalse(calendar.equals(new Object())); + } + + @Test + public void isEqualsValid_equalCalendarDeclaredObject_returnTrue() { + calendar.clear(); + Object equalCalendar = new UniMateCalendar(); + assertTrue(calendar.equals(equalCalendar)); + } + + @Test + public void isEqualsValid_notEqualCalendarDeclaredObject_returnFalse() { + calendar.clear(); + calendar.addEvent(new EventBuilder().build()); + Object nonEqualCalendar = new UniMateCalendar(); + assertFalse(calendar.equals(nonEqualCalendar)); + } + + @Test + public void isEqualsValid_equalCalendarDeclaredCalendar_returnTrue() { + calendar.clear(); + UniMateCalendar equalCalendar = new UniMateCalendar(); + assertTrue(calendar.equals(equalCalendar)); + } + + @Test + public void isEqualsValid_notEqualCalendarDeclaredCalendar_returnFalse() { + calendar.clear(); + calendar.addEvent(new EventBuilder().build()); + UniMateCalendar nonEqualCalendar = new UniMateCalendar(); + assertFalse(calendar.equals(nonEqualCalendar)); + } + + @Test + public void isEqualsValid_thisCalendar_returnTrue() { + assertTrue(calendar.equals(calendar)); + } + + @Test + public void getEarliestEventStartTimeInCurrentWeekTest() { + calendar.clear(); + + assertTrue(calendar.getEarliestEventStartTimeInCurrentWeek().isEmpty()); + } + + @Test + public void getLatestEventEndTimeInCurrentWeek() { + calendar.clear(); + + assertTrue(calendar.getLatestEventEndTimeInCurrentWeek().isEmpty()); + } +} diff --git a/src/test/java/seedu/address/model/event/AllDaysEventListManagerTest.java b/src/test/java/seedu/address/model/event/AllDaysEventListManagerTest.java new file mode 100644 index 00000000000..eef6249b309 --- /dev/null +++ b/src/test/java/seedu/address/model/event/AllDaysEventListManagerTest.java @@ -0,0 +1,64 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.EventBuilder.DEFAULT_START_TIME_STRING; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.EventBuilder; + +public class AllDaysEventListManagerTest { + private static Event sample = new EventBuilder().build(); + private static LocalDateTime eventTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + + private AllDaysEventListManager eventListManager = new AllDaysEventListManager(); + + @Test + public void constructorTest() { + } + + @Test + public void canAddEventTest() { + eventListManager.clear(); + assertTrue(eventListManager.canAddEvent(sample)); + } + + @Test + public void clearTest() { + eventListManager.clear(); + assertTrue(eventListManager.isEmpty()); + eventListManager.addEvent(sample); + eventListManager.clear(); + assertTrue(eventListManager.isEmpty()); + } + + @Test + public void hasEventTest() { + eventListManager.clear(); + assertFalse(eventListManager.hasEvents()); + eventListManager.addEvent(sample); + assertTrue(eventListManager.hasEvents()); + eventListManager.deleteEventAt(eventTime); + assertFalse(eventListManager.hasEvents()); + } + + @Test + public void eventAtTest() { + eventListManager.clear(); + assertTrue(eventListManager.eventAt(eventTime).isEmpty()); + eventListManager.addEvent(sample); + assertEquals(eventListManager.eventAt(eventTime).get(), sample); + } + + @Test + public void canAddEvent_eventOverlap_unsuccessful() { + eventListManager.clear(); + eventListManager.addEvent(sample); + assertFalse(eventListManager.canAddEvent(sample)); + } +} diff --git a/src/test/java/seedu/address/model/event/EventDescriptionTest.java b/src/test/java/seedu/address/model/event/EventDescriptionTest.java new file mode 100644 index 00000000000..a484d3da7d5 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventDescriptionTest.java @@ -0,0 +1,76 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_UNUSED_DESCRIPTION; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.event.exceptions.InvalidDescriptionException; +import seedu.address.testutil.EventDescriptionBuilder; + +public class EventDescriptionTest { + @Test + public void constructorTest() { + assertThrows(NullPointerException.class, () -> new EventDescription(null)); + + assertThrows(InvalidDescriptionException.class, () -> new EventDescription(INVALID_DESCRIPTION)); + } + + @Test + public void isValidTest() { + assertThrows(NullPointerException.class, () -> EventDescription.isValid(null)); + + assertFalse(EventDescription.isValid(INVALID_DESCRIPTION)); + + assertTrue(EventDescription.isValid(VALID_DESCRIPTION)); + + assertTrue(EventDescription.isValid(VALID_UNUSED_DESCRIPTION)); + } + + @Test + public void createUnusedDescriptionTest() { + assertEquals(EventDescription.createUnusedDescription().getDescription(), VALID_UNUSED_DESCRIPTION); + + assertNotEquals(new EventDescriptionBuilder() + .changeDescription(VALID_UNUSED_DESCRIPTION).build().getDescription(), + new EventDescriptionBuilder().build().getDescription()); + } + + @Test + public void getDescriptionTest() { + assertEquals(VALID_DESCRIPTION, new EventDescriptionBuilder().changeDescription( + VALID_DESCRIPTION).build().getDescription()); + } + + @Test + public void equalsTest() { + EventDescription validEventDescription = new EventDescriptionBuilder().build(); + EventDescription equivalentValidEventDescription = new EventDescriptionBuilder().build(); + EventDescription nonEquivalentValidEventDescription = new EventDescriptionBuilder() + .changeDescription(VALID_UNUSED_DESCRIPTION).build(); + Object nonEventDescriptionObject = new Object(); + + assertTrue(validEventDescription.equals(validEventDescription)); + + assertTrue(validEventDescription.equals(equivalentValidEventDescription)); + + assertFalse(validEventDescription.equals(nonEquivalentValidEventDescription)); + + assertFalse(validEventDescription.equals(nonEventDescriptionObject)); + } + + @Test + public void toStringTest() { + assertEquals(new EventDescriptionBuilder().build().toString(), + VALID_DESCRIPTION); + + assertNotEquals(new EventDescriptionBuilder().build().toString(), + VALID_UNUSED_DESCRIPTION); + } +} diff --git a/src/test/java/seedu/address/model/event/EventPeriodTest.java b/src/test/java/seedu/address/model/event/EventPeriodTest.java new file mode 100644 index 00000000000..e46230c6539 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventPeriodTest.java @@ -0,0 +1,203 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_DATE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.model.event.EventPeriod.MAX_TIME_OF_DAY; +import static seedu.address.model.event.EventPeriod.isValidPeriod; +import static seedu.address.testutil.Assert.assertThrows; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.event.exceptions.DateOutOfBoundsException; +import seedu.address.model.event.exceptions.InvalidEventPeriodException; +import seedu.address.testutil.EventPeriodBuilder; + +public class EventPeriodTest { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private static final String VALID_END_DATE_ONE_MINUTE_BEFORE_MIDNIGHT = "2023-01-01 23:59"; + @Test + public void constructorTest() { + assertThrows(DateTimeParseException.class, () -> + new EventPeriod(INVALID_DATE, VALID_END_DATE_EARLIER)); + + assertThrows(DateTimeParseException.class, () -> + new EventPeriod(VALID_START_DATE_EARLIER, INVALID_DATE)); + + assertThrows(DateTimeParseException.class, () -> + new EventPeriod(INVALID_DATE, INVALID_DATE)); + + assertThrows(NullPointerException.class, () -> + new EventPeriod(null, VALID_END_DATE_EARLIER)); + + assertThrows(NullPointerException.class, () -> + new EventPeriod(VALID_START_DATE_EARLIER, null)); + + assertThrows(NullPointerException.class, () -> + new EventPeriod(null, null)); + } + + @Test + public void createNonConflictingPeriodTest() { + EventPeriod nonConflictingPeriod = EventPeriod.createNonConflictingPeriod(); + EventPeriod validPeriod = new EventPeriodBuilder().build(); + + assertFalse(validPeriod.isOverlapping(nonConflictingPeriod)); + } + + @Test + public void isValidPeriodTest() { + assertTrue(isValidPeriod(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER)); + + assertThrows(InvalidEventPeriodException.class, () -> + isValidPeriod(VALID_END_DATE_EARLIER, VALID_START_DATE_EARLIER)); + + assertThrows(InvalidEventPeriodException.class, () -> + isValidPeriod(VALID_START_DATE_EARLIER, VALID_START_DATE_EARLIER)); + + assertThrows(DateTimeParseException.class, () -> + isValidPeriod(INVALID_DATE, VALID_END_DATE_EARLIER)); + + assertThrows(DateTimeParseException.class, () -> + isValidPeriod(VALID_START_DATE_EARLIER, INVALID_DATE)); + + assertThrows(DateTimeParseException.class, () -> + isValidPeriod(INVALID_DATE, INVALID_DATE)); + } + + @Test + public void isOverlappingTest() { + EventPeriod earlierNoOverlapEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + EventPeriod laterNoOverlapEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + EventPeriod earlierOverlappingEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_LATER).build(); + EventPeriod laterOverlappingEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + + assertThrows(NullPointerException.class, () -> earlierNoOverlapEventPeriod.isOverlapping(null)); + + assertTrue(earlierOverlappingEventPeriod.isOverlapping(laterOverlappingEventPeriod)); + + assertTrue(laterOverlappingEventPeriod.isOverlapping(earlierOverlappingEventPeriod)); + + assertFalse(earlierNoOverlapEventPeriod.isOverlapping(laterNoOverlapEventPeriod)); + + assertFalse(laterNoOverlapEventPeriod.isOverlapping(earlierNoOverlapEventPeriod)); + } + + @Test + public void compareToTest() { + EventPeriod earlierEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + EventPeriod laterEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + + assertThrows(NullPointerException.class, () -> earlierEventPeriod.compareTo(null)); + + assertEquals(-1, earlierEventPeriod.compareTo(laterEventPeriod)); + + assertEquals(0, earlierEventPeriod.compareTo(earlierEventPeriod)); + + assertEquals(1, laterEventPeriod.compareTo(earlierEventPeriod)); + } + + @Test + public void getDatesTest() { + EventPeriod validEventPeriod = new EventPeriodBuilder().build(); + + assertFalse(validEventPeriod.getDates().isEmpty()); + } + + @Test + public void boundByDateTest() { + LocalDate input = LocalDate.of(2023, 1, 2); + + EventPeriod startAndEnd = new EventPeriodBuilder().build(); + EventPeriod startNotEnd = new EventPeriodBuilder().changeEnd("2023-01-03 15:00").build(); + EventPeriod expectedStartNotEnd = new EventPeriodBuilder() + .changeEnd(input.atTime(MAX_TIME_OF_DAY).format(DATE_TIME_STRING_FORMATTER)).build(); + EventPeriod endNotStart = new EventPeriodBuilder().changeStart("2023-01-01 15:00").build(); + EventPeriod expectedEndNotStart = new EventPeriodBuilder() + .changeStart(input.atTime(LocalTime.MIDNIGHT).format(DATE_TIME_STRING_FORMATTER)).build(); + EventPeriod outOfBounds = new EventPeriodBuilder() + .changeStartAndEnd("2022-01-03 15:00", "2022-01-04 15:00").build(); + + assertEquals(startAndEnd, startAndEnd.boundPeriodByDate(input)); + + assertEquals(expectedStartNotEnd, startNotEnd.boundPeriodByDate(input)); + + assertEquals(expectedEndNotStart, endNotStart.boundPeriodByDate(input)); + + assertThrows(DateOutOfBoundsException.class, () -> outOfBounds.boundPeriodByDate(input)); + } + + @Test + public void equalsTest() { + EventPeriod validEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_LATER).build(); + EventPeriod equivalentEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_LATER).build(); + EventPeriod laterStartEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + EventPeriod earlierEndEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + Object nonEventPeriodObject = new Object(); + + assertThrows(NullPointerException.class, () -> validEventPeriod.equals(null)); + + assertTrue(validEventPeriod.equals(validEventPeriod)); + + assertTrue(validEventPeriod.equals(equivalentEventPeriod)); + + assertFalse(validEventPeriod.equals(nonEventPeriodObject)); + + assertFalse(validEventPeriod.equals(laterStartEventPeriod)); + + assertFalse(validEventPeriod.equals(earlierEndEventPeriod)); + } + + @Test + public void getDurationTest() { + EventPeriod validEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + EventPeriod fullDayPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_ONE_MINUTE_BEFORE_MIDNIGHT).build(); + + LocalDateTime start = LocalDateTime.parse(VALID_START_DATE_EARLIER, DATE_TIME_FORMATTER); + LocalDateTime end = LocalDateTime.parse(VALID_END_DATE_EARLIER, DATE_TIME_FORMATTER); + LocalDateTime endOneMinuteBeforeMidnight = LocalDateTime + .parse(VALID_END_DATE_ONE_MINUTE_BEFORE_MIDNIGHT, DATE_TIME_FORMATTER); + + assertEquals(validEventPeriod.getDuration(), Duration.between(start, end)); + + assertEquals(fullDayPeriod.getDuration(), + Duration.between(start, endOneMinuteBeforeMidnight).plusMinutes(1)); + } + + @Test + public void toStringTest() { + EventPeriod validEventPeriod = new EventPeriodBuilder() + .changeStartAndEnd(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + String expectedString = "start: " + + VALID_START_DATE_EARLIER + + "; end: " + + VALID_END_DATE_EARLIER; + + assertEquals(validEventPeriod.toString(), expectedString); + } +} diff --git a/src/test/java/seedu/address/model/event/EventTest.java b/src/test/java/seedu/address/model/event/EventTest.java new file mode 100644 index 00000000000..4b5d3c112a7 --- /dev/null +++ b/src/test/java/seedu/address/model/event/EventTest.java @@ -0,0 +1,216 @@ +package seedu.address.model.event; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_UNUSED_DESCRIPTION; +import static seedu.address.testutil.Assert.assertThrows; + +import java.time.LocalTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.EventDescriptionBuilder; +import seedu.address.testutil.EventPeriodBuilder; + +public class EventTest { + private static final EventDescription VALID_EVENT_DESCRIPTION = new EventDescriptionBuilder().build(); + private static final EventPeriod VALID_EVENT_PERIOD = new EventPeriodBuilder().build(); + @Test + public void constructorTest() { + assertThrows(NullPointerException.class, () -> new Event(null, null)); + + assertThrows(NullPointerException.class, () -> new Event(VALID_EVENT_DESCRIPTION, null)); + + assertThrows(NullPointerException.class, () -> new Event(null, VALID_EVENT_PERIOD)); + } + + @Test + public void createNonConflictingEventTest() { + Event nonConflictingEvent = Event.createNonConflictingEvent(); + + assertEquals(nonConflictingEvent.getDescription().getDescription(), + VALID_UNUSED_DESCRIPTION); + } + + @Test + public void getDescriptionTest() { + assertEquals(new EventBuilder().withDescription(VALID_DESCRIPTION).build().getDescription(), + new EventDescriptionBuilder().changeDescription(VALID_DESCRIPTION).build()); + + assertNotEquals(new EventBuilder().withDescription(VALID_DESCRIPTION).build().getDescription(), + new EventDescriptionBuilder().changeDescription(VALID_UNUSED_DESCRIPTION).build()); + } + + @Test + public void getEventPeriodTest() { + assertEquals(new EventBuilder().withStartEndDate(VALID_START_DATE_EARLIER, + VALID_END_DATE_EARLIER).build().getEventPeriod(), + new EventPeriodBuilder().changeStartAndEnd(VALID_START_DATE_EARLIER, + VALID_END_DATE_EARLIER).build()); + + assertNotEquals(new EventBuilder().withStartEndDate(VALID_START_DATE_EARLIER, + VALID_END_DATE_EARLIER).build().getEventPeriod(), + new EventPeriodBuilder().changeStartAndEnd(VALID_START_DATE_LATER, + VALID_END_DATE_LATER).build()); + } + + @Test + public void isConflictingTest() { + Event validEvent = new EventBuilder().withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_LATER).build(); + Event conflictingEvent = new EventBuilder().withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER) + .build(); + Event nonConflictingEvent = new EventBuilder().withStartEndDate(VALID_START_DATE_EARLIER, + VALID_END_DATE_EARLIER).build(); + Event nonConflictingEventOther = new EventBuilder().withStartEndDate(VALID_START_DATE_LATER, + VALID_END_DATE_LATER).build(); + + assertThrows(NullPointerException.class, () -> validEvent.isConflicting(null)); + + assertTrue(validEvent.isConflicting(conflictingEvent)); + + assertTrue(validEvent.isConflicting(validEvent)); + + assertFalse(nonConflictingEvent.isConflicting(nonConflictingEventOther)); + } + + @Test + public void changeDescriptionTest() { + EventDescription changeTo = new EventDescriptionBuilder().changeDescription(VALID_UNUSED_DESCRIPTION).build(); + Event original = new EventBuilder().build(); + + assertThrows(NullPointerException.class, () -> original.changeDescription(null)); + + original.changeDescription(changeTo); + assertEquals(original.getDescription(), changeTo); + } + + @Test + public void changeEventPeriodTest() { + EventPeriod changeTo = new EventPeriodBuilder().changeStartAndEnd(VALID_START_DATE_EARLIER, + VALID_END_DATE_EARLIER).build(); + Event original = new EventBuilder().build(); + + assertThrows(NullPointerException.class, () -> original.changeEventPeriod(null)); + + original.changeEventPeriod(changeTo); + assertEquals(original.getEventPeriod(), changeTo); + } + + @Test + public void getEventDaysTest() { + assertFalse(new EventBuilder().build().getEventDays().isEmpty()); + } + + @Test + public void equalsTest() { + Event validEvent = new EventBuilder().build(); + Event equalEvent = new EventBuilder().build(); + Event differentDescriptionEvent = new EventBuilder().withDescription(VALID_UNUSED_DESCRIPTION).build(); + Event differentPeriodEvent = new EventBuilder().withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER) + .build(); + Object nonEventObject = new Object(); + + assertTrue(validEvent.equals(validEvent)); + + assertTrue(validEvent.equals(equalEvent)); + + assertFalse(validEvent.equals(nonEventObject)); + + assertFalse(validEvent.equals(differentDescriptionEvent)); + + assertFalse(validEvent.equals(differentPeriodEvent)); + } + + @Test + public void compareStartTimeTest() { + EventBuilder earlierEventBuilder = new EventBuilder(); + earlierEventBuilder.withStartEndDate(VALID_START_DATE_EARLIER, VALID_START_DATE_LATER); + EventBuilder laterEventBuilder = new EventBuilder(); + laterEventBuilder.withStartEndDate(VALID_END_DATE_EARLIER, VALID_END_DATE_LATER); + Event earlierEvent = earlierEventBuilder.build(); + Event laterEvent = laterEventBuilder.build(); + + assertThrows(NullPointerException.class, () -> earlierEvent.compareStartTime(null)); + + assertTrue(earlierEvent.compareStartTime(laterEvent) < 0); + + assertTrue(laterEvent.compareStartTime(earlierEvent) > 0); + + assertEquals(0, earlierEvent.compareStartTime(earlierEvent)); + } + + @Test + public void compareEndTimeTest() { + EventBuilder earlierEventBuilder = new EventBuilder(); + earlierEventBuilder.withStartEndDate(VALID_START_DATE_EARLIER, VALID_START_DATE_LATER); + EventBuilder laterEventBuilder = new EventBuilder(); + laterEventBuilder.withStartEndDate(VALID_END_DATE_EARLIER, VALID_END_DATE_LATER); + Event earlierEvent = earlierEventBuilder.build(); + Event laterEvent = laterEventBuilder.build(); + + assertThrows(NullPointerException.class, () -> earlierEvent.compareEndTime(null)); + + assertTrue(earlierEvent.compareEndTime(laterEvent) < 0); + + assertTrue(laterEvent.compareEndTime(earlierEvent) > 0); + + assertEquals(0, earlierEvent.compareEndTime(earlierEvent)); + } + + @Test + public void getStartTimeTest() { + Event event = new EventBuilder().build(); + assertNotNull(event.getStartTime()); + } + + @Test + public void getEndTimeTest() { + Event event = new EventBuilder().build(); + assertNotNull(event.getEndTime()); + } + + @Test + public void getDurationOfEventTest() { + Event event = new EventBuilder().build(); + assertNotNull(event.getDurationOfEvent()); + } + + @Test + public void getDescriptionStringTest() { + Event event = new EventBuilder().build(); + assertNotNull(event.getDescriptionString()); + } + + @Test + public void getDayOfWeekTest() { + EventBuilder nonSingleDayEventBuilder = new EventBuilder(); + nonSingleDayEventBuilder.withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_LATER); + Event nonSingleDayEvent = nonSingleDayEventBuilder.build(); + Event singleDayEvent = new EventBuilder().build(); + + assertThrows(AssertionError.class, nonSingleDayEvent::getDayOfWeek); + + assertNotNull(singleDayEvent.getDayOfWeek()); + } + + @Test + public void getMinutesFromTimeToStartTimeTest() { + EventBuilder eventBuilder = new EventBuilder(); + Event event = eventBuilder.build(); + + assertThrows(NullPointerException.class, () -> event.getMinutesFromTimeToStartTime(null)); + + assertEquals(event.getMinutesFromTimeToStartTime(LocalTime.MIDNIGHT), + MINUTES.between(LocalTime.MIDNIGHT, eventBuilder.getStartTime())); + } +} diff --git a/src/test/java/seedu/address/model/event/SingleDayEventListTest.java b/src/test/java/seedu/address/model/event/SingleDayEventListTest.java new file mode 100644 index 00000000000..f1694ae3ed9 --- /dev/null +++ b/src/test/java/seedu/address/model/event/SingleDayEventListTest.java @@ -0,0 +1,187 @@ +package seedu.address.model.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_UNUSED_DESCRIPTION; +import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.EventBuilder.DEFAULT_START_TIME_STRING; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.event.exceptions.ConflictingEventException; +import seedu.address.model.event.exceptions.EventNotFoundException; +import seedu.address.testutil.EventBuilder; +import seedu.address.testutil.SingleDayEventListBuilder; + + + +public class SingleDayEventListTest { + @Test + public void constructorTest() { + assertThrows(NullPointerException.class, () -> new SingleDayEventList(null)); + } + + @Test + public void isEventAddValidTest() { + Event toBeAdded = new EventBuilder() + .withStartEndDate(VALID_END_DATE_EARLIER, VALID_START_DATE_LATER).build(); + Event startAfterEndSame = new EventBuilder() + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_START_DATE_LATER).build(); + Event startSameEndBefore = new EventBuilder() + .withStartEndDate(VALID_END_DATE_EARLIER, VALID_END_DATE_LATER).build(); + Event startImmediatelyAfter = new EventBuilder() + .withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + Event endImmediatelyBefore = new EventBuilder() + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + + assertThrows(NullPointerException.class, () -> dayEventList.isEventAddValid(null)); + + assertTrue(dayEventList.isEventAddValid(toBeAdded)); + + dayEventList.addEvent(toBeAdded); + + assertFalse(dayEventList.isEventAddValid(toBeAdded)); + + assertFalse(dayEventList.isEventAddValid(startAfterEndSame)); + + assertFalse(dayEventList.isEventAddValid(startSameEndBefore)); + + assertTrue(dayEventList.isEventAddValid(startImmediatelyAfter)); + + assertTrue(dayEventList.isEventAddValid(endImmediatelyBefore)); + } + + @Test + public void addEventTest() { + Event toBeAdded = new EventBuilder().build(); + Event duplicate = new EventBuilder().build(); + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + dayEventList.addEvent(toBeAdded); + + assertThrows(ConflictingEventException.class, () -> dayEventList.addEvent(toBeAdded)); + + assertThrows(ConflictingEventException.class, () -> dayEventList.addEvent(duplicate)); + } + + @Test + public void containsEventTest() { + Event toBeAdded = new EventBuilder().build(); + Event duplicateEvent = new EventBuilder().build(); + Event differentDescriptionEvent = new EventBuilder() + .withDescription(VALID_UNUSED_DESCRIPTION).build(); + Event differentPeriodEvent = new EventBuilder() + .withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + + assertThrows(NullPointerException.class, () -> dayEventList.containsEvent(null)); + + assertFalse(dayEventList.containsEvent(toBeAdded)); + + dayEventList.addEvent(toBeAdded); + + assertTrue(dayEventList.containsEvent(toBeAdded)); + + assertTrue(dayEventList.containsEvent(duplicateEvent)); + + assertFalse(dayEventList.containsEvent(differentDescriptionEvent)); + + assertFalse(dayEventList.containsEvent(differentPeriodEvent)); + } + + @Test + public void removeTest() { + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + Event toBeRemoved = new EventBuilder().build(); + Event duplicateEvent = new EventBuilder().build(); + Event differentDescriptionEvent = new EventBuilder() + .withDescription(VALID_UNUSED_DESCRIPTION).build(); + Event differentPeriodEvent = new EventBuilder() + .withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + + assertThrows(NullPointerException.class, () -> dayEventList.remove(null)); + + assertThrows(EventNotFoundException.class, () -> dayEventList.remove(toBeRemoved)); + + dayEventList.addEvent(toBeRemoved); + + assertThrows(EventNotFoundException.class, () -> dayEventList.remove(differentDescriptionEvent)); + + assertThrows(EventNotFoundException.class, () -> dayEventList.remove(differentPeriodEvent)); + + assertFalse(dayEventList.getDayEventList().isEmpty()); + + dayEventList.remove(toBeRemoved); + + assertTrue(dayEventList.getDayEventList().isEmpty()); + + dayEventList.addEvent(toBeRemoved); + + assertFalse(dayEventList.getDayEventList().isEmpty()); + + dayEventList.remove(duplicateEvent); + + assertTrue(dayEventList.getDayEventList().isEmpty()); + } + + @Test + public void getDayEventListTest() { + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + Event toBeAdded = new EventBuilder().build(); + + assertTrue(dayEventList.getDayEventList().isEmpty()); + + dayEventList.addEvent(toBeAdded); + + assertFalse(dayEventList.getDayEventList().isEmpty()); + assertTrue(dayEventList.getDayEventList().contains(toBeAdded)); + } + + @Test + public void eventAtTimeTest() { + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + Event toBeAdded = new EventBuilder().build(); + dayEventList.addEvent(toBeAdded); + LocalDateTime expectedDateTime = LocalDateTime.parse(DEFAULT_START_TIME_STRING, DATE_TIME_STRING_FORMATTER); + assertEquals(dayEventList.eventAtTime(expectedDateTime).get(), toBeAdded); + } + + @Test + public void equalsTest() { + SingleDayEventList dayEventList = new SingleDayEventListBuilder().build(); + SingleDayEventList comparisonList = new SingleDayEventListBuilder().build(); + Event validEvent = new EventBuilder().withDescription(VALID_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + Event nonConflictingEvent = new EventBuilder().withDescription(VALID_UNUSED_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + Object notSingleDayEventList = new Object(); + + assertTrue(dayEventList.equals(dayEventList)); + + assertTrue(dayEventList.equals(comparisonList)); + + assertFalse(dayEventList.equals(notSingleDayEventList)); + + dayEventList.addEvent(validEvent); + + assertFalse(dayEventList.equals(comparisonList)); + + comparisonList.addEvent(nonConflictingEvent); + + assertFalse(dayEventList.equals(comparisonList)); + + dayEventList.addEvent(nonConflictingEvent); + comparisonList.addEvent(validEvent); + + assertTrue(dayEventList.equals(comparisonList)); + } +} diff --git a/src/test/java/seedu/address/model/person/comparer/AddressComparatorTest.java b/src/test/java/seedu/address/model/person/comparer/AddressComparatorTest.java new file mode 100644 index 00000000000..57ea27b7b3e --- /dev/null +++ b/src/test/java/seedu/address/model/person/comparer/AddressComparatorTest.java @@ -0,0 +1,61 @@ +package seedu.address.model.person.comparer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB_LOWER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_CANDY; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class AddressComparatorTest { + + private static Person p1 = new PersonBuilder(ALICE).build(); + private static Person p2 = new PersonBuilder(BOB).build(); + private static Person p3 = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_CANDY).build(); + private static Person p4 = new PersonBuilder(BOB).withAddress(VALID_ADDRESS_CANDY).build(); + private static Person p5 = new PersonBuilder(BOB).withAddress(VALID_ADDRESS_BOB).build(); + private static Person p6 = new PersonBuilder(BOB).withAddress(VALID_ADDRESS_BOB_LOWER).build(); + + @Test + public void constructor_initialization_test1() { + AddressComparator addressComparator = new AddressComparator(true, true, 0); + assertTrue(addressComparator instanceof AddressComparator); + assertTrue(addressComparator.getIsActive()); + assertTrue(addressComparator.getIsReverse()); + assertEquals(addressComparator.getPriority(), 0); + } + + @Test + public void constructor_initialization_test2() { + AddressComparator addressComparator = new AddressComparator(false, true, 9999); + assertTrue(addressComparator instanceof AddressComparator); + assertFalse(addressComparator.getIsActive()); + assertTrue(addressComparator.getIsReverse()); + assertEquals(addressComparator.getPriority(), -1); + } + + @Test + public void execute_compareSuccess() { + AddressComparator addressComparator = new AddressComparator(true, true, 1); + int addressComparison1 = addressComparator.compare(p1, p3); + int addressComparison2 = addressComparator.compare(p2, p4); + + assertEquals(addressComparator.compare(p3, p4), 0); + assertEquals(addressComparator.compare(p1, p1), 0); + assertTrue(addressComparison1 < 0); + assertTrue(addressComparison2 < 0); + } + + @Test + public void execute_addressDifferentCaseSuccess() { + AddressComparator addressComparator = new AddressComparator(true, true, 1); + assertEquals(addressComparator.compare(p5, p6), 0); + } +} diff --git a/src/test/java/seedu/address/model/person/comparer/EmailComparatorTest.java b/src/test/java/seedu/address/model/person/comparer/EmailComparatorTest.java new file mode 100644 index 00000000000..1270786daa6 --- /dev/null +++ b/src/test/java/seedu/address/model/person/comparer/EmailComparatorTest.java @@ -0,0 +1,63 @@ +package seedu.address.model.person.comparer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_CANDY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_DANIEL; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class EmailComparatorTest { + + private static Person p1 = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).withEmail(VALID_EMAIL_AMY).build(); + private static Person p2 = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).withEmail(VALID_EMAIL_AMY).build(); + private static Person p3 = new PersonBuilder(ALICE).withEmail(VALID_EMAIL_AMY).build(); + private static Person p4 = new PersonBuilder(ALICE).withEmail(VALID_EMAIL_BOB).build(); + private static Person p5 = new PersonBuilder(ALICE).withName(VALID_NAME_CANDY).build(); + private static Person p6 = new PersonBuilder(BOB).withName(VALID_NAME_DANIEL).build(); + + @Test + public void constructor_initialization_test1() { + EmailComparator emailComparator = new EmailComparator(true, true, 0); + assertTrue(emailComparator instanceof EmailComparator); + assertTrue(emailComparator.getIsActive()); + assertTrue(emailComparator.getIsReverse()); + assertEquals(emailComparator.getPriority(), 0); + } + + @Test + public void constructor_initialization_test2() { + EmailComparator emailComparator = new EmailComparator(false, true, 1234); + assertTrue(emailComparator instanceof EmailComparator); + assertFalse(emailComparator.getIsActive()); + assertTrue(emailComparator.getIsReverse()); + assertEquals(emailComparator.getPriority(), -1); + } + + @Test + public void execute_compareSuccess() { + EmailComparator emailComparator = new EmailComparator(true, true, 1); + int emailComparison1 = emailComparator.compare(p1, p2); + int emailComparison2 = emailComparator.compare(p1, p3); + int emailComparison3 = emailComparator.compare(p4, p6); + int emailComparison4 = emailComparator.compare(p3, p5); + int emailComparison5 = emailComparator.compare(p1, p4); + int emailComparison6 = emailComparator.compare(p2, p6); + assertEquals(emailComparison1, 0); + assertEquals(emailComparison2, 0); + assertEquals(emailComparison3, 0); + assertTrue(emailComparison4 > 0); + assertTrue(emailComparison5 < 0); + assertTrue(emailComparison6 < 0); + } + +} diff --git a/src/test/java/seedu/address/model/person/comparer/NameComparatorTest.java b/src/test/java/seedu/address/model/person/comparer/NameComparatorTest.java new file mode 100644 index 00000000000..2892bb3a1a5 --- /dev/null +++ b/src/test/java/seedu/address/model/person/comparer/NameComparatorTest.java @@ -0,0 +1,65 @@ +package seedu.address.model.person.comparer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB_LOWER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_CANDY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_DANIEL; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class NameComparatorTest { + + private static Person p1 = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); + private static Person p2 = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); + private static Person p3 = new PersonBuilder(ALICE).withName(VALID_NAME_CANDY).build(); + private static Person p4 = new PersonBuilder(BOB).withName(VALID_NAME_DANIEL).build(); + private static Person p5 = new PersonBuilder(BOB).withName(VALID_NAME_BOB_LOWER).build(); + + @Test + public void constructor_initialization_test1() { + NameComparator nameComparator = new NameComparator(true, true, 0); + assertTrue(nameComparator instanceof NameComparator); + assertTrue(nameComparator.getIsActive()); + assertTrue(nameComparator.getIsReverse()); + assertEquals(nameComparator.getPriority(), 0); + } + + @Test + public void constructor_initialization_test2() { + NameComparator nameComparator = new NameComparator(false, true, 9999); + assertTrue(nameComparator instanceof NameComparator); + assertFalse(nameComparator.getIsActive()); + assertTrue(nameComparator.getIsReverse()); + assertEquals(nameComparator.getPriority(), -1); + } + + @Test + public void execute_compareSuccess() { + NameComparator nameComparator = new NameComparator(true, true, 1); + int nameComparison1 = nameComparator.compare(p1, p3); + int nameComparison2 = nameComparator.compare(p2, p4); + int nameComparison3 = nameComparator.compare(p4, p1); + + assertEquals(nameComparator.compare(p1, p2), 0); + assertEquals(nameComparator.compare(p1, p1), 0); + assertTrue(nameComparison1 < 0); + assertTrue(nameComparison2 < 0); + assertTrue(nameComparison3 > 0); + } + + @Test + public void execute_compareDifferentCaseSuccess() { + NameComparator nameComparator = new NameComparator(true, true, 1); + assertEquals(nameComparator.compare(p1, p5), 0); + } +} + + diff --git a/src/test/java/seedu/address/model/person/comparer/PhoneComparatorTest.java b/src/test/java/seedu/address/model/person/comparer/PhoneComparatorTest.java new file mode 100644 index 00000000000..dc5eefa522d --- /dev/null +++ b/src/test/java/seedu/address/model/person/comparer/PhoneComparatorTest.java @@ -0,0 +1,52 @@ +package seedu.address.model.person.comparer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +public class PhoneComparatorTest { + + private static Person p1 = new PersonBuilder(ALICE).build(); + private static Person p2 = new PersonBuilder(BOB).build(); + private static Person p3 = new PersonBuilder(ALICE).withPhone(VALID_PHONE_AMY).build(); + private static Person p4 = new PersonBuilder(BOB).withPhone(VALID_PHONE_AMY).build(); + + @Test + public void constructor_initialization_test1() { + PhoneComparator phoneComparator = new PhoneComparator(true, true, 0); + assertTrue(phoneComparator instanceof PhoneComparator); + assertTrue(phoneComparator.getIsActive()); + assertTrue(phoneComparator.getIsReverse()); + assertEquals(phoneComparator.getPriority(), 0); + } + + @Test + public void constructor_initialization_test2() { + PhoneComparator phoneComparator = new PhoneComparator(false, true, 9999); + assertTrue(phoneComparator instanceof PhoneComparator); + assertFalse(phoneComparator.getIsActive()); + assertTrue(phoneComparator.getIsReverse()); + assertEquals(phoneComparator.getPriority(), -1); + } + + @Test + public void execute_compareSuccess() { + PhoneComparator phoneComparator = new PhoneComparator(true, true, 1); + int phoneComparison1 = phoneComparator.compare(p1, p2); + int phoneComparison2 = phoneComparator.compare(p1, p3); + int phoneComparison3 = phoneComparator.compare(p2, p3); + + assertEquals(phoneComparator.compare(p3, p4), 0); + assertTrue(phoneComparison1 > 0); + assertTrue(phoneComparison2 > 0); + assertTrue(phoneComparison3 > 0); + } +} diff --git a/src/test/java/seedu/address/model/task/DeadlineTest.java b/src/test/java/seedu/address/model/task/DeadlineTest.java new file mode 100644 index 00000000000..b88560afe9e --- /dev/null +++ b/src/test/java/seedu/address/model/task/DeadlineTest.java @@ -0,0 +1,76 @@ +package seedu.address.model.task; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.task.Deadline.ABSENT_DEADLINE; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TaskBuilder.DEFAULT_DEADLINE; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.task.exceptions.InvalidDeadlineException; + +class DeadlineTest { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + @Test + public void constructorTest() { + assertThrows(InvalidDeadlineException.class, () -> + new Deadline("Invalid")); + + assertNull(new Deadline((String) null).getDeadline()); + } + + @Test + public void isPresentTest() { + assertTrue(new Deadline(DEFAULT_DEADLINE).isPresent()); + + assertFalse(new Deadline((String) null).isPresent()); + } + + @Test + public void compareToTest() { + Deadline nonPresent = new Deadline((String) null); + Deadline otherNonPresent = new Deadline((LocalDateTime) null); + Deadline present = new Deadline(DEFAULT_DEADLINE); + Deadline presentLater = new Deadline("2023-01-01 14:00"); + + assertEquals(0, nonPresent.compareTo(otherNonPresent)); + + assertEquals(1, presentLater.compareTo(present)); + + assertEquals(-1, present.compareTo(presentLater)); + + assertEquals(1, nonPresent.compareTo(present)); + + assertEquals(-1, present.compareTo(nonPresent)); + } + + @Test + public void getFormattedDateTimeTest() { + assertEquals(DEFAULT_DEADLINE, new Deadline(DEFAULT_DEADLINE).getFormattedDeadline()); + + assertEquals(ABSENT_DEADLINE, new Deadline((LocalDateTime) null).getFormattedDeadline()); + } + + @Test + public void equalsTest() { + Deadline nonPresent = new Deadline((String) null); + Deadline otherNonPresent = new Deadline((LocalDateTime) null); + Deadline present = new Deadline(DEFAULT_DEADLINE); + Deadline presentLater = new Deadline("2023-01-01 14:00"); + + assertTrue(nonPresent.equals(otherNonPresent)); + + assertTrue(present.equals(present)); + + assertFalse(present.equals(presentLater)); + + assertFalse(present.equals(nonPresent)); + } +} diff --git a/src/test/java/seedu/address/model/task/TaskDescriptionTest.java b/src/test/java/seedu/address/model/task/TaskDescriptionTest.java new file mode 100644 index 00000000000..7e08e6cb521 --- /dev/null +++ b/src/test/java/seedu/address/model/task/TaskDescriptionTest.java @@ -0,0 +1,61 @@ +package seedu.address.model.task; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TaskBuilder.DEFAULT_DESCRIPTION; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.task.exceptions.InvalidTaskDescriptionException; + +public class TaskDescriptionTest { + + @Test + public void constructorTest() { + assertThrows(NullPointerException.class, () -> new TaskDescription(null)); + + assertThrows(InvalidTaskDescriptionException.class, () -> new TaskDescription("")); + } + + @Test + public void isValidTest() { + assertThrows(NullPointerException.class, () -> TaskDescription.isValidDescription(null)); + + assertFalse(TaskDescription.isValidDescription("")); + + assertTrue(TaskDescription.isValidDescription("a")); + } + + @Test + public void getDescriptionTest() { + assertEquals("A", new TaskDescription("A").get()); + } + + @Test + public void equalsTest() { + TaskDescription validTaskDescription = new TaskDescription(DEFAULT_DESCRIPTION); + TaskDescription equivalentTaskDescription = new TaskDescription(DEFAULT_DESCRIPTION); + TaskDescription nonEquivalentTaskDescription = new TaskDescription("A"); + Object nonTaskDescriptionObject = new Object(); + + assertTrue(validTaskDescription.equals(validTaskDescription)); + + assertTrue(validTaskDescription.equals(equivalentTaskDescription)); + + assertFalse(validTaskDescription.equals(nonEquivalentTaskDescription)); + + assertFalse(validTaskDescription.equals(nonTaskDescriptionObject)); + } + + @Test + public void compareToTest() { + TaskDescription descriptionA = new TaskDescription("A"); + TaskDescription descriptionB = new TaskDescription("B"); + + assertEquals(1, descriptionB.compareTo(descriptionA)); + assertEquals(-1, descriptionA.compareTo(descriptionB)); + assertEquals(0, descriptionA.compareTo(descriptionA)); + } +} diff --git a/src/test/java/seedu/address/model/task/TaskListTest.java b/src/test/java/seedu/address/model/task/TaskListTest.java new file mode 100644 index 00000000000..d8af3370daf --- /dev/null +++ b/src/test/java/seedu/address/model/task/TaskListTest.java @@ -0,0 +1,129 @@ +package seedu.address.model.task; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalTasks.ASSIGNMENT; +import static seedu.address.testutil.TypicalTasks.HYDRATION; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.task.exceptions.DuplicateTaskException; +import seedu.address.model.task.exceptions.TaskNotFoundException; + +class TaskListTest { + private final TaskList taskList = new TaskList(); + + @Test + public void contains_nullTask_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> taskList.contains(null)); + } + + @Test + public void contains_taskNotInList_returnsFalse() { + assertFalse(taskList.contains(ASSIGNMENT)); + } + + @Test + public void contains_taskInList_returnsTrue() { + taskList.add(ASSIGNMENT); + assertTrue(taskList.contains(ASSIGNMENT)); + } + + @Test + public void add_nullTask_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> taskList.add(null)); + } + + @Test + public void add_duplicateTask_throwsDuplicateTaskException() { + taskList.add(ASSIGNMENT); + assertThrows(DuplicateTaskException.class, () -> taskList.add(ASSIGNMENT)); + } + + @Test + public void remove_nullTask_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> taskList.remove(null)); + } + + @Test + public void remove_taskDoesNotExist_throwsTaskNotFoundException() { + assertThrows(TaskNotFoundException.class, () -> taskList.remove(ASSIGNMENT)); + } + + @Test + public void remove_indexOutOfBounds_throwsIndexOutOfBoundsException() { + assertThrows(IndexOutOfBoundsException.class, () -> taskList.remove(1)); + } + + @Test + public void remove_existingTask_removesTask() { + taskList.add(ASSIGNMENT); + taskList.remove(ASSIGNMENT); + TaskList expectedTaskList = new TaskList(); + assertEquals(expectedTaskList, taskList); + } + + @Test + public void setPersons_nullTaskList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> taskList.setTasks((TaskList) null)); + } + + @Test + public void setTasks_taskList_replacesOwnListWithProvidedTaskList() { + taskList.add(ASSIGNMENT); + TaskList expectedTaskList = new TaskList(); + expectedTaskList.add(HYDRATION); + taskList.setTasks(expectedTaskList); + assertEquals(expectedTaskList, taskList); + } + + @Test + public void setTasks_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> taskList.setTasks((List) null)); + } + + @Test + public void setTasks_list_replacesOwnListWithProvidedList() { + taskList.add(ASSIGNMENT); + List tasks = Collections.singletonList(HYDRATION); + taskList.setTasks(tasks); + TaskList expectedTaskList = new TaskList(); + expectedTaskList.add(HYDRATION); + assertEquals(expectedTaskList, taskList); + } + + @Test + public void setTasks_listWithDuplicateTasks_throwsDuplicateTaskException() { + List listWithDuplicateTasks = Arrays.asList(ASSIGNMENT, ASSIGNMENT); + assertThrows(DuplicateTaskException.class, () -> taskList.setTasks(listWithDuplicateTasks)); + } + + @Test + public void toStringMethod() { + assertEquals(taskList.asUnmodifiableList().toString(), taskList.toString()); + } + + @Test + public void equalsMethod() { + taskList.add(ASSIGNMENT); + TaskList equivalentTaskList = new TaskList(); + equivalentTaskList.add(ASSIGNMENT); + TaskList nonEquivalentTaskList = new TaskList(); + nonEquivalentTaskList.add(HYDRATION); + Object nonTaskList = new Object(); + + assertTrue(taskList.equals(taskList)); + + assertTrue(taskList.equals(equivalentTaskList)); + + assertFalse(taskList.equals(nonEquivalentTaskList)); + + assertFalse(taskList.equals(nonTaskList)); + } +} diff --git a/src/test/java/seedu/address/model/task/TaskManagerTest.java b/src/test/java/seedu/address/model/task/TaskManagerTest.java new file mode 100644 index 00000000000..3534b3d298c --- /dev/null +++ b/src/test/java/seedu/address/model/task/TaskManagerTest.java @@ -0,0 +1,88 @@ +package seedu.address.model.task; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalTasks.ASSIGNMENT; +import static seedu.address.testutil.TypicalTasks.HYDRATION; +import static seedu.address.testutil.TypicalTasks.MEAL; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.task.exceptions.DuplicateTaskException; + +class TaskManagerTest { + private final TaskManager taskManager = new TaskManager(); + + @Test + public void constructor() { + assertEquals(new TaskList().asUnmodifiableList(), taskManager.getTaskList()); + } + + @Test + public void deleteTask_emptyTaskList_throwsIndexOutOfBoundsException() { + assertThrows(IndexOutOfBoundsException.class, () -> taskManager.deleteTask(1)); + } + + @Test + public void addTask_emptyTaskList_success() { + taskManager.addTask(ASSIGNMENT); + assertTrue(taskManager.hasTask(ASSIGNMENT)); + } + + @Test + public void addTask_duplicateTask_throwsDuplicateTaskException() { + taskManager.addTask(ASSIGNMENT); + assertThrows(DuplicateTaskException.class, () -> taskManager.addTask(ASSIGNMENT)); + } + + @Test + public void deleteTask_taskFound_success() { + taskManager.addTask(ASSIGNMENT); + assertEquals(ASSIGNMENT, taskManager.deleteTask(0)); + } + + @Test + public void sortDescription_success() { + taskManager.addTask(ASSIGNMENT); + taskManager.addTask(MEAL); + + taskManager.sortTasksBy("Deadline"); + taskManager.sortTasksBy("Description"); + + assertTrue(taskManager.deleteTask(0).equals(MEAL)); + assertTrue(taskManager.deleteTask(0).equals(ASSIGNMENT)); + } + + @Test + public void sortDeadline_success() { + taskManager.addTask(ASSIGNMENT); + taskManager.addTask(MEAL); + + taskManager.sortTasksBy("Description"); + taskManager.sortTasksBy("Deadline"); + + assertTrue(taskManager.deleteTask(0).equals(ASSIGNMENT)); + assertTrue(taskManager.deleteTask(0).equals(MEAL)); + } + + @Test + public void equalsMethod() { + taskManager.addTask(ASSIGNMENT); + TaskManager equivalentTaskManager = new TaskManager(); + equivalentTaskManager.addTask(ASSIGNMENT); + TaskManager emptyTaskManager = new TaskManager(); + TaskManager differentTaskManager = new TaskManager(); + differentTaskManager.addTask(HYDRATION); + Object notTaskManager = new Object(); + + assertTrue(taskManager.equals(taskManager)); + + assertTrue(taskManager.equals(equivalentTaskManager)); + + assertFalse(taskManager.equals(emptyTaskManager)); + + assertFalse(taskManager.equals(notTaskManager)); + } +} diff --git a/src/test/java/seedu/address/model/task/TaskTest.java b/src/test/java/seedu/address/model/task/TaskTest.java new file mode 100644 index 00000000000..caa3313be36 --- /dev/null +++ b/src/test/java/seedu/address/model/task/TaskTest.java @@ -0,0 +1,140 @@ +package seedu.address.model.task; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.model.task.Task.DATE_TIME_STRING_FORMATTER; +import static seedu.address.testutil.Assert.assertThrows; + +import java.time.LocalDateTime; +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +class TaskTest { + private static final String VALID_DESCRIPTION_STRING = "Eat Nuts"; + private static final TaskDescription VALID_TASK_DESCRIPTION = new TaskDescription(VALID_DESCRIPTION_STRING); + private static final Deadline VALID_DEADLINE = new Deadline("2023-02-11 14:00"); + private static final LocalDateTime VALID_DEADLINE_DATETIME = + LocalDateTime.parse("2023-02-11 14:00", DATE_TIME_STRING_FORMATTER); + + @Test + public void constructorTest() { + assertThrows(NullPointerException.class, () -> new Task((String) null, VALID_DEADLINE_DATETIME)); + + assertThrows(NullPointerException.class, () -> new Task((TaskDescription) null, VALID_DEADLINE)); + + Task taskWithNoDeadline = new Task(VALID_TASK_DESCRIPTION, new Deadline((String) null)); + + assertEquals(taskWithNoDeadline, new Task(VALID_DESCRIPTION_STRING, null)); + } + + @Test + public void getDescriptionTest() { + Task testTask = new Task(VALID_DESCRIPTION_STRING, VALID_DEADLINE_DATETIME); + assertEquals(VALID_TASK_DESCRIPTION, testTask.getDescription()); + assertNotEquals(VALID_DESCRIPTION_STRING, testTask.getDescription()); + } + + @Test + public void getDescriptionStringTest() { + Task testTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + assertEquals(VALID_DESCRIPTION_STRING, testTask.getDescriptionString()); + } + + @Test + public void getDeadlineTest() { + Task testTask = new Task(VALID_DESCRIPTION_STRING, VALID_DEADLINE_DATETIME); + assertEquals(VALID_DEADLINE, testTask.getDeadline()); + Task testTask2 = new Task(VALID_DESCRIPTION_STRING, null); + assertEquals(new Deadline((String) null), testTask2.getDeadline()); + } + + @Test + public void getDeadlineStringTest() { + Task testTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + assertEquals("2023-02-11 14:00", testTask.getDeadlineString()); + + Task testTask2 = new Task(VALID_DESCRIPTION_STRING, null); + assertEquals("-", testTask2.getDeadlineString()); + } + + @Test + public void equalsMethod() { + Task testTask = new Task(VALID_DESCRIPTION_STRING, VALID_DEADLINE_DATETIME); + Task equivalentTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + Task nonEquivalentTask = new Task(VALID_DESCRIPTION_STRING, null); + Task differentDescription = new Task("Eat Fruits", VALID_DEADLINE_DATETIME); + Object notTask = new Object(); + + assertTrue(testTask.equals(testTask)); + + assertTrue(testTask.equals(equivalentTask)); + + assertFalse(testTask.equals(nonEquivalentTask)); + + assertFalse(testTask.equals(differentDescription)); + + assertFalse(testTask.equals(notTask)); + } + + @Test + public void toStringMethod() { + Task testTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + String expected = Task.class.getCanonicalName() + "{description=" + VALID_TASK_DESCRIPTION + ", deadline=" + + VALID_DEADLINE + "}"; + assertEquals(expected, testTask.toString()); + } + + @Test + public void deadlineComparatorTest() { + Comparator comparator = new Task.TaskDeadlineComparator(); + + Task testTask = new Task(VALID_DESCRIPTION_STRING, VALID_DEADLINE_DATETIME); + Task sameTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + Task taskWithNoDeadline = new Task(VALID_DESCRIPTION_STRING, null); + Task differentDescription = new Task("Eat Fruits", VALID_DEADLINE_DATETIME); + Task laterDeadline = new Task(VALID_DESCRIPTION_STRING, + LocalDateTime.parse("2023-02-11 15:00", DATE_TIME_STRING_FORMATTER)); + + assertEquals(0, comparator.compare(testTask, sameTask)); + assertEquals(-1, comparator.compare(testTask, taskWithNoDeadline)); + assertEquals(1, comparator.compare(taskWithNoDeadline, testTask)); + assertTrue(comparator.compare(testTask, differentDescription) > 0); + assertTrue(comparator.compare(differentDescription, testTask) < 0); + assertEquals(-1, comparator.compare(testTask, laterDeadline)); + assertEquals(1, comparator.compare(laterDeadline, testTask)); + } + + @Test + public void descriptionComparatorTest() { + Comparator comparator = new Task.TaskDescriptorComparator(); + + Task testTask = new Task(VALID_DESCRIPTION_STRING, VALID_DEADLINE_DATETIME); + Task sameTask = new Task(VALID_TASK_DESCRIPTION, VALID_DEADLINE); + Task taskWithNoDeadline = new Task(VALID_DESCRIPTION_STRING, null); + Task differentDescription = new Task("Eat Fruits", VALID_DEADLINE_DATETIME); + Task laterDeadline = new Task(VALID_DESCRIPTION_STRING, + LocalDateTime.parse("2023-02-11 15:00", DATE_TIME_STRING_FORMATTER)); + + assertEquals(0, comparator.compare(testTask, sameTask)); + + assertEquals(0, comparator.compare(testTask, taskWithNoDeadline)); + + assertTrue(comparator.compare(testTask, differentDescription) > 0); + assertTrue(comparator.compare(differentDescription, testTask) < 0); + + assertEquals(0, comparator.compare(testTask, laterDeadline)); + } + + @Test + public void deadlineComparatorEqualsTest() { + assertTrue(new Task.TaskDeadlineComparator().equals(new Task.TaskDeadlineComparator())); + } + + @Test + public void descriptionComparatorEqualsTest() { + assertTrue(new Task.TaskDescriptorComparator().equals(new Task.TaskDescriptorComparator())); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java b/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java new file mode 100644 index 00000000000..ee662f63e1b --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedEventTest.java @@ -0,0 +1,65 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.JsonAdaptedEvent.MISSING_FIELD_MESSAGE_FORMAT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalEvents.CONFERENCE; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +public class JsonAdaptedEventTest { + private static final String INVALID_DESCRIPTION = ""; + private static final String INVALID_EVENT_PERIOD = "2010-10-10 12:00 - 2010-10-10 10:00"; + private static final String INVALID_EVENT_PERIOD_SYNTAX = "2010-10-10 10:00-2010-10-10 12:00"; + + private static final String VALID_DESCRIPTION = CONFERENCE.getDescription().getDescription(); + private static final String VALID_EVENT_PERIOD = CONFERENCE.getEventPeriod().getFormattedPeriod(); + + @Test + public void toModelType_validEventDetails_returnsEvent() throws Exception { + JsonAdaptedEvent event = new JsonAdaptedEvent(CONFERENCE); + assertEquals(CONFERENCE, event.toModelType()); + } + + @Test + public void toModelType_invalidDescription_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(INVALID_DESCRIPTION, VALID_EVENT_PERIOD); + String expectedMessage = EventDescription.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullDescription_throwsIllegalValueException() { + JsonAdaptedEvent event = new JsonAdaptedEvent(null, VALID_EVENT_PERIOD); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, EventDescription.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_invalidEventPeriodSyntax_throwsIllegalValueException() { + JsonAdaptedEvent event = + new JsonAdaptedEvent(VALID_DESCRIPTION, INVALID_EVENT_PERIOD_SYNTAX); + String expectedMessage = EventPeriod.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_nullEventPeriod_throwsIllegalValueException() { + JsonAdaptedEvent event = new JsonAdaptedEvent(VALID_DESCRIPTION, null); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, EventPeriod.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + + @Test + public void toModelType_isInvalidEventPeriod_throwsIllegalValueException() { + JsonAdaptedEvent event = new JsonAdaptedEvent(VALID_DESCRIPTION, INVALID_EVENT_PERIOD); + String expectedMessage = EventPeriod.PERIOD_INVALID; + assertThrows(IllegalValueException.class, expectedMessage, event::toModelType); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..4283ef9a77f 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -23,6 +23,8 @@ public class JsonAdaptedPersonTest { private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_DESCRIPTION = ""; + private static final String INVALID_EVENT_PERIOD = "2010-10-10 12:00 - 2010-10-10 10:00"; private static final String VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); @@ -31,6 +33,9 @@ public class JsonAdaptedPersonTest { private static final List VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); + private static final List VALID_EVENTS = BENSON.getEventList().stream() + .map(JsonAdaptedEvent::new) + .collect(Collectors.toList()); @Test public void toModelType_validPersonDetails_returnsPerson() throws Exception { @@ -41,14 +46,15 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +62,15 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,14 +78,15 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -86,14 +94,15 @@ public void toModelType_nullEmail_throwsIllegalValueException() { @Test public void toModelType_invalidAddress_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS, VALID_EVENTS); String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS, VALID_EVENTS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -103,7 +112,25 @@ public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags, VALID_EVENTS); + assertThrows(IllegalValueException.class, person::toModelType); + } + + @Test + public void toModelType_invalidEvents_throwsIllegalValueException() { + List invalidEvents = new ArrayList<>(VALID_EVENTS); + invalidEvents.add(new JsonAdaptedEvent(INVALID_DESCRIPTION, INVALID_EVENT_PERIOD)); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, invalidEvents); + assertThrows(IllegalValueException.class, person::toModelType); + } + + @Test + public void toModelType_duplicateEvents_throwsIllegalValueException() { + List duplicateEvents = new ArrayList<>(VALID_EVENTS); + duplicateEvents.addAll(VALID_EVENTS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, duplicateEvents); assertThrows(IllegalValueException.class, person::toModelType); } diff --git a/src/test/java/seedu/address/storage/JsonAdaptedTaskTest.java b/src/test/java/seedu/address/storage/JsonAdaptedTaskTest.java new file mode 100644 index 00000000000..cf339ac086b --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedTaskTest.java @@ -0,0 +1,56 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalTasks.ASSIGNMENT; +import static seedu.address.testutil.TypicalTasks.HYDRATION; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.task.Deadline; +import seedu.address.model.task.TaskDescription; + +public class JsonAdaptedTaskTest { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Task's %s field is missing!"; + private static final String INVALID_DESCRIPTION = ""; + private static final String VALID_DESCRIPTION = ASSIGNMENT.getDescriptionString(); + private static final String VALID_DEADLINE = ASSIGNMENT.getDeadlineString(); + + + @Test + public void toModelType_validTaskDeadline_returnsEvent() throws Exception { + JsonAdaptedTask task = new JsonAdaptedTask(ASSIGNMENT); + assertEquals(ASSIGNMENT, task.toModelType()); + } + + @Test + public void toModelType_validTaskAbsentDeadline_returnsEvent() throws Exception { + JsonAdaptedTask task = new JsonAdaptedTask(HYDRATION); + assertEquals(HYDRATION, task.toModelType()); + } + + @Test + public void toModelType_invalidDescription_throwsIllegalValueException() { + JsonAdaptedTask task = + new JsonAdaptedTask(INVALID_DESCRIPTION, VALID_DEADLINE); + String expectedMessage = TaskDescription.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, task::toModelType); + } + + @Test + public void toModelType_nullDescription_throwsIllegalValueException() { + JsonAdaptedTask task = new JsonAdaptedTask(null, VALID_DEADLINE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, TaskDescription.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, task::toModelType); + } + + @Test + public void toModelType_nullDeadline_throwsIllegalValueException() { + JsonAdaptedTask task = new JsonAdaptedTask(VALID_DESCRIPTION, null); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Deadline.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, task::toModelType); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java index 4e5ce9200c8..ec84b085aed 100644 --- a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java @@ -60,6 +60,11 @@ public void readAddressBook_invalidAndValidPersonAddressBook_throwDataLoadingExc assertThrows(DataLoadingException.class, () -> readAddressBook("invalidAndValidPersonAddressBook.json")); } + @Test + public void readAddressBook_duplicatePersonAddressBook_throwDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readAddressBook("duplicatePersonAddressBook.json")); + } + @Test public void readAndSaveAddressBook_allInOrder_success() throws Exception { Path filePath = testFolder.resolve("TempAddressBook.json"); diff --git a/src/test/java/seedu/address/storage/JsonCalendarStorageTest.java b/src/test/java/seedu/address/storage/JsonCalendarStorageTest.java new file mode 100644 index 00000000000..deb40818b15 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonCalendarStorageTest.java @@ -0,0 +1,108 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalEvents.REVIEW; +import static seedu.address.testutil.TypicalEvents.TRAINING; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; + +public class JsonCalendarStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonCalendarStorageTest"); + + @TempDir + public Path testFolder; + + @Test + public void readCalendar_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readCalendar(null)); + } + + private java.util.Optional readCalendar(String filePath) throws Exception { + return new JsonCalendarStorage(Paths.get(filePath)).readCalendar(addToTestDataPathIfNotNull(filePath)); + } + + private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { + return prefsFileInTestDataFolder != null + ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) + : null; + } + + @Test + public void read_missingFile_emptyResult() throws Exception { + assertFalse(readCalendar("NonExistentFile.json").isPresent()); + } + + @Test + public void read_notJsonFormat_exceptionThrown() { + assertThrows(DataLoadingException.class, () -> readCalendar("notJsonFormatCalendar.json")); + } + + @Test + public void readCalendar_invalidEventCalendar_throwDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readCalendar("invalidEventCalendar.json")); + } + + @Test + public void readCalendar_invalidAndValidEventCalendar_throwDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readCalendar("invalidAndValidEventCalendar.json")); + } + + @Test + public void readAndSaveCalendar_allInOrder_success() throws Exception { + Path filePath = testFolder.resolve("TempCalendar.json"); + UniMateCalendar original = getTypicalCalendar(); + JsonCalendarStorage jsonCalendarStorage = new JsonCalendarStorage(filePath); + + // Save in new file and read back + jsonCalendarStorage.saveCalendar(original, filePath); + ReadOnlyCalendar readBack = jsonCalendarStorage.readCalendar(filePath).get(); + assertEquals(original, new UniMateCalendar(readBack)); + + // Modify data, overwrite exiting file, and read back + original.addEvent(TRAINING); + jsonCalendarStorage.saveCalendar(original, filePath); + readBack = jsonCalendarStorage.readCalendar(filePath).get(); + assertEquals(original, new UniMateCalendar(readBack)); + + // Save and read without specifying file path + original.addEvent(REVIEW); + jsonCalendarStorage.saveCalendar(original); // file path not specified + readBack = jsonCalendarStorage.readCalendar().get(); // file path not specified + assertEquals(original, new UniMateCalendar(readBack)); + + } + + @Test + public void saveCalendar_nullCalendar_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveCalendar(null, "SomeFile.json")); + } + + /** + * Saves {@code calendar} at the specified {@code filePath}. + */ + private void saveCalendar(ReadOnlyCalendar calendar, String filePath) { + try { + new JsonCalendarStorage(Paths.get(filePath)) + .saveCalendar(calendar, addToTestDataPathIfNotNull(filePath)); + } catch (IOException ioe) { + throw new AssertionError("There should not be an error writing to the file.", ioe); + } + } + + @Test + public void saveCalendar_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveCalendar(new UniMateCalendar(), null)); + } +} diff --git a/src/test/java/seedu/address/storage/JsonSerializableCalendarTest.java b/src/test/java/seedu/address/storage/JsonSerializableCalendarTest.java new file mode 100644 index 00000000000..87d097bce21 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonSerializableCalendarTest.java @@ -0,0 +1,47 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.testutil.TypicalEvents; + +public class JsonSerializableCalendarTest { + + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableCalendarTest"); + private static final Path TYPICAL_EVENTS_FILE = TEST_DATA_FOLDER.resolve("typicalEventsCalendar.json"); + private static final Path INVALID_EVENT_FILE = TEST_DATA_FOLDER.resolve("invalidEventCalendar.json"); + private static final Path DUPLICATE_EVENT_FILE = TEST_DATA_FOLDER.resolve("duplicateEventCalendar.json"); + + @Test + public void toModelType_typicalEventsFile_success() throws Exception { + JsonSerializableCalendar dataFromFile = JsonUtil.readJsonFile(TYPICAL_EVENTS_FILE, + JsonSerializableCalendar.class).get(); + UniMateCalendar calendarFromFile = dataFromFile.toModelType(); + UniMateCalendar typicalEventsCalendar = TypicalEvents.getTypicalCalendar(); + assertEquals(calendarFromFile, typicalEventsCalendar); + } + + @Test + public void toModelType_invalidEventFile_throwsIllegalValueException() throws Exception { + JsonSerializableCalendar dataFromFile = JsonUtil.readJsonFile(INVALID_EVENT_FILE, + JsonSerializableCalendar.class).get(); + assertThrows(IllegalValueException.class, dataFromFile::toModelType); + } + + @Test + public void toModelType_duplicateEvents_throwsIllegalValueException() throws Exception { + JsonSerializableCalendar dataFromFile = JsonUtil.readJsonFile(DUPLICATE_EVENT_FILE, + JsonSerializableCalendar.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableCalendar.MESSAGE_DUPLICATE_EVENT, + dataFromFile::toModelType); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonSerializableTaskManagerTest.java b/src/test/java/seedu/address/storage/JsonSerializableTaskManagerTest.java new file mode 100644 index 00000000000..83b7d577f9b --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonSerializableTaskManagerTest.java @@ -0,0 +1,46 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.task.TaskManager; +import seedu.address.testutil.TypicalTasks; + +public class JsonSerializableTaskManagerTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", + "test", "data", "JsonSerializableTaskManagerTest"); + private static final Path TYPICAL_TASKS_FILE = TEST_DATA_FOLDER.resolve("typicalTasksTaskManager.json"); + private static final Path INVALID_TASK_FILE = TEST_DATA_FOLDER.resolve("invalidTaskTaskManager.json"); + private static final Path DUPLICATE_TASK_FILE = TEST_DATA_FOLDER.resolve("duplicateTaskTaskManager.json"); + + @Test + public void toModelType_typicalTasksFile_success() throws Exception { + JsonSerializableTaskManager dataFromFile = JsonUtil.readJsonFile(TYPICAL_TASKS_FILE, + JsonSerializableTaskManager.class).get(); + TaskManager taskManagerFromFile = dataFromFile.toModelType(); + TaskManager typicalTasksTaskManager = TypicalTasks.getTypicalTaskManager(); + assertEquals(taskManagerFromFile, typicalTasksTaskManager); + } + + @Test + public void toModelType_invalidTaskFile_throwsIllegalValueException() throws Exception { + JsonSerializableTaskManager dataFromFile = JsonUtil.readJsonFile(INVALID_TASK_FILE, + JsonSerializableTaskManager.class).get(); + assertThrows(IllegalValueException.class, dataFromFile::toModelType); + } + + @Test + public void toModelType_duplicateTasks_throwsIllegalValueException() throws Exception { + JsonSerializableTaskManager dataFromFile = JsonUtil.readJsonFile(DUPLICATE_TASK_FILE, + JsonSerializableTaskManager.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableTaskManager.MESSAGE_DUPLICATE_TASK, + dataFromFile::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/JsonTaskManagerStorageTest.java b/src/test/java/seedu/address/storage/JsonTaskManagerStorageTest.java new file mode 100644 index 00000000000..8ce13b5d9f5 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonTaskManagerStorageTest.java @@ -0,0 +1,109 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalTasks.FEATURE; +import static seedu.address.testutil.TypicalTasks.REPORT; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.TaskManager; + +public class JsonTaskManagerStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", + "test", "data", "JsonTaskManagerStorageTest"); + + @TempDir + public Path testFolder; + + @Test + public void readTaskManager_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readTaskManager(null)); + } + + private java.util.Optional readTaskManager(String filePath) throws Exception { + return new JsonTaskManagerStorage(Paths.get(filePath)).readTaskManager(addToTestDataPathIfNotNull(filePath)); + } + + private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { + return prefsFileInTestDataFolder != null + ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) + : null; + } + + @Test + public void read_missingFile_emptyResult() throws Exception { + assertFalse(readTaskManager("NonExistentFile.json").isPresent()); + } + + @Test + public void read_notJsonFormat_exceptionThrown() { + assertThrows(DataLoadingException.class, () -> readTaskManager("notJsonFormatTaskManager.json")); + } + + @Test + public void readTaskManager_invalidTaskTaskManager_throwDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readTaskManager("invalidTaskTaskManager.json")); + } + + @Test + public void readTaskManager_invalidAndValidTaskTaskManager_throwDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readTaskManager("invalidAndValidTaskTaskManager.json")); + } + + @Test + public void readAndSaveTaskManager_allInOrder_success() throws Exception { + Path filePath = testFolder.resolve("TempTaskManager.json"); + TaskManager original = getTypicalTaskManager(); + JsonTaskManagerStorage jsonTaskManagerStorage = new JsonTaskManagerStorage(filePath); + + // Save in new file and read back + jsonTaskManagerStorage.saveTaskManager(original, filePath); + ReadOnlyTaskManager readBack = jsonTaskManagerStorage.readTaskManager(filePath).get(); + assertEquals(original, new TaskManager(readBack)); + + // Modify data, overwrite exiting file, and read back + original.addTask(REPORT); + jsonTaskManagerStorage.saveTaskManager(original, filePath); + readBack = jsonTaskManagerStorage.readTaskManager(filePath).get(); + assertEquals(original, new TaskManager(readBack)); + + // Save and read without specifying file path + original.addTask(FEATURE); + jsonTaskManagerStorage.saveTaskManager(original); // file path not specified + readBack = jsonTaskManagerStorage.readTaskManager().get(); // file path not specified + assertEquals(original, new TaskManager(readBack)); + + } + + @Test + public void saveTaskManager_nullTaskManager_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveTaskManager(null, "SomeFile.json")); + } + + /** + * Saves {@code taskManager} at the specified {@code filePath}. + */ + private void saveTaskManager(ReadOnlyTaskManager taskManager, String filePath) { + try { + new JsonTaskManagerStorage(Paths.get(filePath)) + .saveTaskManager(taskManager, addToTestDataPathIfNotNull(filePath)); + } catch (IOException ioe) { + throw new AssertionError("There should not be an error writing to the file.", ioe); + } + } + + @Test + public void saveTaskManager_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveTaskManager(new TaskManager(), null)); + } +} diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java index 99a16548970..30bd53356c7 100644 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ b/src/test/java/seedu/address/storage/StorageManagerTest.java @@ -2,7 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static seedu.address.testutil.TypicalEvents.getTypicalCalendar; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTasks.getTypicalTaskManager; import java.nio.file.Path; @@ -14,6 +16,10 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.calendar.ReadOnlyCalendar; +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.task.ReadOnlyTaskManager; +import seedu.address.model.task.TaskManager; public class StorageManagerTest { @@ -25,8 +31,10 @@ public class StorageManagerTest { @BeforeEach public void setUp() { JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab")); + JsonCalendarStorage calendarStorage = new JsonCalendarStorage(getTempFilePath("calendar")); + JsonTaskManagerStorage taskManagerStorage = new JsonTaskManagerStorage(getTempFilePath("taskmanager")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs")); - storageManager = new StorageManager(addressBookStorage, userPrefsStorage); + storageManager = new StorageManager(addressBookStorage, calendarStorage, taskManagerStorage, userPrefsStorage); } private Path getTempFilePath(String fileName) { @@ -60,9 +68,50 @@ public void addressBookReadSave() throws Exception { assertEquals(original, new AddressBook(retrieved)); } + @Test + public void calendarReadSave() throws Exception { + /* + * Note: This is an integration test that verifies the StorageManager is properly wired to the + * {@link JsonCalendarStorage} class. + * More extensive testing of UserPref saving/reading is done in {@link JsonCalendarStorageTest} class. + */ + UniMateCalendar original = getTypicalCalendar(); + storageManager.saveCalendar(original); + ReadOnlyCalendar retrieved = storageManager.readCalendar().get(); + assertEquals(original, new UniMateCalendar(retrieved)); + } + + @Test + public void taskManagerReadSave() throws Exception { + /* + * Note: This is an integration test that verifies the StorageManager is properly wired to the + * {@link JsonTaskManagerStorage} class. + * More extensive testing of UserPref saving/reading is done in {@link JsonTaskManagerStorage} class. + */ + TaskManager original = getTypicalTaskManager(); + storageManager.saveTaskManager(original); + ReadOnlyTaskManager retrieved = storageManager.readTaskManager().get(); + assertEquals(original, new TaskManager(retrieved)); + } + + @Test + public void getUserPrefsFilePath() { + assertNotNull(storageManager.getUserPrefsFilePath()); + } + @Test public void getAddressBookFilePath() { assertNotNull(storageManager.getAddressBookFilePath()); } + @Test + public void getCalendarFilePath() { + assertNotNull(storageManager.getCalendarFilePath()); + } + + @Test + public void getTaskManagerFilePath() { + assertNotNull(storageManager.getTaskManagerFilePath()); + } + } diff --git a/src/test/java/seedu/address/testutil/CalendarBuilder.java b/src/test/java/seedu/address/testutil/CalendarBuilder.java new file mode 100644 index 00000000000..203e6cf3d3e --- /dev/null +++ b/src/test/java/seedu/address/testutil/CalendarBuilder.java @@ -0,0 +1,35 @@ +package seedu.address.testutil; + +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; + +/** + * A utility class to help with building Calendar objects. + * Example usage:
    + * {@code Calendar c = new CalendarBuilder().withEvent("Description", "2000-01-01 00:00", "2000-01-01 12:00") + * .build();} + */ +public class CalendarBuilder { + + private UniMateCalendar calendar; + + public CalendarBuilder() { + calendar = new UniMateCalendar(); + } + + public CalendarBuilder(UniMateCalendar calendar) { + this.calendar = calendar; + } + + /** + * Adds a new {@code Event} to the {@code Calendar} that we are building. + */ + public CalendarBuilder withEvent(Event event) { + calendar.addEvent(event); + return this; + } + + public UniMateCalendar build() { + return calendar; + } +} diff --git a/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java new file mode 100644 index 00000000000..711420981fe --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditEventDescriptorBuilder.java @@ -0,0 +1,70 @@ +package seedu.address.testutil; + +import java.time.format.DateTimeFormatter; + +import seedu.address.logic.commands.EditContactEventCommand; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; + +/** + * A builder class for creating an EditContactEventCommand.EditEventDescriptor object. + */ +public class EditEventDescriptorBuilder { + + private EditContactEventCommand.EditEventDescriptor descriptor; + + public EditEventDescriptorBuilder() { + descriptor = new EditContactEventCommand.EditEventDescriptor(); + } + + /** + * Constructs a new EditEventDescriptorBuilder with a given EditEventDescriptor. + */ + public EditEventDescriptorBuilder(EditContactEventCommand.EditEventDescriptor descriptor) { + this.descriptor = new EditContactEventCommand.EditEventDescriptor(descriptor); + } + + /** + * Returns an {@code EditEventDescriptor} with fields containing {@code event}'s details + */ + public EditEventDescriptorBuilder(Event event) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + descriptor = new EditContactEventCommand.EditEventDescriptor(); + descriptor.setEventDescription(event.getDescription()); + descriptor.setStart(event.getEventPeriod().getStart().format(formatter)); + descriptor.setEnd(event.getEventPeriod().getEnd().format(formatter)); + } + + /** + * Sets the {@code eventDescription} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withEventDescription(String eventDescription) { + descriptor.setEventDescription(new EventDescription(eventDescription)); + return this; + } + + /** + * Sets the {@code eventDescription} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withEventStartTime(String eventStartTime) { + descriptor.setStart(eventStartTime); + return this; + } + + /** + * Sets the {@code eventDescription} of the {@code EditEventDescriptor} that we are building. + */ + public EditEventDescriptorBuilder withEventEndTime(String eventEndTime) { + descriptor.setEnd(eventEndTime); + return this; + } + + /** + * Builds and returns the EditContactEventCommand.EditEventDescriptor object. + * + * @return The EditContactEventCommand.EditEventDescriptor object. + */ + public EditContactEventCommand.EditEventDescriptor build() { + return descriptor; + } +} diff --git a/src/test/java/seedu/address/testutil/EventBuilder.java b/src/test/java/seedu/address/testutil/EventBuilder.java new file mode 100644 index 00000000000..674b5192881 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EventBuilder.java @@ -0,0 +1,90 @@ +package seedu.address.testutil; + +import java.time.LocalTime; + +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDescription; +import seedu.address.model.event.EventPeriod; + +/** + * A utility class to help with building event objects. + */ +public class EventBuilder { + public static final String DEFAULT_DESCRIPTION_STRING = "Unused field"; + public static final String DEFAULT_START_TIME_STRING = "2023-01-01 13:00"; + public static final String DEFAULT_END_TIME_STRING = "2023-01-01 15:00"; + + private EventDescription description; + private EventPeriod eventPeriod; + + /** + * Creates an EventBuilder with default description and event periods. + */ + public EventBuilder() { + this.description = new EventDescription(DEFAULT_DESCRIPTION_STRING); + this.eventPeriod = new EventPeriod(DEFAULT_START_TIME_STRING, DEFAULT_END_TIME_STRING); + } + + /** + * Creates an event with the given EventBuilder attributes. + * + * @return event corresponding to EventBuilder's attributes. + */ + public Event build() { + return new Event(this.description, this.eventPeriod); + } + + /** + * Set the description to the user input. + * + * @param description user input. + * @return EventBuilder object with description changed to user input. + */ + public EventBuilder withDescription(String description) { + this.description = new EventDescription(description); + return this; + } + + /** + * Set the start date to the user input while setting the end date to the default. + * + * @param startDate user input in format 'yyyy-MM-dd HH:mm'. + * @return EventBuilder object with start date changed to user input. + */ + public EventBuilder withStartDate(String startDate) { + this.eventPeriod = new EventPeriod(startDate, DEFAULT_END_TIME_STRING); + return this; + } + + /** + * Set the end date to the user input while setting the start date to the default. + * + * @param endDate user input in format 'yyyy-MM-dd HH:mm'. + * @return EventBuilder object with the end date changed to user input. + */ + public EventBuilder withEndDate(String endDate) { + this.eventPeriod = new EventPeriod(DEFAULT_START_TIME_STRING, endDate); + return this; + } + + /** + * Set the start and end date to the user input. + * + * @param start user input start date in format 'yyyy-MM-dd HH:mm'. + * @param end user input end date in format 'yyyy-MM-dd HH:mm'. + * @return EventBuilder object with the start and end date changed to respective user inputs. + */ + public EventBuilder withStartEndDate(String start, String end) { + this.eventPeriod = new EventPeriod(start, end); + return this; + } + + /** + * Get the start time of the EventBuilder, regardless of date. + * + * @return start time of the EventBuilder, regardless of date. + */ + public LocalTime getStartTime() { + return eventPeriod.getStartTime(); + } +} diff --git a/src/test/java/seedu/address/testutil/EventDescriptionBuilder.java b/src/test/java/seedu/address/testutil/EventDescriptionBuilder.java new file mode 100644 index 00000000000..60d81f7b772 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EventDescriptionBuilder.java @@ -0,0 +1,40 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; + +import seedu.address.model.event.EventDescription; + +/** + * Creates an EventDescriptionBuilder with default description. + */ +public class EventDescriptionBuilder { + private static final String DEFAULT_DESCRIPTION = VALID_DESCRIPTION; + private EventDescription eventDescription; + + /** + * Creates an EventPeriodBuilder object with default description; + */ + public EventDescriptionBuilder() { + this.eventDescription = new EventDescription(DEFAULT_DESCRIPTION); + } + + /** + * Creates a EventDescription with given EventDescriptionBuilder attributes. + * + * @return EventDescription corresponding to the EventDescriptionBuilder attributes. + */ + public EventDescription build() { + return this.eventDescription; + } + + /** + * Change the description. + * + * @param newDescription String representing the description to be changed to. + * @return EventDescriptionBuilder object with new description. + */ + public EventDescriptionBuilder changeDescription(String newDescription) { + this.eventDescription = new EventDescription(newDescription); + return this; + } +} diff --git a/src/test/java/seedu/address/testutil/EventPeriodBuilder.java b/src/test/java/seedu/address/testutil/EventPeriodBuilder.java new file mode 100644 index 00000000000..ab0a5a6cac7 --- /dev/null +++ b/src/test/java/seedu/address/testutil/EventPeriodBuilder.java @@ -0,0 +1,62 @@ +package seedu.address.testutil; + +import seedu.address.model.event.EventPeriod; + +/** + * Creates an EventPeriodBuilder with default event period. + */ +public class EventPeriodBuilder { + private static final String DEFAULT_START_DATE_TIME_STRING = "2023-01-02 13:00"; + private static final String DEFAULT_END_DATE_TIME_STRING = "2023-01-02 15:00"; + private EventPeriod eventPeriod; + + /** + * Creates an EventPeriodBuilder object with default event period. + */ + public EventPeriodBuilder() { + this.eventPeriod = new EventPeriod(DEFAULT_START_DATE_TIME_STRING, DEFAULT_END_DATE_TIME_STRING); + } + + /** + * Creates an EventPeriod with given EventPeriodBuilder attributes. + * + * @return EventPeriod corresponding to the EventPeriodBuilder attributes. + */ + public EventPeriod build() { + return this.eventPeriod; + } + + /** + * Change the start date and time to the user input while setting the end date and time to default. + * + * @param newStart user input in format 'yyyy-MM-dd HH:mm'. + * @return EventPeriodBuilder object with start date and time set to user input. + */ + public EventPeriodBuilder changeStart(String newStart) { + this.eventPeriod = new EventPeriod(newStart, DEFAULT_END_DATE_TIME_STRING); + return this; + } + + /** + * Change the end date and time to the user input while setting the start date and time to default. + * + * @param newEnd user input in format 'yyyy-MM-dd HH:mm'. + * @return EventPeriodBuilder object with end date and time set to user input. + */ + public EventPeriodBuilder changeEnd(String newEnd) { + this.eventPeriod = new EventPeriod(DEFAULT_START_DATE_TIME_STRING, newEnd); + return this; + } + + /** + * Change the start date and time and end date and time to the user input. + * + * @param newStart start date and time user input in format 'yyyy-MM-dd HH:mm' + * @param newEnd end date and time user input in format 'yyyy-MM-dd HH:mm' + * @return EventPeriodBuilder object with start and end date and time set to user input. + */ + public EventPeriodBuilder changeStartAndEnd(String newStart, String newEnd) { + this.eventPeriod = new EventPeriod(newStart, newEnd); + return this; + } +} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..d21d5ceaf7d 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -1,8 +1,10 @@ package seedu.address.testutil; import java.util.HashSet; +import java.util.Optional; import java.util.Set; +import seedu.address.model.calendar.UniMateCalendar; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -26,6 +28,7 @@ public class PersonBuilder { private Email email; private Address address; private Set tags; + private Optional calendar = Optional.empty(); /** * Creates a {@code PersonBuilder} with the default details. @@ -47,6 +50,7 @@ public PersonBuilder(Person personToCopy) { email = personToCopy.getEmail(); address = personToCopy.getAddress(); tags = new HashSet<>(personToCopy.getTags()); + calendar = Optional.of(personToCopy.getCalendar()); } /** @@ -89,8 +93,22 @@ public PersonBuilder withEmail(String email) { return this; } + /** + * Sets the {@code Calendar} of the {@code Person} that we are building. + */ + public PersonBuilder withCalendar() { + this.calendar = Optional.of(TypicalEvents.getTypicalCalendar()); + return this; + } + + /** + * Creates a new Person object using the specified attributes. + * If a calendar is present, it is included in the Person object; + * otherwise, a Person object is created without a calendar. + */ public Person build() { - return new Person(name, phone, email, address, tags); + return this.calendar.map(calendar -> new Person(name, phone, email, address, tags, calendar)) + .orElseGet(() -> new Person(name, phone, email, address, tags)); } } diff --git a/src/test/java/seedu/address/testutil/PersonFilterBuilder.java b/src/test/java/seedu/address/testutil/PersonFilterBuilder.java new file mode 100644 index 00000000000..89d3355643a --- /dev/null +++ b/src/test/java/seedu/address/testutil/PersonFilterBuilder.java @@ -0,0 +1,83 @@ +package seedu.address.testutil; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * A utility class to help with building PersonFilter objects. + */ +public class PersonFilterBuilder { + + private FilterCommand.PersonFilter filter; + + public PersonFilterBuilder() { + filter = new FilterCommand.PersonFilter(); + } + + public PersonFilterBuilder(FilterCommand.PersonFilter filter) { + this.filter = new FilterCommand.PersonFilter(filter); + } + + /** + * Returns an {@code EditPersonDescriptor} with fields containing {@code person}'s details + */ + public PersonFilterBuilder(Person person) { + filter = new FilterCommand.PersonFilter(); + filter.setName(person.getName().fullName); + filter.setPhone(person.getPhone().value); + filter.setEmail(person.getEmail().value); + filter.setAddress(person.getAddress().value); + filter.setTags(person.getTags()); + } + + /** + * Sets the {@code Name} of the {@code EditPersonDescriptor} that we are building. + */ + public PersonFilterBuilder withName(String name) { + filter.setName(name); + return this; + } + + /** + * Sets the {@code Phone} of the {@code EditPersonDescriptor} that we are building. + */ + public PersonFilterBuilder withPhone(String phone) { + filter.setPhone(phone); + return this; + } + + /** + * Sets the {@code Email} of the {@code EditPersonDescriptor} that we are building. + */ + public PersonFilterBuilder withEmail(String email) { + filter.setEmail(email); + return this; + } + + /** + * Sets the {@code Address} of the {@code EditPersonDescriptor} that we are building. + */ + public PersonFilterBuilder withAddress(String address) { + filter.setAddress(address); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} + * that we are building. + */ + public PersonFilterBuilder withTags(String... tags) { + Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + filter.setTags(tagSet); + return this; + } + + public FilterCommand.PersonFilter build() { + return filter; + } +} diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..88b951260c2 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -2,14 +2,19 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.time.format.DateTimeFormatter; import java.util.Set; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditContactEventCommand; import seedu.address.model.person.Person; import seedu.address.model.tag.Tag; @@ -59,4 +64,19 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip } return sb.toString(); } + + /** + * Returns the part of command string for the given {@code EditPersonDescriptor}'s details. + */ + public static String getEditPersonEventDetails(EditContactEventCommand.EditEventDescriptor descriptor) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + StringBuilder sb = new StringBuilder(); + descriptor.getEventDescription().ifPresent(desc -> sb.append(PREFIX_EVENT_DESCRIPTION) + .append(desc.getDescription()).append(" ")); + descriptor.getStart().ifPresent(start -> sb.append(PREFIX_EVENT_START_DATE_TIME) + .append(start.format(formatter)).append(" ")); + descriptor.getEnd().ifPresent(end -> sb.append(PREFIX_EVENT_END_DATE_TIME) + .append(end.format(formatter)).append(" ")); + return sb.toString(); + } } diff --git a/src/test/java/seedu/address/testutil/SingleDayEventListBuilder.java b/src/test/java/seedu/address/testutil/SingleDayEventListBuilder.java new file mode 100644 index 00000000000..ec365f7c785 --- /dev/null +++ b/src/test/java/seedu/address/testutil/SingleDayEventListBuilder.java @@ -0,0 +1,41 @@ +package seedu.address.testutil; + +import java.time.LocalDate; + +import seedu.address.model.event.SingleDayEventList; + +/** + * A utility class to help with building SingleDayEventList objects. + */ +public class SingleDayEventListBuilder { + private static final LocalDate validDate = LocalDate.of(2023, 1, 1); + private LocalDate date; + + /** + * Construct a SingleDayEventListBuilder object with default attributes. + */ + public SingleDayEventListBuilder() { + this.date = validDate; + } + + /** + * Change the date of the SingleDayEventListBuilder object. + * + * @param newDate date to be changed to. + * @return SingleDayEventListBuilder object with updated date. + */ + public SingleDayEventListBuilder withDate(LocalDate newDate) { + this.date = newDate; + return this; + } + + /** + * Create the SingleDayEventList object with the attributes of this SingleDayEventListBuilder object. + * + * @return SingleDayEventList object with date corresponding to the SingleDayEventListBuilder object's date + * attribute. + */ + public SingleDayEventList build() { + return new SingleDayEventList(date); + } +} diff --git a/src/test/java/seedu/address/testutil/TaskBuilder.java b/src/test/java/seedu/address/testutil/TaskBuilder.java new file mode 100644 index 00000000000..f72356430bc --- /dev/null +++ b/src/test/java/seedu/address/testutil/TaskBuilder.java @@ -0,0 +1,65 @@ +package seedu.address.testutil; + +import seedu.address.model.task.Deadline; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskDescription; + +/** + * A utility class to help with building Task objects. + */ +public class TaskBuilder { + public static final String DEFAULT_DESCRIPTION = "Submit Assignment"; + public static final String DEFAULT_DEADLINE = "2023-01-01 13:00"; + + private TaskDescription description; + private Deadline deadline; + + /** + * Creates a TaskBuilder with default description and deadline. + */ + public TaskBuilder() { + this.description = new TaskDescription(DEFAULT_DESCRIPTION); + this.deadline = new Deadline(DEFAULT_DEADLINE); + } + + /** + * Creates a task with the given TaskBuilder attributes. + * + * @return task corresponding to TaskBuilder's attributes. + */ + public Task build() { + return new Task(description, deadline); + } + + /** + * Removes the deadline of the task + * + * @return TaskBuilder object with an empty deadline. + */ + public TaskBuilder withNoDeadline() { + this.deadline = new Deadline((String) null); + return this; + } + + /** + * Sets the deadline of the task to the user input. + * + * @param deadline the user input + * @return TaskBuilder object with deadline changed to user input. + */ + public TaskBuilder withDeadline(String deadline) { + this.deadline = new Deadline(deadline); + return this; + } + + /** + * Sets the description of the task to the user input. + * + * @param description the user input. + * @return TaskBuilder object with description changed to user input. + */ + public TaskBuilder withDescription(String description) { + this.description = new TaskDescription(description); + return this; + } +} diff --git a/src/test/java/seedu/address/testutil/TaskManagerBuilder.java b/src/test/java/seedu/address/testutil/TaskManagerBuilder.java new file mode 100644 index 00000000000..22ee9db0b8d --- /dev/null +++ b/src/test/java/seedu/address/testutil/TaskManagerBuilder.java @@ -0,0 +1,28 @@ +package seedu.address.testutil; + +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; + +/** + * A utility class to help with building TaskManager objects. + */ +public class TaskManagerBuilder { + + private TaskManager taskManager; + + public TaskManagerBuilder() { + taskManager = new TaskManager(); + } + + /** + * Adds a new {@code Task} to the {@code TaskManager} that is being built. + */ + public TaskManagerBuilder withTask(Task task) { + taskManager.addTask(task); + return this; + } + + public TaskManager build() { + return taskManager; + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalEvents.java b/src/test/java/seedu/address/testutil/TypicalEvents.java new file mode 100644 index 00000000000..f3a12190122 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalEvents.java @@ -0,0 +1,69 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.CommandTestUtil.VALID_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_END_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_EARLIER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_START_DATE_LATER; +import static seedu.address.logic.commands.CommandTestUtil.VALID_UNUSED_DESCRIPTION; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.calendar.UniMateCalendar; +import seedu.address.model.event.Event; + +/** + * A utility class containing a list of {@code Event} objects to be used in tests. + */ +public class TypicalEvents { + + public static final Event CONFERENCE = new EventBuilder().withDescription("Conference") + .withStartEndDate("2023-11-15 08:30", "2023-11-15 17:00").build(); + + public static final Event LAUNCH = new EventBuilder() + .withDescription("Launch and Marketing Strategy Discussion") + .withStartEndDate("2023-12-05 10:00", "2023-12-05 12:00").build(); + + public static final Event MEETING = new EventBuilder().withDescription("Team Meeting") + .withStartEndDate("2023-10-25 09:00", "2023-10-25 10:30").build(); + + public static final Event WEBINAR = new EventBuilder() + .withDescription("Webinar") + .withStartEndDate("2023-10-30 15:00", "2023-10-30 16:30").build(); + + public static final Event WORKSHOP = new EventBuilder() + .withDescription("Workshop") + .withStartEndDate("2023-11-10 14:00", "2023-11-10 16:30").build(); + + // Manually added + public static final Event TRAINING = new EventBuilder().withDescription("Customer Support Training") + .withStartEndDate("2023-11-20 09:00", "2023-11-20 12:00").build(); + public static final Event REVIEW = new EventBuilder().withDescription("Project Review") + .withStartEndDate("2023-10-29 15:00", "2023-10-29 16:30").build(); + + // Manually added - Event's details found in {@code CommandTestUtil} + public static final Event TEST_EVENT_A = new EventBuilder().withDescription(VALID_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_EARLIER, VALID_END_DATE_EARLIER).build(); + + public static final Event TEST_EVENT_B = new EventBuilder().withDescription(VALID_UNUSED_DESCRIPTION) + .withStartEndDate(VALID_START_DATE_LATER, VALID_END_DATE_LATER).build(); + + private TypicalEvents() {} // prevents instantiation + + /** + * Returns an {@code Calendar} with all the typical events. + */ + public static UniMateCalendar getTypicalCalendar() { + UniMateCalendar calendar = new UniMateCalendar(); + for (Event event : getTypicalEvents()) { + calendar.addEvent(event); + } + return calendar; + } + + public static List getTypicalEvents() { + return new ArrayList<>(Arrays.asList(CONFERENCE, LAUNCH, MEETING, WEBINAR, WORKSHOP)); + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..085883bd44b 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -9,4 +9,6 @@ public class TypicalIndexes { public static final Index INDEX_FIRST_PERSON = Index.fromOneBased(1); public static final Index INDEX_SECOND_PERSON = Index.fromOneBased(2); public static final Index INDEX_THIRD_PERSON = Index.fromOneBased(3); + public static final Index INVALID_INDEX = Index.fromOneBased(999); + public static final Index INDEX_FIRST_EVENT = Index.fromOneBased(1); } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..68ecc8ae0a9 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -26,11 +26,11 @@ public class TypicalPersons { public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") .withPhone("94351253") - .withTags("friends").build(); + .withTags("friends").withCalendar().build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") .withAddress("311, Clementi Ave 2, #02-25") .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); + .withTags("owesMoney", "friends").withCalendar().build(); public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") .withEmail("heinz@example.com").withAddress("wall street").build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") @@ -70,7 +70,19 @@ public static AddressBook getTypicalAddressBook() { return ab; } + public static AddressBook getTypicalUnsortedAddressBook() { + AddressBook ab = new AddressBook(); + for (Person person : getTypicalUnsortedPersons()) { + ab.addPerson(person); + } + return ab; + } + public static List getTypicalPersons() { return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); } + + public static List getTypicalUnsortedPersons() { + return new ArrayList<>(Arrays.asList(CARL, GEORGE, ALICE, FIONA, DANIEL, ELLE, BENSON)); + } } diff --git a/src/test/java/seedu/address/testutil/TypicalTasks.java b/src/test/java/seedu/address/testutil/TypicalTasks.java new file mode 100644 index 00000000000..7cea74c21df --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalTasks.java @@ -0,0 +1,51 @@ +package seedu.address.testutil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskManager; + +/** + * A utility class containing a list of {@code Task} objects to be used in tests + */ +public class TypicalTasks { + public static final Task ASSIGNMENT = new TaskBuilder() + .withDescription("Submit Assignment") + .withDeadline("2023-11-15 08:30").build(); + + public static final Task HYDRATION = new TaskBuilder() + .withDescription("Drink more water!") + .withNoDeadline().build(); + + public static final Task MEAL = new TaskBuilder() + .withDescription("Lunch") + .withDeadline("2023-11-15 15:00").build(); + + // Manually added + public static final Task REPORT = new TaskBuilder() + .withDescription("Finish Report") + .withNoDeadline().build(); + + public static final Task FEATURE = new TaskBuilder() + .withDescription("Push Feature") + .withDeadline("2023-11-15 15:00").build(); + + private TypicalTasks() {} // prevents instantiation + + /** + * Returns a {@code TaskManager} with all typical tasks. + */ + public static TaskManager getTypicalTaskManager() { + TaskManager taskManager = new TaskManager(); + for (Task task : getTypicalTasks()) { + taskManager.addTask(task); + } + return taskManager; + } + + public static List getTypicalTasks() { + return new ArrayList<>(Arrays.asList(ASSIGNMENT, HYDRATION, MEAL)); + } +}