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..293691bae68 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.gradle/ /build/ src/main/resources/docs/ +/bin/ # IDEA files /.idea/ @@ -21,3 +22,10 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +docs/_markbind/logs/ + +.classpath +.project +.settings/ + +export/ diff --git a/README.md b/README.md index 13f5c77403f..3b297b01d21 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2324S1-CS2103T-W11-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-W11-1/tp/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. +## HRMate + +HRMate is a **desktop address book application** that aims to streamline HR processes, by offering an intuitive, CLI-based contact management system with specialised functionalities for HR tasks. + +* Besides normal address book function, HRMate allows HR managers to: + * Managing people by tags + * Importing/Exporting their own csv files + +### Resources +* **User Guide** for HRMate can be found here: [User Guide](https://github.com/AY2324S1-CS2103T-W11-1/tp/blob/master/docs/UserGuide.md). +* **Developer Guide** for HRMate can be found here: [Developer Guide](https://github.com/AY2324S1-CS2103T-W11-1/tp/blob/master/docs/DeveloperGuide.md). +* Information of contributors for HRMate can be found here:[Team Inforation](https://github.com/AY2324S1-CS2103T-W11-1/tp/blob/master/docs/AboutUs.md). +### Acknowledgement +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org) diff --git a/build.gradle b/build.gradle index a2951cc709e..d4845b0230a 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'hrmate.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..54d555ab460 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,59 +1,65 @@ --- -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` +You can reach us at the email + - `mary84060117[at]gmail.com` + - `tan.yuhao.ivan[at]gmail.com` + - `weihong.ong[at]u.nus.edu.sg` + - `nancyqinilm[at]gmail.com` + - `ryanozx[at]u.nus.edu.sg` ## Project team -### John Doe - - +### Chen Qun -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Project Advisor +[[github](https://github.com/jean-cq)] +[[portfolio](team/jean-cq.md)] -### Jane Doe +* Role: Developer, Deliverables and deadlines +* Responsibilities: Ensure project deliverables are done on time and in the right format. +### Ivan Tan - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/ivyy-poison)] +[[portfolio](team/ivyy-poison.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer, Integration +* Responsibilities: In charge of versioning of the code, maintaining the code repository, integrating various parts of the software to create a whole. -### Johnny Doe +### Ong Wei Hong - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/ong-wei-hong)] [[portfolio](team/ong-wei-hong.md)] -* Role: Developer -* Responsibilities: Data +* Role: Developer, Code Quality +* Responsibilities: Looks after code quality, ensures adherence to coding standards, etc. -### Jean Doe +### Qin Nanxin - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/infibeyond)] +[[portfolio](team/infibeyond.md)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Developer, Documentation +* Responsibilities: Ensures the quality of various project documents. -### James Doe +### Ryan Ong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/ryanozx)] +[[portfolio](team/ryanozx.md)] -* Role: Developer -* Responsibilities: UI +* Role: Developer, Testing +* Responsibilities: Ensures the testing of the project is done properly and on time. 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..d1578227519 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,15 +1,24 @@ ---- -layout: page -title: Developer Guide ---- -* Table of Contents -{:toc} + + layout: default.md + title: "Developer Guide" + pageNav: 3 + + +# AB-3 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} +* [Address Book 3](https://se-education.org/addressbook-level3/): HRMate is built on top of AB3 +* [JavaFX](https://openjfx.io/): The GUI framework used in HRMate +* [Jackson](https://github.com/FasterXML/jackson): JSON parsing library used to read and write HRMate's JSON data files +* [MarkBind](https://markbind.org/): Used to generate HRMate's project site -------------------------------------------------------------------------------------------------------------------- @@ -21,14 +30,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,16 +57,16 @@ 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), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point). 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 +74,9 @@ 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`, `LeaveListPanel`, `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` 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) @@ -81,7 +85,7 @@ The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays `Person` and `Leave` objects residing in the `Model`. ### Logic component @@ -89,25 +93,28 @@ 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. 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`. +2. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. +3. The command can communicate with the `Model` when it is executed (e.g. to delete a person). +4. 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. @@ -116,34 +123,46 @@ How the parsing works: ### 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, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the leaves book data as well i.e., all `Leaves` objects (which are contained in a `UniqueLeavesList` object). +* stores the currently 'selected' `Person` objects (e.g., results of `find`) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list changes. +* stores the currently 'selected' `Leave` objects (e.g. results of `find-leave`) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list changes. * 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 the address book data, leaves book data and user preference data in either JSON or CSV format, and read them back into corresponding objects. +* inherits from both `AddressBookStorage`, `LeavesBookStorage` 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`) + + + +Both `AddressBookStorage` and `LeavesBookStorage` contain JSON and CSV implementations. These implementations exist separately +as their methods invoke methods from different Util files - the JSON implementation invokes JsonUtil methods, while the +CSV implementation invokes CsvUtil methods. `SerializableAddressBook`, `SerializableLeavesBook`, `AdaptedPerson` and +`AdaptedLeave` have been abstracted out to promote code reusability, with the use of generics where possible to enforce +type safety. + ### Common classes Classes used by multiple components are in the `seedu.addressbook.commons` package. @@ -154,6 +173,184 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### The Person class + +ManageHR keeps track of employees within the company with the use of `Person` and `UniquePersonList`. The `UniquePersonList` serves as a container for the `Person` objects, +while enforcing the constraints that no 2 employees have the same name. + +The `Person` class contains the following attributes. + + +1. `Name`: The name of the employee. +2. `Phone`: The phone number of the employee. +3. `Email`: The email address of the employee. +4. `Address`: The address of the employee. +5. `Tags`: The customised tag added by the user. + +### The Leave class + +ManageHR keeps track of the leaves of employees within the company with the use of `Leave` and `UniqueLeaveList`. The `UniqueLeaveList` serves as a container for the `Leave` objects, +while enforcing the constraints that no 2 leaves can have same start date and end date for the same employee. + +The `Leave` class contains the following attributes. + + +1. `ComparablePerson`: The employee. +2. `Title`: The title of the leave. +3. `Description`: The description of the leave. +4. `Date start`: The start date of the leave. +5. `Date end`: The end date of the leave. +5. `Status`: The status of the leave. + +`Date start` must be earlier than or the same as the `Date end`. +All the attributes except Description and Status are compulsory fields for adding a leave. + +### Add A Leave application feature + +The add-leave command allows the HR manager to add a leave record for a specific employee. This feature enhances the HRMate system by providing a way to manage and track employee leaves efficiently. +Fields compulsory to enter for `add-leave` as `String` type include: +1. `index`: The index of the person for the leave application. It will be parsed as `Index` type to `AddLeaveCommand` object and converted to `Person` type based on the displayed list when a new `Leave` object created. +2. `title`: The title of the leave, it will be parsed as `Title` type to `AddLeaveCommand` object and to the newly created `Leave` object. +3. `date start`: The start date of the leave in "yyyy-MM-dd" format. It will be parsed together with `date end` as `Range` type to `AddLeaveCommand` object and to the newly created `Leave` object. +4. `date end`: The end date of the leave in "yyyy-MM-dd" format. It will be parsed together with `date start` as `Range` type to `AddLeaveCommand` object and to the newly created `Leave` object. +5. `description`: The description of the leave. It is optional and will be parsed as `NONE` if no description field exists. Otherwise it will be parsed as `Description` type to `AddLeaveCommand` object and to the newly created `Leave` object. + +* CommandException due to unfounded index in the list will be thrown and handled in `AddLeaveCommand`. +* InvalidValueExceptions thrown due to illegal arguments, and EndBeforeStartExceptions thrown due to the end date occurring before the start date, will be handled by ParserException in `AddLeaveCommandParser`. +* CommandException due to duplicated leaves that have exact start and end dates will be thrown and handled at the line `model.addLeave(toAdd)` in `AddLeaveCommand#execute`. + +The activity diagram for adding a leave is as followed: + + +Here is an example usage of the `add-leave` feature: +1. The user uses the `find` command to filter for employees named Martin. +2. The user enters the command `add-leave 1 title/Sample Leave 1 start/2023-11-01 end/2023-11-01` with Martin being index 1. +3. A record of leave with specified title and dates for Martin is created. + +#### Design considerations +The command follows a structured format to ensure ease of use and to minimize errors. The use of an index ensures that the leave is associated with a specific employee. The format of the command is designed to be clear and straightforward. + +### Find an Employee by tags +`FindAllTagCommand` and `FindSomeTagCommand` are implemented similar to `FindCommand`. +They use `TagsContainAllTagsPredicate` and `TagsContainAllTagsPredicate` respectively as predicate to filter the employee list. And then update the displayed employee list of the model + +The following sequence diagram shows how `FindAllTagCommand` executes. + + +#### Design considerations: +`FindAllTagCommand` matches employees with all specified tags while `FindSomeTagCommand` matches employees with any of the specified tags. +The nuance difference is made to cater to users' needs for efficient searching. + +### Adding tag feature + +#### Implementation +`AddTagCommand` is implemented similar to `EditCommand`. +A new `Person` is created with the information from the old `Person`. +The tags are then added before replacing the old `Person` with the new `Person`. + +The following activity diagram summarizes what happens when a user executes a new command: + + + +#### Design considerations: + +**Aspect: How AddTagCommand executes:** +* **Alternative 1 (current choice):** Builts a new Person. + * Pros: Easy to implement (using `EditCommand` as reference), immutability allows for potential redo and undo commands. + * Cons: Memory intensive, costly in terms of time. +* **Alternative 2:** Add tags to `Person`. + * Pros: Memory efficient + * Cons: Mutable `Person` can affect implementation of potential redo and undo commands. + +### Find leave by period feature + +#### Implementation +`FindLeaveByPeriodCommand` is implemented similar to `FindCommand`. It uses a `LeaveInPeriodPredicate` as the predicate to filter +the leaves list. + +The predicate can be in 1 of 4 possible states +* Return true for all leaves - no start and end date is supplied for the query +* Return true for all leaves with at least one day in the period [start, end] inclusive - both start and end dates are supplied for the query +* Return true for all leaves with at least one day occurring on or after the query start date - only the start date is supplied for the query +* Return true for all leaves with at least one day occurring on or before the query end date - only the end date is supplied for the query + +The following sequence diagram shows how `FindLeaveByPeriodCommand` executes. + + +A LeaveInPeriodPredicate is constructed from the query supplied by the user, defining both a start (if provided) and end (if provided). +When the command is executed, the model's FilteredLeaveList is updated to only returns leaves that satisfy the predicate. + +### Import file + +The import feature allows users to import employee records and leave applications in CSV format, increasing portability of +the user's data. The import feature can provide a means of mass adding employee records and leave applications, without having to use +the `add` command repeatedly. Since both importing employee records and leave applications involve similar sequences, +the following will describe the sequence for importing employee records. + +Here is an example usage of the import feature: +1. User executes the `import` command. +2. User navigates to the file to import using the file dialog that opens. +3. User selects the file and clicks on the Open button of the file dialog. +4. All contacts in the address book will be overwritten by the contents of the imported file + +The following activity diagram shows the steps involved in the Import command: + + + +The CSV file is read into a CsvFile object, which is then converted into a CsvSerializableAddressBook object by reading each +row and the corresponding values for each column. The CsvSerializableAddressBook is then converted into an AddressBook instance, +which replaces the current AddressBook instance in the app. + +#### Design considerations +**Aspect: How the user selects files** + +* **Alternative 1 (current choice)**: Select from graphical file dialog + * Pros: More intuitive for the user to navigate and locate the file + * Cons: File dialog is harder to navigate with just the keyboard +* **Alternative 2**: User types in file path in the command + * Pros: The user can perform the operation entirely by keyboard + * Cons: Users are more likely to type in the wrong file name, and selecting a file in a different folder would require + typing out the entire relative file path + + +### Export feature + +The export feature enables users to export employee records and leave applications into CSV format, which can then be opened +in other spreadsheet applications. It allows users to select filtered data to export, providing greater granularity in +control over file content. Since both exporting employee records and leave applications involve similar sequences, the +following will describe the sequence for exporting employee records. + +Here is an example usage of the `export` feature: +1. The user uses the `find-some-tag` command to filter for employees with the `full time` tag +2. The user enters the command `export fulltimers` +3. A file will be created in `{home folder of HRMate}/exports`, with the name `fulltimers.csv`. This file contains employees +with the `full time` tag. + +The following sequence diagram shows the steps involved in the Export command: + + + +ExportContactCommandParser::parseFileName() checks if the file name provided is a valid file name. If the user provided +a path, it strips the path to retain only the file name. Next, it appends a ".csv" extension to the end of the file name +if the user did not supply the extension. + +The export command works by retrieving the filtered employee list in the address book, which contains a list of employee records +that are currently visible in the address book panel. A CsvSerializableAddressBook is constructed from this filtered person list, +which is then serialized into a CsvFile object. CsvUtil then writes the CsvFile instance into a CSV file. This file is +saved in the "export" folder that is created in the same location as the HRMate application file. + +#### Design considerations +**Aspect: Whether to allow the user to select the location to save the file to** + +* Alternative 1 (current choice): Disallow selecting the save location + * Pros: File dialog is not required as all files will be saved in the same place, allowing the entire operation to be + performed using the keyboard + * Cons: Loss of flexibility in choosing the save location, especially if the user wants to save multiple copies with + the same file name in different locations +* Alternative 2: Allow selecting the save location + * Pros: Provides flexibility for the user to choose a preferred save location + * Cons: File dialogs would require the use of a mouse + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -170,54 +367,63 @@ 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: @@ -232,13 +438,6 @@ The following activity diagram summarizes what happens when a user executes a ne * 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}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - - -------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** @@ -257,42 +456,74 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* has a need to manage a sizeable number of employees' information * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: manage HR information faster than a typical mouse/GUI driven app ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| 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 | - -*{More to be added}* +| 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 | +| `* * *` | HR manager | add a new employee | | +| `* * *` | HR manager | delete an employee | remove employees who no longer work here | +| `* * *` | HR manager | find an employee by name | locate details of employees without having to go through the entire list | +| `* * *` | organised HR manager | add/delete a tag to an employee | change the label of an employee | +| `* * *` | organised HR manager | view all my tags | filter by them | +| `* * *` | organised HR manager | find employees by tags | find specific category of employees for higher level workflows | +| `* * *` | HR manager | add a new leave application of an employee | keep track of the leaves applications | +| `* * *` | HR manager | delete employees' leave applications | remove leave applications that have been cancelled by employees | +| `* * *` | HR manager of a large organisation | find all leave applications of an employee | track the amount of leaves taken by an employee | +| `* * * ` | HR manager | approve/reject leave applications | update employees on their leave application status | +| `* *` | HR manager | hide private contact details | minimize chance of someone else seeing them by accident | +| `* *` | HR manager | import/export records in CSV format | open the records in other apps | +| `* * ` | HR manager of a large organisation | find all leave applications in a given period | forecast available manpower to avoid manpower shortages | +| `* * ` | HR manager of a large organisation | find all leave applications with a given status | check which applications are still pending | +| `*` | HR manager of a large organisation | sort employees by name | locate an employee easily | ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `HRMate` and the **Actor** is the `HR Manager`, unless specified otherwise) + +**Use case: Add/delete a tag from an employee** + +**MSS** + +1. User requests to find employee by name +2. HRMate shows a list of employees with the same name +3. User requests to add/delete a tag from a specific employee in the list +4. HRMate adds/deletes tag from specified employee + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given tag is invalid. (already there for add, not there for delete) + + * 3a1. HRMate shows an error message. + + Use case resumes at step 2. -**Use case: Delete a person** + +**Use case: Delete an employee** **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 employees +2. HRMate shows a list of employees +3. User requests to delete a specific employee in the list +4. HRMate deletes the employee Use case ends. @@ -304,20 +535,192 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. HRMate shows an error message. + + Use case resumes at step 2. + +**Use case: Import employee records** + +**MSS** + +1. User requests to import employee records +2. User selects CSV file from file dialog +3. HRMate adds records in CSV file inside its list of employee records +4. HRMate displays a message indicating successful importing of file + + Use case ends. + +**Extensions** + +* 2a. User cancels the file selection operation. + + * 2a1. HRMate closes the file selection window and aborts the operation. + + Use case ends. + +* 3a. File is not of CSV file type + + * 3a1. HRMate displays an error message. + + Use case ends. + +* 3b. File is corrupted, unable to read employee records + + * 3b1. HRMate discards all data read in. + * 3b2. HRMate displays an error message. + + Use case ends. + +**Use case: Export employee records** + +**MSS** + +1. User requests to export employee records. +2. User supplies a name and path to save the records. +3. HRMate creates the CSV file containing the employee records with the specified name at the specified location. +4. HRMate displays a message indicating the file has been created. + + Use case ends. + +**Extensions** + +* 2a. User does not supply a name + * 2a1. HRMate displays an error message + + Use case ends. + +* 2b. User does not supply a path/supplies an invalid path + * 2b1. HRMate creates the CSV file containing the employee records with the specified name at the default location + (same folder where the save files are located) + + Use case resumes at step 3. + +* 3a. HRMate is unable to create the file due to errors (e.g. permission errors) + * 3a1. HRMate aborts the file creation operation. + * 3a2. HRMate displays an error message. + + Use case ends. + +**Use case: List all tags** + +**MSS** + +1. User requests to view all tags +2. HRMate shows a list of all available tags + + Use case ends. + +**Extensions** + +* 2a. There is no existing tag in the system. + + * 2a1. HRMate notifies the user of no existing tag in the system. + + Use case ends. + +**Use case: List employees with at least one specified tags** + +**MSS** + +1. User requests to find employees who match at least one of the specified tags +2. HRMate shows a list of employees who match at least one of the specified tags + + Use case ends. + +**Extensions** + +* 2a. The specified tags do not exist in the system. + + * 2a1. HRMate notifies the user of invalid tags. + + Use case resumes at step 1. +* 2b. User does not provide any tags. + + * 2b1. HRMate notifies the user of missing parameters. + + Use case resumes at step 1. + +**Use case: List employees with all specified tags** + +**MSS** + +1. User requests to view all tags +2. HRMate shows a list of all available tags + + Use case ends. + +**Extensions** + +* 2a. There is no existing tag in the system. + + * 2a1. HRMate notifies the user of no existing tag in the system. + + Use case resumes at step 1. + + +**Use case: Add a leave application** + +**MSS** +1. The HR manager requests to find an employee by name. +2. HRMate shows a list of employees with the same name. +3. The HR manager requests to add a leave application for the selected employee +4. HRMate adds the leave application to the employee's record based on the provided information. + + Use case ends. + +**Extensions:** + +* 2a. The list of employees with the provided name is empty. +* + * 2a1. HRMate informs the HR manager that no employees with the given name were found. + + Use case ends. + +* 3a. The provided employee index is invalid. + + * 3a1. HRMate shows an error message indicating that the employee name is not valid. + Use case resumes at step 2. -*{More to be added}* +* 4a. The provided leave application details are invalid. + + * 4a1. HRMate shows an error message specifying the issue with the provided details (e.g., date format, missing fields). + + Use case resumes at step 3. + ### 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. +2. A user with above average typing speed (60 or more words per minute) 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. +3. Should have an intuitive and user-friendly interface, ensuring that HR managers can easily navigate and use its features without extensive training. +4. HRMate should be capable of handling a growing volume of employee records (up to 1000 persons) without a substantial decrease in performance. It should efficiently manage and store data as the number of employees and records increases over time. +5. HRMate should complete operations involving a single records within 500ms, and operations involving listing, searching, and filtering within 2s. + +## **Appendix: Effort** + +HRMate is built for extensibility. Not much effort is needed to implement new commands that are variations of existing features, but effort is needed to add new entities. + +### Challenges Faced +The first challenge given is understanding the code infrastructure. HRMate uses many design patterns like Facade pattern, Command pattern and MVC pattern. Without knowledge of these patterns, HRMate would seem complicated and difficult to understand. +Overall the understanding that the `Model` is a Facade for `AddressBook` and `LeavesBook`, input commands follow the Command pattern, and that `AddressBook` and `LeavesBook` are the models, JavaFX is used for the view and `ModelManager` functions as the controller greatly help in the understanding of HRMate's infrastructure. +Another significant challenge is the implementation of the leaves module. Given HRMate's immutable design philosophy, some design choices like replacing stale `Leave` with new instances of `Leave` with the updated `Person` are chosen. Care must be taken to update `Leave` everytime a `Person` is replaced like in the `EditCommand`, `AddTagCommand` and `DeleteCommand`. When adding a new potential module like `Report`, we anticipate that care must be taken when the associated `Person` or `Leave` is replaced. + +### Effort Required +To illustrate the difference in effort, the effort needed to create `AddTagCommand` and the `Leave` module will be compared. + +Given that `AddTagCommand` is a specific case of `EditCommand`, much of the logic is similar to `EditCommand`. In this case, `AddTagCommand` copies a `Person`, adds the new `Tag` before calling `Model#SetPerson`. + +Adding a new command is easy compared to the implementation of the `Leave` module. We took inspiration from the `Person` class and created wrapper classes for the fields like `Title`, `Description` and `Date`. One of the significant challenges were the added constraint of comparing two fields, `start` and `end`. This logic was not present in `Person`. For example `Name` validations do not concern `Tag` validations and vice versa. In `Leave` case, `start` affects the validations for `end`, as `end` must be on or after `start`. To resolve this, we created `Leave`'s constructor to use `Range`, whose creation mandates `start` be on or before `end`. This ensures that the created `Leave` adheres to the constraint of `start` being before or on `end`. We anticipate that the creation of other entities like `Report` to require the creation of new wrapper classes, or even validation classes to enforce validations that span across multiple fields. -*{More to be added}* +Efforts were also spent on ensuring data validity for `Person` and `Leave` when the associated object is modified. As mentioned above, one of the challenges faced was the immutable principle that AB3 used. When a `Person` is edited through `EditCommand`, `AddTagCommand` or `DeleteTagCommand`, the old `Person` is replaced by the newly created `Person`. This results in some `Leave` pointing to the stale `Person`. Thus, `Model#SetPerson` and `Model#DeletePerson` must be amended to edit `LeavesBook` as well to avoid any `Leave` pointing to any stale `Person`. We expect that such methods like `Model#SetPerson`, `Model#DeletePerson`, `Model#SetLeave`, `Model#DeleteLeave` must be amended when adding new modules if the new entities is reliant on information from pre-existing entities. +### Achievements of HRMate + * Successfully implemented second entity, `Leave` alongside `Person` with information consistency even when `Person` is edited or deleted. + * Created new way to import data using csv for both `Leave` and `Person`, opening up changing in app data to users who might not understand json. + * Added 19 more commands while increasing code coverage by 2%. + ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X @@ -329,10 +732,8 @@ 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; -testers are expected to do more *exploratory* testing. - -
+Note: These instructions only provide a starting point for testers to work on; +testers are expected to do more exploratory testing. ### Launch and shutdown @@ -340,38 +741,177 @@ testers are expected to do more *exploratory* testing. 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. + 2. 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 +2. Saving window preferences 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.
+ 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 …​ }_ +### Adding an employee +1. Adding an employee while all employees are being shown + + 1. Test case: `add n/John Doe p/98765432 e/johnd@example.com a/John Street, block 123 #01-01`
+ Expected: The employee is added to the list. Details of the added leave shown in the status message. + + 2. Test case: `add n/John Two p/98765431 e/johnt@example.com a/John Street, block 122 #01-01 t/remote`
+ Expected: The employee is added to the list. Error details shown in the status message. Status bar becomes red. + + 3. Test case: `add n/John Two p/98765431 e/johnt@example.com`
+ Expected: The employee is not added to the list. Details of the added leave shown in the status message. + 4. Other incorrect add commands to try: `add`, `add` with missing compulsory fields, `add` with illegal arguments in fields
+ Expected: Similar to previous. + +### Deleting an employee -### 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. + + 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. + + 3. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar becomes red. + + 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting a person after applying a filter + + 1. Prerequisites: Filter to the second person using `find PERSON_NAME` where `PERSON_NAME` is the name of the second person. + + 2. Test case: `delete 1`
+ Expected: First visible contact is delete from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + + 3. Test case: `delete 2`
+ Expected: No person is delete. Error details shown in the status message. Status bar becomes red. + + 4. Follow up actions: `list`
+ Expected: Only the previously deleted person is deleted. + +### Adding a tag +1. Adding a tag to an employee while all employee are being shown - 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. + 1. Prerequisites: List all employee using the list command. Multiple employees in the list. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 2. Test case: `add-tag 1 t/full time`
+ Expected: The tag `full time` is added to employee indexed 1 shown in the employee list. Details of the edited employee shown in the status message. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 3. Test case: `add-tag 1`
+ Expected: No tag is added. Error details shown in the status message. Status bar becomes red. -1. _{ more test cases …​ }_ + 4. Other incorrect add-tag commands to try: `add-tag`, `add-tag`with illegal arguments in fields, `add-tag x` (where x is larger than the list size)
+ Expected: Similar to previous. + +### Editing an employee +1. Editing an employee while all employee are being shown + + 1. Prerequisites: List all employee using the list command. Multiple employees in the list. + + 2. Test case: `edit 2 p/98765432 e/johndoe@example.com t/full-time t/remote`
+ Expected: Thephone number, email address and tags of the employee indexed 2 are changed to 98765432, johndoe@example.com and full-time and remote. Details of the edited employee shown in the status message. + + 3. Test case: `edit 1 a/John street, block 123 #01-01 t/`
+ Expected: The home address of the employee indexed 1 is changed to John street, block 123 #01-01 and all tags removed from the employee shown in the employee list. Details of the edited employee shown in the status message. + + 4. Test case: `edit 1 a/ t/`
+ Expected: No tag is added. Error details shown in the status message. Status bar becomes red. + + 5. Other incorrect edit commands to try: `edit`, `edit` with illegal arguments in fields, `edit x` (where x is larger than the list size)
+ Expected: Similar to previous. + +### Viewing all tags existing +1. Viewing all tags while there are exsiting tags + 1. Prerequisites: There are tags attached to the employees. + + 2. Test case: `view-tag`
+ Expected: Tags shown in the status message. + + +### Finding all tags matched +1. Finding Employees with All Tags in a Valid Scenario + 1. Prerequisites: + 1. Have a dataset with employees having different tags, specifically `remote`, `full time`, `part time` and `on-site`. + 2. List all employees using the list command to identify available tags. + 2. Test Case: `find-all-tag t/remote t/full time` + Expected: GUI Changes: A dedicated interface section displays a list of employees with both tags `remote` and `full time`. Status message indicates the number of matched employees. Verify that employees with additional tags are also displayed. + 3. Test Case: `find-all-tag` + Expected: Error message indicates an invalid command format.Status bar becomes red. + 4. Test Case: `find-all-tag t/Nonexistent tag` + Expected: GUI Changes: A dedicated interface section displays no employee. Status message indicates 0 matched employee. + 5. Test Case: `find-all-tag t/REMOTE` + Expected: GUI Changes: A dedicated interface section displays no employee. Status message indicates 0 matched employee. + 6. Test Case: `find-all-tag t/123!` + Expected: Error message indicates illegal tag names. Status bar becomes red. + 7. Test Case: `find-all-tag t/re` + Expected:Employees with tag named `re` are displayed. Verify that employees with additional tags are also displayed. + +### Finding some tags matched +1. Finding Employees with All Tags in a Valid Scenario + 1. Prerequisites: + 1. Have a dataset with employees having different tags, specifically `remote`, `full time`, `part time` and `on-site`. + 2. List all employees using the list command to identify available tags. + 2. Test Case: `find-some-tag t/remote t/full time` + Expected: GUI Changes: A dedicated interface section displays a list of employees with either tags `remote` and `full time`. Status message indicates the number of matched employees. Verify that employees with additional tags are also displayed. + 3. Test Case: `find-some-tag` + Expected: Error message indicates an invalid command format.Status bar becomes red. + 4. Test Case: `find-some-tag t/Nonexistent tag` + Expected: GUI Changes: A dedicated interface section displays no employee. Status message indicates 0 matched employee. + 5. Test Case: `find-some-tag t/REMOTE` + Expected: GUI Changes: A dedicated interface section displays no employee. Status message indicates 0 matched employee. + 6. Test Case: `find-some-tag t/123!` + Expected: Error message indicates illegal tag names. Status bar becomes red. + 7. Test Case: `find-some-tag t/re` + Expected:Employees with tag named `re` are displayed. Verify that employees with additional tags are also displayed. + +### Adding a leave +1. Adding a leave while all leaves are being shown + + 1. Prerequisites: List all leaves using the list-leaves command. Multiple leaves in the list. + + 2. Test case: `add-leave 1 title/Vacation start/2023-11-15 end/2023-11-20`
+ Expected: The leave is added to the list. Details of the added leave shown in the status message. Timestamp in the status bar is updated. + + 3. Test case: `add-leave 0 title/Conference start/2023-12-01 end/2023-12-03 d/Attending conference`
+ Expected: No leave is added. Error details shown in the status message. Status bar becomes red. + + 4. Other incorrect add-leave commands to try: `add-leave`, `add-leave x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Open `./data/addressbook.json` and delete the first character `{`. Then open HRMate.
+ Expected: HRMate opens an empty address book. + + 2. Add a person using `add` and a leave using `add-leave`, then `exit`. Open `./data/leavesbook.json` and edit the fullName of the name of the employee of a leave. Reopen HRMate.
+ Expected: HRMate restores all data except for the edited leave. + +### Data integrity between `Person` and `Leave` + +1. Dealing with edits to `Person` + + 1. Add a `Person` using `add` and a `Leave` using `add-leave`. Edit the `Person` name using `edit`.
+ Expected: The Employee under the created `Leave` is edited also. + +2. Dealing with deletes to `Person` + + 1. Add a `Person` using `add` and a `Leave` using `add-leave`. Delete the `Person` with `delete`.
+ Expected: The previously created `Leave` is deleted also. + +---------------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Planned Enhancements** + +1. `export-all` would export both `Person` and `Leave`. HR Managers usually wants to move both `Person` and `Leave` to another application, so being able to export both in a single csv sheet would help them do so. + +2. find-all-tags and find-any-tags searches for tags case insensitive. Since tags are used for categorising, HR Managers might see no difference between Intern and intern. Hence allowing case insensitive search in find-all-tags and find-any-tags would help HR Managers find employees more easily. + +3. Mergable `import`. Sometimes, HR Managers want to move data from one app to HRMate without removing all current employees. Allow HRMate to merge a csv file, with an option to overwrite current data or overwrite csv data when a same employee is encountered, would help HR Managers achieve this. -1. _{ more test cases …​ }_ +4. Filtered `clear`. Allowing HR Managers to select a group of employees to delete would be good. For example, they could use `find-any-tags t/intern`, `clear` to delete all interns. Subsequently, `list` would show all non intern employees. 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..7551ee7adfb 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,181 +1,1046 @@ ---- -layout: page -title: User Guide ---- + + layout: default.md + pageNav: 3 + title: "User Guide" + -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. +# HRMate User Guide -* Table of Contents -{:toc} +Welcome to HRMate, the go-to desktop application designed exclusively for HR managers like yourself. + +Discover a seamless one-stop solution within HRMate to oversee your team's contact details, +roles, and streamline leave application processes, so you can keep both your staff and management +satisfied. + +If you are a fast typist, you will love the swift speed of working in HRMate compared to traditional HR apps like +Oracle and SAP. HRMate's [Command Line Interface (CLI)](#glossary) is optimised for you, so you can perform nearly +every task simply by typing short commands ([What is a command?](#glossary)). + +This guide explains how you can set up and use the various features of HRMate, as well as provides +troubleshooting insights to help solve problems you might face while using HRMate. No matter your familiarity with +HRMate or technology, this guide has something for you. + +**New to HRMate?** Dive into our [Quick Start](#quick-start) guide for a step-by-step setup and key features tutorial, +so you can start using HRMate in no time. + +**Need a quick refresher on the commands?** Explore the [Command Summary](#command-summary), a concise compilation +of commands and their usage. + +**Seasoned HRMate user?** Delve into our [Features](#features) section for an in-depth exploration of advanced features +to elevate your HR management experience. + +We trust you will find this guide helpful in maximising your HRMate experience! + +~ _The HRMate team_ + + + +
+ + -------------------------------------------------------------------------------------------------------------------- +
+ +## About HRMate + +At its core, HRMate is a desktop application designed to help HR managers manage their employees' contact details and +leave applications. + +#### Employee + +HRMate stores your employees' contact details, such as their name, phone number, email address, and home address in an [employee list](#glossary). +In addition, HRMate also allows you to [tag](#glossary) your employees and categorise them into groups for easier management. You could categorise them by their roles, such as "Full-time" or "Part-time", or by their departments, such as "Sales" or "Marketing", whichever suits your needs and preferences best. + +#### Leave Applications + +HRMate also allows you to manage your employees' leave applications in a [leave list](#glossary). You can add, approve, and reject leave applications, as well +as view all leave applications at a glance. You can also filter leave applications by their status, such as "PENDING" or "APPROVED", +to help you keep track of the status of each leave application. + +Each leave application is associated with an employee by name and has a title, description, start date, and end date. + +
+ +To facilitate ease of transfer of information, a core feature of HRMate is its ability to import and export employee records and +leave applications in [CSV](#glossary) format. This allows you to easily transfer your employee records and leave applications to and from +HRMate, so you can use HRMate alongside other HR management tools. + +-------------------------------------------------------------------------------------------------------------------- + +
## Quick start -1. Ensure you have Java `11` or above installed in your Computer. +1. Download the latest `hrmate.jar` from [here](https://github.com/AY2324S1-CS2103T-W11-1/tp/releases/tag/v1.4.0) and move the downloaded file to your desired location. +See [How do I move a file?](#q-how-do-i-move-a-file) for help. + +2. Open a terminal or your computer. + +
    +
  • For Mac users: open the Terminal app on your Mac
  • +
  • For Window users: press the windows key, type `powershell` and press "Enter" on your keyboard
  • +
  • See How do I open a terminal? for more information.
  • +
+
+ +3. Use `cd` command in the terminal to navigate to the same location as `hrmate.jar` in step 1. See [How do I navigate files in terminal?](#faq) + +4. Ensure you have `Java 11` or above installed on your Computer. This can be done by typing `java -version` in your terminal and pressing "Enter". + - See How do I download Java 11? if Java is not updated or if the terminal does not recognise the `java` input. + +5. Type `java -jar hrmate.jar` into the terminal and press "Enter" on your keyboard.
+An app similar to the one below should appear in a few seconds. The app is populated with some sample data for you to experiment with.
+We have added some annotations in red so that you can understand the app visually. +![Ui](images/Ui-annotated.png) + +6. You can try typing some commands into the command box and pressing "enter" to execute. We have listed down some commands for you to try. + - `help`: Opens the help window. + - `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`: Adds an employee named `John Doe` to the employee list. + - `list`: Lists all employees. + - `add-leave 1 title/medical leave start/2023-11-11 end/2023-11-11`: Adds a leave entry for the first employee in the current employee list. Note that the default leave status is `PENDING`. + - `delete 2`: Deletes the second employee shown in the current employee list and any leave applications associated with the second employee. + - `exit`: Exits the app. +For new users, especially first-time users, it is very important and helpful to read through the [How to interpret command formats?](#how-to-interpret-command-formats). This section provides a detailed introduction to the overall structure of all commands used in HRmate. + +Also, look through [Features](#features) for commands and the details of each command. If you would like a concise summary of the commands, please check out our [command summary](#command-summary). We hope to aid you in your HR journey! + +-------------------------------------------------------------------------------------------------------------------- + +
+ +## Important things to note -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +**Warning:** + +HRmate is coded to take in the exact commands stated in this user guide. Please do not change the spelling, remove or add symbols, adjust spaces, or change the letter case of the commands. Any changes to the commands, unless otherwise stated in this user guide, can lead to the program being unable to interpret the input. To ensure the best user experience, please follow the commands exactly as instructed in this user guide. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. -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) +> - For a more detailed introduction to the command structure of HRmate and specific information on the parts of a command we can and cannot modify, please refer to [How to interpret command formats?](#how-to-interpret-command-formats). +> +> - Read through our [features](#features) section for detailed information on each command. +> +> - Check out our [command summary](#command-summary) for a concise list of available commands and their formats. -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.
- Some example commands you can try: +
- * `list` : Lists all contacts. +#### Character Limit + +Currently, HRmate's user interface is able to hold 35 to 160 characters on a single line, depending on the letters used. For any inputs longer than the character limit, HRmate will display `...` for the remaining characters after the character limit. + - * `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. +#### Index + +HRmate's lists are indexed from 1 onwards. Anything else apart from positive integers (1, 2, 3, ...) isn't accepted as an index in HRmate. This means that numbers such as 0, -1, 1.2 and characters like "a" and "one" cannot be used as indexes. This is particularly important for index-specific commands (i.e. `edit`, `approve-leave`, etc.) as they require a valid index to function properly. Please also note that the indexes are limited to the number of employees or leaves shown in the current list in HRmate. Inputting an index larger than the currently available index can result in error messages and the command is unable to function correctly. + - * `delete 3` : Deletes the 3rd contact shown in the current list. +
- * `clear` : Deletes all contacts. +#### Name + + HRmate's names are case-sensitive. This means that the names `Alex` and `alex` are interpreted as different names. + - * `exit` : Exits the app. +#### Tag + +HRmate's tags are case-sensitive. This means that tags `Part-Time` and `part-time` are interpreted as different tags. Having redundant tags with similar spelling may decrease efficiency and add unnecessary workload. To see all current tags in use, you may refer to the `view-tag` command. Please make note of this while interacting with tag-related commands like `add-tag`. + -1. Refer to the [Features](#features) below for details of each command. +#### Command Order + +Please note that you are unable to edit employees or leave applications when the respective list is empty. Please start by adding some employees or leave applications before attempting to edit them. + + +#### PDF Version Copy and Paste + +If you are using a PDF version of this document, please 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. + -------------------------------------------------------------------------------------------------------------------- +
+ ## Features -
+The user guide splits HRMate's features into 3 main categories: [Employee-related operations](#employee-related-operations), [Leave-related operations](#leave-related-operations), and [other operations](#other-operations). + +## Employee-related operations + +HRMate allows you to manage your employees' contact details, such as their name, phone number, email address, and home address. + +For instance you can [add](#add-an-employee-record) a new employee, [find](#find-an-employee-record) specific employees, [edit](#edit-an-employee-record) an employee's information, [delete](#delete-an-employee-record) an employee, or [import/export](#import-export-employee-records) employee records in CSV format. + +### Add an employee record +When a new employee joins, use the [`add`](#adding-an-employee-add) command to add their information into HRMate. + +#### Adding an employee: `add` + +Use the `add` command to add an employee to the employee list. + +Here's how to add an employee: + +1. Type in the following command in the [command box](#glossary): `add n/NAME p/PHONE_NUMBER e/EMAIL_ADDRESS a/HOME_ADDRESS [t/TAG]...`, replace `NAME` with employee name, `PHONE_NUMBER` with employee phone number `EMAIL_ADDRESS` with employee email address and `HOME_ADDRESS` with employee home address. `[t/TAG]...` is an optional field with one or more tags, with `TAG` being the name of the tag. + - For instance, if you have an employee name John Doe with phone number 98765432, email johnd@example.com, home address John Street, block 123 #01-01 and tags full-time and remote, type in the command `add n/John Doe p/98765432 e/johnd@example.com a/John Street, block 123 #01-01 t/full-time t/remote` + - If your employee does not require any tags, using the same information as above, the command would be `add n/John Doe p/98765432 e/johnd@example.com a/John Street, block 123 #01-01` + - Please refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information +2. Press "enter" on your keyboard and you should see the person information at the end of the employee list. + +
+ +Here are the potential error messages that you may receive and here's how to fix them: + +| Error message | Why it happens | Fix | +|--------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `add n/NAME p/PHONE_NUMBER e/EMAIL_ADDRESS a/HOME_ADDRESS [t/TAG]...`, replacing `NAME` with employee name, `PHONE_NUMBER` with employee phone number, `EMAIL_ADDRESS` with employee email address, `HOME_ADDRESS` with employee home address. `[t/TAG]...` is optional, with `[t/TAG]` representing one or more tags and `TAG` being the tag name. | +| `FIELD should FORMAT` where `FIELD` is an input like `Names` or `Phone numbers` and `FORMAT` contains additional information about the field's format. | The input does not follow the format prescribed. For example, the entered phone number might contain alphabets. | Follow the on screen message to fix the field in question. For example, `Phone numbers should only contain numbers, and it should be at least 3 digits long` means that the input phone number does not follow the prescribed format. | +| `This employee already exists in the address book` | The provided employee name is already found in HRMate | Use another name for the employee. For example, if trying to add another "John Doe", use the name "John Doe (HR)" to differentiate between the existing John Doe. HRMate does this name checking to prevent unintentional duplicate employee entries. | +
+ + +Currently, HRmate's user interface is able to hold 35 to 160 characters on a single line, depending on the letters used. For any inputs longer than the character limit, HRmate will display ... for the remaining characters after the character limit. + + + + + + +HRmate's names are case-sensitive. This means that the names `Alex` and `alex` are interpreted as different names. + + + +HRmate's tags are case-sensitive. This means that tags `Part-Time` and `part-time` are interpreted as different tags. Having redundant tags with similar spelling may decrease efficiency and add unnecessary workload. To see all current tags in use, you may refer to the `view-tag` command. + + +
+ +### Find an employee record +HRMate makes it easy to find an employee and there are multiple ways of doing so. You can use the [`list`](#listing-all-employees-list) command to list all employees, or use the [`find`](#finding-employees-by-name-find) command to find employees by name. You can also use the [`find-all-tag`](#finding-employees-by-all-specified-tags-find-all-tag) and [`find-some-tag`](#finding-employees-by-at-least-one-specified-tag-find-some-tag) commands to find employees by their associated [tags](#glossary). + +#### Listing all employees: `list` + +Use the `list` command to view all employees that are currently listed in the employee list. + +Here's how to use the `list` command: + +1. Type in the following command in the command box: `list`. +2. Press "enter" on your keyboard and you should see the information of all employees listed in the employee list. + + +This command does not require any parameters. However, to account for possible typing mistakes, HRMate reads in inputs like `list 123`, `list abc`, and `list 1a2b` all as the command `list`. + + +Here are the potential error messages that you may receive and here's how to fix them: + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Unknown command` | The command you inputted is not part of the commands available in HRmate. | Please check the spelling and try again. + +#### Finding employees by name: `find` + +Use the `find` command to find employees by name. This command will find employees whose names contain the specified words that you enter. + +Here's how to find employees by name: + + 1. Type in the following command in the command box `find NAME....` where `NAME...` are to be replaced with one or more names of the employees for search. + * The words for search are case insensitive, meaning you do not have to worry about the capitalization of the words entered. + - For instance, to find employees whose names contain `Martin`, type `find martin` to the command box. + - Take another example, suppose we have employees `alex` and `Alex`, for input `find alex`, both employees will be shown as a reult. + - For another instance, to find employees whose names contain either "*Harry*" or "*Redknapp*, type `find harry redknapp` to the command box. + - However, if you want to find employees whose names contain `martin` but only entered `mar`, NO employee named Martin will be found, instead employee named Mar will be found. Note that he search looks for names containing the entire specified word. + 2. Press "enter" on your keyboard and you should see the employees matched being listed in the employee list. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `find NAME...` where `NAME` is the specified name of the employee to search (case-insensitive). + +* **For advanced users:** + * Employees are displayed in the order in which they are arranged in the employee list. + +#### Viewing All Tags: `view-tag` + +To view all tags currently in use within the employee list, use the `view-tag` command. This will be +useful if you want to find employees by their associated tags using the [`find-all-tag`](#finding-employees-by-all-specified-tags-find-all-tag) and [`find-some-tag`](#finding-employees-by-at-least-one-specified-tag-find-some-tag) commands. + +Here's how to view all currently used tags: + +1. Type in the following command in the command box: `view-tag`. +2. Press "enter" on your keyboard and you should see a list of all tags currently in use within the employee list. + + + +This command does not require any parameters. However, to account for possible typing mistakes, HRMate reads in inputs like `view-tag 123`, `view-tag abc`, and `view-tag 1a2b` all as the command `view-tag`. + + +Here are the potential error messages that you may receive and here's how to fix them: + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Unknown command` | The command you inputted is not part of the commands available in HRmate. | Please check the spelling and try again. + + + + +#### Finding employees by all specified tags: `find-all-tag` + +To find employees by a set of specified tags, use the `find-all-tag` command. This command will find employees that are currently associated with all the specified tags that you enter. + +Here's how to use the `find-all-tag` command: + +1. Type in the following command in the command box `find-all-tag t/TAG [t/MORE_TAGS]...` where `TAG` is to be replaced with the name of the tags. + * At least one tag should be entered for search and more tags can be entered if you want to search for more. + * The tag names are case sensitive, meaning the capitalization of the words entered matters. + - For instance, to find employees with both tag `remote` and tag `full time`, type `find-all-tag t/remote t/full time` to the command box. + - However, if you want to find employees with tag `remote`, but only entered `re` as the tag name, NO employees with tag `remote` will be found, instead, employees with tag `re` will be found. Note that only tags containing the entire specified word are matched. +2. Press "enter" on your keyboard and you should see the employees matched being listed in the employee list. Note that employees with additional tags other than the specified ones will also be displayed. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `find-all-tag t/TAG [t/MORE_TAGS]...` where `TAG` is to be replaced with the name of the tags, and capitalization of the tag names mattered (case-sensitive). At least one tag should be entered for search and more tags can be entered if you want to search for more. | +| `Tags names only allow alphanumeric characters, spaces, and dashes.` | The tags input contains illegal characters | Remove the illegal characters from the input. | + +* **For advanced users:** + * Employees are displayed in the order in which they are arranged in the employee list. + +#### Finding employees by at least one specified tag: `find-some-tag` + +To find employees by at least one of the specified tags, use the `find-some-tag` command. This command will find employees that are currently associated with at least one of the specified tags that you enter. + +Here's how to use the `find-some-tag` command: + +1. Type in the following command in the command box `find-some-tag t/TAG [t/MORE_TAGS]...` where `TAG` is to be replaced with the name of the tags. + * At least one tag should be entered for search and more tags can be entered if you want to search for more. + * The tag names are case sensitive, meaning the capitalization of the words entered matters. + - For instance, to find employees with either tag `remote` and tag `full time`, type `find-some-tag t/remote t/full time` to the command box. + - However, if you want to find employees with tag `remote`, but only entered `re` as the tag name, NO employees with tag `remote` will be found, instead, employees with tag `re` will be found. Note that only tags containing the entire specified word are matched. +2. Press "enter" on your keyboard and you should see the employees matched being listed in the employee list. Note that employees with additional tags other than the specified ones will also be displayed. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `find-all-tag t/TAG [t/MORE_TAGS]...` where `TAG` is to be replaced with the name of the tags, and capitalization of the tag names mattered (case-sensitive). At least one tag should be entered for search and more tags can be entered if you want to search for more. | +| `Tags names only allow alphanumeric characters, spaces, and dashes.` | The tags input contains illegal characters | Remove the illegal characters from the input. | + +* **For advanced users:** + * Employees are displayed in the order in which they are arranged in the employee list. + +
+ +### Edit an employee record +HRMate offers different commands for editing employee records. [`add-tag`](#adding-tags-to-employees-add-tag) and [`delete-tag`](#removing-tags-from-employees-delete-tag) would add and remove an employee's tags while [`edit`](#editing-employee-information-edit) is for editing name, phone number, email address, home address and tags. + +#### Adding tags to employees: `add-tag` + +Use the `add-tag` command to add tags to an employee. This command will add the specified tags to an employee specified by their index in the employee list. + +Here's how to use the `add-tag` command: + +1. Find the employee under the employee list. + + +If the employee is not found, consider using list or any find commands to locate the employee in the employee list. + + +2. Type in the following command in the command box `add-tag INDEX t/TAG...` where `INDEX` is the [index](#glossary) of the employee in the list currently, `TAG` is the name of the tag to be added and `t/TAG...` representing one or more tags. + - For instance, if you want to add the tags full-time and remote to the employee indexed 2, type `add-tag 2 t/full-time t/remote` to the command box. + - Please refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information +3. Press "enter" on your keyboard and you should see the input tags added to the employee specified. + +Here are the potential error messages that you may receive and here's how to fix them: + +| Error message | Why it happens | Fix | +|-----------------------------------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Invalid command format!` | The command you input does not follow the specfied format | Ensure the command you entered follows the following format: `add-tag INDEX t/TAG...`, replacing INDEX with the index of the employee currently, `TAG` is the name of the tag to be added and `t/TAG...` representing one or more tags. | +| `At least one tag must be provided` | No tags were provided | Add tags to the command in the command box. Note that the tags must have a t/ [prefix](#glossary). For example, to add the tag full-time, use `t/full-time`. | +| `The person index provided is invalid` | The index specified does not refer to any employee | Double check if the index appears in the employee list. Alternatively, use [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. Afterwards, use the correct employee index in the `add-tag` command. | +| `The employee already has some of the tags` | The employee already has some of the tags which you are trying to add | Remove the tags the employee has from the input command. For example, for an employee who already has the full-time tag, the command `add-tag 2 t/full-time t/remote` would not work. Instead, try `add-tag 2 t/remote`. | +| `Tags names only allows alphanumeric characters, spaces, and dashes.` | The tags input contains illegal characters | Remove the illegal characters from the input. | +#### Removing tags from employees: `delete-tag` + +Use the `delete-tag` command to remove tags from an employee. This command will remove the specified tags from an employee specified by their index in the employee list. + +Here's how to use the `delete-tag` command: + +1. Get the [index](#glossary) of the employee in the employee list. + + +If the employee is not found, consider using [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. + + +2. Type in the following command in the command box `delete-tag INDEX t/TAG...` where `INDEX` is the index of the employee in the list currently, `TAG` is the name of the tag to be deleted and `t/TAG...` representing one or more tags. + - For instance, if you want to remove the tags full-time and remote to the employee indexed 2, type `delete-tag 2 t/full-time t/remote` to the command box. + - Please refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information +3. Press "enter" on your keyboard and you should see the input tags removed from the employee specified. + +Here are the potential error messages you may receive and here's how to fix them: + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specfied format | Ensure the command you entered follows the following format: `delete-tag INDEX t/TAG...`, replacing INDEX with the index of the employee currently, `TAG` is the name of the tag to be deleted and `t/TAG...` representing one or more tags. | +| `At least one tag must be provided` | No tags were provided | Add tags to the command in the command box. Note that the tags must have a t/ [prefix](#glossary). For example, to remove the tag full-time, use `t/full-time` | +| `The person index provided is invalid` | The index specified does not refer to any employee | Double check if the index appears in the employee list. Alternatively, use [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. Afterwards, use the correct employee index in the `delete-tag` command. | +| `Some of the tags are not found on this employee.` | The employee does not have some of the tags you are trying to delete | Remove the tags not found on the employee from the input command. For example, for an employee without the tag full-time, the command `delete-tag 2 t/full-time t/remote` does not work. Instead try `delete-tag 2 t/remote`.| +| `Tags names only allows alphanumeric characters, spaces, and dashes.` | The tags input contains illegal characters | Remove the illegal characters from the input. | + +
+ +#### Editing employee information : `edit` + +For a more comprehensive editing of an employee's information, use the `edit` command. This command will edit the specified fields of an employee specified by their index in the employee list. + +Here's how to use the `edit` command to edit an employee's information: + +1. Get the [index](#glossary) of the employee under the employee list. + + +If the employee is not found, consider using list or any find commands to locate the employee in the employee list. + + +2. Type in the following command in the command box `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` where `INDEX` is the index of the employee in the list currently, `[n/NAME]`, `[p/PHONE_NUMBER]`, `[e/EMAIL_ADDRESS]`, `[a/HOME_ADDRESS]` are optional fields which require changing, replacing `NAME` with employee name, `PHONE_NUMBER` with employee phone number, `EMAIL_ADDRESS` with employee email address and `HOME_ADDRESS` with employee home address. `[t/TAG]...` is an optional field representing one or more tags where `TAG` is the tag name. Note that at least one field to edit must be present and only the fields present will be edited. + - For example, to change the phone number, email address and tags of the employee indexed 2 to 98765432, johndoe@example.com and full-time and remote, type in the command `edit 2 p/98765432 e/johndoe@example.com t/full-time t/remote`. Note that the name and home address will remain unchanged. + - In another example, to change the home address of the employee indexed 1 to John street, block 123 #01-01 and remove all tags from the employee, type in the command `edit 1 a/John street, block 123 #01-01 t/`. Note that the name, phone number and email_address will remain unchanged. + - Please refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information + + +If the tag prefix is specified, all existing tags under the employee will be removed and replaced with the new tags in the command. +In the first example, the employee will have all tags removed and replaced by 2 tags: full-time and remote. +In the second example, the employee will have all tags removed. No tags will be added since no tags are specified. +Therefore, to avoid unintentionally losing any information while editing tags, we recommend using the add tag and delete-tag commands instead for editing tags. + + +3. Press "enter" on your keyboard and you should see the changes applied to the employee. + +| Error message | Why it happens | Fix | +|--------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` where `INDEX` is the index of the employee in the list currently, `[n/NAME]`, `[p/PHONE_NUMBER]`, `[e/EMAIL_ADDRESS]`, `[a/HOME_ADDRESS]` are optional fields which require changing, replacing `NAME` with employee name, `PHONE_NUMBER` with employee phone number, `EMAIL_ADDRESS` with employee email address and `HOME_ADDRESS` with employee home address. `[t/TAG]...` is an optional field representing one or more tags where `TAG` is the tag name. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message.| +| `FIELD should FORMAT` where `FIELD` is an input like `Names` or `Phone numbers` and `FORMAT` contains additional information about the field's format. | The input does not follow the format prescribed. For example, the entered phone number might contain alphabets. | Follow the on screen message to fix the field in question. For example, `Phone numbers should only contain numbers, and it should be at least 3 digits long` means that the input phone number does not follow the prescribed format. | +| `The person index provided is invalid` | The index specified does not refer to any employee | Double check if the index appears in the employee list. Alternatively, use [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. Afterwards, use the correct employee index in the `edit` command. | +| `At least one field to edit must be provided` | The command you input does not contain any fields to edit | Check if there is any input fields in the command inputted. An input like `edit 1` is not accepted as there is no edits to be made. | +| `This employee already exists in the address book` | The provided employee name is already found in HRMate | Use another name for the employee. For example, if trying to add another "John Doe", use the name "John Doe (HR)" to differentiate between the existing John Doe. HRMate does this name checking to prevent unintentional duplicate employee entries. | + +* **For advanced users:** + * You can remove all the tags of an employee with `edit INDEX t/` (see warning above) + +
+ +### Delete an employee record + +Delete employee records from HRMate using the [`delete`](#deleting-a-record-delete) command. + +#### Deleting a record : `delete` + +Use the `delete` command to delete an employee record from HRMate. This command will delete the specified employee from the employee list. + +Here's how to delete an employee record: + +1. Get the [index](#glossary) of the employee under the employee list. View this image in [quick start](#quick-start) for more information. + +If the employee is not found, consider using list or any find commands to locate the employee in the employee list. + +2. Type in the following command in the command box `delete INDEX` where `INDEX` is to be replaced with the index of the employee in the list currently + - For instance, to remove the whole record of the employee indexed 1, type `delete 1` to the command box. +![Before Delete](images/before-delete.png) + +Once you delete the record, the records will be no longer available in HRMate and cannot be recovered. +Therefore, to avoid unintentionally losing any information of the employee, if you just want to modify some information, we recommend using the +edit command instead to modify the record. + + +3. Press "enter" on your keyboard and you should see the employee removed from the employee list with all leaves related to that employee in the leave list being removed. +![After Delete](images/after-delete.png) + +| Error message | Why it happens | Fix | +|----------------------------------------|-----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `delete INDEX` where `INDEX` is the index of the employee in the list currently. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message. | +| `The person index provided is invalid` | The index specified is not positive or does not refer to any employee | Double check if the index is positive and appears in the employee list. Alternatively, use [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. Afterwards, use the correct employee index in the `delete` command. | +| + +
+ +### Import/export employee records +It's painful having to add in each employee into HRMate manually. That's why HRMate provides [`import`](#importing-employee-records-import) and [`export`](#exporting-employee-records-export) commands, +so you can bring in all your records from Excel with just a single command! + +With the import and export commands, HRMate can read and save files in [CSV](#csv) format, which is supported +by major spreadsheet applications such as Microsoft Excel. + + +#### Importing employee records: `import` + +Use the `import` command to import employee records from a CSV file. This command will import the employee records from +the specified CSV file into HRMate. + + +Imported employee records will overwrite existing employee records in HRMate. Remember to make a copy of your existing +employee records if you want to save them! You can do so by exporting your current records. + + +Here's how you can bring over your records from Excel: -**:information_source: Notes about the command format:**
+1. Export your Excel save file in CSV format. Ensure that the separator is set to be a semicolon(`;`), and that you have +the following fields: Name, Phone, Email, Address, Tags. Note that tags in the Tags field have to be separated by commas. + * You may skip this step if you already have a CSV file (e.g. you are importing a previously exported CSV file generated by + HRMate) -* 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`. + +If you have saved or edited your CSV file in Excel, please note that Excel will likely change the separator into a comma, +which will cause problems when trying to import employee records. To fix this problem, +click here to find out how you +can change the separator to semicolons in Excel. Note that this workaround is currently available only for Windows users. + -* 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`. +2. If you are unsure how your file should look like, you may refer to the following images: + * Here's how your CSV file should look like if you open it up in Notepad: + ![import-employee-notepad](images/import-employee-notepad.png) + * It is okay if your CSV file does not contain the first line `sep=;`, as this is a line added to files exported by HRMate + to help Excel open the file. Do ensure the next line has to be the following: + `Name;Phone;Email;Address;Tags` in order for HRMate to read it. Also note that tags are separated by commas + (see `colleagues, friends` under `Bernice Yu`'s row). + * If you open your CSV file in Excel, your file should look like this: +
+
+ ![import-employee-excel](images/import-employee-excel.png) +
+
+ * Do ensure that the first row contains the following headers: `Name`, `Phone`, `Email`, `Address`, and `Tags`. + Note that tags, if present, should be separated by commas (see cell `E3` for an example). -* 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. +3. In HRMate, type in the following command in the command box: `import` +4. In the file dialog that opens up, go to where you saved your exported CSV file, click on it, and click on the Open button. +5. You should see your employee records show up in HRMate, along with the message "Employee records have been imported from [your file name]!" -* Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +Here are some possible error messages you might encounter and here's how you can fix them: -* 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`. +| Error Message | Why it happens | Fix | +|----------------------------------------------------------------------|------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| Employee records were not imported. | You did not select a file in the file dialog | Retype the command, and make sure to select a CSV file when the file dialog opens | +| Records in file [file name] could not be imported, import cancelled. | Your file likely contains illegal characters or is corrupted | Ensure that your data fulfils the following constraints (to be added) | +| No valid records found in file [file name], import cancelled | Your file either is empty or does not contain a single valid employee record | Ensure that your file is non-empty and fulfils the abovementioned constraints | -* 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. -
-### Viewing help : `help` +#### Exporting employee records : `export` -Shows a message explaning how to access the help page. +Not only can you bring your data into HRMate, you can also bring your data out of HRMate. HRMate's export feature allows you +to export either the entire set of employee records, or employee records with a particular filter applied (e.g. only export +all full-time employees, which are tagged with "Full time"). You can then either store your exported CSV file for future use, +open it in a different application, or send it to another employee for them to import! -![help message](images/helpMessage.png) +Here's how you can export your data out of HRMate: -Format: `help` +1. In HRMate, type in the following command in the command box: `export [file name]`, replacing `[file name]` with the name +you will like to give your file. Your files will be saved in CSV format automatically. + - For instance, if you would like to save your file as `employees.csv`, type in the command `export employees` +2. You should see the message "Employee records have been saved to [file name]!" +3. To retrieve your exported file, go to the folder in which HRMate is stored in your File Explorer (if using Windows) or +Finder (if using Mac OS). From there, click on the `export` folder. +4. You should see your file in the `export` folder. +Here are potential error messages that you may receive and here's how to fix them: -### Adding a person: `add` +| Error Message | Why it happens | Fix | +|---------------------------------------------------------------------|--------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Employee records could not be saved! | You do not have the permission to write the file | Try renaming your file name when typing out the command, especially if the previous name refers to an existing file. If not, move HRMate to a different folder where you can create files, and run the command again. | -Adds a person to the address book. -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) -
+
-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` +## Leave-related operations -Shows a list of all persons in the address book. +HRMate allows you to manage your employees' leave applications with ease. -Format: `list` +For instance, you can [add](#add-a-leave-application) a leave application, [find](#find-a-leave-application) specific leave applications, [edit](#edit-a-leave-application) leave applications, [delete](#delete-a-leave-application) a leave application, and [import/export](#importing-exporting-leave-applications) leave applications with just a few commands. -### Editing a person : `edit` -Edits an existing person in the address book. +### Add a leave application +When an employee applies for leave, you can use the `add-leave` command to add their leave application into HRMate. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +#### Adding a leave application: `add-leave` -* 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. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +Use the `add-leave` command to add a leave application to HRMate. This command will add the specified leave application to the leave list. -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. +Here's how to add a leave application: -### Locating persons by name: `find` +1. Get the [index](#glossary) of the employee under the employee list. View this image in [quick start](#quick-start) for more information. -Finds persons whose names contain any of the given keywords. + +If the employee is not found, consider using list or any find commands to locate the employee in the employee list. + -Format: `find KEYWORD [MORE_KEYWORDS]` +2. Type in the following command in the command box `add-leave INDEX title/TITLE start/START_DATE end/END_DATE [d/DESCRIPTION]`, replace + * `NAME` with the index of the employee applied for leave in the employee list + * `Title` with the title of the leave + * `START_DATE` with the start date of the leave + * `END_DATE` with the end date of the leaves and it must be the same as or later than the start date. Dates are in a format of `yyyy-MM-dd` + * `[d/DESCRIPTION]` is an optional field with `DESCRIPTION` to be replaced with the description of the leave. + * Note that duplicated title and concurrent leave (overlapping date duration of leaves) are allowed. + - For instance, to add a leave of one day on 2023-11-01 to employee indexed 1 with a title of `Sample Leave 1`, type `add-leave 1 title/Sample Leave 1 start/2023-11-01 end/2023-11-01`` to the command box. + - For another instance, to add a leave of two days from 2023-11-01 to 2023-11-02 to employee indexed 2 with a title of `Sample Leave 2` and a description of `Sample Description`, type `add-leave 2 title/Sample Leave 2 start/2023-11-01 end/2023-11-02 d/Sample Description` to the command box. + +There is a status field of leave that is `PENDING` by default when a leave is added. Please DO NOT enter any status field of the leave, no recognitions of the status field and any other prefix are provided. +Therefore, to change the status of leave, please use the approve-leave or reject-leave commands instead to modify the record. + -* 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 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). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +3. Press "enter" on your keyboard and you should see the leave information at the end of the overall leave list. Note that if there is no description field added, the description will be `NONE` by default. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `add-leave INDEX title/TITLE start/START_DATE end/END_DATE [d/DESCRIPTION]`, replace `NAME` with the index of the employee applied for leave in the employee list, `Title` with the title of the leave, `START_DATE` with the start date of the leave and `END_DATE` with the end date of the leaves and it must be the same as or later than the start date. Dates are in a format of `yyyy-MM-dd`. `[d/DESCRIPTION]` is an optional field with `DESCRIPTION` representing the description of the leave. Note that duplicated titles and concurrent leave (overlapping date duration of leaves) are allowed. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message.| +| `This leave has already existed for the employee` | The start date and end date are exactly the same as the existing leave | Double check if the dates for the new leave do not have the same dates as the existing ones by checking against the list from [find-leave](#find-leave-application-belonging-to-an-employee-find-leave) or [find-leave-range](#find-leave-applications-by-time-period-find-leave-range). You may choose to [edit-leave](#editing-a-leave-application-edit-leave) if the new leave has the exact same start and end dates as the existing leave, in other words, they are the same leave. | +| `The person index provided is invalid` | The index specified is not positive or does not refer to any employee | Double check if the index is positive and appears in the employee list. Alternatively, use [list](#listing-all-employees-list) or any [find commands](#find-an-employee-record) to locate the employee in the employee list. Afterwards, use the correct employee index in the `add-leave` command. | +| `Leave titles should only contain alphanumeric characters, spaces, and dashes. It should not be blank` | Title input is blank and/or contains illegal characters | Add title and/or remove the illegal characters from the input | +| `The end date is earlier than the start date!` | The end date input is earlier than the start date input | Double check the date inputs. | +| `Date should be valid and in a format of `yyyy-MM-dd`` | The start date and end date inputs are not in the correct format | Ensure that the inputs of dates are in the format of `yyyy-MM-dd`, for example, `2023-01-01`. | +| `Leave descriptions should only contain alphanumeric characters, spaces, dashes, commas, apostrophes and full stops.` | Description input contains illegal characters | Remove the illegal characters from the input. -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +
-### Deleting a person : `delete` +### Find a leave application -Deletes the specified person from the address book. +HRMate offers different commands for finding leave applications. [`find-leave-range`](#find-leave-applications-by-time-period-find-leave-range) and [`find-leave-status`](#find-leave-applications-by-leave-status-find-leave-status) would find leave applications by time period and leave status respectively while [`find-leave`](#find-leave-applications-belonging-to-an-employee-find-leave) and [`find-all-leave`](#view-all-leaves-find-all-leave) would find leave applications by employee and view all leave applications respectively. -Format: `delete INDEX` +#### Find leave applications by time period: `find-leave-range` -* 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, …​ +To find leave applications by time period, use the `find-leave-range` command. This command will find leave applications that fall within the specified time period. -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. +Here's how to use the `find-leave-range` command: -### Clearing all entries : `clear` +1. Type in the following command in the command box: + `find-leave-range [start/START_DATE] [end/END_DATE]`, and replace `START_DATE` with the start date of the time period, + and `END_DATE` with the end date of the time period, both in the format `yyyy-MM-dd`. + - For instance: if you wanted to view all leave applications that fall within the time period of 2023-10-27 and 2023-11-03 inclusive, + you would type in `find-leave-range start/2023-10-27 end/2023-11-03` in the command box. + - Do note that neither the start or end fields are required: + - If you wanted to view all leave applications that start on or after 2023-10-27, you can type in `find-leave-range start/2023-10-27`. + - If you wanted to view all leave applications that end on or before 2023-11-03, you can type in `find-leave-range end/2023-11-03`. + - If you wanted to view all leave applications, you can type in `find-leave-range` or [`find-all-leave`](#view-all-leaves-find-all-leave) instead. +2. Press `Enter` to execute the command. You should see the leave applications that fall within the time period you specified. -Clears all entries from the address book. -Format: `clear` +Here are some potential error messages that you may receive and here's how to solve them: -### Exiting the program : `exit` -Exits the program. +| Error message | Why it happens | Fix | +|--------------------------------------------------------|----------------------------------------------------------|--------------------------------------------------------------------| +| `The end date is earlier than the start date!` | The end date you provided is earlier than the start date | Make sure that the end date is later than the start date | +| `Date should be valid and in a format of "yyyy-MM-dd"` | The date you provided is not in the format `yyyy-MM-dd` | Make sure that the date you provided is in the format `yyyy-MM-dd` | + +Note: `yyyy-MM-dd` refers to the format of the date in the form of year-month-day. For example, 2023-11-01 refers to 1st November 2023. + +#### Find leave applications by leave status: `find-leave-status` + +To find leave applications by leave status, use the `find-leave-status` command. This command will find leave applications that have the specified status. + +Here's how to use the `find-leave-status` command: + +1. Type in the following command in the command box: + `find-leave-status STATUS`, and replace `STATUS` with the status of the leave applications you wish to view. + - For instance: if you wanted to view all leave applications that have been approved, you would type in `find-leave-status APPROVED` in the command box. + - Do note that the status field is required, and the status must be either `APPROVED`, `PENDING` or `REJECTED`. + - You can only specify one status at a time. + - If you wish to view all leave applications, you can use the [`find-all-leave`](#view-all-leaves-find-all-leave) command instead. +2. Press `Enter` to execute the command. You should see the leave applications that have the status you specified. + + +Here are some potential error messages that you may receive and here's how to solve them: + + +| Error message | Why it happens | Fix | +|-----------------------------------------------------------------------------------------|--------------------------------------|--------------------------------------------------------------------------------------| +| `Command should only contain one of the following words: APPROVED / PENDING / REJECTED` | The status you provided is not valid | Make sure that the status you provided is either `APPROVED`, `PENDING` or `REJECTED` | + +
+ +#### Find leave applications belonging to an employee: `find-leave` + +To find leave applications belonging to an employee, use the `find-leave` command. This command will find leave applications that belong to the employee specified by their index in the employee list. + +Here's how to use the `find-leave` command: + +1. Get the [index](#glossary) of the employee under the employee list. View this image in [quick start](#quick-start) for more information. +2. Type in the following command in the command box: + `find-leave INDEX`, and replace `INDEX` with the index of the employee whose leave applications you wish to view. + - For instance: if you wanted to view all leave applications that belong to the employee with index 1, you would type in `find-leave 1` in the command box. + - Do note that the index field is required, and the index must be a valid number. + - The index must be a positive number. + - The index must be within the range of the number of employees in HRMate. + - If you wish to view all leave applications, you can use the [`find-all-leave`](#view-all-leaves-find-all-leave) command instead. +3. Press `Enter` to execute the command. You should see the leave applications that belong to the employee with the index you specified. + + +![find-leave](images/find-leaveUI.png) + + +Here are some potential error messages that you may receive and here's how to solve them: + + +| Error message | Why it happens | Fix | +|----------------------------------------|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `The person index provided is invalid` | The index you provided does not match with any person in the address book | Make sure that the employee that you are requesting for is currently displayed in the employee list. Then, use the number to the left of the employee's name as the index. | +| `Invalid command format!` | No index was provided, or index provided is not a positive number greater than 0 | Make sure that you provide an index when using this command. If you require assistance identifying the index, refer to the box directly above. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message.| + +#### View all leaves: `find-all-leave` + +Use the `find-all-leave` command to view all leave applications currently stored in HRMate. + +Here's how to view all leave applications in the leave list: + +1. Type in the following command in the command box: + `find-all-leave` +2. Press `Enter` to execute the command. You should see all the leave applications in HRMate. + + +### Edit a leave application + +HRMate offers different commands for editing leave applications. [`approve-leave`](#approve-leave-application-by-index-approve-leave) and [`reject-leave`](#reject-leave-application-by-index-reject-leave) would approve and reject leave applications respectively while [`edit-leave`](#editing-a-leave-application-edit-leave) would edit the leave application comprehensively. + +#### Approve leave application by index: `approve-leave` + +Use the `approve-leave` command to approve a leave application in HRMate. This command will approve the specified leave application. + +Here's how to approve a leave application: + +1. Get the [index](#glossary) of the leave under the leave list. + + +If the leave application is not found, consider using `find-all-leave` or any `find commands` to locate the leave application in the leave list. + + +2. Type in the following command in the command box `approve-leave LEAVE_LIST_INDEX` +3. Press "enter" on your keyboard and the specified leave application is approved. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `approve-leave LEAVE_LIST_INDEX`. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message. +| `The leave index provided is invalid` | The index specified does not refer to any leave application | Double check if the inputted index is correct as specified in the leave list. Alternatively, use [find-all-leave](#view-all-leaves-find-all-leave) or any [find commands](#find-a-leave-application) to locate the leave application in the leave list. Afterwards, use the correct leave index in the `approve-leave` command. | +| `Leave previously approved: ` followed by the leave information | The provided leave application is already approved in HRMate | If this is the leave you would like to approve, you don't have to do anything. + +#### Reject leave application by index: `reject-leave` + +Use the `reject-leave` command to reject a leave application in HRMate. This command will reject the specified leave application. + +Here's how to reject a leave application: + +1. Get the [index](#glossary) of the leave under the leave list. + + +If the leave application is not found, consider using `find-all-leave` or any `find commands` to locate the leave application in the leave list. + + +2. Type in the following command in the command box `reject-leave LEAVE_LIST_INDEX` +3. Press "enter" on your keyboard and the specified leave application is approved. + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `approve-leave LEAVE_LIST_INDEX`. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message. +| `The leave index provided is invalid` | The index specified does not refer to any leave application | Double check if the inputted index is correct as specified in the leave list. Alternatively, use [find-all-leave](#view-all-leaves-find-all-leave) or any [find commands](#find-a-leave-application) to locate the leave application in the leave list. Afterwards, use the correct leave index in the `reject-leave` command. | +| `Leave previously rejected: ` followed by the leave information | The provided leave application is already rejected in HRMate | If this is the leave you would like to reject, you don't have to do anything. + + +#### Editing a leave application: `edit-leave` + +For a more comprehensive editing of a leave application, use the `edit-leave` command to edit a leave application in HRMate. This command will edit the specified leave application. + +Here's how to use the `edit-leave` command to edit a leave application: + +1. Get the [index](#glossary) of the leave under the leave list. + + +If the employee is not found, consider using `find-all-leave` or any `find commands` to locate the leave in the leave list. + + +2. Type in the following command in the command box `edit-leave INDEX [title/TITLE] [start/START_DATE] [end/END_DATE] [d/DESCRIPTION] [s/STATUS]` where `INDEX` is the index of the leave in the list currently, `[title/TITLE]`, `[start/START_DATE]`, `[end/END_DATE]`, `[d/DESCRIPTION]`, `[s/STATUS]` are optional fields which require changing, replacing `TITLE` with the title of the leave, `START_DATE` with the leave's start date, `END_DATE` with the leave's end date, `DESCRIPTION` with the title's description and `STATUS` with the leave's status. Note that at least one field to edit must be present and only the fields present will be edited. + - For example, to change the title and description of the leave indexed 2 to John's sick leave and MC provided, type in the command `edit-leave 2 title/John's sick leave d/MC provided`. Note that the start date, end date, and status will remain unchanged. + - Please refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information + + + +To update the status of a leave application, we recommend you to use the `approve-leave` or `reject-leave` commands to approve or reject the leave applications. It is possible to update the status of the leave applications with the `edit-leave` command, but it must be either APPROVED, PENDING or REJECTED (in all capital letters). + + +3. Press "enter" on your keyboard and you should see the changes applied to the leave. +* Examples: + +| Error message | Why it happens | Fix | +|---------------|----------------|-----| +| `Invalid command format!` | The command you input does not follow the specified format | Ensure the command you entered follows the following format: `edit-leave INDEX [title/TITLE] [start/START_DATE] [end/END_DATE] [d/DESCRIPTION] [s/STATUS]` where `INDEX` is the index of the leave in the list currently, `[title/TITLE]`, `[start/START_DATE]`, `[end/END_DATE]`, `[d/DESCRIPTION]`, `[s/STATUS]` are optional fields which require changing, replacing `TITLE` with the title of the leave, `START_DATE` with the leave's start date, `END_DATE` with the leave's end date, `DESCRIPTION` with the title's description and `STATUS` with the leave's status. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message.| +| `FIELD should FORMAT` where `FIELD` is an input like `Leave titles` or `Date` and `FORMAT` contains additional information about the field's format. | The input does not follow the format prescribed. For example, the entered phone number might contain alphabets. | Follow the on-screen message to fix the field in question. For example, `Leave titles should only contain alphanumeric characters, spaces, and dashes. It should not be blank` means that the input title does not follow the prescribed format. | +|`Date should be valid and in a format of yyyy-MM-dd`| The start or end date format does not conform to the program's standard. | Please update the date format to `yyyy-MM-dd` and reenter the command. | +| `The leave index provided is invalid` | The index specified does not refer to any leave | Double check if the inputted index is correct as specified in the leave list. Alternatively, use [find-all-leave](#view-all-leaves-find-all-leave) or any [find commands](#find-a-leave-application) to locate the leave in the leave list. Afterwards, use the correct leave index in the `edit-leave` command. | +| `At least one field to edit must be provided` | The command you input does not contain any fields to edit | Check if there are any input fields in the command inputted. An input like `edit-leave 1` is not accepted as there are no edits to be made. | +| `The end date is earlier than the start date!` | The end date provided is before the current or provided start date. | Please double-check the inputted dates and make sure the end date is not before the current or provided start date. | + + +
+ +### Delete a leave application + +To delete a leave application, use the [`delete-leave`](#removing-a-leave-application-delete-leave) command. + +#### Removing a leave application: `delete-leave` + +Use the `delete-leave` command to remove a leave application from HRMate. This command will remove the specified leave application from the leave list. + +Here's how to use the `delete-leave` command to remove a leave application: + +1. Type in the following command in the command box: `delete-leave LEAVE_LIST_INDEX`, and replace `LEAVE_LIST_INDEX` with the index of the leave application you wish to remove. + - For instance, referring to the figure below: if you wanted to remove the leave application titled "medical leave" with employee "Bernice Yu", you would type in `delete-leave 2` in the command box. + - Do note that the index of the leave application you wish to remove must be a valid number + - The index must be a positive number that is larger than 0 + - The index cannot exceed the number of entries in the leave book +2. Press "enter" on your keyboard and you should see the leave application removed from HRMate. + + +![delete-leave](images/delete-leaveUI.png) + +Here are some potential error messages that you may receive and here's how to solve them: + + +| Error message | Why it happens | Fix | +|---------------------------------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `The leave index provided is invalid` | The index specified does not refer to any leave application | Double check if the index appears in the leave list. Afterwards, use the correct leave index to the left of the leave application in the `delete-leave` command. | +| `Invalid command format!` | An index was not specified in the command, or the index specified is not a positive number that is greater than 0 | Make sure that you provide an index when using this command. If you require assistance identifying the index, refer to the box directly above. Also check if the index is a positive integer, having an invalid index such as zero, negative numbers, non-integers, and characters can lead to this error message. | + + + +
+ + +### Importing/Exporting leave applications +The import and export feature extends to importing and exporting leaves, with [`import-leave`](#importing-leave-applications-import-leave) and [`export-leave`](#exporting-leave-applications-export-leave). This allows you to generate lists of leave +applications that can be opened in other major spreadsheet applications such as Microsoft Excel. + + +#### Importing leave applications : `import-leave` + +Use the `import-leave` command to import leave applications from a CSV file. This command will import the leave applications from +the specified CSV file into HRMate. + + +Imported leave applications will overwrite existing leave applications in HRMate. Remember to make a copy of your existing +leave applications if you want to save them! You can do so by exporting your current records. + + +Here's how you can bring over your leave applications from Excel: + +1. Export your Excel save file in CSV format. Ensure that the separator is set to be a semicolon(`;`), and that you have + the following fields: Title, Employee, Start, End, Description, Status. + * You may skip this step if you already have a CSV file (e.g. you are importing a previously exported CSV file generated by + HRMate) + + + If you have saved or edited your CSV file in Excel, please note that Excel will likely change the separator into a comma, + which will cause problems when trying to import leave applications. To fix this problem, + click here to find out how you + can change the separator to semicolons in Excel. Note that this workaround is currently available only for Windows users + + +2. If you are unsure how your file should look like, you may refer to the following images: + * Here's how your CSV file should look like if you open it up in Notepad: + ![import-leave-notepad](images/import-leave-notepad.png) + * It is okay if your CSV file does not contain the first line `sep=;`, as this is a line added to files exported by HRMate + to help Excel open the file. Do ensure the next line has to be the following: + `Title;Employee;Start;End;Description;Status` in order for HRMate to read it. + * If you open your CSV file in Excel, your file should look like this: +
+
+ ![import-leave-excel](images/import-leave-excel.png) +
+
+ * Do ensure that the first row contains the following headers: `Title`, `Employee`, `Start`, `End`, `Description`, and `Status`. +3. In HRMate, type in the following command in the command box: `import-leave` and press "Enter" +4. In the file dialog that opens up, go to where you saved your exported CSV file, click on it, and click on the Open button. +5. You should see your leave applications show up in HRMate, along with the message "leave applications have been imported from [your file name]!" + +Here are some possible error messages you might encounter and here's how you can fix them: + +| Error Message | Why it happens | Fix | +|---------------------------------------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| leave applications were not imported | You did not select a file in the file dialog | Retype the command, and make sure to select a CSV file when the file dialog opens | +| Records in file [file name] could not be imported, import cancelled | Your file likely contains illegal characters or is corrupted | Ensure that your data fulfils the following constraints (to be added) | +| No valid records found in file [file name], import cancelled | Your file either is empty or does not contain a single valid leave application | Ensure that your file is non-empty and fulfils the abovementioned constraints | + + +#### Exporting leave applications: `export-leave` + +Not only can you bring your data into HRMate, you can also bring your data out of HRMate. HRMate's export feature allows you +to export either the entire set of leave application records, or leave applications with a particular filter applied (e.g. only export +all leaves in a given time period). You can then either store your exported CSV file for future use, +open it in a different application, or send it to another employee for them to import! + +Here's how you can export your data out of HRMate: + +1. In HRMate, type in the following command in the command box: `export-leave [file name]`, replacing `[file name]` with the name + you will like to give your file. Your files will be saved in CSV format automatically. + - For instance, if you would like to save your file as `today.csv`, type in the command `export today` +2. You should see the message "leave applications have been saved to [file name]!" +3. To retrieve your exported file, go to the folder in which HRMate is stored in your File Explorer (if using Windows) or + Finder (if using Mac OS). From there, click on the `export` folder. +4. You should see your file in the `export` folder. + +Here are potential error messages that you may receive and here's how to fix them: + +| Error Message | Why it happens | Fix | +|-----------------------------------|--------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| leave applications could not be saved! | You do not have the permission to write the file | Try renaming your file name when typing out the command, especially if the previous name refers to an existing file. If not, move HRMate to a different folder where you can create files, and run the command again. | + + + +-------------------------------------------------------------------------------------------------------------------- + +
+ +## Other operations + +This section covers other operations that you can perform in HRMate. + +For information on how to get [help](#getting-help), [reset HRMate](#reset-hrmate), and [exit HRMate](#exit-hrmate), please refer to the respective sections below. + +### Getting help +Should you need any help, you can access this online user guide (this document) with the `help` command. + +#### Getting help : `help` + +Here's how to use the `help` command: + +1. Type in the following command in the command box: `help`. +2. Press "Enter" on your keyboard. +3. The following pop up should show: + + +![help menu](images/help-menu.png) + +4. Click on the "copy link" button to copy the link to the user guide, and paste into any [web browser](#glossary) to access the user guide. Alternatively, click the red button at the corner of the window to close the pop up. + +### Reset HRMate + +After playing with the different features in HRMate, you might feel that it's time to delete HRMate's sample employee +contacts and instead use your own. Rather than manually deleting every employee contact and leave application, here's a +quick and easy way to clear them in the app! + +#### Resetting HRMate by clearing all existing records: `clear` + +Here's how you can remove every employee contact and leave application from the app: + +1. In HRMate, type this command into the [command box](#command-box): `clear` and press Enter. +2. That's it! You should notice that your [Employee List](#employee-list) and [Leave List](#leave-list) are both empty. + You should also notice that the [Command Output Box](#command-output-box) will display the message: + `All employee contacts and leave applications have been cleared!` Now you have a clean slate and can start adding your + own data! + +**Warning:** + +Be careful when using this command - resetting the application is permanent. If you have not already exported your +employee records and +leave applications, you will not be able to get them back! + + +-------------------------------------------------------------------------------------------------------------------- + +
+ +### Exit HRMate +Once you've finished your work, you might want to close HRMate. You can do so by clicking on the "X" button at the top +right corner of the window. Alternatively, If you love typing over clicking, you can close the app via keyboard too! + +#### Close the application: `exit` +Here's how you can close HRMate using your keyboard: + +1. In HRMate, type this command into the [command box](#command-box): `exit` and press enter. +2. That's it! HRMate will now close, and you will find your employee records and leave applications there the next time you +reopen HRMate. -Format: `exit` ### 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. +HRMate data are 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. +HRMate data are saved automatically as a JSON file `[JAR file location]/data/hrmate.json`. Advanced users are welcome to update data directly by editing that data file. + +* **Warning:** + + If your changes to the data file makes its format invalid, HRMate 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. + -
: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. -
-### Archiving data files `[coming in v2.0]` -_Details coming soon ..._ -------------------------------------------------------------------------------------------------------------------- -## FAQ +
+ +## How to interpret command formats + +Example command formats: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...`, `add-tag INDEX t/TAG...` + +The first command is used to add an employee while the second is used to add tags to a specified employee. Let's examine how the command is used. + +| **Command component examples** | **What they mean** | +|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `add`, `add-tag` | The name of the command. The first word of the command is used to specify what command is used. | +| `n/NAME`, `p/PHONE_NUMBER`, `e/EMAIL`, `a/ADDRESS`, `t/TAG` | Mandatory command fields. These fields are used to provide further information that the commmand needs. The start of a field is denoted by a field [prefix](#glossary). | +| `n/`, `p/`, `e/`, `a/`, `t/` | Field prefix. These indicate what the field type is (name, phone number, email address etc.). They have a letter or phrase, followed by a backslash ("/"). This allows fields to be written in any order. For example, `edit 1 n/John Doe p/98765432` would have the same effect as `edit 1 p/98765432 n/John Doe`. | +| `INDEX` | The [index](#glossary) of the command. Some command requires an index to specified which employee or leave to act on. It must be the second word of the command, after the name of the command. | +| `[t/TAG]` | Optional command field. The square brackets ("[" and "]") indicates that a field is optional. However, some commands like [edit](#editing-the-name-phone-number-email-address-home-address-or-tags-of-employees--edit) have additional requirements like at least one of the optional fields must be specified. | +| `...` | Variable optional fields. This indicates that we can supply more than one field of the same type. When coupled with the optional command field ("[" and "]"), this means that zero or more command fields of that type can be provided. For example, `[t/TAG]...` indicates **zero** or more tags can be specified while `t/TAG...` indicates **one** or more tags can be specified. To specify multiple fields, use multiple tag prefixes. For instance, to use full-time and remote as tag fields to a command, type `t/full-time t/remote` | + +**Notes** +* Parameters can be in any order
+ e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also + acceptable. +* 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 `help 123` is executed, it will be interpreted as `help`. +* 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. + +-------------------------------------------------------------------------------------------------------------------- + +
-**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. +## FAQ +#### Q: How do I transfer my data to another computer? +**A**: We recommend using the [import and export command for employees](#importing-exporting-employee-records) and [import and export command for leave applications](#importing-exporting-leave-applications). +
+
+ +#### Q: How do I move a file? +**A**: +* For **Windows** users, first open File Explorer and find the file. Afterwards, drag the selected file's icon to the desired location using your mouse. For more help, please consult the [Microsoft documentation](https://support.microsoft.com/en-gb/office/move-or-copy-an-item-to-another-folder-19768dfe-86c4-40bf-b82c-1c084b624492)
+* For **Mac** users, first open Finder and find the specific file. Then drag it to the desired location using the mouse. For more help, please consult the [Apple documentation](https://support.apple.com/en-sg/guide/mac-help/mh26885/mac). +
+
+ +#### Q: How do I open a terminal? +**A**: +* For **Windows** users, press the Windows key, type in `powershell` and press Enter. The terminal should open. For more help, please consult the [Microsoft documentation](https://learn.microsoft.com/en-us/windows/terminal/install). +* For **Mac** users, open the Terminal app on your Mac. For more help, please consult the [Apple documentation](https://support.apple.com/en-sg/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac#:~:text=Terminal%20for%20me-,Open%20Terminal,%2C%20then%20double%2Dclick%20Terminal.). +
+
+ +#### Q: How do I navigate files in terminal? +**A**: You can use the `cd` command to navigate in terminal. For more information, please consult this [documentation](https://www.ibm.com/docs/en/aix/7.2?topic=directories-changing-another-directory-cd-command). +
+
+ +#### Q: How do I download Java 11? +**A**: Please refer to the official Java installation instructions [here](https://docs.oracle.com/en/java/javase/21/install/overview-jdk-installation.html#GUID-8677A77F-231A-40F7-98B9-1FD0B48C346A). +
+
+ +#### Q: How do I change the separator of my CSV file into semicolons(`;`) in Excel? +**A**: +* If you are a **Windows** user: + 1. In Excel, click on File > Options > Advanced. + 2. Under Editing options, uncheck the `Use system separators` check box. + 3. Change the Decimal separator to a comma (`,`) and the Thousands separator to a period (`.`). + 4. Save your file. When saving your file, change the save type (`Save as type`) to `CSV (Comma delimited) (*.csv)` + 5. After saving your file, remember to reset your Excel separators. Repeat steps 1 and 2, only that this time remember to + check the `Use system separators` check box instead. +* If you are a **Mac** user, unfortunately Excel makes it very difficult to change the separator to semicolons. +Stay tuned for future releases that will extend support for Mac OS! -------------------------------------------------------------------------------------------------------------------- ## Known issues @@ -184,14 +1049,59 @@ _Details coming soon ..._ -------------------------------------------------------------------------------------------------------------------- +
+ ## 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` -**Help** | `help` +| 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` | +| **Add Leave** | `add-leave INDEX title/TITLE start/START_DATE end/END_DATE [d/DESCRIPTION]`
e.g., `add-leave 1 title/Sample Leave 1 start/2023-11-01 end/2023-11-01` | +| **Add Tag** | `add-tag EMPLOYEE_LIST_INDEX TAG`
e.g., `add-tag 3 remote` | +| **Approve Leave** | `approve-leave 1`
e.g., `approve-leave 1` | +| **Clear** | `clear` | +| **Delete** | `delete EMPLOYEE_LIST_INDEX`
e.g., `delete 3` | +| **Delete Leave** | `delete-leave LEAVE_LIST_INDEX`
e.g., `delete-leave 1` | +| **Delete Tag** | `delete-tag EMPLOYEE_LIST_INDEX TAG`
e.g., `delete-tag 3 remote` | +| **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` | +| **Edit Leave** | `edit-leave INDEX [title/TITLE] [start/START_DATE] [end/END_DATE] [d/DESCRIPTION] [s/STATUS]`
e.g., `edit-leave 1 title/medical leave start/2023-11-01` | +| **Exit** | `exit` | +| **Export Contacts** | `export FILE_NAME` | +| **Export Leaves** | `export-leave FILE_NAME` | +| **Find** | `find NAME...​`
e.g., `find James Jake` | +| **Find All Tags** | `find-all-tag [t/TAG]...`
e.g.,`find-all-tag t/remote t/full-time` | +| **Find Some Tags** | `find-some-tag [t/TAG]...`
e.g.,`find-some-tag t/remote t/full-time` | +| **Find Leaves by Period** | `find-leave-range [start/START_DATE] [end/END_DATE]` | +| **Find Leaves by Status** | `find-leave-status STATUS` | +| **Find All Leaves** | `find-all-leave` | +| **Find All Tags** | `find-all-tag [t/TAG]...`
e.g.,`find-all-tag t/remote t/full-time` | +| **Find Leaves** | `find-leave INDEX`
e.g., `fin-leave 1` | +| **Find Leaves by Period** | `find-leave-range [start/START_DATE] [end/END_DATE]` | +| **Find Leaves by Status** | `find-leave-status STATUS` | +| **Find Some Tags** | `find-some-tag [t/TAG]...`
e.g.,`find-some-tag t/remote t/full-time` | +| **Help** | `help` | +| **Import Contacts** | `import` | +| **Import Leaves** | `import-leaves` | +| **List** | `list` | +| **Reject Leave** | `reject-leave 1`
e.g., `reject-leave 1` | +| **View tag** | `view-tag` | +-------------------------------------------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------------------------------------------- + +## Glossary + +| Term | Meaning | +|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| CLI | A text based interface where users type in commands instead of interacting with the application's graphics. Also see: GUI | +| CSV | A text file format that uses commas to separate values. It is supported by a wide range of software, including Microsoft Excel. | +| Command | A line of instructions that you input into the command box. Also see: Command box | +| Command Box | A box for you to input commands. Refer to [quick start](#quick-start) for more information. | +| Command Output Box | A box that displays the results of the command you keyed in. It will tell you whether the command you entered has successfully run, or if there is an error in your command that needs to be fixed. Refer to []() for a picture. | +| Employee List | The Employee list is the list of employees on the left side of the application. Refer to [quick start](#quick-start) for a picture. | +| Leave List | The Leave list is the list of leaves on the right side of the application. Refer to [quick start](#quick-start) for a picture. | +| GUI | A graphical based interface where users interact the the application's graphics like buttons or scrollpanes. Also see: CLI | +| Prefix | A letter or phrase before an input. Refer to [how to interpret command formats](#how-to-interpret-command-formats) for more information. | +| Tag | A text phrase used to categorise employees by. A tag can be a position (intern, senior), department (HR, tech) or any category (full-time, remote). | +| Index | The number labelling each employee in the employee list. Refer to [quick start](#quick-start) for more information. | +| Web browser | An application to serve the web like Internet Explorer, Google Chrome or Firefox. In fact, you are probably using one to access this guide right now! | 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..4134ffc55fe --- /dev/null +++ b/docs/_markbind/layouts/default.md @@ -0,0 +1,66 @@ + + + + +
+ + HRMate +
  • 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/AddLeaveCommandActivityDiagram.puml b/docs/diagrams/AddLeaveCommandActivityDiagram.puml new file mode 100644 index 00000000000..dae508cb6d1 --- /dev/null +++ b/docs/diagrams/AddLeaveCommandActivityDiagram.puml @@ -0,0 +1,21 @@ +@startuml +start +:User executes add customer command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([User provided all required parameters]) +: Index, title, start date, end date of the leave are parsed; +if () then ([description provided]) +:Description of the leave is parsed; + +else([else]) +endif +:New leave is returned; +stop +else([User failed to provide required parameters correctly]) +: Throw ParserException, re-enter the command; +end + +@enduml diff --git a/docs/diagrams/AddTagSequenceDiagram.puml b/docs/diagrams/AddTagSequenceDiagram.puml new file mode 100644 index 00000000000..d7bfb2b1d3f --- /dev/null +++ b/docs/diagrams/AddTagSequenceDiagram.puml @@ -0,0 +1,67 @@ +@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 "a:AddTagCommand" as AddTagCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box +[-> LogicManager : execute(addTag) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(addTag) +activate AddressBookParser + +create AddTagCommand +AddressBookParser -> AddTagCommand +activate AddTagCommand + +AddTagCommand --> AddressBookParser +deactivate AddTagCommand + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddTagCommand : execute() +activate AddTagCommand + +AddTagCommand -> Model : get() +activate Model + +Model --> AddTagCommand +deactivate Model + +AddTagCommand -> Model : setPerson() +activate Model + +Model --> AddTagCommand +deactivate Model + +AddTagCommand -> Model : updateFilteredPersonList() +activate Model + +Model --> AddTagCommand +deactivate Model + +create CommandResult +AddTagCommand --> CommandResult +activate CommandResult + +CommandResult --> AddTagCommand : r +deactivate CommandResult + +AddTagCommand --> LogicManager : r +deactivate AddTagCommand +AddTagCommand -[hidden]-> LogicManager : r +destroy AddTagCommand + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/AddressBookStorageClassDiagram.puml b/docs/diagrams/AddressBookStorageClassDiagram.puml new file mode 100644 index 00000000000..d0863b5afc3 --- /dev/null +++ b/docs/diagrams/AddressBookStorageClassDiagram.puml @@ -0,0 +1,45 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor STORAGE_COLOR +skinparam classBackgroundColor STORAGE_COLOR + +package AddressBookStorage as AddressBookStoragePackage { + +Class "<>\nAddressBookStorage" as AddressBookStorage + +package "Json AddressBook Storage" #F4F6F6{ +Class JsonAddressBookStorage +Class JsonSerializableAddressBook +Class JsonAdaptedPerson +} + +package "Csv AddressBook Storage" #F4F6F6{ +Class CsvAddressBookStorage +Class CsvSerializableAddressBook +Class CsvAdaptedPerson +} + +Class SerializableAddressBook +Class AdaptedPerson +Class AdaptedTag +} +Class HiddenOutside #FFFFFF +HiddenOutside ..> AddressBookStorage + +JsonAddressBookStorage .up.|> AddressBookStorage +JsonAddressBookStorage ..> JsonSerializableAddressBook +JsonSerializableAddressBook --> "*" JsonAdaptedPerson + +CsvAddressBookStorage .up.|> AddressBookStorage +CsvAddressBookStorage ..> CsvSerializableAddressBook +CsvSerializableAddressBook --> "*" CsvAdaptedPerson + +AdaptedPerson --> "*" AdaptedTag + +SerializableAddressBook <|-- JsonSerializableAddressBook +SerializableAddressBook <|-- CsvSerializableAddressBook + +AdaptedPerson <|-- JsonAdaptedPerson +AdaptedPerson <|-- CsvAdaptedPerson +@enduml diff --git a/docs/diagrams/EmployeeObjectDiagram.puml b/docs/diagrams/EmployeeObjectDiagram.puml new file mode 100644 index 00000000000..d7db410a3af --- /dev/null +++ b/docs/diagrams/EmployeeObjectDiagram.puml @@ -0,0 +1,27 @@ +@startuml +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +package PersonObjectDiagram { + Object ":Person" as Employee { + } + + Object ":Name" as Name { + } + Object ":Phone" as Phone { + } + Object ":Email" as Email { + } + Object ":Address" as Address { + } + Object ":Tags" as Tags { + } +} + +Name <-- Employee : has < +Phone <-- Employee : has < +Email <-- Employee : has < +Address <-- Employee : has < +Tags <-- Employee : has < + +@enduml diff --git a/docs/diagrams/ExportSequenceDiagram.puml b/docs/diagrams/ExportSequenceDiagram.puml new file mode 100644 index 00000000000..795cc94a0a5 --- /dev/null +++ b/docs/diagrams/ExportSequenceDiagram.puml @@ -0,0 +1,94 @@ +@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 ":ExportContactCommandParser" as ExportContactCommandParser LOGIC_COLOR +participant "e:ExportContactCommand" as ExportContactCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Storage STORAGE_COLOR_T1 +participant "ab:ReadOnlyFilteredAddressBook" as ReadOnlyFilteredAddressBook STORAGE_COLOR +participant "abStorage:CsvAddressBookStorage" as CsvAddressBookStorage STORAGE_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("export employees") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("export employees") +activate AddressBookParser + +create ExportContactCommandParser +AddressBookParser -> ExportContactCommandParser +activate ExportContactCommandParser + +ExportContactCommandParser -> ExportContactCommandParser : parseFileName("employees") + +create ExportContactCommand +ExportContactCommandParser -> ExportContactCommand +activate ExportContactCommand + +ExportContactCommand --> ExportContactCommandParser +deactivate ExportContactCommand + +ExportContactCommandParser --> AddressBookParser : e +deactivate ExportContactCommandParser +destroy ExportContactCommandParser + +AddressBookParser --> LogicManager : e +deactivate AddressBookParser + +LogicManager -> ExportContactCommand : execute() +activate ExportContactCommand + +create ReadOnlyFilteredAddressBook +ExportContactCommand -> ReadOnlyFilteredAddressBook +activate ReadOnlyFilteredAddressBook + +ReadOnlyFilteredAddressBook -> Model : getFilteredPersonList() +activate Model + +Model -> ReadOnlyFilteredAddressBook +deactivate Model + +ReadOnlyFilteredAddressBook -> ExportContactCommand : ab +deactivate ReadOnlyFilteredAddressBook + +create CsvAddressBookStorage +ExportContactCommand -> CsvAddressBookStorage +activate CsvAddressBookStorage + +CsvAddressBookStorage -> ExportContactCommand +deactivate CsvAddressBookStorage + +ExportContactCommand -> CsvAddressBookStorage : saveAddressBook(ab) +activate CsvAddressBookStorage + +CsvAddressBookStorage -> ExportContactCommand +deactivate CsvAddressBookStorage + +create CommandResult +ExportContactCommand --> CommandResult +activate CommandResult + +CommandResult --> ExportContactCommand : r +deactivate CommandResult + +ExportContactCommand --> LogicManager : r +deactivate ExportContactCommand +ExportContactCommand -[hidden]-> LogicManager : r +destroy ExportContactCommand +destroy ReadOnlyFilteredAddressBook +destroy CsvAddressBookStorage + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindAllTagCommandDiagram.puml b/docs/diagrams/FindAllTagCommandDiagram.puml new file mode 100644 index 00000000000..1b42df1456d --- /dev/null +++ b/docs/diagrams/FindAllTagCommandDiagram.puml @@ -0,0 +1,79 @@ +@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 ":FindAllTagCommandParser" as FindAllTagCommandParser LOGIC_COLOR +participant "predicate:TagsContainAllTagsPredicate" as TagsContainAllTagsPredicate LOGIC_COLOR +participant "d:FindAllTagCommand" as FindAllTagCommand 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("find-all-tag t/remote") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find-all-tag t/remote") +activate AddressBookParser + +create FindAllTagCommandParser +AddressBookParser -> FindAllTagCommandParser +activate FindAllTagCommandParser + +FindAllTagCommandParser --> AddressBookParser +deactivate FindAllTagCommandParser + +AddressBookParser -> FindAllTagCommandParser : parse("t/remote") +activate FindAllTagCommandParser + +create TagsContainAllTagsPredicate +FindAllTagCommandParser -> TagsContainAllTagsPredicate +activate TagsContainAllTagsPredicate + +TagsContainAllTagsPredicate --> FindAllTagCommandParser : predicate +deactivate TagsContainAllTagsPredicate + +create FindAllTagCommand +FindAllTagCommandParser -> FindAllTagCommand : FindAllTagCommand(predicate) +activate FindAllTagCommand + +FindAllTagCommand --> FindAllTagCommandParser +deactivate FindAllTagCommand + +FindAllTagCommandParser --> AddressBookParser +deactivate FindAllTagCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindAllTagCommandParser -[hidden]-> AddressBookParser +destroy FindAllTagCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindAllTagCommand : execute() +activate FindAllTagCommand + +FindAllTagCommand -> Model : updateFilteredPersonList(predicate) +activate Model + +Model --> FindAllTagCommand +deactivate Model + + +create CommandResult +FindAllTagCommand -> CommandResult +activate CommandResult + +CommandResult --> FindAllTagCommand +deactivate CommandResult + +FindAllTagCommand --> LogicManager : result +deactivate FindAllTagCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindLeaveInPeriodCommandSequenceDiagram.puml b/docs/diagrams/FindLeaveInPeriodCommandSequenceDiagram.puml new file mode 100644 index 00000000000..f28733d80b8 --- /dev/null +++ b/docs/diagrams/FindLeaveInPeriodCommandSequenceDiagram.puml @@ -0,0 +1,78 @@ +@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 ":FindLeaveByPeriodCommandParser" as FindLeaveByPeriodCommandParser LOGIC_COLOR +participant "p:LeaveInPeriodPredicate" as LeaveInPeriodPredicate LOGIC_COLOR +participant "c:FindLeaveByPeriodCommand" as FindLeaveByPeriodCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("find-leave-range") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find-leave-range") +activate AddressBookParser + +create FindLeaveByPeriodCommandParser +AddressBookParser -> FindLeaveByPeriodCommandParser +activate FindLeaveByPeriodCommandParser + +create LeaveInPeriodPredicate +FindLeaveByPeriodCommandParser -> LeaveInPeriodPredicate +activate LeaveInPeriodPredicate + +LeaveInPeriodPredicate -> FindLeaveByPeriodCommandParser +deactivate LeaveInPeriodPredicate + +create FindLeaveByPeriodCommand +FindLeaveByPeriodCommandParser -> FindLeaveByPeriodCommand : FindLeaveByPeriodCommand(p) +activate FindLeaveByPeriodCommand + +FindLeaveByPeriodCommand --> FindLeaveByPeriodCommandParser +deactivate FindLeaveByPeriodCommand + +FindLeaveByPeriodCommandParser --> AddressBookParser : c +deactivate FindLeaveByPeriodCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindLeaveByPeriodCommandParser -[hidden]-> AddressBookParser +destroy FindLeaveByPeriodCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindLeaveByPeriodCommand : execute() +activate FindLeaveByPeriodCommand + +FindLeaveByPeriodCommand -> Model : updateFilteredLeaveList(p) +activate Model + +Model --> FindLeaveByPeriodCommand +deactivate Model + +create CommandResult +FindLeaveByPeriodCommand -> CommandResult +activate CommandResult + +CommandResult --> FindLeaveByPeriodCommand +deactivate CommandResult + +FindLeaveByPeriodCommand --> LogicManager : r +deactivate FindLeaveByPeriodCommand + +FindLeaveByPeriodCommand -[hidden]-> LogicManager +destroy LeaveInPeriodPredicate +destroy FindLeaveByPeriodCommand + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ImportActivityDiagram.puml b/docs/diagrams/ImportActivityDiagram.puml new file mode 100644 index 00000000000..404b9c8c650 --- /dev/null +++ b/docs/diagrams/ImportActivityDiagram.puml @@ -0,0 +1,25 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes Import Command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([user selects CSV file]) + :Parse CSV data; + if () then ([CSV file contains valid values]) + :Create ReadOnlyAddressBook; + :Set Model's AddressBook to the new ReadOnlyAddressBook; + else ([else]) + :Return error message indicating file cannot be imported; + + endif + stop +else ([else]) + :Return message indicating no file selected; + stop +endif +@enduml diff --git a/docs/diagrams/LeaveObjectDiagram.puml b/docs/diagrams/LeaveObjectDiagram.puml new file mode 100644 index 00000000000..51db760fcfc --- /dev/null +++ b/docs/diagrams/LeaveObjectDiagram.puml @@ -0,0 +1,28 @@ +@startuml +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +package LeaveObjectDiagram { + Object ":Leave" as Leave { + } + + Object ":Person" as Employee { + } + Object ":Title" as Title { + } + Object "start:Date" as startDate { + } + Object "end:Date" as endDate { + } + Object ":Description" as Description { + } +} + +Employee <-- Leave : has < +Title <-- Leave : has < +startDate <-- Leave : has < +endDate <-- Leave : has < +Description<-- Leave : has < + + +@enduml diff --git a/docs/diagrams/LeavesBookStorageClassDiagram.puml b/docs/diagrams/LeavesBookStorageClassDiagram.puml new file mode 100644 index 00000000000..7909dfbd9e4 --- /dev/null +++ b/docs/diagrams/LeavesBookStorageClassDiagram.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor STORAGE_COLOR +skinparam classBackgroundColor STORAGE_COLOR + +package LeavesBookStorage as LeavesBookStoragePackage { + +Class "<>\nLeavesBookStorage" as LeavesBookStorage + +package "Json LeavesBook Storage" #F4F6F6{ +Class JsonLeavesBookStorage +Class JsonSerializableLeavesBook +Class JsonAdaptedLeave +} + +package "Csv LeavesBook Storage" #F4F6F6{ +Class CsvLeavesBookStorage +Class CsvSerializableLeavesBook +Class CsvAdaptedLeave +} + +Class SerializableLeavesBook +Class AdaptedLeave +} +Class HiddenOutside #FFFFFF +HiddenOutside ..> LeavesBookStorage + +JsonLeavesBookStorage .up.|> LeavesBookStorage +JsonLeavesBookStorage ..> JsonSerializableLeavesBook +JsonSerializableLeavesBook --> "*" JsonAdaptedLeave + +CsvLeavesBookStorage .up.|> LeavesBookStorage +CsvLeavesBookStorage ..> CsvSerializableLeavesBook +CsvSerializableLeavesBook --> "*" CsvAdaptedLeave + +SerializableLeavesBook <|-- JsonSerializableLeavesBook +SerializableLeavesBook <|-- CsvSerializableLeavesBook + +AdaptedLeave <|-- JsonAdaptedLeave +AdaptedLeave <|-- CsvAdaptedLeave +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..c6b0065da73 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -6,9 +6,11 @@ skinparam classBackgroundColor MODEL_COLOR Package Model as ModelPackage <>{ Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook +Class "<>\nReadOnlyLeavesBook" as ReadOnlyLeavesBook Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs Class "<>\nModel" as Model Class AddressBook +Class LeavesBook Class ModelManager Class UserPrefs @@ -20,6 +22,15 @@ Class Name Class Phone Class Tag +Class UniqueLeavesList +Class Leave +Class Title +Class Description +Class Date +Class Status +Class "<>\nComparablePerson" as ComparablePerson + + Class I #FFFFFF } @@ -27,22 +38,35 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook +LeavesBook .up.|> ReadOnlyLeavesBook ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs Model .left.> ReadOnlyAddressBook +Model .right.> ReadOnlyLeavesBook +Model .right.> ReadOnlyUserPrefs ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs +ModelManager -right-> "1" LeavesBook UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList +LeavesBook *--> "1" UniqueLeavesList UniquePersonList --> "~* all" Person +UniqueLeavesList --> "~* all" Leave Person *--> Name Person *--> Phone Person *--> Email Person *--> Address Person *--> "*" Tag +Leave *--> ComparablePerson +Leave *--> Title +Leave *--> Description +Leave *--> "2" Date +Leave *--> Status + +Person .|> ComparablePerson + Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -50,5 +74,11 @@ Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +ComparablePerson -[hidden]right->Title +Title -[hidden]right-> Description +Description -[hidden]right-> Date +Date -[hidden]right-> Status + ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Leave @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..56434da3a2d 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -14,13 +14,10 @@ Class JsonUserPrefsStorage Class "<>\nStorage" as Storage Class StorageManager -package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage -Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag -} + + +Class "<>\nLeavesBookStorage" as LeavesBookStorage } @@ -28,16 +25,14 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Storage StorageManager .up.|> Storage -StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage +StorageManager -left-> "1" AddressBookStorage +StorageManager -right-> "1" LeavesBookStorage +StorageManager -down-> "1" UserPrefsStorage -Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage -down-|> AddressBookStorage +Storage -down-|> LeavesBookStorage +Storage -down-|> UserPrefsStorage JsonUserPrefsStorage .up.|> UserPrefsStorage -JsonAddressBookStorage .up.|> AddressBookStorage -JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..70814c7bd91 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -13,6 +13,8 @@ Class HelpWindow Class ResultDisplay Class PersonListPanel Class PersonCard +Class LeaveListPanel +Class LeaveCard Class StatusBarFooter Class CommandBox } @@ -33,10 +35,12 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" LeaveListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +LeaveListPanel -down-> "*" LeaveCard MainWindow -left-|> UiPart @@ -44,13 +48,17 @@ ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart +LeaveListPanel --|> UiPart +LeaveCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +LeaveCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic +LeaveListPanel -[hidden]left- PersonListPanel PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay 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-annotated.png b/docs/images/Ui-annotated.png new file mode 100644 index 00000000000..14ffb8ec018 Binary files /dev/null and b/docs/images/Ui-annotated.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..842b6006f08 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/after-delete.png b/docs/images/after-delete.png new file mode 100644 index 00000000000..a500d07f9e5 Binary files /dev/null and b/docs/images/after-delete.png differ diff --git a/docs/images/before-delete.png b/docs/images/before-delete.png new file mode 100644 index 00000000000..c6b30edc127 Binary files /dev/null and b/docs/images/before-delete.png differ diff --git a/docs/images/delete-leaveUI.png b/docs/images/delete-leaveUI.png new file mode 100644 index 00000000000..4a9e43b87c2 Binary files /dev/null and b/docs/images/delete-leaveUI.png differ diff --git a/docs/images/find-leaveUI.png b/docs/images/find-leaveUI.png new file mode 100644 index 00000000000..031bbad996d Binary files /dev/null and b/docs/images/find-leaveUI.png differ diff --git a/docs/images/find-some-tagUI.png b/docs/images/find-some-tagUI.png new file mode 100644 index 00000000000..f10cb295e7b Binary files /dev/null and b/docs/images/find-some-tagUI.png differ diff --git a/docs/images/help-menu.png b/docs/images/help-menu.png new file mode 100644 index 00000000000..51f00ed5432 Binary files /dev/null and b/docs/images/help-menu.png differ diff --git a/docs/images/import-employee-excel.png b/docs/images/import-employee-excel.png new file mode 100644 index 00000000000..aba7a682ab4 Binary files /dev/null and b/docs/images/import-employee-excel.png differ diff --git a/docs/images/import-employee-notepad.png b/docs/images/import-employee-notepad.png new file mode 100644 index 00000000000..21f34115b81 Binary files /dev/null and b/docs/images/import-employee-notepad.png differ diff --git a/docs/images/import-leave-excel.png b/docs/images/import-leave-excel.png new file mode 100644 index 00000000000..21c4d1e79af Binary files /dev/null and b/docs/images/import-leave-excel.png differ diff --git a/docs/images/import-leave-notepad.png b/docs/images/import-leave-notepad.png new file mode 100644 index 00000000000..32bf505d380 Binary files /dev/null and b/docs/images/import-leave-notepad.png differ diff --git a/docs/images/infibeyond.png b/docs/images/infibeyond.png new file mode 100644 index 00000000000..e8b14c3ed63 Binary files /dev/null and b/docs/images/infibeyond.png differ diff --git a/docs/images/ivyy-poison.png b/docs/images/ivyy-poison.png new file mode 100644 index 00000000000..087a70f4e1b Binary files /dev/null and b/docs/images/ivyy-poison.png differ diff --git a/docs/images/jean-cq.png b/docs/images/jean-cq.png new file mode 100644 index 00000000000..5f513d25d86 Binary files /dev/null and b/docs/images/jean-cq.png differ diff --git a/docs/images/ong-wei-hong.png b/docs/images/ong-wei-hong.png new file mode 100644 index 00000000000..626cf75af83 Binary files /dev/null and b/docs/images/ong-wei-hong.png differ diff --git a/docs/images/ryanozx.png b/docs/images/ryanozx.png new file mode 100755 index 00000000000..ca9c03174c2 Binary files /dev/null and b/docs/images/ryanozx.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/index.md b/docs/index.md index 7601dbaad0d..ed99ebc82b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,12 @@ --- -layout: page -title: AddressBook Level-3 + layout: default.md + title: "" --- -[![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) +# HRMate +[![CI Status](https://github.com/AY2324S1-CS2103T-W11-1/tp/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/AY2324S1-CS2103T-W11-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103T-W11-1/tp/graph/badge.svg?token=CSDML30OIC)](https://codecov.io/gh/AY2324S1-CS2103T-W11-1/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). 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..a1741d58bb7 --- /dev/null +++ b/docs/site.json @@ -0,0 +1,31 @@ +{ + "baseUrl": "/tp", + "titlePrefix": "", + "titleSuffix": "HRMate", + "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.", + "repo": "https://github.com/AY2324S1-CS2103T-W11-1/tp.git", + "branch": "gh-pages" + }, + "timeZone": "Asia/Singapore" +} diff --git a/docs/stylesheets/main.css b/docs/stylesheets/main.css new file mode 100644 index 00000000000..160fefb0515 --- /dev/null +++ b/docs/stylesheets/main.css @@ -0,0 +1,152 @@ +mark { + background-color: #ff0; + border-radius: 5px; + padding-top: 0; + padding-bottom: 0; +} + +.indented { + padding-left: 20px; +} + +li { + padding: 0.5rem 0; +} + +.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; +} + +.site-nav-list > li { + padding: 0rem; +} + +.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/infibeyond.md b/docs/team/infibeyond.md new file mode 100644 index 00000000000..567efd5443b --- /dev/null +++ b/docs/team/infibeyond.md @@ -0,0 +1,42 @@ + + layout: default.md + title: "Qin Nanxin's Portfolio Page" + + +### Project: HRMate + +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is written in Java with about 10 kLoC. + +Given below are my contributions to the project. + +* **Documentation** + * User Guide: + * Amended introduction and quick start sections [#190](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/190) [#198](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/198) + * Add and update the section Important things to note. [#181](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/181) [#198](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/198) + * Add documentation for the features `list`, `view-tag`, `approve-leave`, `reject-leave`, and `edit-leave`. [#181](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/181) + * Add warnings and reminders about the properties for various parameters. [#189](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/189) + * Updated or removed error messages as the application is changed. [#198](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/198) + * Proofread the user guide and amend spelling and grammatical errors. [#198](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/198) + * Double-check that the user guide's described behaviour is consistent with the application's actual results. [#198](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/198) + * Developer Guide: + * Add implementation and user stories for the commands `view-tag` [#80](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/80) + +* **Feature Development** + * Developed the `view-tag` [#57](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/57), `approve-leave` [#83](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/83), `reject-leave` [#85](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/85), `find-leave` [#96](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/96), and `find-all-leave` [#98](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/98) commands. + * Wrote unit tests for the above commands (in the same PRs). + * Updated the UI to include the Employee's name in the leave list. [#93](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/93) + * Updated Model, Model Manager and Leave classes components to integrate with newly implemented features. [#83](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/83)[#85](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/85)[#96](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/96)[#98](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/98) + * Fix bugs: + * `clear` command clears both the employee and leave application lists instead of only the employee list (approved but not merged); [#168](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/168) + * The `find-all-tag` command now outputs the correct results as described in the user guide. [#170](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/170) + +* **Community** + * Reviewed PRs: [Reviewed PRs](https://github.com/AY2324S1-CS2103T-W11-1/tp/pulls?q=is%3Apr+reviewed-by%3Ainfibeyond) + * Raised 14 bugs during Practical Exam Dry Run. + +* **Project Management** + * Update and document non-feature-specific sections in the user guide. (Introduction, Quick Start, Important things to note, etc.) + * Create the application icon. (used in CS2101) + * Helped summarise and assess bug reports to determine further actions for milestone v1.4. + +* **Code contributions:** [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=infibeyond&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) diff --git a/docs/team/ivyy-poison.md b/docs/team/ivyy-poison.md new file mode 100644 index 00000000000..7140f941cfc --- /dev/null +++ b/docs/team/ivyy-poison.md @@ -0,0 +1,59 @@ +--- + layout: default.md + title: "Ivan's Project Portfolio Page" +--- + +### Project: HRMate + +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based +contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is +written in Java. + +Given below are my contributions to the project. + +* **Project Management** + * Assessed and reproduced bug reports to provide detailed feedback for further action + * Set up and allocated issues to team members + * Iterative and incremental development of HRMate + * Used forking workflow to contribute to the project + +* **New feature**: Added the `delete-tag` command + * What it does: Allows HR managers to delete a tag (or a set of tags) from an entry in the address book + * Justification: This feature improves the user experience by allowing users to delete tags from entries + without having to use the edit command, which is more cumbersome and less intuitive. + * Pull Request: [[PR#52]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/52) + +* **New feature**: Create the `Leave`, `LeavesBook` and `LeavesBookStorage` classes + * What it does: Allows HR managers to keep track of employees' leave applications, and perform operations such + as adding, deleting, approving and rejecting leave applications. + * Justification: This feature, while not directly functional or command-related, serves as the backbone of a key + feature of HRMate, which is the ability to keep track of employees' leave applications. This feature is also + essential for the implementation of the various Leaves commands. + * Pull Request: [[PR#62]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/62) [[PR#64]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/64) + +* **Code coverage** + * Created unit tests various classes to improve code coverage [[PR#52]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/52) [[PR#62]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/62) [[PR#64]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/64) [[PR#86]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/86) + * Improved code coverage of following classes: `PersonEntry`, `UserPref`, `Tag`, `AddressBook` [[PR#159]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/159) + +* **Documentation** + * User Guide: + * Add documentation for the following commands in User Guide: `view-tag`, `add-tag` and `delete-tag` [[PR#41]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/41) + * Add documentation for the following commands in User Guide: `find-leave-range`, `find-leave-status`, `find-all-leave` + `find-leave` and `delete-leave` [[PR#183]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/183) + * Reorder User Guide by rearranging commands into feature categories to allow for more intuitive navigation [[PR#194]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/194) + * Add About section for HRMate to explain key terms and key features of application [[PR#195]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/195) + * Add edits such as removal of excessive anchor links, and rewording of certain sections in User Guide, among others [[PR#194]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/194) [[PR#195]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/195) [[PR#199]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/199) [[PR#204]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/204) + * Developer Guide: + * Contributed to the planning and drafting of planned enhancements. + +* **Community** + * Notable reviews include: + * Review implementation of import / export feature [[PR#53]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/53) [[PR#103]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/103) + * Review implementation of CSV Parser [[PR#58]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/58) + * Review implementation of `find-leave-status` and `find-leave-range` commands [[PR#90]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/90) [[PR#91]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/91) + +* **Miscellaneous** + * Fixed bugs reported during Practical Exam Dry Run [[PR#158]](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/158) + +* Code contributed: [RepoSense Link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ivyy-poison&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) + diff --git a/docs/team/jean-cq.md b/docs/team/jean-cq.md new file mode 100644 index 00000000000..a8567d5cf82 --- /dev/null +++ b/docs/team/jean-cq.md @@ -0,0 +1,33 @@ +--- + layout: default.md + title: "Chen Qun's Portfolio Page" +--- + +### Project: HRMate + +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based +contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is +written in Java with about 15 kLoC. + +Given below are my contributions to the project. + +* **Code Contribution**: [link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=jean-cq&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) +* **Documentation**: + * User Guide: + * Added documentation for the features `find`,`find-all-tag`, `find-one-tag`, `delete` and `add-leave` [#173](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/173) + * Developer Guide [#186](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/186): + * Added use cases of `find-all-tag` and `find-some-tag` [#40](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/40) + * Added use cases of `add-leave` + * Added structure of `Person` class and `Leave` class + * Added implementation of `find-all-tag`, `find-one-tag` and `add-leave` with a sequence diagram of `find-all-tag` and activity diagram of `add-leave`. +* **Enhancements implemented**: + * Developed the `find-all-tag`, `find-some-tag` [#56](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/56) and `add-leave` [#68](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/68) + * Wrote unit tests for the above commands [#160](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/160) [#176](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/176) + * Modified UI for both employee list and leave list to display and made minor changes for better UI [#68](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/68) + * Bug Fixed [#160](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/160): + * Better error message for description field of add-leave command + * Recognise illegal arguments as described as the User Guide for add-leave command +* **Community**: + * Reviewed 15 PRs: [reviewed PRs](https://github.com/AY2324S1-CS2103T-W11-1/tp/pulls?q=is%3Apr+reviewed-by%3A%40me+is%3Aclosed) + * Regular meetings and discussion + * Reported bugs for other teams products during the class and during PE-D diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..17bb35b7446 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -1,11 +1,13 @@ --- -layout: page -title: John Doe's Project Portfolio Page + layout: default.md + title: "John Doe's Project Portfolio Page" --- -### Project: AddressBook Level 3 +### Project: HRMate -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. +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based +contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is +written in Java with about 10 kLoC. Given below are my contributions to the project. diff --git a/docs/team/ong-wei-hong.md b/docs/team/ong-wei-hong.md new file mode 100644 index 00000000000..d41153797de --- /dev/null +++ b/docs/team/ong-wei-hong.md @@ -0,0 +1,35 @@ +--- + layout: default.md + title: "Wei Hong's Portfolio Page" +--- + +### Project: HRMate + +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based +contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is +written in Java with about 10 kLoC. + +Given below are my contributions to the project. + +* **Code contributions:** [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ong-wei-hong&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) +* **Documentation**: + * User Guide: + * Amended intro and quick start sections [#175](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/175) + * Add documentation for the features `help`, `list`, `add`, `edit`, `add-tag`, `delete-tag` [#15](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/15) [#164](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/164) + * Developer Guide: + * Add implementation and user stories for the commands `add-tag`, `remove-tag` [#27](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/27) + * Add Effort section [#182](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/182) + * Add 4 proposed enhancements [#203](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/182) +* **Feature Development**: + * Developed `add-tag` [#49](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/49), `delete-leave` [#77](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/77), `edit-leave`[#155](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/155). + * Wrote unit tests for the above commands (in same PR). + * Fix bugs + * leave display employee object instead of employee name [#97](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/97) + * add-tag command resetting filters after execution [#156](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/156) + * add-tag command throwing wrong errors [#167](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/167) +* **Project Management**: + * Distributed tasks +* **Community**: + * Reviewed 22 PRs: [reviewed PRs](https://github.com/AY2324S1-CS2103T-W11-1/tp/pulls?q=is%3Apr+reviewed-by%3Aong-wei-hong+is%3Aclosed+) + * Reported bugs for other teams during PE-D. + diff --git a/docs/team/ryanozx.md b/docs/team/ryanozx.md new file mode 100644 index 00000000000..fb8cdfa3840 --- /dev/null +++ b/docs/team/ryanozx.md @@ -0,0 +1,49 @@ + + layout: default.md + title: "Ryan's Project Portfolio Page" + + +### Project: HRMate + +HRMate is a desktop address book application that aims to streamline HR processes, by offering an intuitive, CLI-based +contact management system with specialised functionalities for HR tasks. It has a GUI created with JavaFX, and is +written in Java. + +Given below are my contributions to the project. + +* **Code contributions:** [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ryanozx&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) +* **Community** + * Reviewed PRs: [GitHub link](https://github.com/AY2324S1-CS2103T-W11-1/tp/pulls?q=is%3Apr+reviewed-by%3Aryanozx) + * Reported bugs for other teams during PE-D +* **Documentation** + * User Guide: + * Add documentation for the features `clear`, `exit`, `import`, `export`, `import-leave`, `export-leave` + * Developer Guide: + * Add implementation and user stories for the features `import`, `export`, and `find-leave-range` + * Update UML diagrams and descriptions in Design section + * Managed conversion of documentation into MarkBind format +* **Enhancements to existing features** + * Add method in FileUtil to support writing a stream of lines into a file ([#58](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/58)) +* **New Feature**: Import and export commands ([#53](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/53), [#58](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/58), [#101](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/101)) + * Provides ability for users to import and export HRMate data in CSV format for greater portability + * This feature provides greater interchangeability with other spreadsheet applications like Excel, and allows users to save + filtered results that can be either stored elsewhere or sent to others + * Highlights: + * Created abstract classes for AdaptedPerson, AdaptedLeave, SerializableAddressBook and SerializableLeavesBook + * Created CsvUtil to handle the reading and writing of CSV files + * Created CsvFile to support the ability to query CSV files for values in a specified column by column name +* * **New Feature**: Find leaves by time and status ([#90](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/90), [#91](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/91)) + * Provides ability for users to filter leaves by either the time period or the leave status + * This feature allows users to search for leaves more intelligently e.g. what leaves are occurring at a certain period, which leaves + have not been approved yet + * Highlights: + * Created Range class to guarantee that the start dates will not occur after the end dates, and provided the option + of whether to enforce non-null start and end dates +* **Bug Fixes**: + * Fix bug where changing an employee's name does not update the name in the leave application ([#178](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/178)) +* Refactor various parts of the codebase to improve code readability and promote code reuse ([#165](https://github.com/AY2324S1-CS2103T-W11-1/tp/pull/165) and others) +* **Project Management** + * Set up Issues tracker in the repo + * Set up milestones v1.1 - v1.4 and assigned issues to rest of the team + * Managed release of v1.3.2 and v1.4 + * Helped triage bug reports to determine further action for milestone v1.4 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..9e3916198b4 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/address/Main.java @@ -20,6 +20,7 @@ * * By having a separate main class (Main) that doesn't extend Application * to be the entry point of the application, we avoid this issue. + * */ public class Main { private static Logger logger = LogsCenter.getLogger(Main.class); diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..c8a3e36cd30 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -16,15 +16,19 @@ import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonLeavesBookStorage; import seedu.address.storage.JsonUserPrefsStorage; +import seedu.address.storage.LeavesBookStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; @@ -36,7 +40,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, 2, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -58,7 +62,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); + LeavesBookStorage leavesBookStorage = new JsonLeavesBookStorage(userPrefs.getLeavesBookFilePath()); + + storage = new StorageManager(addressBookStorage, userPrefsStorage, leavesBookStorage); model = initModelManager(storage, userPrefs); @@ -73,24 +79,49 @@ public void init() throws Exception { * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - logger.info("Using data file : " + storage.getAddressBookFilePath()); + logger.info("Using addressbook data file : " + storage.getAddressBookFilePath()); Optional addressBookOptional; - ReadOnlyAddressBook initialData; + ReadOnlyAddressBook initialAddressData; + boolean addressBookIsPresent = false; + try { addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { + if (addressBookOptional.isEmpty()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); + } else { + addressBookIsPresent = true; } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialAddressData = 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(); + initialAddressData = new AddressBook(); } - return new ModelManager(initialData, userPrefs); + // Read from LeavesBook + logger.info("Using leavesbook data file : " + storage.getLeavesBookFilePath()); + + Optional leavesBookOptional; + ReadOnlyLeavesBook initialLeavesData; + if (addressBookIsPresent) { + try { + leavesBookOptional = storage.readLeavesBook((AddressBook) initialAddressData); + if (leavesBookOptional.isEmpty()) { + logger.info("Creating a new data file " + storage.getLeavesBookFilePath() + + " populated with an empty LeavesBook."); + } + initialLeavesData = leavesBookOptional.orElseGet(() -> new LeavesBook()); + } catch (DataLoadingException e) { + logger.warning("Data file at " + storage.getLeavesBookFilePath() + " could not be loaded." + + " Will be starting with an empty LeavesBook."); + initialLeavesData = new LeavesBook(); + } + } else { + initialLeavesData = new LeavesBook(); + } + return new ModelManager(initialAddressData, initialLeavesData, userPrefs); } private void initLogging(Config config) { diff --git a/src/main/java/seedu/address/commons/controllers/FileDialogHandler.java b/src/main/java/seedu/address/commons/controllers/FileDialogHandler.java new file mode 100644 index 00000000000..dbabe42d56c --- /dev/null +++ b/src/main/java/seedu/address/commons/controllers/FileDialogHandler.java @@ -0,0 +1,13 @@ +package seedu.address.commons.controllers; + +import java.io.File; +import java.util.Optional; + +import javafx.stage.FileChooser.ExtensionFilter; + +/** + * Interface for a handler that handles file dialogs (e.g. open file dialog) + */ +public interface FileDialogHandler { + Optional openFile(String title, ExtensionFilter... extensions); +} diff --git a/src/main/java/seedu/address/commons/controllers/FileDialogHandlerImpl.java b/src/main/java/seedu/address/commons/controllers/FileDialogHandlerImpl.java new file mode 100644 index 00000000000..09759d59260 --- /dev/null +++ b/src/main/java/seedu/address/commons/controllers/FileDialogHandlerImpl.java @@ -0,0 +1,36 @@ +package seedu.address.commons.controllers; + +import java.io.File; +import java.util.Optional; + +import javafx.stage.FileChooser; +import javafx.stage.FileChooser.ExtensionFilter; +import seedu.address.commons.util.CsvUtil; + +/** + * Handles file dialogs + */ +public class FileDialogHandlerImpl implements FileDialogHandler { + + private static final String EXTENSION_FORMAT = "*%s"; + public static final ExtensionFilter CSV_EXTENSION = new ExtensionFilter("CSV Files", + String.format(EXTENSION_FORMAT, CsvUtil.EXTENSION)); + + /** + * Creates a file dialog for the user to select a file to open + * @param title Title of dialog + * @param extensions Extension filters that contain the permitted file extensions + * @return An Optional object containing the file if selected, else an empty Optional object + */ + @Override + public Optional openFile(String title, ExtensionFilter ...extensions) { + FileChooser fc = new FileChooser(); + fc.setTitle(title); + fc.getExtensionFilters().addAll(extensions); + File selectedFile = fc.showOpenDialog(null); + if (selectedFile != null) { + return Optional.of(selectedFile); + } + return Optional.empty(); + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/CsvMismatchedColumnException.java b/src/main/java/seedu/address/commons/exceptions/CsvMismatchedColumnException.java new file mode 100644 index 00000000000..5dcfd7223d1 --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/CsvMismatchedColumnException.java @@ -0,0 +1,12 @@ +package seedu.address.commons.exceptions; + +/** + * Represents an error where the number of columns of a row not matching the number of columns in the header + */ +public class CsvMismatchedColumnException extends RuntimeException { + private static final String ERROR_MESSAGE = "Incorrect number of columns when parsing CSV row," + + " expected %d columns, encountered %d columns."; + public CsvMismatchedColumnException(int expectedColumnCount, int actualColumnCount) { + super(String.format(ERROR_MESSAGE, expectedColumnCount, actualColumnCount)); + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/CsvMissingFieldException.java b/src/main/java/seedu/address/commons/exceptions/CsvMissingFieldException.java new file mode 100644 index 00000000000..5f6ded3333c --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/CsvMissingFieldException.java @@ -0,0 +1,12 @@ +package seedu.address.commons.exceptions; + +/** + * Represents an error where a requested column is not available in the CSV file + */ +public class CsvMissingFieldException extends Exception { + private static final String MESSAGE_FORMAT = "Field %s is not found in the CSV file"; + + public CsvMissingFieldException(String field) { + super(String.format(MESSAGE_FORMAT, field)); + } +} diff --git a/src/main/java/seedu/address/commons/util/CsvFile.java b/src/main/java/seedu/address/commons/util/CsvFile.java new file mode 100644 index 00000000000..a426af2c154 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/CsvFile.java @@ -0,0 +1,208 @@ +package seedu.address.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.commons.exceptions.CsvMismatchedColumnException; +import seedu.address.commons.exceptions.CsvMissingFieldException; + +/** + * Representation of the contents of a CSV file as an object. + */ +public class CsvFile { + public static final String DELIMITER_PREFIX = "sep="; + public static final String DELIMITER_SPECIFIER = DELIMITER_PREFIX + "%s"; + private final int numColumns; + + private final String delimiter; + private final HashMap columnIndices; + private final String header; + private final List rows; + + /** + * Represents a row of values in the CSV file. Encapsulating the row values + * within this object allows one to extract the value in a column with just + * the column's name, without having to know the column index. + */ + public class CsvRow implements GetValuer { + private final List vals; + + /** + * Constructs a CsvRow object. If the number of values supplied in the row is less + * than the number of columns in the file, the values will be regarded as the values for + * the first n columns, where n is the number of values in the row. The remaining columns + * will be treated as empty values (empty string). + * @param values String containing the values of the row delimited by the CSV delimiter + * @throws CsvMismatchedColumnException Thrown if the number of values in the row is greater + * than the number of columns in the file + */ + public CsvRow(String values) throws CsvMismatchedColumnException { + String[] row = values.split(delimiter); + boolean hasTooManyColumns = row.length > numColumns; + if (hasTooManyColumns) { + throw new CsvMismatchedColumnException(numColumns, row.length); + } + + vals = new ArrayList<>(Arrays.asList(row)); + padRow(); + } + + /** + * Constructs a CsvRow object. If the number of values supplied in the row is less + * than the number of columns in the file, the values will be regarded as the values for + * the first n columns, where n is the number of values in the row. The remaining columns + * will be treated as empty values (empty string). + * @param object Object that implements CsvParsable, which returns a list of string values + * when CsvParsable::getCsvValues() is called + * @throws CsvMismatchedColumnException if the number of values in the row is greater + * than the number of columns in the file + */ + public CsvRow(CsvParsable object) throws CsvMismatchedColumnException { + requireNonNull(object); + List values = object.getCsvValues(); + boolean hasTooManyColumns = values.size() > numColumns; + if (hasTooManyColumns) { + throw new CsvMismatchedColumnException(numColumns, values.size()); + } + + vals = values.stream().map(Object::toString).collect(Collectors.toList()); + padRow(); + } + + /** + * Pads the end of the row with additional values (empty strings), in the event the number + * of values in the row is less than the number of columns in the file. This + * ensures that getValue() will always return a value, whether the value of the field + * or an empty string. It is up to the user to handle the empty string case. + */ + private void padRow() { + boolean isDonePadding = vals.size() == numColumns; + while (!isDonePadding) { + vals.add(""); + isDonePadding = vals.size() == numColumns; + } + } + + /** + * Returns the value in the row for a specified field. + * @param field Name of field whose value should be returned + * @return String representation of the value + * @throws CsvMissingFieldException if the field does not exist in the file (no such + * column exists) + */ + public String getValue(String field) throws CsvMissingFieldException { + boolean isColumnPresent = columnIndices.containsKey(field); + if (!isColumnPresent) { + throw new CsvMissingFieldException(field); + } + + int fieldIdx = columnIndices.get(field); + return vals.get(fieldIdx); + } + + /** + * Returns the string representation of the row. This method is used to generate the + * string representation of the row to write into the CSV file, since the values are + * delimited by the delimiter. + * @return String representation of row + */ + public String printRow() { + return String.join(delimiter, vals); + } + } + + /** + * Constructs a CsvFile object. This constructor is called when constructing a CsvFile from a CSV file. + * @param headers Headers in string representation delimited by specified delimiter. + * @param delimiter The character used to delimit values in the CSV file. This delimiter should be used in both + * header and rows. + */ + public CsvFile(String headers, String delimiter) { + requireNonNull(headers); + requireNonNull(delimiter); + + this.delimiter = delimiter; + rows = new ArrayList<>(); + header = headers.trim(); + + String[] columnHeaders = headers.split(delimiter); + numColumns = columnHeaders.length; + + columnIndices = new HashMap<>(); + for (int i = 0; i < columnHeaders.length; ++i) { + columnIndices.put(columnHeaders[i], i); + } + } + + /** + * Constructs a CsvFile object. This constructor is called when constructing a CsvFile from a list of objects + * to be written into a CSV file. + * @param headers List of header names + * @param delimiter The character used to delimit values in the CSV file + */ + public CsvFile(List headers, String delimiter) { + requireNonNull(headers); + requireNonNull(delimiter); + + this.delimiter = delimiter; + rows = new ArrayList<>(); + numColumns = headers.size(); + header = String.join(delimiter, headers); + + columnIndices = new HashMap<>(); + for (int i = 0; i < headers.size(); ++i) { + columnIndices.put(headers.get(i), i); + } + } + + /** + * Adds a row of values into the CSV file. This is useful if adding a row that is read from the CSV file. + * @param row String representation of row to be added to the file, delimited by the delimiter + */ + public void addRow(String row) { + CsvRow newRow = new CsvRow(row); + rows.add(newRow); + } + + /** + * Adds a row of values generated by an object into the CSV file. + * This is useful if adding a row from a serialised object. + * @param object Object that implements CsvParsable, which is required + * to generated a row of string values for the CSV row + */ + public void addRow(CsvParsable object) { + CsvRow newRow = new CsvRow(object); + rows.add(newRow); + } + + /** + * Returns a stream of strings representing the header and rows of the CSV file. The first string in the + * stream contains the header, while the rest of the stream consists of each row of values. + * @return Stream of strings containing the headers and values + */ + public Stream getFileStream() { + // Since the semicolon/pipe is not a common delimiter, adding an extra line at the start of the CSV file + // to indicate the delimiter used will improve interchangeability between applications without having to + // perform further configurations + String delimiterIndicator = String.format(DELIMITER_SPECIFIER, delimiter); + Stream headerStream = Stream.of(delimiterIndicator, header); + Stream valuesStream = rows.stream().map(CsvRow::printRow); + + return Stream.concat(headerStream, valuesStream); + } + + /** + * Returns a stream of rows + * @return Steam of rows + */ + public Stream getRows() { + return rows.stream(); + } +} + diff --git a/src/main/java/seedu/address/commons/util/CsvParsable.java b/src/main/java/seedu/address/commons/util/CsvParsable.java new file mode 100644 index 00000000000..d6f40a4d1c8 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/CsvParsable.java @@ -0,0 +1,14 @@ +package seedu.address.commons.util; + +import java.util.List; + +/** + * Interface for objects that can be serialised into CSV rows + */ +public interface CsvParsable { + /** + * Returns a list of string values that can be serialized into a CSV row + * @return List of string values + */ + List getCsvValues(); +} diff --git a/src/main/java/seedu/address/commons/util/CsvUtil.java b/src/main/java/seedu/address/commons/util/CsvUtil.java new file mode 100644 index 00000000000..62b95ef5d46 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/CsvUtil.java @@ -0,0 +1,96 @@ +package seedu.address.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +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.CsvMismatchedColumnException; +import seedu.address.commons.exceptions.DataLoadingException; + +/** + * Converts a CSV file to a CsvFile object and vice-versa + */ +public class CsvUtil { + + public static final String DELIMITER = ";"; + public static final String EXTENSION = ".csv"; + private static final String NOT_CSV_FILETYPE_ERROR_MESSAGE = "File %s is not a CSV file"; + private static final Logger logger = LogsCenter.getLogger(JsonUtil.class); + + /** + * Takes a CSV filepath and reads its contents into a CsvFile object. Rows that do not match the header row will + * be skipped over. + * @param filePath CSV filepath to read from + * @return CsvFile containing the contents of the CSV file + * @throws DataLoadingException if file cannot be read + */ + public static Optional readCsvFile(Path filePath) throws DataLoadingException { + requireNonNull(filePath); + + boolean isCsvFileType = filePath.toString().endsWith(EXTENSION); + if (!isCsvFileType) { + throw new DataLoadingException(new Exception( + String.format(NOT_CSV_FILETYPE_ERROR_MESSAGE, filePath))); + } + if (!Files.exists(filePath)) { + return Optional.empty(); + } + + logger.info("CSV file " + filePath + " found."); + return readRows(filePath); + } + + /** + * Reads the CSV file at the provided file path into a CsvFile, which is then returned + * @param filePath Path containing the CSV file + * @return Optional containing the CsvFile if successfully read + * @throws DataLoadingException if file cannot be read + */ + private static Optional readRows(Path filePath) throws DataLoadingException { + CsvFile readFile; + + try (BufferedReader br = new BufferedReader(new FileReader(filePath.toFile()))) { + String firstLine = br.readLine(); + char readFileDelimiter; + if (firstLine.startsWith(CsvFile.DELIMITER_PREFIX)) { + readFileDelimiter = firstLine.charAt(CsvFile.DELIMITER_PREFIX.length()); + String header = br.readLine(); + readFile = new CsvFile(header, String.valueOf(readFileDelimiter)); + } else { + readFile = new CsvFile(firstLine, DELIMITER); + } + + String row; + while ((row = br.readLine()) != null) { + try { + readFile.addRow(row); + } catch (CsvMismatchedColumnException ignored) { + // if the row cannot be added, just move on to the next row + } + } + } catch (IOException e) { + throw new DataLoadingException(e); + } + return Optional.of(readFile); + } + + /** + * Saves the CsvFile to the specified file path. + * @param file CsvFile object containing headers and values to be saved + * @param filePath Path of file to be written to. + * @throws IOException if file cannot be opened or created + */ + public static void saveCsvFile(CsvFile file, Path filePath) throws IOException { + requireNonNull(file); + requireNonNull(filePath); + + FileUtil.writeToFile(filePath, file.getFileStream()); + } +} diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index b1e2767cdd9..0ef32f553d6 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -1,10 +1,13 @@ package seedu.address.commons.util; +import java.io.BufferedWriter; +import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.stream.Stream; /** * Writes and reads files @@ -18,7 +21,7 @@ public static boolean isFileExists(Path file) { } /** - * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, + * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get}, * otherwise returns false. * @param path A string representing the file path. Cannot be null. */ @@ -80,4 +83,26 @@ public static void writeToFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(CHARSET)); } + /** + * Writes given stream of string to a file. + * Will create the file if it does not exist yet. + * @param file Path of file to write to + * @param content Stream of strings to write + * @throws IOException if the file cannot be opened or created + */ + public static void writeToFile(Path file, Stream content) throws IOException { + String filename = file.toString(); + try (FileWriter fileWriter = new FileWriter(filename)) { + BufferedWriter writer = new BufferedWriter(fileWriter); + content.forEach(line -> { + try { + writer.write(line); + writer.newLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + writer.close(); + } + } } diff --git a/src/main/java/seedu/address/commons/util/GetValuer.java b/src/main/java/seedu/address/commons/util/GetValuer.java new file mode 100644 index 00000000000..a51ce6ea940 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/GetValuer.java @@ -0,0 +1,16 @@ +package seedu.address.commons.util; + +import seedu.address.commons.exceptions.CsvMissingFieldException; + +/** + * Interface for an object that returns a string value when provided with a field to query for + */ +public interface GetValuer { + /** + * Returns a string value associated with the queried field + * @param field Name of field to query for + * @return String value associated with queried field + * @throws CsvMissingFieldException if no value is associated with the queried field + */ + String getValue(String field) throws CsvMissingFieldException; +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..223752b47a9 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -65,4 +65,24 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents an integer + * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}, -1, -2, -3, ..., {@code Integer.MIN_VALUE}
    + * Will return false for any other non-null string input + * e.g. empty string, "+1", " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * + * @param s string to be tested + * @return true if {@code s} represents an integer + */ + public static boolean isInteger(String s) { + requireNonNull(s); + + try { + Integer.parseInt(s); + return true; + } catch (NumberFormatException nfe) { + return false; + } + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..d29b40f503d 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,6 +8,8 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; /** @@ -33,6 +35,7 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** * Returns the user prefs' address book file path. */ @@ -47,4 +50,20 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the LeavesBook. + * + * @see seedu.address.model.Model#getLeavesBook() + */ + ReadOnlyLeavesBook getLeavesBook(); + + /** + * Returns the user prefs' leaves book file path. + */ + Path getLeavesBookFilePath(); + + /** Returns an unmodifiable view of the filtered list of leaves */ + ObservableList getFilteredLeaveList(); + } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..7b4db4b59de 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -15,6 +15,8 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -52,6 +54,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE try { storage.saveAddressBook(model.getAddressBook()); + storage.saveLeavesBook(model.getLeavesBook()); } catch (AccessDeniedException e) { throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); } catch (IOException ioe) { @@ -66,6 +69,8 @@ public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); } + + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); @@ -85,4 +90,17 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public ReadOnlyLeavesBook getLeavesBook() { + return model.getLeavesBook(); + } + @Override + public ObservableList getFilteredLeaveList() { + return model.getFilteredLeaveList(); + } + @Override + public Path getLeavesBookFilePath() { + return model.getLeavesBookFilePath(); + } } diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..2bc675b0ba6 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,6 +5,7 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; /** @@ -14,11 +15,22 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + + public static final String MESSAGE_NO_STATUS_PREFIX = "Status is PENDING by default, do not enter `s/` \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX = "The leave index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_LEAVES_LISTED_OVERVIEW = "%1$d leaves listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + public static final String EMPLOYEE_HEADER = "Employee: "; + public static final String TITLE_HEADER = "; Title: "; + public static final String START_HEADER = "; Start: "; + public static final String END_HEADER = "; End: "; + public static final String STATUS_HEADER = "; Status: "; + public static final String DESCRIPTION_HEADER = "; Description: "; + /** * Returns an error message indicating the duplicate prefixes. */ @@ -48,4 +60,22 @@ public static String format(Person person) { return builder.toString(); } + /** + * Formats the {@code leave} for display to the user. + */ + public static String format(Leave leave) { + final StringBuilder builder = new StringBuilder(); + builder.append(EMPLOYEE_HEADER) + .append(leave.getEmployee().getName()) + .append(TITLE_HEADER) + .append(leave.getTitle()) + .append(START_HEADER) + .append(leave.getStart()) + .append(END_HEADER) + .append(leave.getEnd()) + .append(STATUS_HEADER) + .append(leave.getStatus()); + return builder.toString(); + } + } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..4228a9cdcc6 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,11 +1,11 @@ package seedu.address.logic.commands; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -14,29 +14,29 @@ import seedu.address.model.person.Person; /** - * Adds a person to the address book. + * Adds an employee to the address book. */ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an employee to the address book. " + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + PREFIX_PERSON_NAME + "NAME " + + PREFIX_PERSON_PHONE + "PHONE " + + PREFIX_PERSON_EMAIL + "EMAIL " + + PREFIX_PERSON_ADDRESS + "ADDRESS " + + "[" + PREFIX_PERSON_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_PERSON_NAME + "John Doe " + + PREFIX_PERSON_PHONE + "98765432 " + + PREFIX_PERSON_EMAIL + "johnd@example.com " + + PREFIX_PERSON_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_PERSON_TAG + "full time " + + PREFIX_PERSON_TAG + "owesMoney"; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_SUCCESS = "New employee added: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This employee already exists in the address book"; private final Person toAdd; diff --git a/src/main/java/seedu/address/logic/commands/AddLeaveCommand.java b/src/main/java/seedu/address/logic/commands/AddLeaveCommand.java new file mode 100644 index 00000000000..378030ccd0f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddLeaveCommand.java @@ -0,0 +1,113 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; + +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.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Title; +import seedu.address.model.person.Person; + +/** + * Adds an employee to the address book. + */ +public class AddLeaveCommand extends Command { + + public static final String COMMAND_WORD = "add-leave"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a leave of an employee to the leave book. " + + "Parameters: " + + "INDEX " + + PREFIX_LEAVE_TITLE + "TITLE " + + PREFIX_LEAVE_DATE_START + "DATE START " + + PREFIX_LEAVE_DATE_END + "DATE END " + + "[" + PREFIX_LEAVE_DESCRIPTION + "DESCRIPTION] " + + "Example: " + COMMAND_WORD + " " + + "1 " + + PREFIX_LEAVE_TITLE + "John's Paternity Leave " + + PREFIX_LEAVE_DATE_START + "2023-10-28 " + + PREFIX_LEAVE_DATE_END + "2023-10-29 " + + PREFIX_LEAVE_DESCRIPTION + "John's Paternity Leave Description [OPTIONAL] "; + + public static final String MESSAGE_SUCCESS = "New leave is added : %1$s"; + public static final String MESSAGE_DUPLICATE_LEAVE = "This leave has already existed for the employee"; + + private Leave toAdd; + + private final Index index; + private final Title title; + private final Range dateRange; + private final Description description; + + /** + * Creates an AddLeaveCommand to add the specified {@code Leave} + */ + public AddLeaveCommand(Index index, Title title, Range dates, Description description) { + requireNonNull(index); + requireNonNull(title); + requireNonNull(dates); + requireNonNull(description); + this.index = index; + this.title = title; + this.dateRange = dates; + this.description = description; + } + + @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); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + toAdd = new Leave(personToEdit, title, dateRange, description); + + if (model.hasLeave(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_LEAVE); + } + + model.addLeave(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 AddLeaveCommand)) { + return false; + } + + AddLeaveCommand otherAddLeaveCommand = (AddLeaveCommand) other; + return index.equals(otherAddLeaveCommand.index) && title.equals(otherAddLeaveCommand.title) + && dateRange.equals(otherAddLeaveCommand.dateRange) + && description.equals(otherAddLeaveCommand.description); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("title", title) + .add("description", description) + .add("start", dateRange.getStartDate().get()) + .add("end", dateRange.getEndDate().get()) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddTagCommand.java b/src/main/java/seedu/address/logic/commands/AddTagCommand.java new file mode 100644 index 00000000000..98521d90788 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddTagCommand.java @@ -0,0 +1,95 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; + +import java.util.Collection; +import java.util.HashSet; +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.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Adds a tag to an employee in the address book + */ +public class AddTagCommand extends Command { + + public static final String COMMAND_WORD = "add-tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds tags to the employee identified " + + "by the index number used in the displayed employee list.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_PERSON_TAG + "TAG...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PERSON_TAG + "full time"; + + public static final String MESSAGE_NO_TAGS_ADDED = "At least one tag must be provided."; + public static final String MESSAGE_DUPLICATE_TAG = "This employee already has some of the tags."; + public static final String MESSAGE_ADD_TAG_SUCCESS = "Edited Employee : %1$s"; + + private final Index index; + private final Collection tagsToAdd; + + /** + * @param index of the employee in the filtered employee list to add tags to + * @param tagsToAdd tags to add to the employee + */ + public AddTagCommand(Index index, Collection tagsToAdd) { + requireNonNull(index); + requireNonNull(tagsToAdd); + + this.index = index; + this.tagsToAdd = new HashSet<>(tagsToAdd); + } + + @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); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + if (personToEdit.hasAnyTags(tagsToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + + Person editedPerson = new Person(personToEdit); + editedPerson.addTags(tagsToAdd); + model.setPerson(personToEdit, editedPerson); + + return new CommandResult(String.format(MESSAGE_ADD_TAG_SUCCESS, Messages.format(editedPerson))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddTagCommand)) { + return false; + } + + AddTagCommand otherAddTagCommand = (AddTagCommand) other; + return index.equals(otherAddTagCommand.index) && tagsToAdd.equals(otherAddTagCommand.tagsToAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("tagsToAdd", tagsToAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ApproveLeaveCommand.java b/src/main/java/seedu/address/logic/commands/ApproveLeaveCommand.java new file mode 100644 index 00000000000..16f0db2e3ae --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ApproveLeaveCommand.java @@ -0,0 +1,101 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX; + +import javafx.collections.ObservableList; +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.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.model.leave.Title; +import seedu.address.model.person.ComparablePerson; + +/** + * Approves an existing leave in the leave book. + */ +public class ApproveLeaveCommand extends Command { + public static final String COMMAND_WORD = "approve-leave"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Approves leave request identified " + + "by the index number used in the leave book. " + + "The specified leave request will be approved.\n" + + "Parameters: INDEX (must be a positive integer within in the range of the Leave List)\n) " + + "Example: " + COMMAND_WORD + " 1 "; + + public static final String MESSAGE_APPROVE_LEAVE_SUCCESS = "Approved Leave: %1$s"; + public static final String MESSAGE_DUPLICATE_LEAVE_APPROVE = "Leave previously approved: %1$s"; + + private final Index index; + + /** + * @param index of the leave in the leave book to approve + */ + public ApproveLeaveCommand(Index index) { + requireNonNull(index); + + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ObservableList leaveList = model.getFilteredLeaveList(); + + if (index.getZeroBased() >= leaveList.size()) { + throw new CommandException(MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + Leave leaveToApprove = leaveList.get(index.getZeroBased()); + + if (leaveToApprove.getStatus().getStatusType().equals(StatusType.APPROVED)) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_LEAVE_APPROVE, Messages.format(leaveToApprove))); + } + + Leave approvedLeave = createApprovedLeave(leaveToApprove); + + model.setLeave(leaveToApprove, approvedLeave); + return new CommandResult(String.format(MESSAGE_APPROVE_LEAVE_SUCCESS, Messages.format(approvedLeave))); + } + + private static Leave createApprovedLeave(Leave leaveToApprove) { + assert leaveToApprove != null; + + ComparablePerson employee = leaveToApprove.getEmployee(); + Title title = leaveToApprove.getTitle(); + Description description = leaveToApprove.getDescription(); + Range dateRange = Range.createNonNullRange(leaveToApprove.getStart(), leaveToApprove.getEnd()); + Status approvedStatus = Status.of(StatusType.APPROVED); + + return new Leave(employee, title, dateRange, description, approvedStatus); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ApproveLeaveCommand)) { + return false; + } + + ApproveLeaveCommand otherApproveLeaveCommand = (ApproveLeaveCommand) other; + return index.equals(otherApproveLeaveCommand.index); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..dc07dff086b 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.Model; /** @@ -11,13 +12,14 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "All employee contacts and leave records have been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.setLeavesBook(new LeavesBook()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..758311f356f 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -12,18 +12,18 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes an employee identified using it's displayed index from the address book. */ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" + + ": Deletes the employee identified by the index number used in the displayed employee list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Employee: %1$s"; private final Index targetIndex; diff --git a/src/main/java/seedu/address/logic/commands/DeleteLeaveCommand.java b/src/main/java/seedu/address/logic/commands/DeleteLeaveCommand.java new file mode 100644 index 00000000000..5950446c7d8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteLeaveCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import javafx.collections.ObservableList; +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.leave.Leave; + +/** + * Deletes a Leave identified using it's displayed index from the address book. + */ +public class DeleteLeaveCommand extends Command { + + public static final String COMMAND_WORD = "delete-leave"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the leave identified by the index number used in the displayed leave list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_LEAVE_SUCCESS = "Deleted Leave: %1$s"; + + private final Index targetIndex; + + /** + * @param targetIndex of the leave in the filtered leave list to delete + */ + public DeleteLeaveCommand(Index targetIndex) { + requireNonNull(targetIndex); + + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + ObservableList lastShownList = model.getFilteredLeaveList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + Leave leaveToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteLeave(leaveToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_LEAVE_SUCCESS, Messages.format(leaveToDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DeleteLeaveCommand)) { + return false; + } + + DeleteLeaveCommand otherDeleteLeaveCommand = (DeleteLeaveCommand) other; + + return targetIndex.equals(otherDeleteLeaveCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteTagCommand.java b/src/main/java/seedu/address/logic/commands/DeleteTagCommand.java new file mode 100644 index 00000000000..cd0e89a641f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteTagCommand.java @@ -0,0 +1,100 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.Collection; +import java.util.HashSet; +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.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Deletes tags from an existing employee in the address book. + */ +public class DeleteTagCommand extends Command { + public static final String COMMAND_WORD = "delete-tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete tags from the employee identified " + + "by the index number used in the displayed employee list.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_PERSON_TAG + "TAG...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PERSON_TAG + "remote"; + + public static final String MESSAGE_NO_TAGS_REMOVED = "At least one tag must be provided."; + public static final String MESSAGE_MISSING_TAGS = "Some of the tags are not found on this employee."; + public static final String MESSAGE_REMOVE_TAG_SUCCESS = "Edited Employee : %1$s"; + + private final Index index; + private final Collection tagsToRemove; + + /** + * @param index of the employee in the filtered employee list to remove tags from + * @param tags tags to remove from the employee + */ + public DeleteTagCommand(Index index, Collection tags) { + requireNonNull(index); + requireNonNull(tags); + + this.index = index; + this.tagsToRemove = new HashSet<>(tags); + } + + + @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); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + if (!personToEdit.hasAllTags(tagsToRemove)) { + throw new CommandException(MESSAGE_MISSING_TAGS); + } + + Person editedPerson = new Person(personToEdit); + editedPerson.removeTags(tagsToRemove); + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + + return new CommandResult(String.format(MESSAGE_REMOVE_TAG_SUCCESS, Messages.format(editedPerson))); + } + + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteTagCommand)) { + return false; + } + + DeleteTagCommand otherDeleteTagCommand = (DeleteTagCommand) other; + return index.equals(otherDeleteTagCommand.index) && tagsToRemove.equals(otherDeleteTagCommand.tagsToRemove); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("tagsToAdd", tagsToRemove) + .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..1f0d959f4b7 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,11 +1,11 @@ package seedu.address.logic.commands; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; @@ -29,28 +29,28 @@ import seedu.address.model.tag.Tag; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing employee in the address book. */ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the employee identified " + + "by the index number used in the displayed employee list. " + "Existing values will be overwritten by the input values.\n" + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_PERSON_NAME + "NAME] " + + "[" + PREFIX_PERSON_PHONE + "PHONE] " + + "[" + PREFIX_PERSON_EMAIL + "EMAIL] " + + "[" + PREFIX_PERSON_ADDRESS + "ADDRESS] " + + "[" + PREFIX_PERSON_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_PERSON_PHONE + "91234567 " + + PREFIX_PERSON_EMAIL + "johndoe@example.com"; - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Employee: %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_DUPLICATE_PERSON = "This employee already exists in the address book."; private final Index index; private final EditPersonDescriptor editPersonDescriptor; diff --git a/src/main/java/seedu/address/logic/commands/EditLeaveCommand.java b/src/main/java/seedu/address/logic/commands/EditLeaveCommand.java new file mode 100644 index 00000000000..c2326757b77 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditLeaveCommand.java @@ -0,0 +1,229 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +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.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.model.leave.exceptions.DuplicateLeaveException; +import seedu.address.model.leave.exceptions.EndBeforeStartException; + +/** + * Edits the details of an existing leave in the leave book. + */ +public class EditLeaveCommand extends Command { + + public static final String COMMAND_WORD = "edit-leave"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the leave identified " + + "by the index number used in the displayed leave list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameter: INDEX (must be a positive integer) " + + "[" + PREFIX_LEAVE_TITLE + "TITLE] " + + "[" + PREFIX_LEAVE_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_LEAVE_DATE_START + "START] " + + "[" + PREFIX_LEAVE_DATE_END + "END]" + + "[" + PREFIX_LEAVE_STATUS + "STATUS]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_LEAVE_TITLE + "medical leave " + + PREFIX_LEAVE_DATE_START + "2023-10-23"; + + public static final String MESSAGE_EDIT_LEAVE_SUCCESS = "Edited Leave: %1$s"; + + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + + public static final String MESSAGE_DUPLICATED_LEAVE = + "Leave entry with matching employee, start date and end already exists"; + + private final Index index; + private final EditLeaveDescriptor editLeaveDescriptor; + + /** + * @param index of the leave in the filtered leave list to edit + * @param editLeaveDescriptor details to edit the leave with + */ + public EditLeaveCommand(Index index, EditLeaveDescriptor editLeaveDescriptor) { + requireNonNull(index); + requireNonNull(editLeaveDescriptor); + + this.index = index; + this.editLeaveDescriptor = editLeaveDescriptor; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredLeaveList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + Leave leaveToEdit = lastShownList.get(index.getZeroBased()); + try { + Leave editedLeave = createEditedLeave(leaveToEdit, editLeaveDescriptor); + model.setLeave(leaveToEdit, editedLeave); + return new CommandResult(String.format(MESSAGE_EDIT_LEAVE_SUCCESS, Messages.format(editedLeave))); + } catch (EndBeforeStartException ebse) { + throw new CommandException(Range.MESSAGE_END_BEFORE_START_ERROR); + } catch (DuplicateLeaveException dle) { + throw new CommandException(MESSAGE_DUPLICATED_LEAVE); + } + } + + private static Leave createEditedLeave(Leave leaveToEdit, EditLeaveDescriptor editLeaveDescriptor) + throws EndBeforeStartException { + assert leaveToEdit != null; + + Title updatedTitle = editLeaveDescriptor.getTitle().orElse(leaveToEdit.getTitle()); + Description updatedDescription = editLeaveDescriptor.getDescription().orElse(leaveToEdit.getDescription()); + + Date startDate = editLeaveDescriptor.getStart().orElse(leaveToEdit.getStart()); + Date endDate = editLeaveDescriptor.getEnd().orElse(leaveToEdit.getEnd()); + Range updatedRange = Range.createNonNullRange(startDate, endDate); + Status updatedStatus = editLeaveDescriptor.getStatus().orElse(leaveToEdit.getStatus()); + + return new Leave(leaveToEdit.getEmployee(), updatedTitle, updatedRange, updatedDescription, updatedStatus); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EditLeaveCommand)) { + return false; + } + + EditLeaveCommand otherEditLeaveCommand = (EditLeaveCommand) other; + return index.equals(otherEditLeaveCommand.index) + && editLeaveDescriptor.equals(otherEditLeaveCommand.editLeaveDescriptor); + } + + /** + * Stores the deatils to edit the leave with. Each non-empty field value will replace the + * corresponsing field value of the leave. + */ + public static class EditLeaveDescriptor { + + private Title title; + private Description description; + private Date start; + private Date end; + private Status status; + + public EditLeaveDescriptor() {} + + /** + * Copy constructor. + */ + public EditLeaveDescriptor(EditLeaveDescriptor toCopy) { + setTitle(toCopy.title); + setDescription(toCopy.description); + setStart(toCopy.start); + setEnd(toCopy.end); + setStatus(toCopy.status); + } + + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(title, description, start, end, status); + } + + public void setTitle(Title title) { + this.title = title; + } + + public Optional getTitle() { + return Optional.ofNullable(title); + } + + public void setDescription(Description description) { + this.description = description; + } + + public Optional<Description> getDescription() { + return Optional.ofNullable(description); + } + + public void setStart(Date start) throws EndBeforeStartException { + boolean isStartAfterEnd = this.end != null && start.isAfter(this.end); + if (isStartAfterEnd) { + throw new EndBeforeStartException(); + } + this.start = start; + } + + public Optional<Date> getStart() { + return Optional.ofNullable(start); + } + + public void setEnd(Date end) throws EndBeforeStartException { + boolean isEndBeforeStart = this.start != null && end.isBefore(this.start); + if (isEndBeforeStart) { + throw new EndBeforeStartException(); + } + this.end = end; + } + + public Optional<Date> getEnd() { + return Optional.ofNullable(end); + } + + public void setStatus(Status status) { + this.status = status; + } + + public Optional<Status> getStatus() { + return Optional.ofNullable(status); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EditLeaveDescriptor)) { + return false; + } + + EditLeaveDescriptor otherEditLeaveDescriptor = (EditLeaveDescriptor) other; + return Objects.equals(title, otherEditLeaveDescriptor.title) + && Objects.equals(description, otherEditLeaveDescriptor.description) + && Objects.equals(start, otherEditLeaveDescriptor.start) + && Objects.equals(end, otherEditLeaveDescriptor.end) + && Objects.equals(status, otherEditLeaveDescriptor.status); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("title", title) + .add("description", description) + .add("start", start) + .add("end", end) + .add("status", status) + .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..7b9229ea9ab 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,7 +9,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting HR Mate as requested ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..cd5f09d1294 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,29 @@ +package seedu.address.logic.commands; + +import java.nio.file.Path; + +/** + * Base ExportCommand class that contains fields shared by commands that export files, + * such as file paths, destination paths, and messages + */ +public abstract class ExportCommand extends Command { + public static final String EXPORT_DEST = "export"; + + public static final String MESSAGE_USAGE = "%s: Exports %s records to CSV file. " + + "Parameters: " + + "FILENAME"; + + public static final String MESSAGE_SUCCESS = "%s records have been saved to %s!"; + + public static final String MESSAGE_FAILED = "%s records could not be saved!"; + + public final Path filePath; + + /** + * Creates an ExportCommand to export records to the specified file name + * @param filePath Path to save file to + */ + public ExportCommand(Path filePath) { + this.filePath = filePath; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExportContactCommand.java b/src/main/java/seedu/address/logic/commands/ExportContactCommand.java new file mode 100644 index 00000000000..5ff9d894c02 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportContactCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyFilteredAddressBook; +import seedu.address.storage.CsvAddressBookStorage; + +/** + * Exports AddressBook to a CSV file + */ +public class ExportContactCommand extends ExportCommand { + public static final String COMMAND_WORD = "export"; + public static final String MESSAGE_USAGE = String.format(ExportCommand.MESSAGE_USAGE, COMMAND_WORD, "employee"); + public static final String MESSAGE_FAILED = String.format(ExportCommand.MESSAGE_FAILED, "Employee"); + + /** + * Creates an ExportContactCommand to export records to the specified file name + * @param filePath Path to save file to + */ + public ExportContactCommand(Path filePath) { + super(filePath); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + ReadOnlyFilteredAddressBook filteredAddressBook = new ReadOnlyFilteredAddressBook(model); + CsvAddressBookStorage abStorage = new CsvAddressBookStorage(filePath); + abStorage.saveAddressBook(filteredAddressBook); + } catch (IOException e) { + throw new CommandException(MESSAGE_FAILED); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, "Employee", filePath)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExportContactCommand)) { + return false; + } + + ExportContactCommand otherExportContactCommand = (ExportContactCommand) other; + return filePath.equals(otherExportContactCommand.filePath); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExportLeaveCommand.java b/src/main/java/seedu/address/logic/commands/ExportLeaveCommand.java new file mode 100644 index 00000000000..1e91696ba34 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportLeaveCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyFilteredLeavesBook; +import seedu.address.storage.CsvLeavesBookStorage; + +/** + * Exports LeavesBook to a CSV file + */ +public class ExportLeaveCommand extends ExportCommand { + public static final String COMMAND_WORD = "export-leave"; + + public static final String MESSAGE_USAGE = String.format(ExportCommand.MESSAGE_USAGE, COMMAND_WORD, "leaves"); + public static final String MESSAGE_FAILED = String.format(ExportCommand.MESSAGE_FAILED, "Leaves"); + + /** + * Creates an ExportLeaveCommand to export records to the specified file name + * @param filePath Path to save file to + */ + public ExportLeaveCommand(Path filePath) { + super(filePath); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + ReadOnlyFilteredLeavesBook filteredLeavesBook = new ReadOnlyFilteredLeavesBook(model); + CsvLeavesBookStorage lvStorage = new CsvLeavesBookStorage(filePath); + lvStorage.saveLeavesBook(filteredLeavesBook); + } catch (IOException e) { + throw new CommandException(MESSAGE_FAILED); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, "Leaves", filePath)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExportLeaveCommand)) { + return false; + } + + ExportLeaveCommand otherExportLeaveCommand = (ExportLeaveCommand) other; + return filePath.equals(otherExportLeaveCommand.filePath); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindAllLeaveCommand.java b/src/main/java/seedu/address/logic/commands/FindAllLeaveCommand.java new file mode 100644 index 00000000000..63878b50f07 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindAllLeaveCommand.java @@ -0,0 +1,42 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_LEAVES; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; + +/** + * Find all leaves in the leave list. + */ +public class FindAllLeaveCommand extends Command { + public static final String COMMAND_WORD = "find-all-leave"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View all leaves available in the leave list.\n"; + public static final String MESSAGE_FIND_LEAVE_NONE = "There are currently no leave requests."; + public static final String MESSAGE_LEAVE_COUNT = "Current # of Leave Request(s): %d"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredLeaveList(PREDICATE_SHOW_ALL_LEAVES); + int leaveSize = model.getFilteredLeaveList().size(); + + logger.info(""); //dummy logger + + if (leaveSize == 0) { + return new CommandResult(MESSAGE_FIND_LEAVE_NONE); + } + + return new CommandResult(String.format(MESSAGE_LEAVE_COUNT, leaveSize)); + } + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindAllTagCommand.java b/src/main/java/seedu/address/logic/commands/FindAllTagCommand.java new file mode 100644 index 00000000000..3a1353e4740 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindAllTagCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +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.person.TagsContainAllTagsPredicate; + + + +/** + * Find Persons with the exact tags + */ +public class FindAllTagCommand extends Command { + + public static final String COMMAND_WORD = "find-all-tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all employees whose tags match all " + + "the specified tags exactly (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: " + + "[" + PREFIX_PERSON_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_PERSON_TAG + "full time" + + PREFIX_PERSON_TAG + "remote"; + + private final TagsContainAllTagsPredicate predicate; + private final Logger logger = LogsCenter.getLogger(getClass()); + + /** + * @param predicate tags to match with employees + */ + public FindAllTagCommand(TagsContainAllTagsPredicate predicate) { + requireNonNull(predicate); + this.predicate = predicate; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + logger.info("predicate: " + this.predicate); //dummy logger + + 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 FindAllTagCommand)) { + return false; + } + + FindAllTagCommand otherFindAllTagCommand = (FindAllTagCommand) other; + return predicate.equals(otherFindAllTagCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..690dc857dbe 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -9,13 +9,13 @@ /** * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Keyword matching is case-insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all employees whose names contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; diff --git a/src/main/java/seedu/address/logic/commands/FindLeaveByPeriodCommand.java b/src/main/java/seedu/address/logic/commands/FindLeaveByPeriodCommand.java new file mode 100644 index 00000000000..4dc2456bf1f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindLeaveByPeriodCommand.java @@ -0,0 +1,69 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_LEAVES_LISTED_OVERVIEW; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.leave.LeaveInPeriodPredicate; + +/** + * Returns a list of leaves that coincide with one or more days of a given period. + */ +public class FindLeaveByPeriodCommand extends Command { + public static final String COMMAND_WORD = "find-leave-range"; + public static final String MESSAGE_USAGE = "Finds all leaves that happen within the queried range." + + "The start and end dates are optional - if none are supplied, all leaves are returned." + + "If only one of them is supplied, all leaves that end on and after the queried start date are" + + " returned (if the start date is supplied) or all leaves that start before and on the queried" + + " end date are returned (if the end date is supplied).\n" + + "Parameters: " + + "[" + PREFIX_LEAVE_DATE_START + "START_DATE] " + + "[" + PREFIX_LEAVE_DATE_END + "END_DATE]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_LEAVE_DATE_START + "2023-10-30" + " " + + PREFIX_LEAVE_DATE_END + "2023-10-31"; + + private final LeaveInPeriodPredicate predicate; + + + /** + * Constructs a FindLeaveByPeriodCommand for query for all leaves that occur in a given period + * @param predicate Predicate containing period that leaves should coincide with + */ + public FindLeaveByPeriodCommand(LeaveInPeriodPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredLeaveList(predicate); + + return new CommandResult( + String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, model.getFilteredLeaveList().size()) + ); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof FindLeaveByPeriodCommand)) { + return false; + } + FindLeaveByPeriodCommand otherCommand = (FindLeaveByPeriodCommand) other; + return this.predicate.equals(otherCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindLeaveByStatusCommand.java b/src/main/java/seedu/address/logic/commands/FindLeaveByStatusCommand.java new file mode 100644 index 00000000000..bfe3a560ffb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindLeaveByStatusCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_LEAVES_LISTED_OVERVIEW; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.leave.LeaveHasStatusPredicate; + +/** + * Returns a list of leaves that have a particular status + */ +public class FindLeaveByStatusCommand extends Command { + public static final String COMMAND_WORD = "find-leave-status"; + public static final String MESSAGE_USAGE = "Finds all leaves that contain the given status." + + "Parameters: STATUS" + + "Example: " + COMMAND_WORD + " " + + "APPROVED"; + public static final String MESSAGE_FAILED = "Command should only contain one of the following words: " + + "APPROVED / PENDING / REJECTED"; + private final LeaveHasStatusPredicate predicate; + + /** + * Constructs a FindLeaveByStatusCommand object + * @param predicate Predicate containing status that leaves should have + */ + public FindLeaveByStatusCommand(LeaveHasStatusPredicate predicate) { + requireNonNull(predicate); + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredLeaveList(predicate); + + return new CommandResult( + String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, model.getFilteredLeaveList().size())); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FindLeaveByStatusCommand)) { + return false; + } + + FindLeaveByStatusCommand otherCommand = (FindLeaveByStatusCommand) other; + return this.predicate.equals(otherCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindLeaveCommand.java b/src/main/java/seedu/address/logic/commands/FindLeaveCommand.java new file mode 100644 index 00000000000..23e3520dcc4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindLeaveCommand.java @@ -0,0 +1,81 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.LogsCenter; +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.leave.LeaveContainsPersonPredicate; +import seedu.address.model.person.Person; + +/** + * Find Leaves with the exact employee of the given index + */ +public class FindLeaveCommand extends Command { + public static final String COMMAND_WORD = "find-leave"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all leaves whose employee matches " + + "the given index of the employee and displays them as a list with index numbers.\n" + + "Parameters: INDEX (must be a positive integer within in the range of the Employee List)\n" + + "Example: " + COMMAND_WORD + " " + + "1"; + + private final Index index; + private final Logger logger = LogsCenter.getLogger(getClass()); + + /** + * @param index the corresponding employee of the index to match with employees + */ + public FindLeaveCommand(Index index) { + requireNonNull(index); + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ObservableList<Person> lastShownPersonList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownPersonList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person employee = lastShownPersonList.get(index.getZeroBased()); + LeaveContainsPersonPredicate predicate = new LeaveContainsPersonPredicate(employee); + model.updateFilteredLeaveList(predicate); + + logger.info("predicate: " + predicate); //dummy logger + + return new CommandResult( + String.format(Messages.MESSAGE_LEAVES_LISTED_OVERVIEW, model.getFilteredLeaveList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindLeaveCommand)) { + return false; + } + + FindLeaveCommand otherFindLeaveCommand = (FindLeaveCommand) other; + return index.equals(otherFindLeaveCommand.index); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .toString(); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/FindSomeTagCommand.java b/src/main/java/seedu/address/logic/commands/FindSomeTagCommand.java new file mode 100644 index 00000000000..5491153a699 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindSomeTagCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; + +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.person.TagsContainSomeTagsPredicate; + +/** + * Find Persons with some specified tags + */ +public class FindSomeTagCommand extends Command { + + public static final String COMMAND_WORD = "find-some-tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all employees whose tags match some " + + "specified tags (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: " + + "[" + PREFIX_PERSON_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_PERSON_TAG + "full time" + + PREFIX_PERSON_TAG + "remote"; + + private final TagsContainSomeTagsPredicate predicate; + + /** + * @param predicate tags to match with employees + */ + public FindSomeTagCommand(TagsContainSomeTagsPredicate predicate) { + requireNonNull(predicate); + this.predicate = predicate; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + + 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 FindSomeTagCommand)) { + return false; + } + + FindSomeTagCommand otherFindSomeTagCommand = (FindSomeTagCommand) other; + return predicate.equals(otherFindSomeTagCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ImportContactCommand.java b/src/main/java/seedu/address/logic/commands/ImportContactCommand.java new file mode 100644 index 00000000000..3c4b5c20d9b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportContactCommand.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_LEAVES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.io.File; +import java.util.Optional; + +import seedu.address.commons.controllers.FileDialogHandler; +import seedu.address.commons.controllers.FileDialogHandlerImpl; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.storage.CsvAddressBookStorage; + +/** + * Imports records from file + */ +public class ImportContactCommand extends Command { + + public static final String COMMAND_WORD = "import"; + + public static final String MESSAGE_SUCCESS = "Employee records have been imported from %s!"; + + public static final String MESSAGE_NO_FILE_SELECTED = "Employee records were not imported."; + + public static final String MESSAGE_FAILED = "Records in file %s could not be imported, " + + "import cancelled."; + + public static final String MESSAGE_EMPTY_ADDRESS_BOOK = "No valid records found in file %s, " + + "import cancelled."; + + private final FileDialogHandler fileHandler; + + /** + * Constructs a default ImportCommand that triggers a file dialog + */ + public ImportContactCommand() { + fileHandler = new FileDialogHandlerImpl(); + } + + /** + * Constructs an ImportCommand that uses the specified fileHandler. This constructor is primarily used to construct + * ImportCommands that use mock FileDialogHandlers. + * @param fileHandler FileDialogHandler to invoke when executing the command. + */ + public ImportContactCommand(FileDialogHandler fileHandler) { + this.fileHandler = fileHandler; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional<File> importedFile = fileHandler.openFile("Open Record File", + FileDialogHandlerImpl.CSV_EXTENSION); + + if (importedFile.isPresent()) { + String filename = importedFile.get().getName(); + CsvAddressBookStorage tempAddressBook = new CsvAddressBookStorage(importedFile.get().toPath()); + + try { + ReadOnlyAddressBook newAddressBook = tempAddressBook.readAddressBook() + .orElseThrow(() -> new CommandException( + String.format(MESSAGE_EMPTY_ADDRESS_BOOK, filename))); + model.setAddressBook(newAddressBook); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredLeaveList(PREDICATE_SHOW_ALL_LEAVES); + return new CommandResult(String.format(MESSAGE_SUCCESS, filename)); + } catch (DataLoadingException e) { + throw new CommandException(String.format(MESSAGE_FAILED, filename)); + } + } + return new CommandResult(MESSAGE_NO_FILE_SELECTED); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ImportLeaveCommand.java b/src/main/java/seedu/address/logic/commands/ImportLeaveCommand.java new file mode 100644 index 00000000000..52f842c2b1c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportLeaveCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_LEAVES; + +import java.io.File; +import java.util.Optional; + +import seedu.address.commons.controllers.FileDialogHandler; +import seedu.address.commons.controllers.FileDialogHandlerImpl; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyLeavesBook; +import seedu.address.storage.CsvLeavesBookStorage; + +/** + * Imports Leaves from file + */ +public class ImportLeaveCommand extends Command { + public static final String COMMAND_WORD = "import-leave"; + + public static final String MESSAGE_SUCCESS = "Leave records have been imported from %s!"; + + public static final String MESSAGE_NO_FILE_SELECTED = "Leave records were not imported."; + + public static final String MESSAGE_FAILED = "Records in file %s could not be imported, " + + "import cancelled."; + + public static final String MESSAGE_EMPTY_LEAVES_BOOK = "No valid records found in file %s, " + + "import cancelled."; + + private final FileDialogHandler fileHandler; + + /** + * Constructs a default ImportLeaveCommand that triggers a file dialog + */ + public ImportLeaveCommand() { + fileHandler = new FileDialogHandlerImpl(); + } + + /** + * Constructs an ImportLeaveCommand that uses the specified fileHandler. This constructor is primarily used to + * construct ImportLeaveCommands that use mock FileDialogHandlers. + * @param fileHandler FileDialogHandler to invoke when executing the command. + */ + public ImportLeaveCommand(FileDialogHandler fileHandler) { + this.fileHandler = fileHandler; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional<File> importedFile = fileHandler.openFile("Open Leaves File", + FileDialogHandlerImpl.CSV_EXTENSION); + + if (importedFile.isPresent()) { + String filename = importedFile.get().getName(); + CsvLeavesBookStorage tempLeavesBook = new CsvLeavesBookStorage(importedFile.get().toPath()); + + try { + ReadOnlyLeavesBook newLeavesBook = tempLeavesBook.readLeavesBook((AddressBook) model.getAddressBook()) + .orElseThrow(() -> new CommandException( + String.format(MESSAGE_EMPTY_LEAVES_BOOK, filename))); + model.setLeavesBook(newLeavesBook); + model.updateFilteredLeaveList(PREDICATE_SHOW_ALL_LEAVES); + return new CommandResult(String.format(MESSAGE_SUCCESS, filename)); + } catch (DataLoadingException e) { + throw new CommandException(String.format(MESSAGE_FAILED, filename)); + } + } + return new CommandResult(MESSAGE_NO_FILE_SELECTED); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..40c743e9fc7 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -6,13 +6,13 @@ import seedu.address.model.Model; /** - * Lists all persons in the address book to the user. + * Lists all employees in the address book to the user. */ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all employees"; @Override diff --git a/src/main/java/seedu/address/logic/commands/RejectLeaveCommand.java b/src/main/java/seedu/address/logic/commands/RejectLeaveCommand.java new file mode 100644 index 00000000000..3843edd4104 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RejectLeaveCommand.java @@ -0,0 +1,103 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX; + +import javafx.collections.ObservableList; +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.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.model.person.ComparablePerson; + +/** + * Rejects an existing leave in the leave book. + */ +public class RejectLeaveCommand extends Command { + public static final String COMMAND_WORD = "reject-leave"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Reject leave request identified " + + "by the index number used in the leave book. " + + "The specified leave request will be rejected.\n" + + "Parameters: INDEX (must be a positive integer within in the range of the Leave List)\n) " + + "Example: " + COMMAND_WORD + " 1 "; + + public static final String MESSAGE_REJECT_LEAVE_SUCCESS = "Rejected Leave: %1$s"; + public static final String MESSAGE_DUPLICATE_LEAVE_REJECT = "Leave previously rejected: %1$s"; + + private final Index index; + + /** + * @param index of the leave in the leave book to reject + */ + public RejectLeaveCommand(Index index) { + requireNonNull(index); + + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + ObservableList<Leave> leaveList = model.getFilteredLeaveList(); + + if (index.getZeroBased() >= leaveList.size()) { + throw new CommandException(MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + Leave leaveToReject = leaveList.get(index.getZeroBased()); + + if (leaveToReject.getStatus().getStatusType().equals(Status.StatusType.REJECTED)) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_LEAVE_REJECT, Messages.format(leaveToReject))); + } + + Leave rejectedLeave = createRejectedLeave(leaveToReject); + + model.setLeave(leaveToReject, rejectedLeave); + return new CommandResult(String.format(MESSAGE_REJECT_LEAVE_SUCCESS, Messages.format(rejectedLeave))); + } + + private static Leave createRejectedLeave(Leave leaveToReject) { + assert leaveToReject != null; + + ComparablePerson employee = leaveToReject.getEmployee(); + Title title = leaveToReject.getTitle(); + Description description = leaveToReject.getDescription(); + Date start = leaveToReject.getStart(); + Date end = leaveToReject.getEnd(); + Range dateRange = Range.createNonNullRange(start, end); + Status rejectedStatus = Status.of(Status.StatusType.REJECTED); + + return new Leave(employee, title, dateRange, description, rejectedStatus); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RejectLeaveCommand)) { + return false; + } + + RejectLeaveCommand otherRejectedLeaveCommand = (RejectLeaveCommand) other; + return index.equals(otherRejectedLeaveCommand.index); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewTagCommand.java b/src/main/java/seedu/address/logic/commands/ViewTagCommand.java new file mode 100644 index 00000000000..b357a088f71 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewTagCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * View all tags available in alphabetically order. + */ +public class ViewTagCommand extends Command { + public static final String COMMAND_WORD = "view-tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View all tags available in alphabetically order.\n" + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_VIEW_TAG_NONE = "There are currently no tags"; + private final Logger logger = LogsCenter.getLogger(getClass()); + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + List<Person> people = model.getFilteredPersonList(); + ArrayList<String> tagsAll = new ArrayList<>(); + logger.info(""); //dummy logger + + if (people.isEmpty()) { + return new CommandResult(MESSAGE_VIEW_TAG_NONE); + } + + String tags = "Available tag(s):" + "\n"; + + for (Person person : people) { + Set<Tag> temp = person.getTags(); + for (Tag t : temp) { + if (!tagsAll.contains(t.toString())) { + tagsAll.add(t.toString()); + } + } + } + + Collections.sort(tagsAll); + for (String s : tagsAll) { + tags = tags + s + "\n"; + } + return new CommandResult(tags); + } + + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..144e31978a4 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,11 +1,11 @@ package seedu.address.logic.parser; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import java.util.Set; import java.util.stream.Stream; @@ -31,19 +31,22 @@ public class AddCommandParser implements Parser<AddCommand> { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_PERSON_NAME, PREFIX_PERSON_PHONE, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_ADDRESS, PREFIX_PERSON_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON_NAME, PREFIX_PERSON_ADDRESS, + PREFIX_PERSON_PHONE, PREFIX_PERSON_EMAIL) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set<Tag> tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON_NAME, PREFIX_PERSON_PHONE, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_ADDRESS); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON_NAME).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PERSON_PHONE).get()); + Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_PERSON_EMAIL).get()); + Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_PERSON_ADDRESS).get()); + Set<Tag> tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_PERSON_TAG)); Person person = new Person(name, phone, email, address, tagList); diff --git a/src/main/java/seedu/address/logic/parser/AddLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/AddLeaveCommandParser.java new file mode 100644 index 00000000000..d392c720d4d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddLeaveCommandParser.java @@ -0,0 +1,130 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_NO_STATUS_PREFIX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; + +import java.util.Optional; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Title; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddLeaveCommandParser implements Parser<AddLeaveCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddLeaveCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DATE_START, PREFIX_LEAVE_DATE_END, + PREFIX_LEAVE_DESCRIPTION, PREFIX_LEAVE_STATUS); + if (!arePrefixesPresent(argMultimap, PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DATE_START, + PREFIX_LEAVE_DATE_END)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLeaveCommand.MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DATE_START, + PREFIX_LEAVE_DATE_END, PREFIX_LEAVE_DESCRIPTION); + + checkNoStatusProvided(argMultimap); + + Index index = extractIndexFromInput(argMultimap); + Title title = extractTitleFromInput(argMultimap); + Range dateRange = extractRangeFromInput(argMultimap); + Description description = extractDescriptionFromInput(argMultimap); + + return new AddLeaveCommand(index, title, dateRange, description); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Verifies that the user does not add a status to the command, which previously caused an + * uncaught exception that resulted in the app being non-responsive. + * @param argMultimap Multimap containing command arguments. + * @throws ParseException if a status was provided in the command. + */ + private void checkNoStatusProvided(ArgumentMultimap argMultimap) throws ParseException { + if (arePrefixesPresent(argMultimap, PREFIX_LEAVE_STATUS)) { + throw new ParseException(String.format(MESSAGE_NO_STATUS_PREFIX, AddLeaveCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses the index from the preamble in the multimap. + * @param argMultimap Multimap containing command arguments. + * @return Index parsed from preamble. + * @throws ParseException if the preamble does not contain a valid index. + */ + private Index extractIndexFromInput(ArgumentMultimap argMultimap) throws ParseException { + // Note: since ParserUtil::parseIndex can also throw InvalidIndexException for numeric values that are not + // positive Integer values, future work could include catching this exception, which inherits from + // ParseException, and having it throw a different message instead + try { + return ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLeaveCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Parses the title from the command. + * @param argMultimap Multimap containing command arguments. + * @return Parsed Title instance. + * @throws ParseException if the title provided is not valid. + */ + private Title extractTitleFromInput(ArgumentMultimap argMultimap) throws ParseException { + // this assertion should be true as we already checked for prefix existence previously + assert(argMultimap.getValue(PREFIX_LEAVE_TITLE).isPresent()); + return ParserUtil.parseTitle(argMultimap.getValue(PREFIX_LEAVE_TITLE).get()); + } + + /** + * Parses the range from the command. + * @param argMultimap Multimap containing command arguments. + * @return Parsed Range instance. + * @throws ParseException if the start and end dates provided are not valid. + */ + private Range extractRangeFromInput(ArgumentMultimap argMultimap) throws ParseException { + assert(argMultimap.getValue(PREFIX_LEAVE_DATE_START).isPresent() + && argMultimap.getValue(PREFIX_LEAVE_DATE_END).isPresent()); + return ParserUtil.parseNonNullRange(argMultimap.getValue(PREFIX_LEAVE_DATE_START).get(), + argMultimap.getValue(PREFIX_LEAVE_DATE_END).get()); + } + + /** + * Parses the description from the command if it is provided. Otherwise, the description will be + * replaced with a placeholder value (NULL). + * @param argMultimap Multimap containing command arguments. + * @return Parsed Description instance if provided in the command, otherwise a Description instance + * containing a placeholder value. + * @throws ParseException if a description was provided in the command, but is invalid. + */ + private Description extractDescriptionFromInput(ArgumentMultimap argMultimap) throws ParseException { + Optional<String> descriptionValue = argMultimap.getValue(PREFIX_LEAVE_DESCRIPTION); + if (descriptionValue.isPresent()) { + return ParserUtil.parseDescription(descriptionValue.get()); + } else { + return new Description(Description.DESCRIPTION_PLACEHOLDER); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java new file mode 100644 index 00000000000..8b4a92542e5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java @@ -0,0 +1,51 @@ +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_PERSON_TAG; + +import java.util.Collection; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddTagCommand Object + */ +public class AddTagCommandParser implements Parser<AddTagCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the AddTagCommand + * and returns an AddTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE), pe); + } + + Collection<String> tags = argMultimap.getAllValues(PREFIX_PERSON_TAG); + + if (isTagsEmpty(tags)) { + throw new ParseException(AddTagCommand.MESSAGE_NO_TAGS_ADDED); + } + + return new AddTagCommand(index, ParserUtil.parseTags(tags)); + } + + private boolean isTagsEmpty(Collection<String> tags) { + requireNonNull(tags); + + return tags.isEmpty(); + + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..71980c26cc1 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,32 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddLeaveCommand; +import seedu.address.logic.commands.AddTagCommand; +import seedu.address.logic.commands.ApproveLeaveCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLeaveCommand; +import seedu.address.logic.commands.DeleteTagCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditLeaveCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportContactCommand; +import seedu.address.logic.commands.ExportLeaveCommand; +import seedu.address.logic.commands.FindAllLeaveCommand; +import seedu.address.logic.commands.FindAllTagCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindLeaveByPeriodCommand; +import seedu.address.logic.commands.FindLeaveByStatusCommand; +import seedu.address.logic.commands.FindLeaveCommand; +import seedu.address.logic.commands.FindSomeTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportContactCommand; +import seedu.address.logic.commands.ImportLeaveCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.RejectLeaveCommand; +import seedu.address.logic.commands.ViewTagCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -56,26 +74,80 @@ public Command parseCommand(String userInput) throws ParseException { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); + case EditCommand.COMMAND_WORD: + return new EditCommandParser().parse(arguments); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + case ListCommand.COMMAND_WORD: return new ListCommand(); - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); + case AddTagCommand.COMMAND_WORD: + return new AddTagCommandParser().parse(arguments); - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); + case DeleteTagCommand.COMMAND_WORD: + return new DeleteTagCommandParser().parse(arguments); + + case FindAllTagCommand.COMMAND_WORD: + return new FindAllTagCommandParser().parse(arguments); + + case FindSomeTagCommand.COMMAND_WORD: + return new FindSomeTagCommandParser().parse(arguments); + + case ViewTagCommand.COMMAND_WORD: + return new ViewTagCommand(); + + case ImportContactCommand.COMMAND_WORD: + return new ImportContactCommand(); + + case ImportLeaveCommand.COMMAND_WORD: + return new ImportLeaveCommand(); + + case ExportContactCommand.COMMAND_WORD: + return new ExportContactCommandParser().parse(arguments); + + case ExportLeaveCommand.COMMAND_WORD: + return new ExportLeaveCommandParser().parse(arguments); + + case DeleteLeaveCommand.COMMAND_WORD: + return new DeleteLeaveCommandParser().parse(arguments); + + case AddLeaveCommand.COMMAND_WORD: + return new AddLeaveCommandParser().parse(arguments); + + case ApproveLeaveCommand.COMMAND_WORD: + return new ApproveLeaveCommandParser().parse(arguments); + + case EditLeaveCommand.COMMAND_WORD: + return new EditLeaveCommandParser().parse(arguments); + + case FindAllLeaveCommand.COMMAND_WORD: + return new FindAllLeaveCommand(); + + case FindLeaveCommand.COMMAND_WORD: + return new FindLeaveCommandParser().parse(arguments); + + case FindLeaveByPeriodCommand.COMMAND_WORD: + return new FindLeaveByPeriodCommandParser().parse(arguments); + + case FindLeaveByStatusCommand.COMMAND_WORD: + return new FindLeaveByStatusCommandParser().parse(arguments); + + case RejectLeaveCommand.COMMAND_WORD: + return new RejectLeaveCommandParser().parse(arguments); default: logger.finer("This user input caused a ParseException: " + userInput); diff --git a/src/main/java/seedu/address/logic/parser/ApproveLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/ApproveLeaveCommandParser.java new file mode 100644 index 00000000000..2b148a66e94 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ApproveLeaveCommandParser.java @@ -0,0 +1,33 @@ +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.ParserUtil.parseIndex; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ApproveLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ApproveLeave object + */ +public class ApproveLeaveCommandParser implements Parser<ApproveLeaveCommand> { + /** + * Parses the given {@code String} of arguments in the context of the ApproveLeaveCommand + * and returns an ApproveLeaveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ApproveLeaveCommand parse(String args) throws ParseException { + requireNonNull(args); + Index index; + + try { + index = parseIndex(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ApproveLeaveCommand.MESSAGE_USAGE), pe); + } + + return new ApproveLeaveCommand(index); + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..0cf3e7f718a 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,15 @@ public class CliSyntax { /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_PERSON_NAME = new Prefix("n/"); + public static final Prefix PREFIX_PERSON_PHONE = new Prefix("p/"); + public static final Prefix PREFIX_PERSON_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_PERSON_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_PERSON_TAG = new Prefix("t/"); + public static final Prefix PREFIX_LEAVE_TITLE = new Prefix("title/"); + public static final Prefix PREFIX_LEAVE_DATE_START = new Prefix("start/"); + public static final Prefix PREFIX_LEAVE_DATE_END = new Prefix("end/"); + public static final Prefix PREFIX_LEAVE_DESCRIPTION = new Prefix("d/"); + public static final Prefix PREFIX_LEAVE_STATUS = new Prefix("s/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteLeaveCommandParser.java new file mode 100644 index 00000000000..5b4935d6ccb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteLeaveCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteLeaveCommand object + */ +public class DeleteLeaveCommandParser implements Parser<DeleteLeaveCommand> { + + /** + * Parses the given {@code String} of arguments inthe context of the DeleteLeaveCommand + * and returns a DeleteLeaveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteLeaveCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteLeaveCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteLeaveCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteTagCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteTagCommandParser.java new file mode 100644 index 00000000000..481138ca745 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteTagCommandParser.java @@ -0,0 +1,55 @@ +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_PERSON_TAG; + +import java.util.Collection; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteTagCommand Object + */ +public class DeleteTagCommandParser implements Parser<DeleteTagCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteTagCommand + * and returns an DeleteTagCommand object for execution. + * + * @param args arguments to be parsed + * @return DeleteTagCommand object + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTagCommand.MESSAGE_USAGE), pe); + } + + Collection<String> tags = argMultimap.getAllValues(PREFIX_PERSON_TAG); + + if (isTagsEmpty(tags)) { + throw new ParseException(DeleteTagCommand.MESSAGE_NO_TAGS_REMOVED); + } + + return new DeleteTagCommand(index, ParserUtil.parseTags(tags)); + } + + private boolean isTagsEmpty(Collection<String> tags) throws ParseException { + assert tags != null; + + return tags.isEmpty() || (tags.size() == 1 && tags.contains("")); + + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..1a49411980d 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,11 +2,11 @@ import static java.util.Objects.requireNonNull; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import java.util.Collection; import java.util.Collections; @@ -32,7 +32,8 @@ public class EditCommandParser implements Parser<EditCommand> { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_PERSON_NAME, PREFIX_PERSON_PHONE, PREFIX_PERSON_EMAIL, + PREFIX_PERSON_ADDRESS, PREFIX_PERSON_TAG); Index index; @@ -42,23 +43,24 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PERSON_NAME, PREFIX_PERSON_PHONE, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_ADDRESS); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + if (argMultimap.getValue(PREFIX_PERSON_NAME).isPresent()) { + editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_PERSON_NAME).get())); } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + if (argMultimap.getValue(PREFIX_PERSON_PHONE).isPresent()) { + editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PERSON_PHONE).get())); } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + if (argMultimap.getValue(PREFIX_PERSON_EMAIL).isPresent()) { + editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_PERSON_EMAIL).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + if (argMultimap.getValue(PREFIX_PERSON_ADDRESS).isPresent()) { + editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_PERSON_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_PERSON_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); diff --git a/src/main/java/seedu/address/logic/parser/EditLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/EditLeaveCommandParser.java new file mode 100644 index 00000000000..454d59a8e23 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditLeaveCommandParser.java @@ -0,0 +1,144 @@ +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_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; + +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditLeaveCommand; +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.exceptions.EndBeforeStartException; + +/** + * Parses input arguments and creates a new EditLeaveCommand object + */ +public class EditLeaveCommandParser implements Parser<EditLeaveCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the EditLeaveCommand + * and returns an EditLeaveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditLeaveCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DESCRIPTION, + PREFIX_LEAVE_DATE_START, PREFIX_LEAVE_DATE_END, PREFIX_LEAVE_STATUS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DESCRIPTION, PREFIX_LEAVE_DATE_START, + PREFIX_LEAVE_DATE_END, PREFIX_LEAVE_STATUS); + + Index index = extractIndexFromInput(argMultimap); + EditLeaveDescriptor editLeaveDescriptor = new EditLeaveDescriptor(); + + setTitleIfExists(editLeaveDescriptor, argMultimap); + setDescriptionIfExists(editLeaveDescriptor, argMultimap); + setStartIfExists(editLeaveDescriptor, argMultimap); + setEndIfExists(editLeaveDescriptor, argMultimap); + setStatusIfExists(editLeaveDescriptor, argMultimap); + + if (!editLeaveDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditLeaveCommand.MESSAGE_NOT_EDITED); + } + + return new EditLeaveCommand(index, editLeaveDescriptor); + } + + /** + * Retrieves index value from ArgumentMultimap object. + * @param argMultimap ArgumentMultimap containing index. + * @return Index constructed from value in argMultimap. + * @throws ParseException if no valid index is found in argMultimap. + */ + private Index extractIndexFromInput(ArgumentMultimap argMultimap) throws ParseException { + try { + return ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditLeaveCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Modifies the title field of editLeaveDescriptor to contain the value of the title in argMultimap. + * @param editLeaveDescriptor EditLeaveDescriptor object to modify. + * @param argMultimap Multimap containing title value. + * @throws ParseException if title is not valid. + */ + private void setTitleIfExists(EditLeaveDescriptor editLeaveDescriptor, ArgumentMultimap argMultimap) + throws ParseException { + Optional<String> title = argMultimap.getValue(PREFIX_LEAVE_TITLE); + if (title.isPresent()) { + editLeaveDescriptor.setTitle(ParserUtil.parseTitle(title.get())); + } + } + + /** + * Modifies the title field of editLeaveDescriptor to contain the value of the description in argMultimap. + * @param editLeaveDescriptor EditLeaveDescriptor object to modify. + * @param argMultimap Multimap containing description value. + * @throws ParseException if description is not valid. + */ + private void setDescriptionIfExists(EditLeaveDescriptor editLeaveDescriptor, ArgumentMultimap argMultimap) + throws ParseException { + Optional<String> desc = argMultimap.getValue(PREFIX_LEAVE_DESCRIPTION); + if (desc.isPresent()) { + editLeaveDescriptor.setDescription(ParserUtil.parseDescription(desc.get())); + } + } + + /** + * Modifies the title field of editLeaveDescriptor to contain the value of the start in argMultimap. + * @param editLeaveDescriptor EditLeaveDescriptor object to modify. + * @param argMultimap Multimap containing start value. + * @throws ParseException if start is not valid. + */ + private void setStartIfExists(EditLeaveDescriptor editLeaveDescriptor, ArgumentMultimap argMultimap) + throws ParseException { + Optional<String> start = argMultimap.getValue(PREFIX_LEAVE_DATE_START); + if (start.isPresent()) { + try { + editLeaveDescriptor.setStart(ParserUtil.parseDate(start.get())); + } catch (EndBeforeStartException e) { + throw new ParseException(Range.MESSAGE_END_BEFORE_START_ERROR); + } + } + } + + /** + * Modifies the end field of editLeaveDescriptor to contain the value of the end in argMultimap. + * @param editLeaveDescriptor EditLeaveDescriptor object to modify. + * @param argMultimap Multimap containing end value. + * @throws ParseException if end is not valid. + */ + private void setEndIfExists(EditLeaveDescriptor editLeaveDescriptor, ArgumentMultimap argMultimap) + throws ParseException { + Optional<String> end = argMultimap.getValue(PREFIX_LEAVE_DATE_END); + if (end.isPresent()) { + try { + editLeaveDescriptor.setEnd(ParserUtil.parseDate(end.get())); + } catch (EndBeforeStartException e) { + throw new ParseException(Range.MESSAGE_END_BEFORE_START_ERROR); + } + } + } + + /** + * Modifies the status field of editLeaveDescriptor to contain the value of the status in argMultimap. + * @param editLeaveDescriptor EditLeaveDescriptor object to modify. + * @param argMultimap Multimap containing status value. + * @throws ParseException if status is not valid. + */ + private void setStatusIfExists(EditLeaveDescriptor editLeaveDescriptor, ArgumentMultimap argMultimap) + throws ParseException { + Optional<String> status = argMultimap.getValue(PREFIX_LEAVE_STATUS); + if (status.isPresent()) { + editLeaveDescriptor.setStatus(ParserUtil.parseStatus(status.get())); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 00000000000..67baf2ff56a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,70 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.commons.util.CsvUtil; +import seedu.address.commons.util.FileUtil; +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments for child Export parsers + */ +public abstract class ExportCommandParser { + + private final Path exportFolder = Path.of(ExportCommand.EXPORT_DEST); + + /** + * Parses the given {@code String} of arguments in the context of the ExportCommand + * and returns a file name for the child Export commands to save to. + * @param args String containing file name to parse + * @param messageUsage Message to display if file name is not of the expected format + * @throws ParseException if the user input does not conform to the expected format + */ + public Path parseFileName(String args, String messageUsage) throws ParseException { + String trimmedArgs = args.trim(); + + boolean inputPathIsDirectory = trimmedArgs.endsWith("/"); + + if (!FileUtil.isValidPath(trimmedArgs) || inputPathIsDirectory) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, messageUsage)); + } + + Path fileName = getFileName(trimmedArgs); + + if (fileName.toString().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, messageUsage)); + } + + String fileWithExtension = appendExtension(fileName.toString()); + return exportFolder.resolve(fileWithExtension); + } + + private Path getFileName(String path) { + Path filePath = Paths.get(path); + System.out.println(filePath); + return filePath.getFileName(); + } + + /** + * Strips the extension provided by the user (if any) and appends the CSV extension + * @param path File path provided by user + * @return File path containing CSV extension + */ + private String appendExtension(String path) { + int extensionPos = path.lastIndexOf('.'); + boolean pathContainsExtension = extensionPos > -1; + + String pathStrippedExtension; + if (pathContainsExtension) { + pathStrippedExtension = path.substring(0, extensionPos); + } else { + pathStrippedExtension = path; + } + + return pathStrippedExtension.concat(CsvUtil.EXTENSION); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ExportContactCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportContactCommandParser.java new file mode 100644 index 00000000000..b0a1152da25 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportContactCommandParser.java @@ -0,0 +1,18 @@ +package seedu.address.logic.parser; + +import java.nio.file.Path; + +import seedu.address.logic.commands.ExportContactCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input into a ExportContactCommand + */ +public class ExportContactCommandParser extends ExportCommandParser implements Parser<ExportContactCommand> { + + @Override + public ExportContactCommand parse(String userInput) throws ParseException { + Path fileName = parseFileName(userInput, ExportContactCommand.MESSAGE_USAGE); + return new ExportContactCommand(fileName); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ExportLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportLeaveCommandParser.java new file mode 100644 index 00000000000..94382d83219 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportLeaveCommandParser.java @@ -0,0 +1,18 @@ +package seedu.address.logic.parser; + +import java.nio.file.Path; + +import seedu.address.logic.commands.ExportLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input into a ExportLeaveCommand + */ +public class ExportLeaveCommandParser extends ExportCommandParser implements Parser<ExportLeaveCommand> { + + @Override + public ExportLeaveCommand parse(String userInput) throws ParseException { + Path fileName = parseFileName(userInput, ExportLeaveCommand.MESSAGE_USAGE); + return new ExportLeaveCommand(fileName); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindAllTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindAllTagCommandParser.java new file mode 100644 index 00000000000..4687631535b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindAllTagCommandParser.java @@ -0,0 +1,57 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.logic.commands.FindAllTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagsContainAllTagsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new FindAllTagCommand object + */ +public class FindAllTagCommandParser implements Parser<FindAllTagCommand> { + public static final String MESSAGE_FORMAT_REMINDER = + String.format("\nFormat reminder: %1$s", FindAllTagCommand.MESSAGE_USAGE); + public static final String MESSAGE_INVALID_TAG = + "Tags names only allows alphanumeric characters, spaces, and dashes."; + /** + * Parses the given {@code String} of arguments in the context of the FindAllTagCommand + * and returns a FindAllTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindAllTagCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON_TAG); + if (args.isEmpty() || !isPrefixPresent(argMultimap, PREFIX_PERSON_TAG)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindAllTagCommand.MESSAGE_USAGE)); + } + + List<String> tagArguments = argMultimap.getAllValues(PREFIX_PERSON_TAG); + List<Tag> tagList = new ArrayList<>(); + for (String keyword : tagArguments) { + try { + Tag tag = new Tag(keyword); + tagList.add(tag); + } catch (IllegalArgumentException ie) { + throw new ParseException(MESSAGE_INVALID_TAG + MESSAGE_FORMAT_REMINDER, ie); + } + } + + return new FindAllTagCommand(new TagsContainAllTagsPredicate(tagList)); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean isPrefixPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParser.java b/src/main/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParser.java new file mode 100644 index 00000000000..6654d48360e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParser.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; + +import seedu.address.logic.commands.FindLeaveByPeriodCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.LeaveInPeriodPredicate; +import seedu.address.model.leave.Range; + +/** + * Parses input arguments and creates a FindLeaveByPeriodCommand object + */ +public class FindLeaveByPeriodCommandParser implements Parser<FindLeaveByPeriodCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the FindLeaveByPeriodCommand + * and returns a FindLeaveByPeriod object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public FindLeaveByPeriodCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_LEAVE_DATE_START, PREFIX_LEAVE_DATE_END); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_LEAVE_DATE_START, PREFIX_LEAVE_DATE_END); + + String startDate = argMultimap.getValue(PREFIX_LEAVE_DATE_START).orElse(null); + String endDate = argMultimap.getValue(PREFIX_LEAVE_DATE_END).orElse(null); + + Range dateRange = ParserUtil.parseNullableRange(startDate, endDate); + + LeaveInPeriodPredicate predicate = new LeaveInPeriodPredicate(dateRange); + return new FindLeaveByPeriodCommand(predicate); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindLeaveByStatusCommandParser.java b/src/main/java/seedu/address/logic/parser/FindLeaveByStatusCommandParser.java new file mode 100644 index 00000000000..b56afc45151 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindLeaveByStatusCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.commands.FindLeaveByStatusCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.LeaveHasStatusPredicate; +import seedu.address.model.leave.Status; + +/** + * Parses input arguments and creates a FindLeaveByStatusCommand object + */ +public class FindLeaveByStatusCommandParser implements Parser<FindLeaveByStatusCommand> { + /** + * Parses the given {@code String} of arguments in the context of the FindLeaveByStatusCommand + * and returns a FindLeaveByPeriod object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public FindLeaveByStatusCommand parse(String userInput) throws ParseException { + try { + // trims the input and is case-agnostic + String trimmedUpperCaseInput = userInput.trim().toUpperCase(); + LeaveHasStatusPredicate predicate = new LeaveHasStatusPredicate( + Status.of(trimmedUpperCaseInput) + ); + return new FindLeaveByStatusCommand(predicate); + } catch (IllegalArgumentException | NullPointerException e) { + throw new ParseException(FindLeaveByStatusCommand.MESSAGE_FAILED); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/FindLeaveCommandParser.java new file mode 100644 index 00000000000..1cbeac0e098 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindLeaveCommandParser.java @@ -0,0 +1,35 @@ +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.ParserUtil.parseIndex; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.FindLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new FindLeaveCommand object + */ +public class FindLeaveCommandParser implements Parser<FindLeaveCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the FindLeaveCommand + * and returns a FindLeaveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindLeaveCommand parse(String args) throws ParseException { + requireNonNull(args); + Index index; + + try { + index = parseIndex(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindLeaveCommand.MESSAGE_USAGE), pe); + } + + return new FindLeaveCommand(index); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/FindSomeTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindSomeTagCommandParser.java new file mode 100644 index 00000000000..f70574ae988 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindSomeTagCommandParser.java @@ -0,0 +1,58 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.logic.commands.FindAllTagCommand; +import seedu.address.logic.commands.FindSomeTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagsContainSomeTagsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new FindSomeTagCommand object + */ +public class FindSomeTagCommandParser implements Parser<FindSomeTagCommand> { + public static final String MESSAGE_FORMAT_REMINDER = + String.format("\nFormat reminder: %1$s", FindAllTagCommand.MESSAGE_USAGE); + public static final String MESSAGE_INVALID_TAG = + "Tags names only allows alphanumeric characters, spaces, and dashes."; + /** + * Parses the given {@code String} of arguments in the context of the FindSomeTagCommand + * and returns a FindSomeTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindSomeTagCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PERSON_TAG); + if (args.isEmpty() || !isPrefixPresent(argMultimap, PREFIX_PERSON_TAG)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindSomeTagCommand.MESSAGE_USAGE)); + } + + List<String> tagArguments = argMultimap.getAllValues(PREFIX_PERSON_TAG); + List<Tag> tagList = new ArrayList<>(); + for (String keyword : tagArguments) { + try { + Tag tag = new Tag(keyword); + tagList.add(tag); + } catch (IllegalArgumentException ie) { + throw new ParseException(MESSAGE_INVALID_TAG + MESSAGE_FORMAT_REMINDER, ie); + } + } + + return new FindSomeTagCommand(new TagsContainSomeTagsPredicate(tagList)); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean isPrefixPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..c4e968414b4 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,13 +2,21 @@ import static java.util.Objects.requireNonNull; +import java.time.format.DateTimeParseException; import java.util.Collection; import java.util.HashSet; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.parser.exceptions.InvalidIndexException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.model.leave.exceptions.EndBeforeStartException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -20,7 +28,7 @@ */ public class ParserUtil { - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_INDEX = "Index is not an integer."; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -29,9 +37,17 @@ public class ParserUtil { */ public static Index parseIndex(String oneBasedIndex) throws ParseException { String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + + // Once trimmed, first check the completely invalid formats, ie empty string, or non-integer + // Then check if is a non-zero unsigned integer + + if (!StringUtil.isInteger(trimmedIndex)) { throw new ParseException(MESSAGE_INVALID_INDEX); } + + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new InvalidIndexException(); + } return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } @@ -43,11 +59,12 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { */ public static Name parseName(String name) throws ParseException { requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { + try { + String trimmedName = name.trim(); + return new Name(trimmedName); + } catch (IllegalArgumentException e) { throw new ParseException(Name.MESSAGE_CONSTRAINTS); } - return new Name(trimmedName); } /** @@ -58,11 +75,12 @@ public static Name parseName(String name) throws ParseException { */ public static Phone parsePhone(String phone) throws ParseException { requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { + try { + String trimmedPhone = phone.trim(); + return new Phone(trimmedPhone); + } catch (IllegalArgumentException e) { throw new ParseException(Phone.MESSAGE_CONSTRAINTS); } - return new Phone(trimmedPhone); } /** @@ -73,11 +91,12 @@ public static Phone parsePhone(String phone) throws ParseException { */ public static Address parseAddress(String address) throws ParseException { requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { + try { + String trimmedAddress = address.trim(); + return new Address(trimmedAddress); + } catch (IllegalArgumentException e) { throw new ParseException(Address.MESSAGE_CONSTRAINTS); } - return new Address(trimmedAddress); } /** @@ -88,11 +107,12 @@ public static Address parseAddress(String address) throws ParseException { */ public static Email parseEmail(String email) throws ParseException { requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { + try { + String trimmedEmail = email.trim(); + return new Email(trimmedEmail); + } catch (IllegalArgumentException e) { throw new ParseException(Email.MESSAGE_CONSTRAINTS); } - return new Email(trimmedEmail); } /** @@ -103,15 +123,18 @@ public static Email parseEmail(String email) throws ParseException { */ public static Tag parseTag(String tag) throws ParseException { requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { + try { + String trimmedTag = tag.trim(); + return new Tag(trimmedTag); + } catch (IllegalArgumentException e) { throw new ParseException(Tag.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); } /** * Parses {@code Collection<String> tags} into a {@code Set<Tag>}. + * + * @throws ParseException if at least one of the tags in {@code Collection<String> tags} is invalid. */ public static Set<Tag> parseTags(Collection<String> tags) throws ParseException { requireNonNull(tags); @@ -121,4 +144,125 @@ public static Set<Tag> parseTags(Collection<String> tags) throws ParseException } return tagSet; } + + //=========== LeavesBook ================================================================================ + + /** + * Parses a {@code String title} into a {@code String}. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if a title cannot be constructed due to illegal characters + */ + public static Title parseTitle(String title) throws ParseException { + requireNonNull(title); + try { + String trimmedTitle = title.trim(); + return new Title(trimmedTitle); + } catch (IllegalArgumentException e) { + throw new ParseException(Title.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String start} {@code String end} into an {@code Range}. Both start and + * end must be non-null. + * Leading and trailing whitespaces will be trimmed. + * + * @param start Non-null string containing the start date + * @param end Non-null string containing the end date + * @throws NullPointerException if either start or end is empty + * @throws ParseException if the given {@code start} and {@code end} is invalid, or if + * the end date is before the start date + */ + public static Range parseNonNullRange(String start, String end) throws NullPointerException, ParseException { + requireNonNull(end); + requireNonNull(start); + + try { + String trimmedStart = start.trim(); + String trimmedEnd = end.trim(); + + Date startDate = parseDate(trimmedStart); + Date endDate = parseDate(trimmedEnd); + + return Range.createNonNullRange(startDate, endDate); + } catch (EndBeforeStartException e) { + throw new ParseException(Range.MESSAGE_END_BEFORE_START_ERROR); + } + } + + /** + * Parses a {@code String date} into an {@code Date}. + * Leading and trailing whitespaces will be trimmed. + * + * @param date Non-null string containing the date + * @throws ParseException if the given {@code start} and {@code end} is invalid, or if + * the end date is before the start date + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + try { + String trimmedDate = date.trim(); + return Date.of(trimmedDate); + } catch (DateTimeParseException e) { + throw new ParseException(Date.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String start} {@code String end} into an {@code Range}. + * Start and end can be null. + * Leading and trailing whitespaces will be trimmed. + * + * @param start String containing the start date / null + * @param end String containing the end date / null + * @throws ParseException if the given {@code start} and {@code end} is invalid, or if + * the end date is before the start date + */ + public static Range parseNullableRange(String start, String end) throws ParseException { + try { + boolean hasStart = start != null; + boolean hasEnd = end != null; + Date startDate = hasStart ? parseDate(start.trim()) : null; + Date endDate = hasEnd ? parseDate(end.trim()) : null; + + return Range.createNullableRange(startDate, endDate); + } catch (EndBeforeStartException e) { + throw new ParseException(Range.MESSAGE_END_BEFORE_START_ERROR); + } + } + + /** + * Parses a {@code String description} into a {@code Description}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if description is not valid. + */ + public static Description parseDescription(String description) throws ParseException { + requireNonNull(description); + try { + String trimmedDescription = description.trim(); + if (trimmedDescription.isEmpty()) { + trimmedDescription = Description.DESCRIPTION_PLACEHOLDER; + } + return new Description(trimmedDescription); + } catch (IllegalArgumentException e) { + throw new ParseException(Description.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String status} into a {@code Status}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if status does not match any valid statuses. + */ + public static Status parseStatus(String status) throws ParseException { + requireNonNull(status); + try { + String trimmedStatus = status.trim(); + return Status.of(trimmedStatus); + } catch (IllegalArgumentException e) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + } } diff --git a/src/main/java/seedu/address/logic/parser/RejectLeaveCommandParser.java b/src/main/java/seedu/address/logic/parser/RejectLeaveCommandParser.java new file mode 100644 index 00000000000..b9f7fa2db74 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RejectLeaveCommandParser.java @@ -0,0 +1,33 @@ +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.ParserUtil.parseIndex; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.RejectLeaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new RejectLeaveCommand object + */ +public class RejectLeaveCommandParser implements Parser<RejectLeaveCommand> { + /** + * Parses the given {@code String} of arguments in the context of the RejectLeaveCommand + * and returns an RejectLeaveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RejectLeaveCommand parse(String args) throws ParseException { + requireNonNull(args); + Index index; + + try { + index = parseIndex(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RejectLeaveCommand.MESSAGE_USAGE), pe); + } + + return new RejectLeaveCommand(index); + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/InvalidIndexException.java b/src/main/java/seedu/address/logic/parser/exceptions/InvalidIndexException.java new file mode 100644 index 00000000000..13aba9f6ae0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/exceptions/InvalidIndexException.java @@ -0,0 +1,11 @@ +package seedu.address.logic.parser.exceptions; + +/** + * Represents a parse error encountered by a parser. Whereby the index is invalid. + */ +public class InvalidIndexException extends ParseException { + public static final String MESSAGE_INVALID_INDEX = "Index is invalid!"; + public InvalidIndexException() { + super(MESSAGE_INVALID_INDEX); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..4e883817995 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -6,8 +6,11 @@ import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.ComparablePerson; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.exceptions.PersonNotFoundException; + /** * Wraps all data at the address-book level @@ -62,11 +65,21 @@ public void resetData(ReadOnlyAddressBook newData) { /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ - public boolean hasPerson(Person person) { + public boolean hasPerson(ComparablePerson person) { requireNonNull(person); return persons.contains(person); } + public Person getPerson(ComparablePerson target) throws PersonNotFoundException { + requireNonNull(target); + for (Person person : persons) { + if (person.isSamePerson(target)) { + return person; + } + } + throw new PersonNotFoundException(); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. diff --git a/src/main/java/seedu/address/model/LeavesBook.java b/src/main/java/seedu/address/model/LeavesBook.java new file mode 100644 index 00000000000..f3603773192 --- /dev/null +++ b/src/main/java/seedu/address/model/LeavesBook.java @@ -0,0 +1,135 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.UniqueLeaveList; +import seedu.address.model.person.Person; + +/** + * Wraps all data at the leaves-book level + * Duplicates are not allowed (by .isSameLeave comparison) + */ +public class LeavesBook implements ReadOnlyLeavesBook { + + private final UniqueLeaveList leaves; + + /* + * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + leaves = new UniqueLeaveList(); + } + + public LeavesBook() { + } + + /** + * Creates an LeavesBook using the Leaves in the {@code toBeCopied} + */ + public LeavesBook(ReadOnlyLeavesBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the leave list with {@code leaves}. + * {@code leaves} must not contain duplicate leaves. + */ + public void setLeaves(List<Leave> leaves) { + this.leaves.setLeaves(leaves); + } + + /** + * Resets the existing data of this {@code LeavesBook} with {@code newData}. + */ + public void resetData(ReadOnlyLeavesBook newData) { + requireNonNull(newData); + + setLeaves(newData.getLeaveList()); + } + + //// leave-level operations + + /** + * Returns true if a leave with the same identity as {@code leave} exists in the leaves book. + */ + public boolean hasLeave(Leave leave) { + requireNonNull(leave); + return leaves.contains(leave); + } + + /** + * Adds a leave to the leaves book. + * The leave must not already exist in the leaves book. + */ + public void addLeave(Leave l) { + leaves.add(l); + } + + /** + * Replaces the given leave {@code target} in the list with {@code editedLeave}. + * {@code target} must exist in the leaves book. + * The leave identity of {@code editedLeave} must not be the same as another existing leave in the leaves book. + */ + public void setLeave(Leave target, Leave editedLeave) { + requireNonNull(editedLeave); + + leaves.setLeave(target, editedLeave); + } + + /** + * Removes {@code key} from this {@code LeavesBook}. + * {@code key} must exist in the leaves book. + */ + public void removeLeave(Leave key) { + leaves.remove(key); + } + + public void removePerson(Person p) { + leaves.removePerson(p); + } + + public void setPerson(Person target, Person editedPerson) { + leaves.setPerson(target, editedPerson); + } + + //// util methods + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("leaves", leaves.asUnmodifiableObservableList()) + .toString(); + } + + @Override + public ObservableList<Leave> getLeaveList() { + return leaves.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof LeavesBook + && leaves.equals(((LeavesBook) other).leaves); + } + + @Override + public int hashCode() { + return leaves.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..c48ce2876e9 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -5,6 +5,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; /** @@ -13,6 +14,8 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate<Person> PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate<Leave> PREDICATE_SHOW_ALL_LEAVES = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -52,6 +55,8 @@ public interface Model { /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + ReadOnlyLeavesBook getLeavesBook(); + /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -79,9 +84,54 @@ public interface Model { /** Returns an unmodifiable view of the filtered person list */ ObservableList<Person> getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered leave list */ + ObservableList<Leave> getFilteredLeaveList(); + /** * 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<Person> predicate); + + void deleteLeave(Leave leaveToDelete); + + /** + * Replaces leave book data with the data in {@code leavesBook}. + */ + void setLeavesBook(ReadOnlyLeavesBook leavesBook); + + /** + * Returns true if a leave with the same identity as {@code leave} exists in the leave book. + */ + boolean hasLeave(Leave leave); + + + /** + * Adds the given leave. + * {@code leave} must not already exist in the leave book. + */ + void addLeave(Leave leave); + + /** + * Replaces the given leave {@code target} with {@code editedPerson}. + * {@code target} must exist in the leave book. + * The leave identity of {@code editedLeave} must not be the same as another existing leave in the leave book. + */ + void setLeave(Leave target, Leave editedLeave); + + /** + * Returns the user prefs' address book file path. + */ + Path getLeavesBookFilePath(); + + /** + * Sets the user prefs' address book file path. + */ + void setLeavesBookFilePath(Path leavesBookFilePath); + + /** + * Updates the filter of the filtered leave list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredLeaveList(Predicate<Leave> predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..2cbd7d66c06 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -11,6 +11,8 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; /** @@ -20,24 +22,30 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final AddressBook addressBook; + private final LeavesBook leavesBook; private final UserPrefs userPrefs; private final FilteredList<Person> filteredPersons; + private final FilteredList<Leave> filteredLeaves; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a ModelManager with the given addressBook, leavesBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyLeavesBook leavesBook, + ReadOnlyUserPrefs userPrefs) { + requireAllNonNull(addressBook, leavesBook, userPrefs); + logger.fine("Initializing with address book: " + addressBook + + " and leaves book: " + leavesBook + + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + this.leavesBook = new LeavesBook(leavesBook); + filteredLeaves = new FilteredList<>(this.leavesBook.getLeaveList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new AddressBook(), new LeavesBook(), new UserPrefs()); } //=========== UserPrefs ================================================================================== @@ -75,6 +83,17 @@ public void setAddressBookFilePath(Path addressBookFilePath) { userPrefs.setAddressBookFilePath(addressBookFilePath); } + @Override + public Path getLeavesBookFilePath() { + return userPrefs.getLeavesBookFilePath(); + } + + @Override + public void setLeavesBookFilePath(Path leavesBookFilePath) { + requireNonNull(leavesBookFilePath); + userPrefs.setAddressBookFilePath(leavesBookFilePath); + } + //=========== AddressBook ================================================================================ @Override @@ -87,6 +106,7 @@ public ReadOnlyAddressBook getAddressBook() { return addressBook; } + @Override public boolean hasPerson(Person person) { requireNonNull(person); @@ -96,6 +116,7 @@ public boolean hasPerson(Person person) { @Override public void deletePerson(Person target) { addressBook.removePerson(target); + leavesBook.removePerson(target); } @Override @@ -109,6 +130,25 @@ public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); addressBook.setPerson(target, editedPerson); + leavesBook.setPerson(target, editedPerson); + } + + //=========== LeavesBook ================================================================================ + @Override + public ReadOnlyLeavesBook getLeavesBook() { + return leavesBook; + } + + @Override + public void deleteLeave(Leave leaveToDelete) { + leavesBook.removeLeave(leaveToDelete); + } + + @Override + public void setLeave(Leave target, Leave editedLeave) { + requireAllNonNull(target, editedLeave); + + leavesBook.setLeave(target, editedLeave); } //=========== Filtered Person List Accessors ============================================================= @@ -127,6 +167,40 @@ public void updateFilteredPersonList(Predicate<Person> predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + //=========== LeavesBook ================================================================================ + @Override + public void setLeavesBook(ReadOnlyLeavesBook leavesBook) { + this.leavesBook.resetData(leavesBook); + } + + //=========== Filtered Leave List Accessors ============================================================= + + @Override + public boolean hasLeave(Leave leave) { + requireNonNull(leave); + return leavesBook.hasLeave(leave); + } + + @Override + public void addLeave(Leave leave) { + leavesBook.addLeave(leave); + updateFilteredLeaveList(PREDICATE_SHOW_ALL_LEAVES); + } + + /** + * Returns an unmodifiable view of the list of {@code Leave} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList<Leave> getFilteredLeaveList() { + return filteredLeaves; + } + + @Override + public void updateFilteredLeaveList(Predicate<Leave> predicate) { + requireNonNull(predicate); + filteredLeaves.setPredicate(predicate); + } @Override public boolean equals(Object other) { @@ -140,9 +214,23 @@ public boolean equals(Object other) { } ModelManager otherModelManager = (ModelManager) other; + // TODO implement leaves import so the below test case passes return addressBook.equals(otherModelManager.addressBook) + && leavesBook.equals(otherModelManager.leavesBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredPersons.equals(otherModelManager.filteredPersons) + && filteredLeaves.equals(otherModelManager.filteredLeaves); } + @Override + public String toString() { + return new ToStringBuilder(this) + .add("addressBook", addressBook) + .add("leavesBook", leavesBook) + .add("userPrefs", userPrefs) + .add("filteredPersons", filteredPersons) + .add("filteredLeaves", filteredLeaves) + .toString(); + } } + diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..0e03be91254 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -14,4 +14,5 @@ public interface ReadOnlyAddressBook { */ ObservableList<Person> getPersonList(); + } diff --git a/src/main/java/seedu/address/model/ReadOnlyFilteredAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyFilteredAddressBook.java new file mode 100644 index 00000000000..57c20466c35 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyFilteredAddressBook.java @@ -0,0 +1,25 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.person.Person; + +/** + * Wrapper for a filtered list of people to enable the export of these + * records in CSV format + */ +public class ReadOnlyFilteredAddressBook implements ReadOnlyAddressBook { + private final ObservableList<Person> persons; + + /** + * Constructs a ReadOnlyFilteredAddressBook object + * @param model Current model of the address book data + */ + public ReadOnlyFilteredAddressBook(Model model) { + this.persons = model.getFilteredPersonList(); + } + + @Override + public ObservableList<Person> getPersonList() { + return persons; + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyFilteredLeavesBook.java b/src/main/java/seedu/address/model/ReadOnlyFilteredLeavesBook.java new file mode 100644 index 00000000000..3406fc6aa09 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyFilteredLeavesBook.java @@ -0,0 +1,27 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.leave.Leave; + +/** + * Wrapper for a filtered list of people to enable the export of these + * records in CSV format + */ +public class ReadOnlyFilteredLeavesBook implements ReadOnlyLeavesBook { + private final ObservableList<Leave> leaves; + + /** + * Constructs a ReadOnlyFilteredAddressBook object + * @param model Current model of the address book data + */ + public ReadOnlyFilteredLeavesBook(Model model) { + this.leaves = model.getFilteredLeaveList(); + } + + @Override + public ObservableList<Leave> getLeaveList() { + return leaves; + } +} + + diff --git a/src/main/java/seedu/address/model/ReadOnlyLeavesBook.java b/src/main/java/seedu/address/model/ReadOnlyLeavesBook.java new file mode 100644 index 00000000000..c8601da75bd --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyLeavesBook.java @@ -0,0 +1,16 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.leave.Leave; + +/** + * Unmodifiable view of a leaves book + */ +public interface ReadOnlyLeavesBook { + + /** + * Returns an unmodifiable view of the leaves list. + * This list will not contain any duplicate leaves. + */ + ObservableList<Leave> getLeaveList(); +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c73..4c6705bc86e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -3,14 +3,14 @@ import java.nio.file.Path; import seedu.address.commons.core.GuiSettings; - /** * Unmodifiable view of user prefs. */ public interface ReadOnlyUserPrefs { - GuiSettings getGuiSettings(); Path getAddressBookFilePath(); + Path getLeavesBookFilePath(); + } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..04cbf4c8cf5 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -7,7 +7,6 @@ import java.util.Objects; import seedu.address.commons.core.GuiSettings; - /** * Represents User's preferences. */ @@ -15,12 +14,12 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path leavesBookFilePath = Paths.get("data" , "leavesbook.json"); /** * Creates a {@code UserPrefs} with default values. */ public UserPrefs() {} - /** * Creates a {@code UserPrefs} with the prefs in {@code userPrefs}. */ @@ -28,7 +27,6 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { this(); resetData(userPrefs); } - /** * Resets the existing data of this {@code UserPrefs} with {@code newUserPrefs}. */ @@ -36,47 +34,51 @@ public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setLeavesBookFilePath(newUserPrefs.getLeavesBookFilePath()); } public GuiSettings getGuiSettings() { return guiSettings; } - public void setGuiSettings(GuiSettings guiSettings) { requireNonNull(guiSettings); this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { return addressBookFilePath; } + public Path getLeavesBookFilePath() { + return leavesBookFilePath; + } + public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); this.addressBookFilePath = addressBookFilePath; } + public void setLeavesBookFilePath(Path leavesBookFilePath) { + requireNonNull(leavesBookFilePath); + this.leavesBookFilePath = leavesBookFilePath; + } + @Override public boolean equals(Object other) { if (other == this) { return true; } - // instanceof handles nulls if (!(other instanceof UserPrefs)) { return false; } - UserPrefs otherUserPrefs = (UserPrefs) other; return guiSettings.equals(otherUserPrefs.guiSettings) && addressBookFilePath.equals(otherUserPrefs.addressBookFilePath); } - @Override public int hashCode() { return Objects.hash(guiSettings, addressBookFilePath); } - @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -84,5 +86,4 @@ public String toString() { sb.append("\nLocal data file location : " + addressBookFilePath); return sb.toString(); } - } diff --git a/src/main/java/seedu/address/model/leave/Date.java b/src/main/java/seedu/address/model/leave/Date.java new file mode 100644 index 00000000000..09dd3f13704 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Date.java @@ -0,0 +1,94 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; + +/** + * Represents a Date in the address book. + */ +public class Date { + + public static final String MESSAGE_CONSTRAINTS = + "Date should be valid and in a format of `yyyy-MM-dd`"; + + private static final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT); + private final LocalDate date; + + /** + * Constructs a Date object from a LocalDate object + * @param date LocalDate containing date + */ + private Date(LocalDate date) { + this.date = date; + } + + /** + * Factory method for constructing a Date object with a LocalDate object. + * + * @param date LocalDate to construct Date from + * @return Date object + */ + public static Date of(LocalDate date) { + requireNonNull(date); + return new Date(date); + } + + /** + * Factory method for constructing a Date object with a String. + * + * @param date String containing date in "yyyy-MM-dd" format + * @return Date object + * @throws DateTimeParseException if the string does not follow the format + */ + public static Date of(String date) throws DateTimeParseException { + requireNonNull(date); + return new Date(LocalDate.parse(date, formatter)); + } + + /** + * Returns true if this date occurs before the other date + * @param other Date to compare against + * @return True if this date occurs before the other date, False otherwise + */ + public boolean isBefore(Date other) { + return date.isBefore(other.date); + } + + /** + * Returns true if this date occurs after the other date + * @param other Date to compare against + * @return True if this date occurs after the other date, False otherwise + */ + public boolean isAfter(Date other) { + return date.isAfter(other.date); + } + + public LocalDate getDate() { + return date; + } + @Override + public String toString() { + return date.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Date + && date.equals(((Date) other).date)); + } + + @Override + public int hashCode() { + return date.hashCode(); + } + + public String toFormattedString() { + return formatter.format(date); + } +} diff --git a/src/main/java/seedu/address/model/leave/Description.java b/src/main/java/seedu/address/model/leave/Description.java new file mode 100644 index 00000000000..6ca4678e738 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Description.java @@ -0,0 +1,65 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Leave's description in the leaves book. + */ +public class Description { + + public static final String DESCRIPTION_PLACEHOLDER = "NONE"; + public static final String MESSAGE_CONSTRAINTS = "Leave descriptions should only contain" + + " alphanumeric characters, spaces, dashes, commas, apostrophes and full stops."; + public static final String VALIDATION_REGEX = "^[\\p{Alnum} \\-',.]*$"; + + private final String description; + + /** + * Constructs a {@code Description}. + * + * @param description A valid description. + * @throws IllegalArgumentException if description is not empty and contains illegal characters + */ + public Description(String description) throws IllegalArgumentException { + requireNonNull(description); + checkArgument(isValidDescription(description), MESSAGE_CONSTRAINTS); + this.description = description; + } + + /** + * Returns a default description that is empty. + * + * @return A default description that is empty. + */ + public static Description getDefault() { + return new Description(""); + } + + /** + * Returns true if a given string is a valid description. + */ + public static boolean isValidDescription(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return description; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Description // instanceof handles nulls + && description.equals(((Description) other).description)); // state check + } + + /** + * Returns whether the description is empty. + * @return True if empty, false otherwise. + */ + public boolean isEmpty() { + return description.isEmpty(); + } +} diff --git a/src/main/java/seedu/address/model/leave/Leave.java b/src/main/java/seedu/address/model/leave/Leave.java new file mode 100644 index 00000000000..ebf27e1229d --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Leave.java @@ -0,0 +1,184 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.leave.exceptions.EndBeforeStartException; +import seedu.address.model.person.ComparablePerson; +import seedu.address.model.person.Person; + +/** + * Represents a Leave request of an employee in the address book + */ +public class Leave { + + private final ComparablePerson employee; + private final Title title; + private final Description description; + private final Date start; + private final Date end; + private final Status status; + + /** + * Constructs a Leave object. Takes in a Person object, title and description, date range. + * Requires all fields to be non-null. + * + * @param employee Employee that implements ComparablePerson + * @param title Leave title + * @param dateRange Range representing start and end dates of leaves, both start and end cannot be null + * @param description Leave description + */ + public Leave(ComparablePerson employee, Title title, Range dateRange, Description description) + throws EndBeforeStartException { + requireAllNonNull(employee, title, description, dateRange); + requireNonNull(employee.getName()); + assert(dateRange.getStartDate().isPresent() && dateRange.getEndDate().isPresent()); + + this.employee = employee; + this.title = title; + this.description = description; + this.start = dateRange.getStartDate().get(); + this.end = dateRange.getEndDate().get(); + this.status = Status.getDefault(); + } + + /** + * Constructs a Leave object without the optional description field. + * + * @param employee Employee that implements ComparablePerson + * @param title Leave title + * @param dateRange Range representing start and end dates of leaves, both start and end cannot be null + */ + public Leave(ComparablePerson employee, Title title, Range dateRange) + throws EndBeforeStartException { + requireAllNonNull(employee, title, dateRange); + requireNonNull(employee.getName()); + assert(dateRange.getStartDate().isPresent() && dateRange.getEndDate().isPresent()); + + this.employee = employee; + this.title = title; + this.description = Description.getDefault(); + this.start = dateRange.getStartDate().get(); + this.end = dateRange.getEndDate().get(); + this.status = Status.getDefault(); + } + + /** + * Constructs a Leave object with status. + * + * @param employee Employee that implements ComparablePerson + * @param title Leave title + * @param dateRange Range representing start and end dates of leaves, both start and end cannot be null + * @param description Leave description + * @param status Leave status + */ + public Leave(ComparablePerson employee, Title title, Range dateRange, Description description, Status status) + throws EndBeforeStartException { + requireAllNonNull(employee, title, description, dateRange, status); + requireNonNull(employee.getName()); + assert(dateRange.getStartDate().isPresent() && dateRange.getEndDate().isPresent()); + + this.employee = employee; + this.title = title; + this.description = description; + this.start = dateRange.getStartDate().get(); + this.end = dateRange.getEndDate().get(); + this.status = status; + } + + public ComparablePerson getEmployee() { + return employee; + } + + public Title getTitle() { + return title; + } + + public Description getDescription() { + return description; + } + + public Date getStart() { + return start; + } + + public Date getEnd() { + return end; + } + + public Status getStatus() { + return status; + } + + public boolean belongsTo(Person employee) { + return this.employee.isSamePerson(employee); + } + + /** + * Creates a new Leave instance with all fields identical to the leave the method is called on, + * except with a new person + * @param p Person to replace the person field in the leave with + * @return New Leave instance containing the person + */ + public Leave copyWithNewPerson(Person p) { + requireNonNull(p); + return new Leave(p, title, Range.createNonNullRange(start, end), description, status); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Leave)) { + return false; + } + + Leave otherLeave = (Leave) o; + return otherLeave.getEmployee().isSamePerson(getEmployee()) + && otherLeave.getTitle().equals(getTitle()) + && otherLeave.getDescription().equals(getDescription()) + && otherLeave.getStart().equals(getStart()) + && otherLeave.getEnd().equals(getEnd()) + && otherLeave.getStatus().equals(getStatus()); + } + + /** + * Returns true if both leaves have the same identity and data fields. + */ + public boolean isSameLeave(Leave otherLeave) { + if (otherLeave == this) { + return true; + } + + return otherLeave != null + && otherLeave.getEmployee().isSamePerson(getEmployee()) + && otherLeave.getStart().equals(getStart()) + && otherLeave.getEnd().equals(getEnd()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("Employee: ") + .append(getEmployee().getName()) + .append(" Title: ") + .append(getTitle()) + .append(" Start: ") + .append(getStart()) + .append(" End: ") + .append(getEnd()) + .append(" Status: ") + .append(getStatus()); + return builder.toString(); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(employee, title, description, start, end); + } +} diff --git a/src/main/java/seedu/address/model/leave/LeaveContainsPersonPredicate.java b/src/main/java/seedu/address/model/leave/LeaveContainsPersonPredicate.java new file mode 100644 index 00000000000..6db75172c81 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/LeaveContainsPersonPredicate.java @@ -0,0 +1,43 @@ +package seedu.address.model.leave; + +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Tests that a {@code Leave}'s {@code employee} matches the employee of the given index. + */ +public class LeaveContainsPersonPredicate implements Predicate<Leave> { + private final Person employee; + public LeaveContainsPersonPredicate(Person employee) { + this.employee = employee; + } + + @Override + public boolean test(Leave leave) { + return employee.isSamePerson(leave.getEmployee()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof seedu.address.model.leave.LeaveContainsPersonPredicate)) { + return false; + } + + seedu.address.model.leave.LeaveContainsPersonPredicate otherLeaveContainsPersonPredicate = + (seedu.address.model.leave.LeaveContainsPersonPredicate) other; + return employee.equals(otherLeaveContainsPersonPredicate.employee); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("employee", employee).toString(); + } +} + diff --git a/src/main/java/seedu/address/model/leave/LeaveHasStatusPredicate.java b/src/main/java/seedu/address/model/leave/LeaveHasStatusPredicate.java new file mode 100644 index 00000000000..6bd593501c4 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/LeaveHasStatusPredicate.java @@ -0,0 +1,40 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +/** + * Predicate to test if a leave has a given status + */ +public class LeaveHasStatusPredicate implements Predicate<Leave> { + private final Status status; + + /** + * Constructs a LeaveHasStatusPredicate object. + * @param status Status to match against + */ + public LeaveHasStatusPredicate(Status status) { + requireNonNull(status); + this.status = status; + } + + @Override + public boolean test(Leave leave) { + return leave.getStatus().equals(status); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LeaveHasStatusPredicate)) { + return false; + } + + LeaveHasStatusPredicate otherPred = (LeaveHasStatusPredicate) other; + return this.status.equals(otherPred.status); + } + +} diff --git a/src/main/java/seedu/address/model/leave/LeaveInPeriodPredicate.java b/src/main/java/seedu/address/model/leave/LeaveInPeriodPredicate.java new file mode 100644 index 00000000000..947089b8502 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/LeaveInPeriodPredicate.java @@ -0,0 +1,62 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +/** + * Predicate to test if a leave's period intersects with the + * queried period. + */ +public class LeaveInPeriodPredicate implements Predicate<Leave> { + private final Date start; + private final Date end; + + /** + * Constructs a LeaveInPeriodPredicate object. There are 4 possible inputs for the range + * 1) Both start and end dates are supplied - return only leaves that have periods + * intersecting with this period + * 2) Only the start date is supplied - return only leaves whose end date lies on or + * is after the provided start date + * 3) Only the end date is supplied - return only leaves whose start date lies on or + * is after the provided end date + * 4) No start date or end date is supplied - return all leaves + * @param dateRange Range object representing query range + */ + public LeaveInPeriodPredicate(Range dateRange) { + requireNonNull(dateRange); + this.start = dateRange.getStartDate().orElse(null); + this.end = dateRange.getEndDate().orElse(null); + } + + @Override + public boolean test(Leave leave) { + boolean hasStartDate = start != null; + boolean hasEndDate = end != null; + + boolean isLeaveEndBeforeQueryStart = hasStartDate && leave.getEnd().isBefore(start); + boolean isLeaveStartAfterQueryEnd = hasEndDate && leave.getStart().isAfter(end); + + return !isLeaveEndBeforeQueryStart && !isLeaveStartAfterQueryEnd; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof LeaveInPeriodPredicate)) { + return false; + } + + LeaveInPeriodPredicate otherPredicate = (LeaveInPeriodPredicate) other; + boolean hasMatchingStarts = ( + this.start != null && otherPredicate.start != null && this.start.equals(otherPredicate.start)) + || (this.start == null && otherPredicate.start == null); + boolean hasMatchingEnds = ( + this.end != null && otherPredicate.end != null && this.end.equals(otherPredicate.end)) + || (this.end == null && otherPredicate.end == null); + return hasMatchingStarts && hasMatchingEnds; + } +} diff --git a/src/main/java/seedu/address/model/leave/PersonEntry.java b/src/main/java/seedu/address/model/leave/PersonEntry.java new file mode 100644 index 00000000000..12addfc5d97 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/PersonEntry.java @@ -0,0 +1,44 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.person.ComparablePerson; +import seedu.address.model.person.Name; + +/** + * Represents the Employee field of a JsonAdaptedLeave object in storage + */ +public class PersonEntry implements ComparablePerson { + + private final Name name; + + /** + * Constructs a {@code PersonEntry}. + * + * @param name + */ + public PersonEntry(String name) { + requireNonNull(name); + this.name = new Name(name); + } + + @Override + public Name getName() { + return name; + } + + @Override + public boolean isSamePerson(ComparablePerson otherPerson) { + if (otherPerson == this) { + return true; + } + + return otherPerson != null + && otherPerson.getName().equals(getName()); + } + + @Override + public String toString() { + return name.toString(); + } +} diff --git a/src/main/java/seedu/address/model/leave/Range.java b/src/main/java/seedu/address/model/leave/Range.java new file mode 100644 index 00000000000..b154eaff578 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Range.java @@ -0,0 +1,105 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; + +import seedu.address.model.leave.exceptions.EndBeforeStartException; + + +/** + * Represents a range of dates + * Guarantees: End date will not be before start date, if both are present + */ +public class Range { + public static final String MESSAGE_END_BEFORE_START_ERROR = + "The end date is earlier than the start date!"; + private final Date startDate; + private final Date endDate; + + /** + * Constructs a Range object. Since construction is only possible via the static methods, + * it is guaranteed that the end date will not be before the start date, if both are present + * @param startDate Start date of range/null + * @param endDate End date of range/null + */ + private Range(Date startDate, Date endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Constructs a Range object with a non-null start date and a non-null end date + * @param startDate Start date of range + * @param endDate End date of range + * @return Range object representing a range of dates from start date to end date inclusive + * @throws EndBeforeStartException if end date is before start date + * @throws NullPointerException if start or end date is null + */ + public static Range createNonNullRange(Date startDate, Date endDate) throws EndBeforeStartException, + NullPointerException { + requireNonNull(startDate); + requireNonNull(endDate); + + if (endDate.isBefore(startDate)) { + throw new EndBeforeStartException(); + } + + return new Range(startDate, endDate); + } + + /** + * Constructs a Range object with nullable start date and nullable end date + * @param startDate Start date of range + * @param endDate End date of range + * @return Range object representing a range of dates from start date (if present) to end date (if present) + * inclusive + * @throws EndBeforeStartException if end date is before start date + */ + public static Range createNullableRange(Date startDate, Date endDate) throws EndBeforeStartException { + boolean hasStartDate = startDate != null; + boolean hasEndDate = endDate != null; + + if (hasStartDate && hasEndDate && endDate.isBefore(startDate)) { + throw new EndBeforeStartException(); + } + + return new Range(startDate, endDate); + } + + /** + * Returns the start date of the range as an Optional + * @return Start date of range. Can be an empty Optional if no start date was provided. + */ + public Optional<Date> getStartDate() { + return Optional.ofNullable(startDate); + } + + /** + * Returns the end date of the range as an Optional + * @return End date of range. Can be an empty Optional if no end date was provided. + */ + public Optional<Date> getEndDate() { + return Optional.ofNullable(endDate); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof Range)) { + return false; + } + + Range otherRange = (Range) other; + boolean hasMatchingStart = (this.startDate != null && otherRange.startDate != null + && this.startDate.equals(otherRange.startDate)) + || (this.startDate == null && otherRange.startDate == null); + boolean hasMatchingEnd = (this.endDate != null && otherRange.endDate != null + && this.endDate.equals(otherRange.endDate)) + || (this.endDate == null && otherRange.endDate == null); + return hasMatchingStart && hasMatchingEnd; + } +} diff --git a/src/main/java/seedu/address/model/leave/Status.java b/src/main/java/seedu/address/model/leave/Status.java new file mode 100644 index 00000000000..612fe81421a --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Status.java @@ -0,0 +1,96 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; + +/** + * Represents the status of a leave request. + */ +public class Status { + + /** + * Represents all possible status types of a leave request. + */ + public enum StatusType { + PENDING, APPROVED, REJECTED + } + + public static final String MESSAGE_CONSTRAINTS = "Status should be one of the following: " + + "PENDING, APPROVED, REJECTED"; + private static final String REGEX = "PENDING|APPROVED|REJECTED"; + private final StatusType status; + + /** + * Constructs a Status object from a StatusType enum + * @param status One of PENDING, APPROVED, or REJECTED + */ + private Status(StatusType status) { + requireNonNull(status); + this.status = status; + } + + /** + * Returns a {@code Status} object given a {@code String} status. + * + * @param status String containing status + * @return Status object + * @throws IllegalArgumentException if string does not match known status types + */ + public static Status of(String status) throws IllegalArgumentException { + requireNonNull(status); + if (!isValidStatus(status)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + return new Status(StatusType.valueOf(status)); + } + + /** + * Returns a {@code Status} object given a {@code StatusType} status. + * + * @param status StatusType value + * @return Status object + * @throws NullPointerException if no status supplied + */ + public static Status of(StatusType status) throws NullPointerException { + requireNonNull(status); + return new Status(status); + } + + /** + * Returns whether the string matches the value of a valid StatusType + * @param status String to check + * @return True if string matches a StatusType value, False otherwise + */ + public static boolean isValidStatus(String status) { + return status.matches(REGEX); + } + + /** + * Returns a {@code Status} object with a default PENDING status + * + * @return Status object + */ + public static Status getDefault() { + return new Status(StatusType.PENDING); + } + + public StatusType getStatusType() { + return status; + } + + @Override + public String toString() { + return status.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Status + && status.equals(((Status) other).status)); + } + + @Override + public int hashCode() { + return status.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/leave/Title.java b/src/main/java/seedu/address/model/leave/Title.java new file mode 100644 index 00000000000..1a7fa47a410 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/Title.java @@ -0,0 +1,49 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Leave's title in the leaves book. + */ +public class Title { + + public static final String MESSAGE_CONSTRAINTS = "Leave titles should only contain" + + " alphanumeric characters, spaces, and dashes." + + "It should not be blank"; + public static final String VALIDATION_REGEX = "^[\\p{Alnum} \\-']+$"; + + private final String title; + + /** + * Constructs a {@code Title}. + * + * @param title A valid title. + * @throws IllegalArgumentException if title + */ + public Title(String title) throws NullPointerException, IllegalArgumentException { + requireNonNull(title); + checkArgument(isValidTitle(title), MESSAGE_CONSTRAINTS); + this.title = title; + } + + /** + * Returns true if a given string is a valid title. + */ + public static boolean isValidTitle(String test) { + return test.trim().length() > 0 && test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return title; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Title // instanceof handles nulls + && title.equals(((Title) other).title)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/leave/UniqueLeaveList.java b/src/main/java/seedu/address/model/leave/UniqueLeaveList.java new file mode 100644 index 00000000000..94b3f396bb2 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/UniqueLeaveList.java @@ -0,0 +1,173 @@ +package seedu.address.model.leave; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.leave.exceptions.DuplicateLeaveException; +import seedu.address.model.leave.exceptions.LeaveNotFoundException; +import seedu.address.model.person.Person; + +/** + * A list of leaves that enforces uniqueness between its elements and does not allow nulls. + * A leave is considered unique by comparing using {@code Leave#isSameLeave(Leave)}. As such, adding and updating of + * leaves uses Leave#isSameLeave(Leave) for equality so as to ensure that the leave being added or updated is + * unique in terms of identity in the UniqueLeaveList. However, the removal of a leave uses Leave#equals(Object) so + * as to ensure that the leave with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Leave#isSameLeave(Leave) + */ +public class UniqueLeaveList implements Iterable<Leave> { + + private final ObservableList<Leave> internalList = FXCollections.observableArrayList(); + private final ObservableList<Leave> internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent leave as the given argument. + */ + public boolean contains(Leave leave) { + requireNonNull(leave); + return internalList.stream().anyMatch(leave::isSameLeave); + } + + /** + * Adds a leave to the list. + * The leave must not already exist in the list. + */ + public void add(Leave toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateLeaveException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the leave {@code target} in the list with {@code editedLeave}. + * {@code target} must exist in the list. + * The leave identity of {@code editedLeave} must not be the same as another existing leave in the list. + */ + public void setLeave(Leave target, Leave editedLeave) { + requireAllNonNull(target, editedLeave); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new LeaveNotFoundException(); + } + + if (!target.isSameLeave(editedLeave) && contains(editedLeave)) { + throw new DuplicateLeaveException(); + } + + internalList.set(index, editedLeave); + } + + /** + * Returns true if {@code leaves} contains only unique leaves. + */ + private boolean leavesAreUnique(List<Leave> leaves) { + for (int i = 0; i < leaves.size() - 1; i++) { + for (int j = i + 1; j < leaves.size(); j++) { + if (leaves.get(i).isSameLeave(leaves.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Replaces the contents of this list with {@code leaves}. + * {@code leaves} must not contain duplicate leaves. + */ + public void setLeaves(List<Leave> leaves) { + requireAllNonNull(leaves); + if (!leavesAreUnique(leaves)) { + throw new DuplicateLeaveException(); + } + + internalList.setAll(leaves); + } + + public void setLeaves(UniqueLeaveList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Removes the equivalent leave from the list. + * The leave must exist in the list. + */ + public void remove(Leave toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new LeaveNotFoundException(); + } + } + + /** + * Removes the leaves which belongs to Person p. + * @see Leave#belongsTo(Person) + */ + public void removePerson(Person p) { + requireNonNull(p); + List<Leave> toRemove = internalList.stream().filter((l) -> l.belongsTo(p)).collect(Collectors.toList()); + toRemove.forEach(this::remove); + } + + /** + * Replaces leaves belonging to {@code target} with {@code editedPerson} + * @see Leave#belongsTo(Person) + */ + public void setPerson(Person target, Person editedPerson) { + requireAllNonNull(target, editedPerson); + List<Leave> toEdit = internalList.stream().filter((l) -> l.belongsTo(target)).collect(Collectors.toList()); + toEdit.forEach((l) -> setLeave(l, l.copyWithNewPerson(editedPerson))); + } + + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList<Leave> asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator<Leave> iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueLeaveList)) { + return false; + } + + UniqueLeaveList otherUniqueLeaveList = (UniqueLeaveList) other; + return internalList.equals(otherUniqueLeaveList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } +} diff --git a/src/main/java/seedu/address/model/leave/exceptions/DuplicateLeaveException.java b/src/main/java/seedu/address/model/leave/exceptions/DuplicateLeaveException.java new file mode 100644 index 00000000000..4d74fffe63d --- /dev/null +++ b/src/main/java/seedu/address/model/leave/exceptions/DuplicateLeaveException.java @@ -0,0 +1,11 @@ +package seedu.address.model.leave.exceptions; + +/** + * Signals that the operation will result in duplicate Leaves (Leaves are considered duplicates if they have the same + * identity). + */ +public class DuplicateLeaveException extends RuntimeException { + public DuplicateLeaveException() { + super("Operation would result in duplicate leaves of the same person."); + } +} diff --git a/src/main/java/seedu/address/model/leave/exceptions/EndBeforeStartException.java b/src/main/java/seedu/address/model/leave/exceptions/EndBeforeStartException.java new file mode 100644 index 00000000000..06d2b2cd4d2 --- /dev/null +++ b/src/main/java/seedu/address/model/leave/exceptions/EndBeforeStartException.java @@ -0,0 +1,10 @@ +package seedu.address.model.leave.exceptions; + +/** + * Signals that the end date is before the start date. + */ +public class EndBeforeStartException extends RuntimeException { + public EndBeforeStartException() { + super("End date cannot be before start date."); + } +} diff --git a/src/main/java/seedu/address/model/leave/exceptions/LeaveNotFoundException.java b/src/main/java/seedu/address/model/leave/exceptions/LeaveNotFoundException.java new file mode 100644 index 00000000000..6049220d71e --- /dev/null +++ b/src/main/java/seedu/address/model/leave/exceptions/LeaveNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.leave.exceptions; + +/** + * Signals that the operation was unable to find the specified leave. + */ +public class LeaveNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..bab1b366b35 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -23,15 +23,16 @@ public class Address { * Constructs an {@code Address}. * * @param address A valid address. + * @throws IllegalArgumentException if address is not a valid address. */ - public Address(String address) { + public Address(String address) throws IllegalArgumentException { requireNonNull(address); checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); value = address; } /** - * Returns true if a given string is a valid email. + * Returns true if a given string is a valid address. */ public static boolean isValidAddress(String test) { return test.matches(VALIDATION_REGEX); diff --git a/src/main/java/seedu/address/model/person/ComparablePerson.java b/src/main/java/seedu/address/model/person/ComparablePerson.java new file mode 100644 index 00000000000..d40027edcec --- /dev/null +++ b/src/main/java/seedu/address/model/person/ComparablePerson.java @@ -0,0 +1,22 @@ +package seedu.address.model.person; + +/** + * Interface to allow for comparing Person objects by name + */ +public interface ComparablePerson { + + /** + * Returns true if both persons have the same name. + * + * @param otherPerson + * @return + */ + boolean isSamePerson(ComparablePerson otherPerson); + + /** + * Returns the Name field of a ComparablePerson object + * + * @return + */ + Name getName(); +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..8312f152d6f 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -37,8 +37,9 @@ public class Email { * Constructs an {@code Email}. * * @param email A valid email address. + * @throws IllegalArgumentException if email is not valid. */ - public Email(String email) { + public Email(String email) throws IllegalArgumentException { requireNonNull(email); checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); value = email; diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..f95ddadf040 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -16,16 +16,17 @@ public class Name { * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "^[\\p{Alpha}][\\p{Alpha} \\d-/()]*$"; - public final String fullName; + private final String fullName; /** * Constructs a {@code Name}. * * @param name A valid name. + * @throws IllegalArgumentException if the name contains illegal values */ - public Name(String name) { + public Name(String name) throws IllegalArgumentException { requireNonNull(name); checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); fullName = name; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index 62d19be2977..1cd445af15c 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -19,7 +19,7 @@ public NameContainsKeywordsPredicate(List<String> keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().toString(), keyword)); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..3f3f7980e5a 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -1,7 +1,9 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Objects; @@ -14,7 +16,7 @@ * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated, immutable. */ -public class Person { +public class Person implements ComparablePerson { // Identity fields private final Name name; @@ -37,6 +39,21 @@ public Person(Name name, Phone phone, Email email, Address address, Set<Tag> tag this.tags.addAll(tags); } + /** + * Constructor for Person object that is used to copy another Person object. + * + * @param toCopy Person object to copy. + */ + public Person(Person toCopy) { + requireNonNull(toCopy); + this.name = toCopy.name; + this.phone = toCopy.phone; + this.email = toCopy.email; + this.address = toCopy.address; + this.tags.addAll(toCopy.tags); + } + + @Override public Name getName() { return name; } @@ -61,11 +78,75 @@ public Set<Tag> getTags() { return Collections.unmodifiableSet(tags); } + /** + * Adds tags in toAdd to tags + */ + public void addTags(Collection<Tag> toAdd) { + tags.addAll(toAdd); + } + + /** + * Add tag to tags + */ + public void addTag(Tag tag) { + tags.add(tag); + } + + /** + * Checks if any tag in tags is in this.tags + */ + public boolean hasAnyTags(Collection<Tag> tags) { + return tags.stream().anyMatch(this.tags::contains); + } + + /** + * Checks if this person has all the tags in the argument collection. + * + * @param tags Collection of tags to check. + * @return true if this person has all the tags in the argument collection, false otherwise. + */ + public boolean hasAllTags(Collection<Tag> tags) { + assert tags != null; + assert !tags.isEmpty(); + return this.tags.containsAll(tags); + } + + /** + * Checks if this person has the argument tag. + * + * @param tag Tag to check. + * @return true if this person has the argument tag, false otherwise. + */ + public boolean hasTag(Tag tag) { + assert tag != null; + return this.tags.contains(tag); + } + + /** + * Removes the tags in the argument collection from this person. + * + * @param tags Collection of tags to remove. + */ + public void removeTags(Collection<Tag> tags) { + assert hasAllTags(tags); + this.tags.removeAll(tags); + } + + /** + * Removes a tag from this person. + * + * @param tag Tag to remove. + */ + public void removeTag(Tag tag) { + assert hasTag(tag); + this.tags.remove(tag); + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. */ - public boolean isSamePerson(Person otherPerson) { + public boolean isSamePerson(ComparablePerson otherPerson) { if (otherPerson == this) { return true; } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..54e6aa603dd 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -19,8 +19,9 @@ public class Phone { * Constructs a {@code Phone}. * * @param phone A valid phone number. + * @throws IllegalArgumentException if phone number is not a valid phone number. */ - public Phone(String phone) { + public Phone(String phone) throws IllegalArgumentException { requireNonNull(phone); checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); value = phone; diff --git a/src/main/java/seedu/address/model/person/TagsContainAllTagsPredicate.java b/src/main/java/seedu/address/model/person/TagsContainAllTagsPredicate.java new file mode 100644 index 00000000000..ba2752266bd --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagsContainAllTagsPredicate.java @@ -0,0 +1,45 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class TagsContainAllTagsPredicate implements Predicate<Person> { + private final List<Tag> tags; + + public TagsContainAllTagsPredicate(List<Tag> tags) { + this.tags = tags; + } + + @Override + public boolean test(Person person) { + Set<Tag> personTags = person.getTags(); + return personTags.containsAll(tags); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagsContainAllTagsPredicate)) { + return false; + } + + TagsContainAllTagsPredicate otherNameContainsKeywordsPredicate = (TagsContainAllTagsPredicate) other; + return tags.equals(otherNameContainsKeywordsPredicate.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tags", tags).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/TagsContainSomeTagsPredicate.java b/src/main/java/seedu/address/model/person/TagsContainSomeTagsPredicate.java new file mode 100644 index 00000000000..941c20df9c9 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagsContainSomeTagsPredicate.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + + + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class TagsContainSomeTagsPredicate implements Predicate<Person> { + private final List<Tag> tags; + + public TagsContainSomeTagsPredicate(List<Tag> tags) { + this.tags = tags; + } + + @Override + public boolean test(Person person) { + return tags.stream() + .anyMatch(tag -> person.getTags().contains(tag)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagsContainSomeTagsPredicate)) { + return false; + } + + TagsContainSomeTagsPredicate otherNameContainsKeywordsPredicate = (TagsContainSomeTagsPredicate) other; + return tags.equals(otherNameContainsKeywordsPredicate.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tags", tags).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..40e955f1081 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -31,9 +31,9 @@ public class UniquePersonList implements Iterable<Person> { /** * Returns true if the list contains an equivalent person as the given argument. */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); + public boolean contains(ComparablePerson person) { + requireNonNull(person); + return internalList.stream().anyMatch(person::isSamePerson); } /** @@ -55,11 +55,10 @@ public void add(Person toAdd) { */ public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { + if (!contains(target)) { throw new PersonNotFoundException(); } + int index = internalList.indexOf(target); if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { throw new DuplicatePersonException(); diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..feb469bd587 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,9 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String MESSAGE_CONSTRAINTS = + "Tags names only allows alphanumeric characters, spaces, and dashes."; + public static final String VALIDATION_REGEX = "^[\\p{Alnum} -]+$"; public final String tagName; @@ -18,8 +19,9 @@ public class Tag { * Constructs a {@code Tag}. * * @param tagName A valid tag name. + * @throws IllegalArgumentException if the tag does not have a valid name. */ - public Tag(String tagName) { + public Tag(String tagName) throws IllegalArgumentException { requireNonNull(tagName); checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; diff --git a/src/main/java/seedu/address/storage/AdaptedLeave.java b/src/main/java/seedu/address/storage/AdaptedLeave.java new file mode 100644 index 00000000000..e2f014a842f --- /dev/null +++ b/src/main/java/seedu/address/storage/AdaptedLeave.java @@ -0,0 +1,167 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.time.format.DateTimeParseException; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.PersonEntry; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.model.leave.exceptions.EndBeforeStartException; +import seedu.address.model.person.ComparablePerson; + +/** + * Base class of AdaptedLeave used to serialise and deserialise the Leave class into different formats + */ +abstract class AdaptedLeave implements ToModelTyper<Leave> { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Leave's %s field is missing!"; + + protected final String start; + protected final String end; + protected final String title; + protected final String description; + protected final String status; + protected final ComparablePerson employee; + + /** + * Initialises fields in the AdaptedLeave base class + * @param start Start date of leave + * @param end End date of leave + * @param title Title of leave + * @param description Description of leave + * @param status Status of leave + * @param employee Name of employee + * @throws IllegalArgumentException if name of employee violates naming constraints + * @throws NullPointerException if null is passed in for employee + */ + public AdaptedLeave(String start, String end, String title, String description, + String status, String employee) + throws IllegalArgumentException, NullPointerException { + this.start = start; + this.end = end; + this.title = title; + this.description = description; + this.status = status; + this.employee = new PersonEntry(employee); + } + + /** + * Constructs an AdaptedLeave instance from a Leave object + * @param leave Leave to extract fields from + */ + public AdaptedLeave(Leave leave) { + requireNonNull(leave); + this.start = leave.getStart().toFormattedString(); + this.end = leave.getEnd().toFormattedString(); + this.title = leave.getTitle().toString(); + this.description = leave.getDescription().toString(); + this.status = leave.getStatus().toString(); + this.employee = leave.getEmployee(); + } + + /** + * Converts this Jackson-friendly adapted leave object into the model's {@code Leave} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted leave. + */ + @Override + public Leave toModelType() throws IllegalValueException { + final Title modelTitle = createTitle(); + final Description modelDescription = createDescription(); + final Range dateRange = createRange(); + final Status modelStatus = createStatus(); + final ComparablePerson modelEmployee = validateEmployee(); + return new Leave(modelEmployee, modelTitle, dateRange, modelDescription, modelStatus); + } + + /** + * Checks validity of title field and creates a Title object + * @return Title object containing the title field + * @throws IllegalValueException if the field does not satisfy Title's data constraints + */ + private Title createTitle() throws IllegalValueException { + checkNullField(title, "title"); + if (!Title.isValidTitle(title)) { + throw new IllegalValueException(Title.MESSAGE_CONSTRAINTS); + } + return new Title(title); + } + + /** + * Checks if the field is null + * @param field Field to check + * @param fieldName Name of field + * @throws IllegalValueException if field is null + */ + private void checkNullField(Object field, String fieldName) throws IllegalValueException { + if (field == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, fieldName)); + } + } + + /** + * Checks validity of description field and creates a Description object + * @return Description object containing the description field + * @throws IllegalValueException if the field does not satisfy Description's data constraints + */ + private Description createDescription() throws IllegalValueException { + checkNullField(description, "description"); + if (!Description.isValidDescription(description)) { + throw new IllegalValueException(Description.MESSAGE_CONSTRAINTS); + } + return new Description(description); + } + + /** + * Creates a Range object from the start and end fields + * @return Range representing the range of dates from start to end + * @throws IllegalValueException if the start and end dates are not in the correct format or the + * end date is before the start date + */ + private Range createRange() throws IllegalValueException { + checkNullField(start, "start"); + checkNullField(end, "end"); + + try { + final Date modelStart = Date.of(start); + final Date modelEnd = Date.of(end); + return Range.createNonNullRange(modelStart, modelEnd); + } catch (DateTimeParseException e) { + throw new IllegalValueException(Date.MESSAGE_CONSTRAINTS); + } catch (EndBeforeStartException e) { + throw new IllegalValueException(Range.MESSAGE_END_BEFORE_START_ERROR); + } + } + + /** + * Checks validity of status field and creates a Status object + * @return Status object containing the status field + * @throws IllegalValueException if the field does not satisfy Status's data constraints + */ + private Status createStatus() throws IllegalValueException { + checkNullField(status, "status"); + if (!Status.isValidStatus(status)) { + throw new IllegalValueException(Status.MESSAGE_CONSTRAINTS); + } + return Status.of(status); + } + + /** + * Checks that employee is non-null and employee has a name. It is not necessary to + * test for name validity, or whether the full name in Name is non-null, as these checks + * have already been performed during the initialisation of the Name object. + * @return the same ComparablePerson instance + * @throws IllegalValueException if either the employee field is null or the employee's name + * field is null + */ + private ComparablePerson validateEmployee() throws IllegalValueException { + checkNullField(employee, "employee"); + checkNullField(employee.getName(), "employee name"); + return employee; + } +} diff --git a/src/main/java/seedu/address/storage/AdaptedPerson.java b/src/main/java/seedu/address/storage/AdaptedPerson.java new file mode 100644 index 00000000000..baac69b1ede --- /dev/null +++ b/src/main/java/seedu/address/storage/AdaptedPerson.java @@ -0,0 +1,101 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.IllegalValueException; +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; + +/** + * Base class of AdaptedPerson used to serialise and deserialise the Person class into different formats + */ +abstract class AdaptedPerson implements ToModelTyper<Person> { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + + protected final String name; + protected final String phone; + protected final String email; + protected final String address; + final List<AdaptedTag> tags = new ArrayList<>(); + + /** + * Initialises fields in the AdaptedPerson base class + */ + public AdaptedPerson(String name, String phone, String email, String address, List<AdaptedTag> tags) { + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + if (tags != null) { + this.tags.addAll(tags); + } + } + + /** + * Converts a given {@code Person} into this class + */ + public AdaptedPerson(Person source) { + name = source.getName().toString(); + phone = source.getPhone().value; + email = source.getEmail().value; + address = source.getAddress().value; + tags.addAll(source.getTags().stream() + .map(AdaptedTag::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person. + */ + public Person toModelType() throws IllegalValueException { + final List<Tag> personTags = new ArrayList<>(); + for (AdaptedTag tag : tags) { + personTags.add(tag.toModelType()); + } + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + if (!Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelName = new Name(name); + + if (phone == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); + } + if (!Phone.isValidPhone(phone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelPhone = new Phone(phone); + + if (email == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); + } + if (!Email.isValidEmail(email)) { + throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } + final Email modelEmail = new Email(email); + + if (address == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); + } + if (!Address.isValidAddress(address)) { + throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = new Address(address); + + final Set<Tag> modelTags = new HashSet<>(personTags); + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/AdaptedTag.java similarity index 87% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/seedu/address/storage/AdaptedTag.java index 0df22bdb754..4bff2c9f753 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/address/storage/AdaptedTag.java @@ -7,9 +7,9 @@ import seedu.address.model.tag.Tag; /** - * Jackson-friendly version of {@link Tag}. + * Serializable-friendly version of {@link Tag}. */ -class JsonAdaptedTag { +class AdaptedTag { private final String tagName; @@ -17,14 +17,14 @@ class JsonAdaptedTag { * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. */ @JsonCreator - public JsonAdaptedTag(String tagName) { + public AdaptedTag(String tagName) { this.tagName = tagName; } /** * Converts a given {@code Tag} into this class for Jackson use. */ - public JsonAdaptedTag(Tag source) { + public AdaptedTag(Tag source) { tagName = source.tagName; } @@ -44,5 +44,4 @@ public Tag toModelType() throws IllegalValueException { } return new Tag(tagName); } - } diff --git a/src/main/java/seedu/address/storage/CsvAdaptedLeave.java b/src/main/java/seedu/address/storage/CsvAdaptedLeave.java new file mode 100644 index 00000000000..4060dcaa33c --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvAdaptedLeave.java @@ -0,0 +1,74 @@ +package seedu.address.storage; + +import java.util.Arrays; +import java.util.List; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.util.CsvParsable; +import seedu.address.commons.util.GetValuer; +import seedu.address.model.leave.Leave; + +/** + * CSV-friendly version of {@link Leave} + */ +public class CsvAdaptedLeave extends AdaptedLeave implements CsvParsable { + public static final String TITLE_HEADER = "Title"; + public static final String EMPLOYEE_HEADER = "Employee"; + public static final String START_HEADER = "Start"; + public static final String END_HEADER = "End"; + public static final String DESCRIPTION_HEADER = "Description"; + public static final String STATUS_HEADER = "Status"; + + /** + * Constructs a CsvAdaptedLeave + * @param start Start date of leave + * @param end End date of leave + * @param title Title of leave + * @param description Description of leave + * @param status Status of leave + * @param employee Name of employee + */ + private CsvAdaptedLeave(String start, String end, String title, String description, + String status, String employee) { + super(start, end, title, description, status, employee); + } + + /** + * Converts a given {@code Leave} into this class + */ + public CsvAdaptedLeave(Leave source) { + super(source); + } + + @Override + public List<String> getCsvValues() { + String[] fieldValues = {title, employee.getName().toString(), start, end, description, status}; + return Arrays.asList(fieldValues); + } + + /** + * Returns a list of the column headers + * @return List of column headers + */ + public static List<String> getHeader() { + String[] headers = {TITLE_HEADER, EMPLOYEE_HEADER, START_HEADER, + END_HEADER, DESCRIPTION_HEADER, STATUS_HEADER}; + return Arrays.asList(headers); + } + + /** + * Constructs a CsvAdaptedLeave object from an object where values are queried using getValue() + * @param row An object that contains values associated with a Leave, queried using getValue() + * @return A CsvAdaptedLeave object with field values from the CsvRow + * @throws CsvMissingFieldException if the CsvRow does not contain the field requested + */ + public static CsvAdaptedLeave deserialiseLeave(GetValuer row) throws CsvMissingFieldException { + String title = row.getValue(TITLE_HEADER); + String employee = row.getValue(EMPLOYEE_HEADER); + String start = row.getValue(START_HEADER); + String end = row.getValue(END_HEADER); + String description = row.getValue(DESCRIPTION_HEADER); + String status = row.getValue(STATUS_HEADER); + return new CsvAdaptedLeave(start, end, title, description, status, employee); + } +} diff --git a/src/main/java/seedu/address/storage/CsvAdaptedPerson.java b/src/main/java/seedu/address/storage/CsvAdaptedPerson.java new file mode 100644 index 00000000000..6fc5b9a0913 --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvAdaptedPerson.java @@ -0,0 +1,103 @@ +package seedu.address.storage; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.util.CsvParsable; +import seedu.address.commons.util.GetValuer; +import seedu.address.model.person.Person; + +/** + * CSV-friendly version of {@link Person} + */ +class CsvAdaptedPerson extends AdaptedPerson implements CsvParsable { + /** + * Initialises fields in the CsvAdaptedPerson base class + */ + public static final String TAG_DELIMITER = ","; + + public static final String NAME_HEADER = "Name"; + public static final String PHONE_HEADER = "Phone"; + public static final String EMAIL_HEADER = "Email"; + public static final String ADDRESS_HEADER = "Address"; + public static final String TAGS_HEADER = "Tags"; + + /** + * Constructs a CsvAdaptedPerson + * @param name Name of person + * @param phone Phone number of person + * @param email Email address of person + * @param address Address of person + * @param tags Tags associated with person + */ + private CsvAdaptedPerson(String name, String phone, String email, String address, List<AdaptedTag> tags) { + super(name, phone, email, address, tags); + } + + /** + * Converts a given {@code Person} into this class + */ + public CsvAdaptedPerson(Person source) { + super(source); + } + + /** + * Returns list of string values of the fields in Person + * @return List of string values of fields + */ + @Override + public List<String> getCsvValues() { + String serialisedTags = serialiseTags(); + String[] fieldValues = {name, phone, email, address, serialisedTags}; + return Arrays.asList(fieldValues); + } + + /** + * Serialises the tags into a single string - a different delimiter has to be used to avoid problems in + * deserializing the Person string + * @return CSV string representation of tags + */ + private String serialiseTags() { + List<String> tagNames = tags.stream().map(AdaptedTag::getTagName).collect(Collectors.toList()); + return String.join(TAG_DELIMITER, tagNames); + } + + /** + * Constructs a CsvAdaptedPerson object from an object where values are queried using getValue() + * @param row An object that contains values associated with a Person, queried using getValue() + * @return A CsvAdaptedPerson object with field values from the CsvRow + * @throws CsvMissingFieldException if the CsvRow does not contain the field requested + */ + public static CsvAdaptedPerson deserialisePerson(GetValuer row) throws CsvMissingFieldException { + String name = row.getValue(NAME_HEADER); + String phone = row.getValue(PHONE_HEADER); + String email = row.getValue(EMAIL_HEADER); + String address = row.getValue(ADDRESS_HEADER); + List<AdaptedTag> tags = deserialiseTags(row.getValue(TAGS_HEADER)); + + return new CsvAdaptedPerson(name, phone, email, address, tags); + } + + /** + * Returns a list of the column headers + * @return List of the column headers + */ + public static List<String> getHeader() { + String[] headers = {NAME_HEADER, PHONE_HEADER, EMAIL_HEADER, ADDRESS_HEADER, TAGS_HEADER}; + return Arrays.asList(headers); + } + + /** + * Deserializes the CSV string representation of the tags + * @param tags CSV string representation of tags + * @return List of tags + */ + private static List<AdaptedTag> deserialiseTags(String tags) { + return Arrays.stream(tags.split(TAG_DELIMITER)) + .filter(tagName -> !Objects.equals(tagName, "")) + .map(AdaptedTag::new).collect(Collectors.toList()); + } +} diff --git a/src/main/java/seedu/address/storage/CsvAddressBookStorage.java b/src/main/java/seedu/address/storage/CsvAddressBookStorage.java new file mode 100644 index 00000000000..8a460f9b007 --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvAddressBookStorage.java @@ -0,0 +1,99 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.ReadOnlyAddressBook; + +/** + * Represents a CSV storage for the address book + */ +public class CsvAddressBookStorage implements AddressBookStorage { + private final Path filePath; + + /** + * Constructs a CsvAddressBook + * @param filePath + */ + public CsvAddressBookStorage(Path filePath) { + this.filePath = filePath; + } + + @Override + public Path getAddressBookFilePath() { + return filePath; + } + + @Override + public Optional<ReadOnlyAddressBook> readAddressBook() throws DataLoadingException { + return readAddressBook(filePath); + } + + @Override + public Optional<ReadOnlyAddressBook> readAddressBook(Path filePath) throws DataLoadingException { + requireNonNull(filePath); + + Optional<CsvFile> file = CsvUtil.readCsvFile(filePath); + if (file.isEmpty()) { + return Optional.empty(); + } + List<CsvAdaptedPerson> persons = getPersons(file.get()); + + try { + if (persons.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new CsvSerializableAddressBook(persons).toModelType()); + } catch (IllegalValueException e) { + throw new DataLoadingException(e); + } + } + + /** + * Returns a list of CsvAdaptedPersons initialised with values read from a CsvFile object + * @param file CsvFile containing field values of Persons + * @return List of CsvAdaptedPersons + */ + private static List<CsvAdaptedPerson> getPersons(CsvFile file) { + return file.getRows().map(row -> { + try { + return CsvAdaptedPerson.deserialisePerson(row); + } catch (CsvMissingFieldException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + saveAddressBook(addressBook, filePath); + } + + @Override + public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + requireNonNull(addressBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + List<CsvAdaptedPerson> csvPersons = addressBook.getPersonList().stream() + .map(CsvAdaptedPerson::new).collect(Collectors.toList()); + CsvSerializableAddressBook csvAddressBook = new CsvSerializableAddressBook(csvPersons); + CsvFile fileToSave = csvAddressBook.saveAddressBook(); + + CsvUtil.saveCsvFile(fileToSave, filePath); + } +} diff --git a/src/main/java/seedu/address/storage/CsvLeavesBookStorage.java b/src/main/java/seedu/address/storage/CsvLeavesBookStorage.java new file mode 100644 index 00000000000..5da13537fc1 --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvLeavesBookStorage.java @@ -0,0 +1,95 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyLeavesBook; + +/** + * Represents a CSV storage for the leaves book + */ +public class CsvLeavesBookStorage implements LeavesBookStorage { + private final Path filePath; + + public CsvLeavesBookStorage(Path filePath) { + this.filePath = filePath; + } + @Override + public Path getLeavesBookFilePath() { + return filePath; + } + + @Override + public Optional<ReadOnlyLeavesBook> readLeavesBook(AddressBook addressBook) throws DataLoadingException { + return readLeavesBook(filePath, addressBook); + } + + @Override + public Optional<ReadOnlyLeavesBook> readLeavesBook(Path filePath, AddressBook addressBook) + throws DataLoadingException { + + Optional<CsvFile> file = CsvUtil.readCsvFile(filePath); + if (file.isEmpty()) { + return Optional.empty(); + } + List<CsvAdaptedLeave> leaves = getLeaves(file.get()); + + try { + if (leaves.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new CsvSerializableLeavesBook(leaves).toModelType(addressBook)); + } catch (IllegalValueException e) { + throw new DataLoadingException(e); + } + } + + /** + * Returns a list of CsvAdaptedLeaves initialised with values read from a CsvFile object + * @param file CsvFile containing field values of Leaves + * @return List of CsvAdaptedLeaves + */ + private static List<CsvAdaptedLeave> getLeaves(CsvFile file) { + return file.getRows().map(row -> { + try { + return CsvAdaptedLeave.deserialiseLeave(row); + } catch (CsvMissingFieldException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook) throws IOException { + saveLeavesBook(leavesBook, filePath); + } + + @Override + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook, Path filePath) throws IOException { + requireNonNull(leavesBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + List<CsvAdaptedLeave> csvLeaves = leavesBook.getLeaveList().stream() + .map(CsvAdaptedLeave::new).collect(Collectors.toList()); + CsvSerializableLeavesBook csvLeavesBook = new CsvSerializableLeavesBook(csvLeaves); + CsvFile fileToSave = csvLeavesBook.saveLeavesBook(); + + CsvUtil.saveCsvFile(fileToSave, filePath); + } +} diff --git a/src/main/java/seedu/address/storage/CsvSerializableAddressBook.java b/src/main/java/seedu/address/storage/CsvSerializableAddressBook.java new file mode 100644 index 00000000000..2b47593d852 --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvSerializableAddressBook.java @@ -0,0 +1,27 @@ +package seedu.address.storage; + +import java.util.List; + +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; + + +/** + * An Immutable AddressBook that is serializable to CSV format. + */ +class CsvSerializableAddressBook extends SerializableAddressBook<CsvAdaptedPerson> { + + public CsvSerializableAddressBook(List<CsvAdaptedPerson> persons) { + this.persons.addAll(persons); + } + + /** + * Creates a CsvFile from the list of AdaptedPersons in the serializableAddressBook + * @return CsvFile containing records of all persons in this address book + */ + public CsvFile saveAddressBook() { + CsvFile addressBookFile = new CsvFile(CsvAdaptedPerson.getHeader(), CsvUtil.DELIMITER); + persons.forEach(addressBookFile::addRow); + return addressBookFile; + } +} diff --git a/src/main/java/seedu/address/storage/CsvSerializableLeavesBook.java b/src/main/java/seedu/address/storage/CsvSerializableLeavesBook.java new file mode 100644 index 00000000000..80e8b9c32f3 --- /dev/null +++ b/src/main/java/seedu/address/storage/CsvSerializableLeavesBook.java @@ -0,0 +1,25 @@ +package seedu.address.storage; + +import java.util.List; + +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; + +/** + * An Immutable LeavesBook that is serializable to CSV format. + */ +public class CsvSerializableLeavesBook extends SerializableLeavesBook<CsvAdaptedLeave> { + public CsvSerializableLeavesBook(List<CsvAdaptedLeave> leaves) { + this.leaves.addAll(leaves); + } + + /** + * Creates a CsvFile from the list of AdaptedLeaves in the serializableLeaveBook + * @return CsvFile containing records of all persons in this address book + */ + public CsvFile saveLeavesBook() { + CsvFile leavesBookFile = new CsvFile(CsvAdaptedLeave.getHeader(), CsvUtil.DELIMITER); + leaves.forEach(leavesBookFile::addRow); + return leavesBookFile; + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedLeave.java b/src/main/java/seedu/address/storage/JsonAdaptedLeave.java new file mode 100644 index 00000000000..981cc9cafb1 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedLeave.java @@ -0,0 +1,73 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.model.leave.Leave; + +/** + * Jackson-friendly version of {@link Leave}. + */ +public class JsonAdaptedLeave extends AdaptedLeave { + /** + * Converts a given {@code Leave} into this class for Jackson use. + */ + public JsonAdaptedLeave(Leave source) { + super(source); + } + private JsonAdaptedLeave(String start, String end, String title, String description, + String status, String employee) { + super(start, end, title, description, status, employee); + } + /** + * Constructs a {@code JsonAdaptedLeave} with the given leave details. The purpose of this static method + * is to enable checking of the Employee instance to ensure it is non-null before initialising the base + * AdaptedLeave instance. + */ + @JsonCreator + public static JsonAdaptedLeave of(@JsonProperty("start") String start, @JsonProperty("end") String end, + @JsonProperty("title") String title, + @JsonProperty("description") String description, + @JsonProperty("status") String status, + @JsonProperty("employee") Employee employee) { + requireNonNull(employee); + return new JsonAdaptedLeave(start, end, title, description, status, employee.getName().getFullName()); + } + + /** + * Helper class to access nested field in serialized JSON Leave object + */ + public static class Employee { + private final Name name; + + /** + * Constructs an Employee instance + * @param name Name of employee + */ + @JsonCreator + public Employee(@JsonProperty("name") Name name) { + requireNonNull(name); + this.name = name; + } + public Name getName() { + return name; + } + } + + /** + * Helper class to access nested field in serialized JSON Leave object + */ + public static class Name { + private final String fullName; + @JsonCreator + public Name(@JsonProperty("fullName") String fullName) { + this.fullName = fullName; + } + + public String getFullName() { + return fullName; + } + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..0e615ce0b91 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -1,109 +1,30 @@ package seedu.address.storage; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import seedu.address.commons.exceptions.IllegalValueException; -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; /** * Jackson-friendly version of {@link Person}. */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List<JsonAdaptedTag> tags = new ArrayList<>(); - +class JsonAdaptedPerson extends AdaptedPerson { /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List<JsonAdaptedTag> tags) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tags != null) { - this.tags.addAll(tags); - } + @JsonProperty("tags") List<AdaptedTag> tags) { + super(name, phone, email, address, tags); } /** * Converts a given {@code Person} into this class for Jackson use. */ public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); + super(source); } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List<Tag> personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tags) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set<Tag> modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - } diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java index 41e06f264e1..bdd6df840ef 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -21,7 +21,7 @@ public class JsonAddressBookStorage implements AddressBookStorage { private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - private Path filePath; + private final Path filePath; public JsonAddressBookStorage(Path filePath) { this.filePath = filePath; @@ -47,7 +47,7 @@ public Optional<ReadOnlyAddressBook> readAddressBook(Path filePath) throws DataL Optional<JsonSerializableAddressBook> jsonAddressBook = JsonUtil.readJsonFile( filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { + if (jsonAddressBook.isEmpty()) { return Optional.empty(); } diff --git a/src/main/java/seedu/address/storage/JsonLeavesBookStorage.java b/src/main/java/seedu/address/storage/JsonLeavesBookStorage.java new file mode 100644 index 00000000000..117810e0b33 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonLeavesBookStorage.java @@ -0,0 +1,79 @@ +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.AddressBook; +import seedu.address.model.ReadOnlyLeavesBook; + +/** + * A class to access LeavesBook data stored as a json file on the hard disk. + */ +public class JsonLeavesBookStorage implements LeavesBookStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonLeavesBookStorage.class); + + private final Path filePath; + + public JsonLeavesBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getLeavesBookFilePath() { + return filePath; + } + + @Override + public Optional<ReadOnlyLeavesBook> readLeavesBook(AddressBook addressBook) throws DataLoadingException { + return readLeavesBook(filePath, addressBook); + } + + /** + * Similar to {@link #readLeavesBook(AddressBook addressBook)} ()} + * @param filePath location of the data. Cannot be null. + * @throws DataLoadingException if the file is not found. + */ + public Optional<ReadOnlyLeavesBook> readLeavesBook(Path filePath, AddressBook addressBook) + throws DataLoadingException { + requireNonNull(filePath); + + Optional<JsonSerializableLeavesBook> jsonLeavesBook = JsonUtil.readJsonFile( + filePath, JsonSerializableLeavesBook.class); + if (jsonLeavesBook.isEmpty()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonLeavesBook.get().toModelType(addressBook)); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataLoadingException(ive); + } + } + + @Override + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook) throws IOException { + saveLeavesBook(leavesBook, filePath); + } + + /** + * Similar to {@link #saveLeavesBook(ReadOnlyLeavesBook)} + * @param filePath location of the data. Cannot be null. + */ + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook, Path filePath) throws IOException { + requireNonNull(leavesBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableLeavesBook(leavesBook), filePath); + } +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..1c660c0d58e 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -1,6 +1,5 @@ package seedu.address.storage; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -8,20 +7,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; /** * An Immutable AddressBook that is serializable to JSON format. */ @JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List<JsonAdaptedPerson> persons = new ArrayList<>(); +class JsonSerializableAddressBook extends SerializableAddressBook<JsonAdaptedPerson> { /** * Constructs a {@code JsonSerializableAddressBook} with the given persons. @@ -39,22 +31,4 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List<JsonAdaptedPers public JsonSerializableAddressBook(ReadOnlyAddressBook source) { persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::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 AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - } diff --git a/src/main/java/seedu/address/storage/JsonSerializableLeavesBook.java b/src/main/java/seedu/address/storage/JsonSerializableLeavesBook.java new file mode 100644 index 00000000000..8ca62ec10c1 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonSerializableLeavesBook.java @@ -0,0 +1,33 @@ +package seedu.address.storage; + +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.model.ReadOnlyLeavesBook; + +/** + * An Immutable LeavesBook that is serializable to JSON format. + */ +@JsonRootName(value = "leavesbook") +public class JsonSerializableLeavesBook extends SerializableLeavesBook<JsonAdaptedLeave> { + /** + * Constructs a {@code JsonSerializableLeavesBook} with the given leaves. + */ + @JsonCreator + public JsonSerializableLeavesBook(@JsonProperty("leaves") List<JsonAdaptedLeave> leaves) { + this.leaves.addAll(leaves); + } + + /** + * Converts a given {@code ReadOnlyLeavesBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableLeavesBook}. + */ + public JsonSerializableLeavesBook(ReadOnlyLeavesBook source) { + leaves.addAll(source.getLeaveList().stream().map(JsonAdaptedLeave::new).collect(Collectors.toList())); + } +} diff --git a/src/main/java/seedu/address/storage/LeavesBookStorage.java b/src/main/java/seedu/address/storage/LeavesBookStorage.java new file mode 100644 index 00000000000..7bab0176208 --- /dev/null +++ b/src/main/java/seedu/address/storage/LeavesBookStorage.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.AddressBook; +import seedu.address.model.ReadOnlyLeavesBook; + +/** + * Represents a storage for {@link seedu.address.model.LeavesBook}. + */ +public interface LeavesBookStorage { + + /** + * Returns the file path of the data file. + */ + Path getLeavesBookFilePath(); + + /** + * Returns LeavesBook data as a {@link seedu.address.model.LeavesBook}. + * Returns {@code null} if storage file is not found. + * + * @throws DataLoadingException if loading the data from storage failed. + */ + Optional<ReadOnlyLeavesBook> readLeavesBook(AddressBook addressBook) throws DataLoadingException; + + /** + * @see #getLeavesBookFilePath() + */ + Optional<ReadOnlyLeavesBook> readLeavesBook(Path filePath, AddressBook addressBook) throws DataLoadingException; + + /** + * Saves the given {@link ReadOnlyLeavesBook} to the storage. + * @param leavesBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveLeavesBook(ReadOnlyLeavesBook leavesBook) throws IOException; + + /** + * @see #saveLeavesBook(ReadOnlyLeavesBook) + */ + void saveLeavesBook(ReadOnlyLeavesBook leavesBook, Path filePath) throws IOException; +} diff --git a/src/main/java/seedu/address/storage/SerializableAddressBook.java b/src/main/java/seedu/address/storage/SerializableAddressBook.java new file mode 100644 index 00000000000..1e7773b104d --- /dev/null +++ b/src/main/java/seedu/address/storage/SerializableAddressBook.java @@ -0,0 +1,33 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.AddressBook; +import seedu.address.model.person.Person; + +/** + * An abstract class for an Immutable AddressBook that can be serialised to different types. + */ +abstract class SerializableAddressBook<T extends AdaptedPerson> { + public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + protected final List<T> persons = new ArrayList<>(); + + /** + * Converts this address book into the model's {@code AddressBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public AddressBook toModelType() throws IllegalValueException { + AddressBook addressBook = new AddressBook(); + for (T adaptedPerson : persons) { + Person person = adaptedPerson.toModelType(); + if (addressBook.hasPerson(person)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); + } + addressBook.addPerson(person); + } + return addressBook; + } +} diff --git a/src/main/java/seedu/address/storage/SerializableLeavesBook.java b/src/main/java/seedu/address/storage/SerializableLeavesBook.java new file mode 100644 index 00000000000..6cbc3c84f28 --- /dev/null +++ b/src/main/java/seedu/address/storage/SerializableLeavesBook.java @@ -0,0 +1,38 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.leave.Leave; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * An abstract class for an Immutable LeavesBook that can be serialised to different types. + */ +abstract class SerializableLeavesBook<T extends AdaptedLeave> { + public static final String MESSAGE_DUPLICATE_LEAVE = "Leaves list contains duplicate leave(s)."; + protected final List<T> leaves = new ArrayList<>(); + + /** + * Converts this leaves book into the model's {@code LeavesBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public LeavesBook toModelType(AddressBook addressBook) throws IllegalValueException, PersonNotFoundException { + LeavesBook leavesBook = new LeavesBook(); + for (T adaptedLeave : leaves) { + Leave leave = adaptedLeave.toModelType(); + if (leavesBook.hasLeave(leave)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_LEAVE); + } + if (!addressBook.hasPerson(leave.getEmployee())) { + continue; + } + leavesBook.addLeave(leave); + } + return leavesBook; + } +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 9fba0c7a1d6..559555ec0ee 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,14 +5,16 @@ import java.util.Optional; import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, UserPrefsStorage, LeavesBookStorage { @Override Optional<UserPrefs> readUserPrefs() throws DataLoadingException; @@ -29,4 +31,13 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + @Override + Path getLeavesBookFilePath(); + + @Override + Optional<ReadOnlyLeavesBook> readLeavesBook(AddressBook addressBook) throws DataLoadingException; + + @Override + void saveLeavesBook(ReadOnlyLeavesBook leavesBook) throws IOException; + } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..8297a86d06f 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -7,7 +7,9 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; @@ -18,8 +20,20 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; + private LeavesBookStorage leavesBookStorage; private UserPrefsStorage userPrefsStorage; + /** + * Creates a {@code StorageManager} with the given {@code AddressBookStorage}, + * {@code UserPrefStorage}, and {@code LeavesBookStorage}. + */ + public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage, + LeavesBookStorage leavesBookStorage) { + this.addressBookStorage = addressBookStorage; + this.userPrefsStorage = userPrefsStorage; + this.leavesBookStorage = leavesBookStorage; + } + /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ @@ -75,4 +89,32 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + // ================ LeavesBook methods ============================== + @Override + public Path getLeavesBookFilePath() { + return leavesBookStorage.getLeavesBookFilePath(); + } + + @Override + public Optional<ReadOnlyLeavesBook> readLeavesBook(AddressBook addressBook) throws DataLoadingException { + return readLeavesBook(leavesBookStorage.getLeavesBookFilePath(), addressBook); + } + + @Override + public Optional<ReadOnlyLeavesBook> readLeavesBook(Path filePath, AddressBook addressBook) + throws DataLoadingException { + logger.fine("Attempting to read data from file: " + filePath); + return leavesBookStorage.readLeavesBook(filePath, addressBook); + } + + @Override + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook) throws IOException { + saveLeavesBook(leavesBook, leavesBookStorage.getLeavesBookFilePath()); + } + + @Override + public void saveLeavesBook(ReadOnlyLeavesBook leavesBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + leavesBookStorage.saveLeavesBook(leavesBook, filePath); + } } diff --git a/src/main/java/seedu/address/storage/ToModelTyper.java b/src/main/java/seedu/address/storage/ToModelTyper.java new file mode 100644 index 00000000000..5edcc09ab3b --- /dev/null +++ b/src/main/java/seedu/address/storage/ToModelTyper.java @@ -0,0 +1,12 @@ +package seedu.address.storage; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * Interface for objects that implement toModelType(), which converts an object from + * an adapted variant to the models' variant + * @param <T> Type of object in the model + */ +public interface ToModelTyper<T> { + T toModelType() throws IllegalValueException; +} diff --git a/src/main/java/seedu/address/ui/AddressStatusBarFooter.java b/src/main/java/seedu/address/ui/AddressStatusBarFooter.java new file mode 100644 index 00000000000..167ff1e23d7 --- /dev/null +++ b/src/main/java/seedu/address/ui/AddressStatusBarFooter.java @@ -0,0 +1,30 @@ +package seedu.address.ui; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; + + + +/** + * A ui for the status bar that is displayed at the footer of the application. + */ +public class AddressStatusBarFooter extends UiPart<Region> { + + private static final String FXML = "AddressStatusBarFooter.fxml"; + + @FXML + private Label saveAddressBookLocationStatus; + + /** + * Creates a {@code StatusBarFooter} with the given {@code Path}. + */ + public AddressStatusBarFooter(Path saveLocation) { + super(FXML); + saveAddressBookLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..021baeddf60 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,9 +15,8 @@ */ public class HelpWindow extends UiPart<Stage> { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; - + public static final String USERGUIDE_URL = "https://ay2324s1-cs2103t-w11-1.github.io/tp/UserGuide.html"; + public static final String URL_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; @@ -34,7 +33,7 @@ public class HelpWindow extends UiPart<Stage> { */ public HelpWindow(Stage root) { super(FXML, root); - helpMessage.setText(HELP_MESSAGE); + helpMessage.setText(URL_MESSAGE); } /** diff --git a/src/main/java/seedu/address/ui/LeaveCard.java b/src/main/java/seedu/address/ui/LeaveCard.java new file mode 100644 index 00000000000..52acd8d2961 --- /dev/null +++ b/src/main/java/seedu/address/ui/LeaveCard.java @@ -0,0 +1,80 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.leave.Leave; + +/** + * An UI component that displays information of a {@code leave}. + */ +public class LeaveCard extends UiPart<Region> { + + private static final String FXML = "LeaveListCard.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 <a href="https://github.com/se-edu/addressbook-level4/issues/336">The issue on AddressBook level 4</a> + */ + + public final Leave leave; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label title; + @FXML + private Label description; + @FXML + private Label dateStart; + @FXML + private Label dateEnd; + @FXML + private FlowPane status; + + /** + * Creates a {@code LeaveCode} with the given {@code Leave} and index to display. + */ + public LeaveCard(Leave leave, int displayedIndex) { + super(FXML); + this.leave = leave; + if (leave == null) { + throw new IllegalArgumentException("Leave cannot be null."); + } + + String statusType = leave.getStatus().toString(); + + String styleClass; + switch (statusType) { + case "PENDING": + styleClass = "status-pending"; + break; + case "APPROVED": + styleClass = "status-approved"; + break; + case "REJECTED": + styleClass = "status-rejected"; + break; + default: + styleClass = ""; + } + + id.setText(displayedIndex + ". "); + title.setText(leave.getTitle().toString()); + name.setText("Employee: " + leave.getEmployee().getName().toString()); + description.setText("Description:\n" + leave.getDescription()); + dateStart.setText("Date Start: " + leave.getStart().toString()); + dateEnd.setText("Date End: " + leave.getEnd().toString()); + status.getStyleClass().setAll(styleClass); + status.getChildren().add(new Label(leave.getStatus().toString())); + } +} diff --git a/src/main/java/seedu/address/ui/LeaveListPanel.java b/src/main/java/seedu/address/ui/LeaveListPanel.java new file mode 100644 index 00000000000..8b9a4e495f3 --- /dev/null +++ b/src/main/java/seedu/address/ui/LeaveListPanel.java @@ -0,0 +1,49 @@ +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 javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.leave.Leave; + +/** + * Panel containing the list of persons. + */ +public class LeaveListPanel extends UiPart<Region> { + private static final String FXML = "LeaveListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(LeaveListPanel.class); + + @FXML + private ListView<Leave> leaveListView; + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public LeaveListPanel(ObservableList<Leave> leaveList) { + super(FXML); + leaveListView.setItems(leaveList); + leaveListView.setCellFactory(listView -> new LeaveListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Leave} using a {@code LeaveCard}. + */ + class LeaveListViewCell extends ListCell<Leave> { + @Override + protected void updateItem(Leave leave, boolean empty) { + super.updateItem(leave, empty); + + if (empty || leave == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new LeaveCard(leave, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/LeaveStatusBarFooter.java similarity index 55% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/seedu/address/ui/LeaveStatusBarFooter.java index b577f829423..c232267895c 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/LeaveStatusBarFooter.java @@ -10,19 +10,19 @@ /** * A ui for the status bar that is displayed at the footer of the application. */ -public class StatusBarFooter extends UiPart<Region> { +public class LeaveStatusBarFooter extends UiPart<Region> { - private static final String FXML = "StatusBarFooter.fxml"; + private static final String FXML = "LeaveStatusBarFooter.fxml"; @FXML - private Label saveLocationStatus; + private Label saveLeavesBookLocationStatus; /** * Creates a {@code StatusBarFooter} with the given {@code Path}. */ - public StatusBarFooter(Path saveLocation) { + public LeaveStatusBarFooter(Path saveLocation) { super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + saveLeavesBookLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..25d573f1aba 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -32,6 +32,7 @@ public class MainWindow extends UiPart<Stage> { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private LeaveListPanel leaveListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -44,11 +45,17 @@ public class MainWindow extends UiPart<Stage> { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane leaveListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @FXML - private StackPane statusbarPlaceholder; + private StackPane addressStatusbarPlaceholder; + + @FXML + private StackPane leaveStatusbarPlaceholder; /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. @@ -113,11 +120,17 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + leaveListPanel = new LeaveListPanel(logic.getFilteredLeaveList()); + leaveListPanelPlaceholder.getChildren().add(leaveListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); + AddressStatusBarFooter addressStatusBarFooter = new AddressStatusBarFooter(logic.getAddressBookFilePath()); + LeaveStatusBarFooter leaveStatusBarFooter = new LeaveStatusBarFooter(logic.getLeavesBookFilePath()); + + addressStatusbarPlaceholder.getChildren().add(addressStatusBarFooter.getRoot()); + leaveStatusbarPlaceholder.getChildren().add(leaveStatusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..5f17da256e7 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -48,7 +48,7 @@ public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); + name.setText(person.getName().toString()); phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 7d98e84eedf..dd646a1ba89 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -16,10 +16,13 @@ public class ResultDisplay extends UiPart<Region> { @FXML private TextArea resultDisplay; + /** + * Constructs ResultDisplay with starting welcome message. + */ public ResultDisplay() { super(FXML); + resultDisplay.setText("Welcome to HR Mate! Type 'help' to know more commands :D"); } - public void setFeedbackToUser(String feedbackToUser) { requireNonNull(feedbackToUser); resultDisplay.setText(feedbackToUser); diff --git a/src/main/resources/view/AddressStatusBarFooter.fxml b/src/main/resources/view/AddressStatusBarFooter.fxml new file mode 100644 index 00000000000..8e0f1031d6d --- /dev/null +++ b/src/main/resources/view/AddressStatusBarFooter.fxml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.ColumnConstraints?> +<?import javafx.scene.layout.GridPane?> + +<GridPane styleClass="status-bar" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10" /> + </columnConstraints> + <Label fx:id="saveAddressBookLocationStatus" /> +</GridPane> diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..29dacd6123c 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -350,3 +350,29 @@ -fx-background-radius: 2; -fx-font-size: 11; } +#status { + -fx-hgap: 7; + -fx-vgap: 3; +} +#status .label { + -fx-text-fill: white; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} +#status.status-pending .label { + -fx-background-color: orange; +} + +#status.status-approved .label { + -fx-background-color: green; +} + +#status.status-rejected .label { + -fx-background-color: red; +} + + + + diff --git a/src/main/resources/view/LeaveListCard.fxml b/src/main/resources/view/LeaveListCard.fxml new file mode 100644 index 00000000000..ba1ca8ce89a --- /dev/null +++ b/src/main/resources/view/LeaveListCard.fxml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.ColumnConstraints?> +<?import javafx.scene.layout.FlowPane?> +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.layout.Region?> +<?import javafx.scene.layout.VBox?> + +<HBox id="cardPane" fx:id="cardPane" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"> + <GridPane HBox.hgrow="ALWAYS"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10" prefWidth="150" /> + </columnConstraints> + <VBox alignment="CENTER_LEFT" minHeight="105" GridPane.columnIndex="0"> + <padding> + <Insets top="5" right="5" bottom="5" left="15" /> + </padding> + <HBox spacing="5" alignment="CENTER_LEFT"> + <Label fx:id="id" styleClass="cell_big_label"> + <minWidth> + <!-- Ensures that the label text is never truncated --> + <Region fx:constant="USE_PREF_SIZE" /> + </minWidth> + </Label> + <Label fx:id="title" styleClass="cell_big_label" text="\$title" /> + </HBox> + <FlowPane fx:id="status" /> + <Label fx:id="name" styleClass="cell_small_label" text="\$name" /> + <Label fx:id="dateStart" styleClass="cell_small_label" text="\$dateStart" /> + <Label fx:id="dateEnd" styleClass="cell_small_label" text="\$dateEnd" /> + + <HBox spacing="5" alignment="CENTER_LEFT"> + <Label fx:id="description" styleClass="cell_small_label" text="\$description"> + <minWidth> + <!-- Ensures that the label text is never truncated --> + <Region fx:constant="USE_PREF_SIZE" /> + </minWidth> + </Label> + </HBox> + </VBox> + </GridPane> +</HBox> diff --git a/src/main/resources/view/LeaveListPanel.fxml b/src/main/resources/view/LeaveListPanel.fxml new file mode 100644 index 00000000000..f15da567f43 --- /dev/null +++ b/src/main/resources/view/LeaveListPanel.fxml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import java.net.URL?> +<?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.shape.Line?> + +<VBox xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"> + <stylesheets> + <URL value="@PanelStyle.css" /> <!-- Adjust the path to your CSS file relative to your resources directory --> + </stylesheets> + + <VBox spacing="5" alignment="CENTER_LEFT"> + <padding> + <Insets bottom="5" /> + </padding> + <Label fx:id="leavesList" styleClass="title_label" text="Leave List"/> + <Line startX="0.0" startY="0.0" endX="88.0" endY="0.0" styleClass="separation_line"> + </Line> + </VBox> + <ListView fx:id="leaveListView" VBox.vgrow="ALWAYS" /> +</VBox> diff --git a/src/main/resources/view/LeaveStatusBarFooter.fxml b/src/main/resources/view/LeaveStatusBarFooter.fxml new file mode 100644 index 00000000000..c7618f14d8a --- /dev/null +++ b/src/main/resources/view/LeaveStatusBarFooter.fxml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.ColumnConstraints?> +<?import javafx.scene.layout.GridPane?> + +<GridPane styleClass="status-bar" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10" /> + </columnConstraints> + <Label fx:id="saveLeavesBookLocationStatus" /> +</GridPane> diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..dc037e036a5 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,13 +6,14 @@ <?import javafx.scene.control.Menu?> <?import javafx.scene.control.MenuBar?> <?import javafx.scene.control.MenuItem?> -<?import javafx.scene.control.SplitPane?> <?import javafx.scene.image.Image?> <?import javafx.scene.layout.StackPane?> +<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.VBox?> + <fx:root type="javafx.stage.Stage" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" - title="Address App" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> + title="HR Mate" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> <icons> <Image url="@/images/address_book_32.png" /> </icons> @@ -21,6 +22,7 @@ <stylesheets> <URL value="@DarkTheme.css" /> <URL value="@Extensions.css" /> + <URL value="@MenuBar.css" /> </stylesheets> <VBox> @@ -46,14 +48,28 @@ </padding> </StackPane> - <VBox fx:id="personList" styleClass="pane-with-border" minWidth="340" prefWidth="340" VBox.vgrow="ALWAYS"> - <padding> - <Insets top="10" right="10" bottom="10" left="10" /> - </padding> - <StackPane fx:id="personListPanelPlaceholder" VBox.vgrow="ALWAYS"/> - </VBox> + <HBox HBox.hgrow="ALWAYS" VBox.vgrow="ALWAYS"> + <VBox HBox.hgrow="ALWAYS"> + <VBox fx:id="personList" styleClass="pane-with-border" minWidth="340" prefWidth="340" HBox.hgrow="ALWAYS" VBox.vgrow="ALWAYS"> + <padding> + <Insets top="10" right="10" bottom="10" left="10" /> + </padding> + <StackPane fx:id="personListPanelPlaceholder" VBox.vgrow="ALWAYS"/> + </VBox> + <StackPane fx:id="addressStatusbarPlaceholder" VBox.vgrow="NEVER" /> + </VBox> + + <VBox HBox.hgrow="ALWAYS"> + <VBox fx:id="leaveList" styleClass="pane-with-border" minWidth="340" prefWidth="340" HBox.hgrow="ALWAYS" VBox.vgrow="ALWAYS"> + <padding> + <Insets top="10" right="10" bottom="10" left="10" /> + </padding> + <StackPane fx:id="leaveListPanelPlaceholder" VBox.vgrow="ALWAYS"/> + </VBox> + <StackPane fx:id="leaveStatusbarPlaceholder" VBox.vgrow="NEVER" /> + </VBox> + </HBox> - <StackPane fx:id="statusbarPlaceholder" VBox.vgrow="NEVER" /> </VBox> </Scene> </scene> diff --git a/src/main/resources/view/MenuBar.css b/src/main/resources/view/MenuBar.css new file mode 100644 index 00000000000..b900e3e9706 --- /dev/null +++ b/src/main/resources/view/MenuBar.css @@ -0,0 +1,7 @@ + +/* Custom style for the submenu */ +.menu-item .label { + -fx-text-fill: #FFFFFF; /* Text color */ + -fx-font-size: 14px; + -fx-alignment: center; +} diff --git a/src/main/resources/view/PanelStyle.css b/src/main/resources/view/PanelStyle.css new file mode 100644 index 00000000000..c4205910814 --- /dev/null +++ b/src/main/resources/view/PanelStyle.css @@ -0,0 +1,9 @@ +.title_label { + -fx-font-size: 20px; /* Font size */ + -fx-text-fill: #FFFFFF; /* Text color (red in this example) */ +} + +.separation_line { + -fx-stroke: #808080; /* Set the color you desire (black in this example) */ + -fx-stroke-width: 2; +} diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index a1bb6bbace8..c1a76b9df32 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,8 +1,24 @@ <?xml version="1.0" encoding="UTF-8"?> +<?import javafx.geometry.Insets?> +<?import java.net.URL?> <?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Label?> <?import javafx.scene.layout.VBox?> +<?import javafx.scene.shape.Line?> <VBox xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"> + <stylesheets> + <URL value="@PanelStyle.css" /> <!-- Adjust the path to your CSS file relative to your resources directory --> +</stylesheets> + +<VBox spacing="5" alignment="CENTER_LEFT"> + <padding> + <Insets bottom="5" /> + </padding> + <Label fx:id="employeeList" styleClass="title_label" text="Employee List"/> + <Line startX="0.0" startY="0.0" endX="125.0" endY="0.0" styleClass="separation_line"> + </Line> + </VBox> <ListView fx:id="personListView" VBox.vgrow="ALWAYS" /> </VBox> diff --git a/src/test/data/CsvAddressBookStorageTest/invalidPersonAddressBook.csv b/src/test/data/CsvAddressBookStorageTest/invalidPersonAddressBook.csv new file mode 100644 index 00000000000..6235296542b --- /dev/null +++ b/src/test/data/CsvAddressBookStorageTest/invalidPersonAddressBook.csv @@ -0,0 +1,2 @@ +Name;Phone;Email;Address;Tags +Person with invalid name field: Ha!ns Mu@ster;9482424;hans@example.com;4th street; diff --git a/src/test/data/CsvAddressBookStorageTest/missingFieldCsvFile.csv b/src/test/data/CsvAddressBookStorageTest/missingFieldCsvFile.csv new file mode 100644 index 00000000000..090cdd80234 --- /dev/null +++ b/src/test/data/CsvAddressBookStorageTest/missingFieldCsvFile.csv @@ -0,0 +1,11 @@ +Name;Phone;Email;Address +Alice Pauline;94351253;alice@example.com;123, Jurong West Ave 6, #08-111 +Benson Meier;98765432;johnd@example.com;311, Clementi Ave 2, #02-25 +Carl Kurz;95352563;heinz@example.com;wall street +Daniel Meier;87652533;cornelia@example.com;10th street +Elle Meyer;9482224;werner@example.com;michegan ave +Fiona Kunz;9482427;lydia@example.com;little tokyo +George Best;9482442;anna@example.com;4th street +Georgia Best;9484442;georgia@example.com;4th street +Michael Miller;9482242;michael@example.com;2th street +David Chan;9484342;david@example.com;7th street diff --git a/src/test/data/CsvAddressBookStorageTest/notCsvFile.txt b/src/test/data/CsvAddressBookStorageTest/notCsvFile.txt new file mode 100644 index 00000000000..348980632c7 --- /dev/null +++ b/src/test/data/CsvAddressBookStorageTest/notCsvFile.txt @@ -0,0 +1 @@ +not a CSV file! diff --git a/src/test/data/CsvAddressBookStorageTest/validAndInvalidPersonAddressBook.csv b/src/test/data/CsvAddressBookStorageTest/validAndInvalidPersonAddressBook.csv new file mode 100644 index 00000000000..d92857176c4 --- /dev/null +++ b/src/test/data/CsvAddressBookStorageTest/validAndInvalidPersonAddressBook.csv @@ -0,0 +1,3 @@ +Name;Phone;Email;Address;Tags +Valid Person;9482424;hans@example.com;4th street; +Person With Invalid Phone Field;948asdf424;hans@example.com;4th street; diff --git a/src/test/data/CsvFiles/emptyAddressBook.csv b/src/test/data/CsvFiles/emptyAddressBook.csv new file mode 100644 index 00000000000..9be7ec46f9a --- /dev/null +++ b/src/test/data/CsvFiles/emptyAddressBook.csv @@ -0,0 +1 @@ +Name;Phone;Email;Address;Tags diff --git a/src/test/data/CsvFiles/typicalLeavesBook.csv b/src/test/data/CsvFiles/typicalLeavesBook.csv new file mode 100644 index 00000000000..d6940a64b30 --- /dev/null +++ b/src/test/data/CsvFiles/typicalLeavesBook.csv @@ -0,0 +1,5 @@ +sep=; +Title;Employee;Start;End;Description;Status +Alice's Maternity Leave;Alice Pauline;2020-01-01;2020-01-05;Alice's Maternity Leave Description;PENDING +Benson's Paternity Leave;Benson Meier;2020-01-01;2020-01-05;Benson's Paternity Leave Description;PENDING +Alice's Maternity Leave 2;Alice Pauline;2020-01-03;2020-01-04;;PENDING diff --git a/src/test/data/CsvFiles/typicalPersonsAddressBook.csv b/src/test/data/CsvFiles/typicalPersonsAddressBook.csv new file mode 100644 index 00000000000..97de9db767b --- /dev/null +++ b/src/test/data/CsvFiles/typicalPersonsAddressBook.csv @@ -0,0 +1,12 @@ +sep=; +Name;Phone;Email;Address;Tags +Alice Pauline;94351253;alice@example.com;123, Jurong West Ave 6, #08-111;friends +Benson Meier;98765432;johnd@example.com;311, Clementi Ave 2, #02-25;owesMoney,friends +Carl Kurz;95352563;heinz@example.com;wall street; +Daniel Meier;87652533;cornelia@example.com;10th street;friends +Elle Meyer;9482224;werner@example.com;michegan ave; +Fiona Kunz;9482427;lydia@example.com;little tokyo; +George Best;9482442;anna@example.com;4th street; +Georgia Best;9484442;georgia@example.com;4th street;full time +Michael Miller;9482242;michael@example.com;2th street;remote,full time +David Chan;9484342;david@example.com;7th street;remote,part time diff --git a/src/test/data/CsvLeavesBookStorageTest/notCsvFile.txt b/src/test/data/CsvLeavesBookStorageTest/notCsvFile.txt new file mode 100644 index 00000000000..348980632c7 --- /dev/null +++ b/src/test/data/CsvLeavesBookStorageTest/notCsvFile.txt @@ -0,0 +1 @@ +not a CSV file! diff --git a/src/test/data/CsvLeavesBookStorageTest/validAndInvalidLeavesBook.csv b/src/test/data/CsvLeavesBookStorageTest/validAndInvalidLeavesBook.csv new file mode 100644 index 00000000000..cdfebd54bef --- /dev/null +++ b/src/test/data/CsvLeavesBookStorageTest/validAndInvalidLeavesBook.csv @@ -0,0 +1,5 @@ +sep=; +Title;Employee;Start;End;Description;Status +Alice's Maternity Leave;Alice Pauline;2020-01-01;2020-01-02;Alice's Maternity Leave Description;PENDING +Benson's Paternity Leave;Benson Meier;2020-01-02;2020-01-01;Benson's Paternity Leave Description;PENDING +Alice's Maternity Leave 2;Alice Pauline;2020-01-03;2020-01-04;;PENDING diff --git a/src/test/data/CsvUtilTest/CsvFileMatchingColumns.csv b/src/test/data/CsvUtilTest/CsvFileMatchingColumns.csv new file mode 100644 index 00000000000..ae404d90564 --- /dev/null +++ b/src/test/data/CsvUtilTest/CsvFileMatchingColumns.csv @@ -0,0 +1,2 @@ +first;second;third +firstVal;secondVal;thirdVal diff --git a/src/test/data/CsvUtilTest/CsvFileMismatchColumns.csv b/src/test/data/CsvUtilTest/CsvFileMismatchColumns.csv new file mode 100644 index 00000000000..c3699b834d0 --- /dev/null +++ b/src/test/data/CsvUtilTest/CsvFileMismatchColumns.csv @@ -0,0 +1,4 @@ +first;second;third +firstVal;secondVal;thirdVal +fourthVal;fifthVal +sixthVal;seventhVal;eighthVal;ninthVal diff --git a/src/test/data/JsonLeavesBookStorageTest/InvalidDateFields.json b/src/test/data/JsonLeavesBookStorageTest/InvalidDateFields.json new file mode 100644 index 00000000000..8ed09d8a854 --- /dev/null +++ b/src/test/data/JsonLeavesBookStorageTest/InvalidDateFields.json @@ -0,0 +1,16 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2015/10/02", + "end": "2015/10/02", + "title": "Sick Leave", + "description": "Dealing with a bug", + "status": "PENDING" + } + ] +} diff --git a/src/test/data/JsonLeavesBookStorageTest/InvalidEmployeeName.json b/src/test/data/JsonLeavesBookStorageTest/InvalidEmployeeName.json new file mode 100644 index 00000000000..05b91a16eda --- /dev/null +++ b/src/test/data/JsonLeavesBookStorageTest/InvalidEmployeeName.json @@ -0,0 +1,16 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Tan" + } + }, + "start": "2020-01-01", + "end": "2020-01-02", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "PENDING" + } + ] +} diff --git a/src/test/data/JsonLeavesBookStorageTest/NotJsonFormat.json b/src/test/data/JsonLeavesBookStorageTest/NotJsonFormat.json new file mode 100644 index 00000000000..192eafffe12 --- /dev/null +++ b/src/test/data/JsonLeavesBookStorageTest/NotJsonFormat.json @@ -0,0 +1 @@ +Not Json Format diff --git a/src/test/data/JsonLeavesBookStorageTest/ValidLeaves.json b/src/test/data/JsonLeavesBookStorageTest/ValidLeaves.json new file mode 100644 index 00000000000..323d6fd3c51 --- /dev/null +++ b/src/test/data/JsonLeavesBookStorageTest/ValidLeaves.json @@ -0,0 +1,40 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-01", + "end": "2020-01-05", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "PENDING" + }, + { + "employee": { + "name": { + "fullName": "Benson Meier" + } + }, + "start": "2020-01-01", + "end": "2020-01-05", + "title": "Benson's Paternity Leave", + "description": "Benson's Paternity Leave Description", + "status": "PENDING" + }, + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-03", + "end": "2020-01-04", + "title": "Alice's Maternity Leave 2", + "description": "", + "status": "PENDING" + } + ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..c5d78e75fb3 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -42,5 +42,23 @@ "email" : "anna@example.com", "address" : "4th street", "tags" : [ ] + }, { + "name" : "Georgia Best", + "phone" : "9484442", + "email" : "georgia@example.com", + "address" : "4th street", + "tags" : [ "full time" ] + }, { + "name" : "Michael Miller", + "phone" : "9482242", + "email" : "michael@example.com", + "address" : "2th street", + "tags" : [ "full time", "remote" ] + }, { + "name" : "David Chan", + "phone" : "9484342", + "email" : "david@example.com", + "address" : "7th street", + "tags" : [ "part time", "remote"] } ] } diff --git a/src/test/data/JsonSerializableLeavesBookTest/ValidLeaves.json b/src/test/data/JsonSerializableLeavesBookTest/ValidLeaves.json new file mode 100644 index 00000000000..323d6fd3c51 --- /dev/null +++ b/src/test/data/JsonSerializableLeavesBookTest/ValidLeaves.json @@ -0,0 +1,40 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-01", + "end": "2020-01-05", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "PENDING" + }, + { + "employee": { + "name": { + "fullName": "Benson Meier" + } + }, + "start": "2020-01-01", + "end": "2020-01-05", + "title": "Benson's Paternity Leave", + "description": "Benson's Paternity Leave Description", + "status": "PENDING" + }, + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-03", + "end": "2020-01-04", + "title": "Alice's Maternity Leave 2", + "description": "", + "status": "PENDING" + } + ] +} diff --git a/src/test/data/JsonSerializableLeavesBookTest/duplicateLeaves.json b/src/test/data/JsonSerializableLeavesBookTest/duplicateLeaves.json new file mode 100644 index 00000000000..4da9cadaf59 --- /dev/null +++ b/src/test/data/JsonSerializableLeavesBookTest/duplicateLeaves.json @@ -0,0 +1,28 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-01", + "end": "2020-01-02", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "PENDING" + }, + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020-01-01", + "end": "2020-01-02", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "APPROVED" + } + ] +} diff --git a/src/test/data/JsonSerializableLeavesBookTest/invalidFieldLeaves.json b/src/test/data/JsonSerializableLeavesBookTest/invalidFieldLeaves.json new file mode 100644 index 00000000000..878882efcef --- /dev/null +++ b/src/test/data/JsonSerializableLeavesBookTest/invalidFieldLeaves.json @@ -0,0 +1,16 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Pauline" + } + }, + "start": "2020/01/01", + "end": "2020-01-02", + "title": "Alice's Maternity Leave*#", + "description": "Alice's Maternity Leave Description*#", + "status": "pending" + } + ] +} diff --git a/src/test/data/JsonSerializableLeavesBookTest/invalidPersonLeaves.json b/src/test/data/JsonSerializableLeavesBookTest/invalidPersonLeaves.json new file mode 100644 index 00000000000..05b91a16eda --- /dev/null +++ b/src/test/data/JsonSerializableLeavesBookTest/invalidPersonLeaves.json @@ -0,0 +1,16 @@ +{ + "leaves": [ + { + "employee": { + "name": { + "fullName": "Alice Tan" + } + }, + "start": "2020-01-01", + "end": "2020-01-02", + "title": "Alice's Maternity Leave", + "description": "Alice's Maternity Leave Description", + "status": "PENDING" + } + ] +} diff --git a/src/test/java/seedu/address/commons/util/CsvFileTest.java b/src/test/java/seedu/address/commons/util/CsvFileTest.java new file mode 100644 index 00000000000..d71fc362c7e --- /dev/null +++ b/src/test/java/seedu/address/commons/util/CsvFileTest.java @@ -0,0 +1,207 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.CsvMismatchedColumnException; +import seedu.address.commons.exceptions.CsvMissingFieldException; + +public class CsvFileTest { + private static final String TEST_DELIMITER = ";"; + private static final List<String> TEST_HEADER_LIST = Arrays.asList("first", "second", "third"); + private static final String TEST_HEADER = String.join(TEST_DELIMITER, TEST_HEADER_LIST); + private static final String NON_EXISTENT_HEADER = "fourth"; + private static final List<String> FIRST_ROW_VALS = Arrays.asList("firstVal", "secondVal", "thirdVal"); + + private static final String FIRST_ROW = String.join(TEST_DELIMITER, FIRST_ROW_VALS); + + @Test + public void constructor_stringHeader_returnsCsvFile() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + // Only way to retrieve header for now is to obtain the file stream and + // extract the second string from the stream (since the first string contains the delimiter) + List<String> lines = getLines(file); + + assert(lines.size() == 2); + assertDelimiter(lines, TEST_DELIMITER); + assertEquals(lines.get(1), TEST_HEADER); + } + + private List<String> getLines(CsvFile file) { + return file.getFileStream().collect(Collectors.toList()); + } + + private void assertDelimiter(List<String> lines, String delimiter) { + assert(!lines.isEmpty()); + assertEquals(lines.get(0), String.format(CsvFile.DELIMITER_SPECIFIER, delimiter)); + } + @Test + public void constructor_listHeader_returnsCsvFile() { + CsvFile file = new CsvFile(TEST_HEADER_LIST, TEST_DELIMITER); + List<String> lines = getLines(file); + + assert(lines.size() == 2); + assertDelimiter(lines, TEST_DELIMITER); + String expectedResult = String.join(TEST_DELIMITER, TEST_HEADER_LIST); + assertEquals(lines.get(1), expectedResult); + } + + @Test + public void addRow_stringRow() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + + file.addRow(FIRST_ROW); + List<String> lines = getLines(file); + // 1 line for header, 1 line for row + assert(lines.size() == 3); + assertEquals(lines.get(2), FIRST_ROW); + } + + @Test + public void addRow_listRow() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + + file.addRow(new MockCsvParsable(FIRST_ROW_VALS)); + List<String> lines = getLines(file); + assert(lines.size() == 3); + assertEquals(lines.get(2), FIRST_ROW); + } + + private static class MockCsvParsable implements CsvParsable { + private final List<String> values; + + public MockCsvParsable(List<String> values) { + this.values = values; + } + public List<String> getCsvValues() { + return values; + } + } + + @Test + public void getFileStream() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + List<String> secondRowVals = Arrays.asList("fourthVal", "fifthVal", "sixthVal"); + String secondRow = String.join(TEST_DELIMITER, secondRowVals); + + file.addRow(FIRST_ROW); + file.addRow(secondRow); + List<String> lines = getLines(file); + + assert(lines.size() == 4); + assertDelimiter(lines, TEST_DELIMITER); + assertEquals(lines.get(1), TEST_HEADER); + assertEquals(lines.get(2), FIRST_ROW); + assertEquals(lines.get(3), secondRow); + } + + @Test + public void getRows_success() { + // Also doubles as the test for CsvRow constructor via string, where the number + // of values supplied equals to the number of columns in the header + + // Additionally, this can be taken as the test for CsvRow printRow() + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + file.addRow(FIRST_ROW); + + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + assertEquals(rows.get(0).printRow(), FIRST_ROW); + } + + private List<CsvFile.CsvRow> getRows(CsvFile file) { + return file.getRows().collect(Collectors.toList()); + } + + @Test + public void csvRowAddRow_nullRow_throwsException() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + assertThrows(NullPointerException.class, () -> file.addRow((CsvParsable) null)); + } + @Test + public void csvRowStringConstructor_fewerColsThanHeader_success() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + List<String> vals = Arrays.asList("firstVal", "secondVal"); + String row = String.join(TEST_DELIMITER, vals); + + file.addRow(row); + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + List<String> expectedVals = Arrays.asList("firstVal", "secondVal", ""); + String expectedRow = String.join(TEST_DELIMITER, expectedVals); + assertEquals(rows.get(0).printRow(), expectedRow); + } + + @Test + public void csvRowStringConstructor_moreColsThanHeader_throwsException() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + List<String> vals = Arrays.asList("firstVal", "secondVal", "thirdVal", "fourthVal"); + String row = String.join(TEST_DELIMITER, vals); + + assertThrows(CsvMismatchedColumnException.class, () -> file.addRow(row)); + } + + @Test + public void csvRowListConstructor_sameColsAsHeader_success() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + file.addRow(new MockCsvParsable(FIRST_ROW_VALS)); + + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + assertEquals(rows.get(0).printRow(), FIRST_ROW); + } + + @Test + public void csvRowListConstructor_fewerColsThanHeader_success() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + List<String> vals = Arrays.asList("firstVal", "secondVal"); + + file.addRow(new MockCsvParsable(vals)); + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + List<String> expectedVals = Arrays.asList("firstVal", "secondVal", ""); + String expectedRow = String.join(TEST_DELIMITER, expectedVals); + assertEquals(rows.get(0).printRow(), expectedRow); + } + + @Test + public void csvRowListConstructor_moreColsThanHeader_throwsException() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + List<String> vals = Arrays.asList("firstVal", "secondVal", "thirdVal", "fourthVal"); + + assertThrows(CsvMismatchedColumnException.class, () -> file.addRow(new MockCsvParsable(vals))); + } + + @Test + public void csvRowGetValue_allValuesPresent_success() throws Exception { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + file.addRow(new MockCsvParsable(FIRST_ROW_VALS)); + + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + + CsvFile.CsvRow firstCsvRow = rows.get(0); + for (int i = 0; i < TEST_HEADER_LIST.size(); ++i) { + assertEquals(firstCsvRow.getValue(TEST_HEADER_LIST.get(i)), + FIRST_ROW_VALS.get(i)); + } + } + + @Test + public void csvRowGetValue_missingField_throwsException() { + CsvFile file = new CsvFile(TEST_HEADER, TEST_DELIMITER); + file.addRow(new MockCsvParsable(FIRST_ROW_VALS)); + + List<CsvFile.CsvRow> rows = getRows(file); + assert(rows.size() == 1); + CsvFile.CsvRow firstCsvRow = rows.get(0); + assertThrows(CsvMissingFieldException.class, () -> + firstCsvRow.getValue(NON_EXISTENT_HEADER)); + } +} diff --git a/src/test/java/seedu/address/commons/util/CsvUtilTest.java b/src/test/java/seedu/address/commons/util/CsvUtilTest.java new file mode 100644 index 00000000000..29266b06562 --- /dev/null +++ b/src/test/java/seedu/address/commons/util/CsvUtilTest.java @@ -0,0 +1,121 @@ +package seedu.address.commons.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.testutil.FileAndPathUtil; + +public class CsvUtilTest { + private static final Path TEST_FOLDER_PATH = Paths.get( + "src", "test", "data", "CsvUtilTest"); + private static final String DELIMITER = ";"; + private static final List<String> HEADER_LIST = Arrays.asList("first", "second", "third"); + private static final String HEADER = String.join(DELIMITER, HEADER_LIST); + + private static final List<String> FIRST_ROW_VALS = Arrays.asList("firstVal", "secondVal", "thirdVal"); + private static final String FIRST_ROW = String.join(DELIMITER, FIRST_ROW_VALS); + + @Test + public void readCsvFile_nullFilePath_throwsException() { + assertThrows(NullPointerException.class, () -> CsvUtil.readCsvFile(null)); + } + + @Test + public void readCsvFile_wrongFileType_throwsException() { + Path invalidExtensionFile = addToTestDataPathIfNotNull("badExtension.txt"); + assertThrows(DataLoadingException.class, () -> CsvUtil.readCsvFile(invalidExtensionFile)); + } + + private Path addToTestDataPathIfNotNull(String fileName) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_FOLDER_PATH, fileName); + } + + @Test + public void readCsvFile_nonExistentFile_returnsEmpty() throws Exception { + Path nonExistentFile = addToTestDataPathIfNotNull("nonExistentFile.csv"); + Optional<CsvFile> csvFile = CsvUtil.readCsvFile(nonExistentFile); + assertFalse(csvFile.isPresent()); + } + + @Test + public void readCsvFile_columnCountMatchesHeader_returnsFile() throws Exception { + Path file = addToTestDataPathIfNotNull("CsvFileMatchingColumns.csv"); + Optional<CsvFile> csvFile = CsvUtil.readCsvFile(file); + + List<String> lines = csvFile.get().getFileStream() + .collect(Collectors.toList()); + assert(lines.size() == 3); + // first line in lines should be "sep=;" + assertDelimiter(lines); + assertEquals(lines.get(1), HEADER); + assertEquals(lines.get(2), FIRST_ROW); + } + + private void assertDelimiter(List<String> lines) { + assert(!lines.isEmpty()); + assertEquals(lines.get(0), String.format(CsvFile.DELIMITER_SPECIFIER, DELIMITER)); + } + + @Test + public void readCsvFile_columnCountMismatch_skipsMisMatch() throws Exception { + // if no. of values provided for the row is less than the number of columns, row + // is still read into the CsvFile as the other columns will be padded + + // if no. of values provided for the row is more than the number of columns, the + // entire row will be skipped + Path file = addToTestDataPathIfNotNull("CsvFileMismatchColumns.csv"); + Optional<CsvFile> csvFile = CsvUtil.readCsvFile(file); + List<String> secondRowVals = Arrays.asList("fourthVal", "fifthVal", ""); + String expectedSecondRow = String.join(DELIMITER, secondRowVals); + + List<String> lines = csvFile.get().getFileStream() + .collect(Collectors.toList()); + assert(lines.size() == 4); + assertDelimiter(lines); + assertEquals(lines.get(1), HEADER); + assertEquals(lines.get(2), FIRST_ROW); + assertEquals(lines.get(3), expectedSecondRow); + } + + @Test + public void saveCsvFile_nullFile_throwsException() { + assertThrows(NullPointerException.class, () -> CsvUtil.saveCsvFile(null, + addToTestDataPathIfNotNull("nonRead.csv"))); + } + + @Test + public void saveCsvFile_nullPath_throwsException() { + CsvFile file = new CsvFile(HEADER, DELIMITER); + assertThrows(NullPointerException.class, () -> CsvUtil.saveCsvFile(file, + addToTestDataPathIfNotNull(null))); + } + + @Test + public void saveAndReadCsvFile_success() throws Exception { + CsvFile file = new CsvFile(HEADER, DELIMITER); + file.addRow(FIRST_ROW); + Path savePath = addToTestDataPathIfNotNull("tempFile.csv"); + CsvUtil.saveCsvFile(file, savePath); + + Optional<CsvFile> readFile = CsvUtil.readCsvFile(savePath); + List<String> lines = readFile.get().getFileStream() + .collect(Collectors.toList()); + + assert(lines.size() == 3); + assertDelimiter(lines); + assertEquals(lines.get(1), HEADER); + assertEquals(lines.get(2), FIRST_ROW); + FileAndPathUtil.cleanupCreatedFiles(savePath); + } +} diff --git a/src/test/java/seedu/address/commons/util/FileUtilTest.java b/src/test/java/seedu/address/commons/util/FileUtilTest.java index 1fe5478c756..aeabc6535cd 100644 --- a/src/test/java/seedu/address/commons/util/FileUtilTest.java +++ b/src/test/java/seedu/address/commons/util/FileUtilTest.java @@ -1,13 +1,21 @@ package seedu.address.commons.util; +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.io.IOException; +import java.nio.file.Path; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; public class FileUtilTest { - + private static final String newLine = System.lineSeparator(); + @TempDir + public Path testFolder; @Test public void isValidPath() { // valid path @@ -20,4 +28,28 @@ public void isValidPath() { assertThrows(NullPointerException.class, () -> FileUtil.isValidPath(null)); } + @Test + public void writeAndReadToFile_stringInput_success() throws IOException { + Path filePath = testFolder.resolve("testFile.txt"); + String input = "Hello" + newLine + "world!" + newLine; + + FileUtil.writeToFile(filePath, input); + + String readString = FileUtil.readFromFile(filePath); + assertEquals(input, readString); + } + + @Test + public void writeAndReadToFile_streamInput_success() throws IOException { + Path filePath = testFolder.resolve("testFile.txt"); + String firstLine = "Hello"; + String secondLine = "world!"; + + Stream<String> lineStream = Stream.of(firstLine, secondLine); + FileUtil.writeToFile(filePath, lineStream); + + String expectedResult = "Hello" + newLine + "world!" + newLine; + String readString = FileUtil.readFromFile(filePath); + assertEquals(expectedResult, readString); + } } diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/seedu/address/commons/util/StringUtilTest.java index c56d407bf3f..4f62c326f1f 100644 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ b/src/test/java/seedu/address/commons/util/StringUtilTest.java @@ -140,4 +140,17 @@ public void getDetails_nullGiven_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> StringUtil.getDetails(null)); } + @Test + public void isIntegerMethod() { + assertTrue(StringUtil.isInteger("1")); + assertTrue(StringUtil.isInteger("123")); + assertTrue(StringUtil.isInteger("-123")); // Signed negative integers + assertTrue(StringUtil.isInteger("+123")); // Signed positive integers + + assertFalse(StringUtil.isInteger("")); // Empty string + assertFalse(StringUtil.isInteger(" ")); // Whitespace + assertFalse(StringUtil.isInteger("a")); // Non-numeric characters + assertFalse(StringUtil.isInteger("123a")); // Non-numeric characters + } + } diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index baf8ce336a2..bd6d3920119 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -29,6 +29,7 @@ import seedu.address.model.UserPrefs; import seedu.address.model.person.Person; import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonLeavesBookStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.StorageManager; import seedu.address.testutil.PersonBuilder; @@ -47,8 +48,10 @@ public class LogicManagerTest { public void setUp() { JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); + JsonLeavesBookStorage leavesBookStorage = + new JsonLeavesBookStorage(temporaryFolder.resolve("leavesBook.json")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); - StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); + StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage, leavesBookStorage); logic = new LogicManager(model, storage); } @@ -87,6 +90,11 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredPersonList().remove(0)); } + @Test + public void getFilteredLeaveList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredLeaveList().remove(0)); + } + /** * Executes the command and confirms that * - no exceptions are thrown <br> @@ -123,7 +131,7 @@ private void assertCommandException(String inputCommand, String expectedMessage) */ private void assertCommandFailure(String inputCommand, Class<? extends Throwable> expectedException, String expectedMessage) { - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); } diff --git a/src/test/java/seedu/address/logic/MessagesTest.java b/src/test/java/seedu/address/logic/MessagesTest.java new file mode 100644 index 00000000000..fba494b7396 --- /dev/null +++ b/src/test/java/seedu/address/logic/MessagesTest.java @@ -0,0 +1,56 @@ +package seedu.address.logic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.Messages.EMPLOYEE_HEADER; +import static seedu.address.logic.Messages.END_HEADER; +import static seedu.address.logic.Messages.START_HEADER; +import static seedu.address.logic.Messages.STATUS_HEADER; +import static seedu.address.logic.Messages.TITLE_HEADER; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_DESCRIPTION; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_END; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_PERSON; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_START; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_STATUS; +import static seedu.address.testutil.LeaveBuilder.DEFAULT_TITLE; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.leave.Leave; +import seedu.address.testutil.LeaveBuilder; + +public class MessagesTest { + @Test + public void format_leave() { + // with description + String expectedOutput = EMPLOYEE_HEADER + DEFAULT_PERSON.getName() + + TITLE_HEADER + DEFAULT_TITLE + + START_HEADER + DEFAULT_START + + END_HEADER + DEFAULT_END + + STATUS_HEADER + DEFAULT_STATUS; + Leave leaveWithDescription = new LeaveBuilder() + .withEmployee(DEFAULT_PERSON) + .withTitle(DEFAULT_TITLE.toString()) + .withStart(DEFAULT_START) + .withEnd(DEFAULT_END) + .withStatus(DEFAULT_STATUS.getStatusType()) + .withDescription(DEFAULT_DESCRIPTION.toString()) + .build(); + assertEquals(Messages.format(leaveWithDescription), expectedOutput); + + // without description + expectedOutput = EMPLOYEE_HEADER + DEFAULT_PERSON.getName() + + TITLE_HEADER + DEFAULT_TITLE + + START_HEADER + DEFAULT_START + + END_HEADER + DEFAULT_END + + STATUS_HEADER + DEFAULT_STATUS; + Leave leaveWithoutDescription = new LeaveBuilder() + .withEmployee(DEFAULT_PERSON) + .withTitle(DEFAULT_TITLE.toString()) + .withStart(DEFAULT_START) + .withEnd(DEFAULT_END) + .withStatus(DEFAULT_STATUS.getStatusType()) + .withDescription("") + .build(); + assertEquals(Messages.format(leaveWithoutDescription), expectedOutput); + } +} diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java index 162a0c86031..1b523617a58 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java @@ -2,6 +2,7 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.BeforeEach; @@ -23,14 +24,14 @@ public class AddCommandIntegrationTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); } @Test public void execute_newPerson_success() { Person validPerson = new PersonBuilder().build(); - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); expectedModel.addPerson(validPerson); assertCommandSuccess(new AddCommand(validPerson), model, diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..b67e83a16e2 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -21,7 +21,9 @@ import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; import seedu.address.testutil.PersonBuilder; @@ -85,7 +87,7 @@ public void toStringMethod() { } /** - * A default model stub that have all of the methods failing. + * A default model stub that have all the methods failing. */ private class ModelStub implements Model { @Override @@ -152,11 +154,96 @@ public void setPerson(Person target, Person editedPerson) { public ObservableList<Person> getFilteredPersonList() { throw new AssertionError("This method should not be called."); } + @Override + public ObservableList<Leave> getFilteredLeaveList() { + throw new AssertionError("This method should not be called."); + } @Override public void updateFilteredPersonList(Predicate<Person> predicate) { throw new AssertionError("This method should not be called."); } + + /** + * Replaces leave book data with the data in {@code leavesBook}. + * + * @param leave + */ + @Override + public void deleteLeave(Leave leave) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setLeavesBook(ReadOnlyLeavesBook leavesBook) { + throw new AssertionError("This method should not be called."); + } + + /** + * Returns true if a leave with the same identity as {@code leave} exists in the leave book. + * + * @param leave + */ + @Override + public boolean hasLeave(Leave leave) { + return false; + } + + /** + * Adds the given leave. + * {@code leave} must not already exist in the leave book. + * + * @param leave + */ + @Override + public void addLeave(Leave leave) { + throw new AssertionError("This method should not be called."); + } + + /** + * Replaces the given leave {@code target} with {@code editedPerson}. + * {@code target} must exist in the leave book. + * The leave identity of {@code editedLeave} must not be the same as another existing leave in the leave book. + * + * @param target + * @param editedLeave + */ + @Override + public void setLeave(Leave target, Leave editedLeave) { + throw new AssertionError("This method should not be called."); + } + + + /** + * Returns the user prefs' address book file path. + */ + @Override + public Path getLeavesBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + /** + * Sets the user prefs' address book file path. + * + * @param leavesBookFilePath + */ + @Override + public void setLeavesBookFilePath(Path leavesBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + /** + * Returns the LeavesBook + */ + @Override + public ReadOnlyLeavesBook getLeavesBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredLeaveList(Predicate<Leave> predicate) { + throw new AssertionError("This method should not be called."); + } } /** diff --git a/src/test/java/seedu/address/logic/commands/AddLeaveCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddLeaveCommandIntegrationTest.java new file mode 100644 index 00000000000..1fee0fff675 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddLeaveCommandIntegrationTest.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.testutil.LeaveBuilder; + +/** + * Contains integration tests (interaction with the Model) for {@code AddCommand}. + */ +public class AddLeaveCommandIntegrationTest { + + private Model model; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + } + + @Test + public void execute_newLeave_success() { + Leave validLeave = new LeaveBuilder().build(); + + Model expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); + expectedModel.addLeave(validLeave); + + Range dateRange = Range.createNonNullRange(validLeave.getStart(), validLeave.getEnd()); + + assertCommandSuccess(new AddLeaveCommand(INDEX_FIRST_LEAVE, validLeave.getTitle(), + dateRange, validLeave.getDescription()), model, + String.format(AddLeaveCommand.MESSAGE_SUCCESS, Messages.format(validLeave)), + expectedModel); + } + + @Test + public void constructor_nullLeave_throwsNullPointerException() { + Leave leaveInList = model.getLeavesBook().getLeaveList().get(0); + Range validDateRange = Range.createNonNullRange(leaveInList.getStart(), leaveInList.getEnd()); + + //index null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(null, leaveInList.getTitle(), + validDateRange, leaveInList.getDescription())); + + //title null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_FIRST_LEAVE, null, + validDateRange, leaveInList.getDescription())); + + //range null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_FIRST_LEAVE, leaveInList.getTitle(), + null, leaveInList.getDescription())); + + //description null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_FIRST_LEAVE, leaveInList.getTitle(), + validDateRange, null)); + + //all null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(null, null, + null, null)); + + } + + @Test + public void execute_duplicateLeave_throwsCommandException() { + Leave leaveInList = model.getLeavesBook().getLeaveList().get(0); + + Range dateRange = Range.createNonNullRange(leaveInList.getStart(), leaveInList.getEnd()); + assertCommandFailure(new AddLeaveCommand(INDEX_FIRST_LEAVE, leaveInList.getTitle(), + dateRange, leaveInList.getDescription()), model, + AddLeaveCommand.MESSAGE_DUPLICATE_LEAVE); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/AddLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/AddLeaveCommandTest.java new file mode 100644 index 00000000000..c59897d65ab --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddLeaveCommandTest.java @@ -0,0 +1,397 @@ +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_LEAVE_DATE_END; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_START; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_LEAVE; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +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.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Title; +import seedu.address.model.person.Person; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.PersonBuilder; + +public class AddLeaveCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + @Test + public void constructor_nullLeave_throwsNullPointerException() { + Range validDateRange = Range.createNonNullRange(Date.of(VALID_LEAVE_DATE_START), + Date.of(VALID_LEAVE_DATE_END)); + //index null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(null, new Title(VALID_LEAVE_TITLE), + validDateRange, new Description(VALID_LEAVE_DESCRIPTION))); + + //title null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_THIRD_LEAVE, null, + validDateRange, new Description(VALID_LEAVE_DESCRIPTION))); + + //range null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_THIRD_LEAVE, + new Title(VALID_LEAVE_TITLE), + null, new Description(VALID_LEAVE_DESCRIPTION))); + + //description null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(INDEX_THIRD_LEAVE, + new Title(VALID_LEAVE_TITLE), + validDateRange, null)); + + //all null + assertThrows(NullPointerException.class, () -> new AddLeaveCommand(null, null, + null, null)); + } + + @Test + public void execute_leaveAcceptedByModel_success() throws Exception { + + AddLeaveCommandTest.ModelStubAcceptingLeaveAdded modelStub = new AddLeaveCommandTest + .ModelStubAcceptingLeaveAdded(); + Leave validLeave = new LeaveBuilder().build(); + + Range dateRange = Range.createNonNullRange(validLeave.getStart(), validLeave.getEnd()); + + + CommandResult commandResult = new AddLeaveCommand(INDEX_FIRST_LEAVE, validLeave.getTitle(), + dateRange, validLeave.getDescription()).execute(modelStub); + + assertEquals(String.format(AddLeaveCommand.MESSAGE_SUCCESS, Messages.format(validLeave)), + commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validLeave), modelStub.leavesAdded); + } + + @Test + public void execute_duplicateLeave_throwsCommandException() { + Leave validLeave = new LeaveBuilder().build(); + Range dateRange = Range.createNonNullRange(validLeave.getStart(), validLeave.getEnd()); + + AddLeaveCommand addLeaveCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, validLeave.getTitle(), + dateRange, validLeave.getDescription()); + + AddLeaveCommandTest.ModelStub modelStub = new AddLeaveCommandTest.ModelStubWithLeave(validLeave); + + assertThrows(CommandException.class, + AddLeaveCommand.MESSAGE_DUPLICATE_LEAVE, () -> addLeaveCommand.execute(modelStub)); + } + + @Test + public void equals() { + Person alice = new PersonBuilder().withName("Alice").build(); + Person bob = new PersonBuilder().withName("Bob").build(); + Leave aliceLeave = new LeaveBuilder().withEmployee(alice).build(); + Leave aliceLeaveDifferentTitle = new LeaveBuilder().withEmployee(alice).withTitle("Different Title").build(); + Leave aliceLeaveDifferentRange = new LeaveBuilder().withEmployee(alice) + .withStart(Date.of("2000-01-01")).withEnd(Date.of("2000-01-01")).build(); + Leave aliceLeaveDifferentDescription = new LeaveBuilder().withEmployee(alice) + .withDescription("Different Description").build(); + Leave bobLeave = new LeaveBuilder().withEmployee(bob).build(); + Range aliceDateRange = Range.createNonNullRange(aliceLeave.getStart(), aliceLeave.getEnd()); + Range aliceDifferentDateRange = Range.createNonNullRange(aliceLeaveDifferentRange.getStart(), + aliceLeaveDifferentRange.getEnd()); + Range bobDateRange = Range.createNonNullRange(bobLeave.getStart(), aliceLeave.getEnd()); + + AddLeaveCommand addAliceCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, aliceLeave.getTitle(), + aliceDateRange, aliceLeave.getDescription()); + AddLeaveCommand addAliceDifferentTitleCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, + aliceLeaveDifferentTitle.getTitle(), + aliceDateRange, aliceLeave.getDescription()); + AddLeaveCommand addAliceDifferentRangeCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, + aliceLeave.getTitle(), + aliceDifferentDateRange, aliceLeave.getDescription()); + AddLeaveCommand addAliceDifferentDescriptionCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, + aliceLeave.getTitle(), + aliceDifferentDateRange, aliceLeaveDifferentDescription.getDescription()); + AddLeaveCommand addBobCommand = new AddLeaveCommand(INDEX_SECOND_LEAVE, bobLeave.getTitle(), + bobDateRange, bobLeave.getDescription()); + + // same object -> returns true + assertTrue(addAliceCommand.equals(addAliceCommand)); + + // same values -> returns true + AddLeaveCommand addAliceCommandCopy = new AddLeaveCommand(INDEX_FIRST_LEAVE, aliceLeave.getTitle(), + aliceDateRange, aliceLeave.getDescription()); + assertTrue(addAliceCommand.equals(addAliceCommandCopy)); + + // different types -> returns false + assertFalse(addAliceCommand.equals(1)); + + // null -> returns false + assertFalse(addAliceCommand.equals(null)); + + // different leave -> returns false + assertFalse(addAliceCommand.equals(addBobCommand)); + + //Title is not equal + assertFalse(addAliceCommand.equals(addAliceDifferentTitleCommand)); + + //Range is not equal + assertFalse(addAliceCommand.equals(addAliceDifferentRangeCommand)); + + //Description is not equal + assertFalse(addAliceCommand.equals(addAliceDifferentDescriptionCommand)); + } + + @Test + public void toStringMethod() { + Person alice = new PersonBuilder().withName("Alice").build(); + Leave aliceLeave = new LeaveBuilder().withEmployee(alice).withStart(ALICE_LEAVE.getStart()) + .withEnd(ALICE_LEAVE.getEnd()).build(); + Range aliceDateRange = Range.createNonNullRange(aliceLeave.getStart(), aliceLeave.getEnd()); + AddLeaveCommand addAliceCommand = new AddLeaveCommand(INDEX_FIRST_LEAVE, aliceLeave.getTitle(), + aliceDateRange, aliceLeave.getDescription()); + String expected = AddLeaveCommand.class.getCanonicalName() + "{title=" + ALICE_LEAVE.getTitle() + + ", description=" + ALICE_LEAVE.getDescription() + ", start=" + ALICE_LEAVE.getStart() + + ", end=" + ALICE_LEAVE.getEnd() + "}"; + assertEquals(expected, addAliceCommand.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 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 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 ObservableList<Person> getFilteredPersonList() { + return model.getFilteredPersonList(); + } + @Override + public ObservableList<Leave> getFilteredLeaveList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate<Person> predicate) { + throw new AssertionError("This method should not be called."); + } + + /** + * Replaces leave book data with the data in {@code leavesBook}. + * + * @param leave + */ + @Override + public void deleteLeave(Leave leave) { + throw new AssertionError( + "This method should not be called."); + } + + @Override + public void setLeavesBook(ReadOnlyLeavesBook leavesBook) { + throw new AssertionError("This method should not be called."); + } + + /** + * Returns true if a leave with the same identity as {@code leave} exists in the leave book. + * + * @param leave + */ + @Override + public boolean hasLeave(Leave leave) { + return false; + } + + /** + * Adds the given leave. + * {@code leave} must not already exist in the leave book. + * + * @param leave + */ + @Override + public void addLeave(Leave leave) { + throw new AssertionError( + "This method should not be called."); + } + + /** + * Replaces the given leave {@code target} with {@code editedPerson}. + * {@code target} must exist in the leave book. + * The leave identity of {@code editedLeave} must not be the same as another existing leave in the leave book. + * + * @param target + * @param editedLeave + */ + @Override + public void setLeave(Leave target, Leave editedLeave) { + throw new AssertionError("This method should not be called."); + } + + + /** + * Returns the user prefs' address book file path. + */ + @Override + public Path getLeavesBookFilePath() { + throw new AssertionError( + "This method should not be called."); + } + + /** + * Sets the user prefs' address book file path. + * + * @param leavesBookFilePath + */ + @Override + public void setLeavesBookFilePath(Path leavesBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + /** + * Returns the LeavesBook + */ + @Override + public ReadOnlyLeavesBook getLeavesBook() { + throw new AssertionError( + "This method should not be called."); + } + + @Override + public void updateFilteredLeaveList(Predicate<Leave> predicate) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single leave. + */ + private class ModelStubWithLeave extends ModelStub { + private final Leave leave; + + ModelStubWithLeave(Leave leave) { + requireNonNull(leave); + this.leave = leave; + } + + @Override + public boolean hasLeave(Leave leave) { + requireNonNull(leave); + return this.leave.isSameLeave(leave); + } + } + + /** + * A Model stub that always accept the leave being added. + */ + private class ModelStubAcceptingLeaveAdded extends ModelStub { + final ArrayList<Leave> leavesAdded = new ArrayList<>(); + + @Override + public boolean hasLeave(Leave leave) { + requireNonNull(leave); + return leavesAdded.stream().anyMatch(leave::isSameLeave); + } + + @Override + public void addLeave(Leave leave) { + requireNonNull(leave); + leavesAdded.add(leave); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return new AddressBook(); + } + + @Override + public ReadOnlyLeavesBook getLeavesBook() { + return new LeavesBook(); + } + } + +} diff --git a/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java b/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java new file mode 100644 index 00000000000..99ff93e46ca --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java @@ -0,0 +1,179 @@ +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.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +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.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +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.LeavesBook; +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.tag.Tag; + +/** + * Contains integration tests (interaction with the Model) and unit tests for AddTagCommand. + */ +public class AddTagCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void execute_oneTagUnfilteredList_success() { + AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_HUSBAND))); + + Person firstPerson = model.getFilteredPersonList().get(0); + + Person editedPerson = new Person(firstPerson); + editedPerson.addTag(new Tag(VALID_TAG_HUSBAND)); + + String expectedMessage = String.format(AddTagCommand.MESSAGE_ADD_TAG_SUCCESS, Messages.format(editedPerson)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + + assertCommandSuccess(addTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_multipleTagsUnfilteredList_success() { + Index indexLastPerson = Index.fromOneBased(model.getFilteredPersonList().size()); + + AddTagCommand addTagCommand = new AddTagCommand( + indexLastPerson, List.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + + Person lastPerson = model.getFilteredPersonList().get(indexLastPerson.getZeroBased()); + + Person editedPerson = new Person(lastPerson); + editedPerson.addTags(List.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + + String expectedMessage = String.format(AddTagCommand.MESSAGE_ADD_TAG_SUCCESS, Messages.format(editedPerson)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setPerson(lastPerson, editedPerson); + + assertCommandSuccess(addTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_noTagsSpecifiedFilteredList_success() { + AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, List.of()); + + Person editedPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + String expectedMessage = String.format(AddTagCommand.MESSAGE_ADD_TAG_SUCCESS, Messages.format(editedPerson)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + + assertCommandSuccess(addTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_filteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_HUSBAND))); + + Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + + Person editedPerson = new Person(personInFilteredList); + editedPerson.addTag(new Tag(VALID_TAG_HUSBAND)); + + String expectedMessage = String.format(AddTagCommand.MESSAGE_ADD_TAG_SUCCESS, + Messages.format(editedPerson)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + showPersonAtIndex(expectedModel, INDEX_FIRST_PERSON); + expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); + + assertCommandSuccess(addTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateTag_failure() { + Tag tag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()) + .getTags().stream().findFirst().get(); + AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, List.of(tag)); + + assertCommandFailure(addTagCommand, model, AddTagCommand.MESSAGE_DUPLICATE_TAG); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddTagCommand addTagCommand = new AddTagCommand(outOfBoundIndex, List.of(new Tag(VALID_TAG_FRIEND))); + + assertCommandFailure(addTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @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()); + + AddTagCommand addTagCommand = new AddTagCommand(outOfBoundIndex, List.of(new Tag(VALID_TAG_FRIEND))); + + assertCommandFailure(addTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final AddTagCommand standardCommand = new AddTagCommand( + INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + + // same values -> returns true + AddTagCommand commandWithSameValues = new AddTagCommand( + INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_HUSBAND), new Tag(VALID_TAG_FRIEND))); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // 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 AddTagCommand( + INDEX_SECOND_PERSON, List.of(new Tag(VALID_TAG_HUSBAND), new Tag(VALID_TAG_FRIEND))))); + + // different tagsToAdd -> returns false + assertFalse(standardCommand.equals(new AddTagCommand(INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_HUSBAND))))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + Collection<Tag> tagsToAdd = new HashSet<>(List.of(new Tag(VALID_TAG_FRIEND))); + AddTagCommand addTagCommand = new AddTagCommand(index, tagsToAdd); + String expected = AddTagCommand.class.getCanonicalName() + "{index=" + index + ", tagsToAdd=" + + tagsToAdd + "}"; + assertEquals(expected, addTagCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ApproveLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/ApproveLeaveCommandTest.java new file mode 100644 index 00000000000..c50342949fa --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ApproveLeaveCommandTest.java @@ -0,0 +1,98 @@ +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_INVALID_LEAVE_DISPLAYED_INDEX; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.TestUtil; + +public class ApproveLeaveCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void execute_approveLeave_success() { + Index indexLastLeave = TestUtil.getLastLeaveIndex(model); + Leave originalLeave = TestUtil.getLeave(model, indexLastLeave); + LeaveBuilder leaveInList = new LeaveBuilder(originalLeave); + Leave approvedLeave = leaveInList.withStatus(StatusType.APPROVED).build(); + ApproveLeaveCommand approveLeaveCommand = new ApproveLeaveCommand(indexLastLeave); + String expectedMessage = String.format( + ApproveLeaveCommand.MESSAGE_APPROVE_LEAVE_SUCCESS, + Messages.format(approvedLeave)); + Model expectedModel = new ModelManager( + getTypicalAddressBook(), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setLeave(originalLeave, approvedLeave); + assertCommandSuccess(approveLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateApproveLeave_failure() { + Index indexLastLeave = TestUtil.getLastLeaveIndex(model); + Leave originalLeave = TestUtil.getLeave(model, indexLastLeave); + LeaveBuilder leaveInList = new LeaveBuilder(originalLeave); + Leave approvedLeave = leaveInList.withStatus(StatusType.APPROVED).build(); + model.setLeave(originalLeave, approvedLeave); + ApproveLeaveCommand approveLeaveCommand = new ApproveLeaveCommand(indexLastLeave); + String expectedMessage = String.format( + ApproveLeaveCommand.MESSAGE_DUPLICATE_LEAVE_APPROVE, + Messages.format(approvedLeave)); + assertCommandFailure(approveLeaveCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndex_failure() { + Index outOfBoundIndex = TestUtil.getInvalidLeaveIndex(model); + ApproveLeaveCommand approveLeaveCommand = new ApproveLeaveCommand(outOfBoundIndex); + String expectedMessage = MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX; + assertCommandFailure(approveLeaveCommand, model, expectedMessage); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + ApproveLeaveCommand approveLeaveCommand = new ApproveLeaveCommand(index); + String expected = ApproveLeaveCommand.class.getCanonicalName() + "{index=" + index + "}"; + assertEquals(expected, approveLeaveCommand.toString()); + } + + @Test + public void equalsMethod() { + final Index index = Index.fromOneBased(1); + final ApproveLeaveCommand standardCommand = new ApproveLeaveCommand(index); + + // same values -> returns true + ApproveLeaveCommand commandWithSameValues = new ApproveLeaveCommand(index); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // 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 + Index differentIndex = Index.fromOneBased(2); + assertFalse(standardCommand.equals(new ApproveLeaveCommand(differentIndex))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java index 80d9110c03a..4ab05ce27c0 100644 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java @@ -1,11 +1,13 @@ package seedu.address.logic.commands; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.Test; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; @@ -13,7 +15,7 @@ public class ClearCommandTest { @Test - public void execute_emptyAddressBook_success() { + public void execute_emptyAddressBookLeavesList_success() { Model model = new ModelManager(); Model expectedModel = new ModelManager(); @@ -22,11 +24,31 @@ 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(), getTypicalLeavesBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); expectedModel.setAddressBook(new AddressBook()); + expectedModel.setLeavesBook(new LeavesBook()); assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); } + @Test + public void execute_nonEmptyLeavesList_success() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel.setAddressBook(new AddressBook()); + expectedModel.setLeavesBook(new LeavesBook()); + + assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_clearCommand_success() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel.setAddressBook(new AddressBook()); + expectedModel.setLeavesBook(new LeavesBook()); + + assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); + } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..c494ea41b41 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -2,11 +2,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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_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.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import static seedu.address.testutil.Assert.assertThrows; import java.util.ArrayList; @@ -16,7 +21,10 @@ import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.Model; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.LeaveContainsPersonPredicate; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.testutil.EditPersonDescriptorBuilder; @@ -36,23 +44,29 @@ public class CommandTestUtil { public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; - - 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; - public static final String PHONE_DESC_BOB = " " + PREFIX_PHONE + VALID_PHONE_BOB; - public static final String EMAIL_DESC_AMY = " " + PREFIX_EMAIL + VALID_EMAIL_AMY; - public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; - public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; - public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; - public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; - public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; - - public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names - public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - 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 VALID_TAG_REMOTE = "remote"; + public static final String VALID_TAG_FULL_TIME = "full time"; + + public static final String NAME_DESC_AMY = " " + PREFIX_PERSON_NAME + VALID_NAME_AMY; + public static final String NAME_DESC_BOB = " " + PREFIX_PERSON_NAME + VALID_NAME_BOB; + public static final String PHONE_DESC_AMY = " " + PREFIX_PERSON_PHONE + VALID_PHONE_AMY; + public static final String PHONE_DESC_BOB = " " + PREFIX_PERSON_PHONE + VALID_PHONE_BOB; + public static final String EMAIL_DESC_AMY = " " + PREFIX_PERSON_EMAIL + VALID_EMAIL_AMY; + public static final String EMAIL_DESC_BOB = " " + PREFIX_PERSON_EMAIL + VALID_EMAIL_BOB; + public static final String ADDRESS_DESC_AMY = " " + PREFIX_PERSON_ADDRESS + VALID_ADDRESS_AMY; + public static final String ADDRESS_DESC_BOB = " " + PREFIX_PERSON_ADDRESS + VALID_ADDRESS_BOB; + public static final String TAG_DESC_FRIEND = " " + PREFIX_PERSON_TAG + VALID_TAG_FRIEND; + public static final String TAG_DESC_HUSBAND = " " + PREFIX_PERSON_TAG + VALID_TAG_HUSBAND; + public static final String TAG_DESC_REMOTE = " " + PREFIX_PERSON_TAG + VALID_TAG_REMOTE; + public static final String TAG_DESC_FULL_TIME = " " + PREFIX_PERSON_TAG + VALID_TAG_FULL_TIME; + public static final String TAG_EMPTY = " " + PREFIX_PERSON_TAG; + + public static final String INVALID_NAME_DESC = " " + PREFIX_PERSON_NAME + "James&"; // '&' not allowed in names + public static final String INVALID_PHONE_DESC = " " + PREFIX_PERSON_PHONE + "911a"; // 'a' not allowed in phones + public static final String INVALID_EMAIL_DESC = " " + PREFIX_PERSON_EMAIL + "bob!yahoo"; // missing '@' symbol + public static final String INVALID_ADDRESS_DESC = " " + + PREFIX_PERSON_ADDRESS; // empty string not allowed for addresses + public static final String INVALID_TAG_DESC = " " + PREFIX_PERSON_TAG + "hubby*"; // '*' not allowed in tags public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; @@ -60,6 +74,34 @@ public class CommandTestUtil { public static final EditCommand.EditPersonDescriptor DESC_AMY; public static final EditCommand.EditPersonDescriptor DESC_BOB; + public static final String VALID_LEAVE_TITLE = "Medical leave"; + public static final String VALID_LEAVE_DESCRIPTION = "going on medical leave"; + public static final String VALID_LEAVE_DATE_START = "2023-10-30"; + public static final String VALID_LEAVE_DATE_END = "2023-10-31"; + public static final String VALID_LEAVE_STATUS_APPROVED = "APPROVED"; + public static final String VALID_LEAVE_STATUS_REJECTED = "REJECTED"; + public static final String DESCRIPTION_EMPTY = " " + PREFIX_LEAVE_DESCRIPTION; + + public static final String VALID_LEAVE_TITLE_DESC = " " + PREFIX_LEAVE_TITLE + VALID_LEAVE_TITLE; + public static final String VALID_LEAVE_DESCRIPTION_DESC = " " + PREFIX_LEAVE_DESCRIPTION + VALID_LEAVE_DESCRIPTION; + public static final String VALID_LEAVE_START_DATE_DESC = " " + PREFIX_LEAVE_DATE_START + VALID_LEAVE_DATE_START; + public static final String VALID_LEAVE_END_DATE_DESC = " " + PREFIX_LEAVE_DATE_END + VALID_LEAVE_DATE_END; + public static final String VALID_LEAVE_STATUS_DESC = " " + PREFIX_LEAVE_STATUS + VALID_LEAVE_STATUS_APPROVED; + + public static final String INVALID_LEAVE_TITLE_DESC = " " + PREFIX_LEAVE_TITLE + "Medical leave&"; + public static final String INVALID_LEAVE_DESCRIPTION_DESC = " " + PREFIX_LEAVE_DESCRIPTION + "Going to childcare&"; + public static final String INVALID_LEAVE_DATE_START_DESC = " " + PREFIX_LEAVE_DATE_START + "2023-13-11"; + public static final String INVALID_LEAVE_DATE_END_DESC = " " + PREFIX_LEAVE_DATE_END + "2024-12-32"; + public static final String INVALID_LEAVE_LATE_DATE_START_DESC = " " + PREFIX_LEAVE_DATE_START + + VALID_LEAVE_DATE_END; + public static final String INVALID_LEAVE_EARLY_DATE_END_DESC = " " + PREFIX_LEAVE_DATE_END + VALID_LEAVE_DATE_START; + public static final String INVALID_LEAVE_STATUS_DESC = " " + PREFIX_LEAVE_STATUS + "NONSENSE"; + + public static final String INVALID_LEAVE_DATE_START_LATE_DESC = " " + PREFIX_LEAVE_DATE_START + + VALID_LEAVE_DATE_END; + public static final String INVALID_LEAVE_DATE_END_EARLY_DESC = " " + PREFIX_LEAVE_DATE_END + + VALID_LEAVE_DATE_START; + static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) @@ -105,11 +147,15 @@ public static void assertCommandFailure(Command command, Model actualModel, Stri // we are unable to defensively copy the model for comparison later, so we can // only do so by copying its components. AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook()); - List<Person> expectedFilteredList = new ArrayList<>(actualModel.getFilteredPersonList()); + List<Person> expectedFilteredPersonList = new ArrayList<>(actualModel.getFilteredPersonList()); + LeavesBook expectedLeavesBook = new LeavesBook(actualModel.getLeavesBook()); + List<Leave> expectedFilteredLeaveList = new ArrayList<>(actualModel.getFilteredLeaveList()); assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); assertEquals(expectedAddressBook, actualModel.getAddressBook()); - assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); + assertEquals(expectedFilteredPersonList, actualModel.getFilteredPersonList()); + assertEquals(expectedLeavesBook, actualModel.getLeavesBook()); + assertEquals(expectedFilteredLeaveList, actualModel.getFilteredLeaveList()); } /** * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the @@ -119,10 +165,18 @@ public static void showPersonAtIndex(Model model, Index targetIndex) { assertTrue(targetIndex.getZeroBased() < model.getFilteredPersonList().size()); Person person = model.getFilteredPersonList().get(targetIndex.getZeroBased()); - final String[] splitName = person.getName().fullName.split("\\s+"); + final String[] splitName = person.getName().toString().split("\\s+"); model.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); assertEquals(1, model.getFilteredPersonList().size()); } + /** + * Updates {@code model}'s filtered list to show only the leave that belongs to the {@code person} + */ + public static void showLeaveByPerson(Model model, Person person) { + LeaveContainsPersonPredicate p = new LeaveContainsPersonPredicate(person); + model.updateFilteredLeaveList(p); + } + } diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandIntegrationTest.java new file mode 100644 index 00000000000..f0f8592829c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteCommandIntegrationTest.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static seedu.address.logic.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +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.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.PersonEntry; +import seedu.address.model.person.Person; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.PersonBuilder; + + +public class DeleteCommandIntegrationTest { + private Model model; + + private final Person alice = new PersonBuilder() + .withName(ALICE.getName().toString()) + .withAddress(ALICE.getAddress().toString()) + .withEmail(ALICE.getEmail().toString()) + .withPhone(ALICE.getPhone().toString()).build(); + private final Person bob = new PersonBuilder() + .withName(BOB.getName().toString()) + .withAddress(BOB.getAddress().toString()) + .withEmail(BOB.getEmail().toString()) + .withPhone(BOB.getPhone().toString()).build(); + + private final Leave aliceLeave = new LeaveBuilder().withEmployee( + new PersonEntry(ALICE.getName().toString())).build(); + private final Leave bobLeave = new LeaveBuilder().withEmployee( + new PersonEntry(BOB.getName().toString())).build(); + + @Test + public void execute_deletePerson_success() { + // Checks that deleting a person deletes them from both address book as well as their associated leaves + // from the leaves book + Index deleteCommandIndex = Index.fromOneBased(1); + DeleteCommand command = new DeleteCommand(deleteCommandIndex); + + assertNotEquals(aliceLeave.getEmployee(), alice); + assertNotEquals(bobLeave.getEmployee(), bob); + + AddressBook ab = new AddressBook(); + ab.addPerson(alice); + ab.addPerson(bob); + LeavesBook lb = new LeavesBook(); + lb.addLeave(aliceLeave); + lb.addLeave(bobLeave); + model = new ModelManager(ab, lb, new UserPrefs()); + + AddressBook expectedAb = new AddressBook(); + expectedAb.addPerson(bob); + LeavesBook expectedLb = new LeavesBook(); + expectedLb.addLeave(bobLeave); + Model expectedModel = new ModelManager(expectedAb, expectedLb, new UserPrefs()); + + // ensure that we will be deleting alice + assertEquals(model.getFilteredPersonList().get(0), alice); + + assertCommandSuccess(command, model, + String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(alice)), + expectedModel); + } + + @Test + public void execute_indexOutOfBounds_throwsCommandException() { + Index deleteCommandIndex = Index.fromOneBased(2); + DeleteCommand command = new DeleteCommand(deleteCommandIndex); + + AddressBook ab = new AddressBook(); + ab.addPerson(alice); + LeavesBook lb = new LeavesBook(); + model = new ModelManager(ab, lb, new UserPrefs()); + + assertCommandFailure(command, model, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java index b6f332eabca..c42ebb3ab7b 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java @@ -8,6 +8,7 @@ import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.Test; @@ -25,7 +26,7 @@ */ public class DeleteCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); @Test public void execute_validIndexUnfilteredList_success() { @@ -35,7 +36,7 @@ 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.getLeavesBook(), new UserPrefs()); expectedModel.deletePerson(personToDelete); assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); @@ -59,7 +60,7 @@ 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.getLeavesBook(), new UserPrefs()); expectedModel.deletePerson(personToDelete); showNoPerson(expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/DeleteLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteLeaveCommandTest.java new file mode 100644 index 00000000000..33209d9bfe9 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteLeaveCommandTest.java @@ -0,0 +1,103 @@ +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.CommandTestUtil.showLeaveByPerson; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +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.leave.Leave; +import seedu.address.testutil.TypicalPersons; + +public class DeleteLeaveCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Leave leaveToDelete = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + DeleteLeaveCommand deleteLeaveCommand = new DeleteLeaveCommand(INDEX_FIRST_LEAVE); + + String expectedMessage = String.format(DeleteLeaveCommand.MESSAGE_DELETE_LEAVE_SUCCESS, + Messages.format(leaveToDelete)); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); + expectedModel.deleteLeave(leaveToDelete); + + assertCommandSuccess(deleteLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundsIndex = Index.fromOneBased(model.getFilteredLeaveList().size() + 1); + DeleteLeaveCommand deleteLeaveCommand = new DeleteLeaveCommand(outOfBoundsIndex); + + assertCommandFailure(deleteLeaveCommand, model, Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showLeaveByPerson(model, TypicalPersons.BENSON); + + Leave leaveToDelete = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + DeleteLeaveCommand command = new DeleteLeaveCommand(INDEX_FIRST_LEAVE); + + String expectedMessage = String.format(DeleteLeaveCommand.MESSAGE_DELETE_LEAVE_SUCCESS, + Messages.format(leaveToDelete)); + + Model expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); + expectedModel.deleteLeave(leaveToDelete); + showNoLeave(expectedModel); + + assertCommandSuccess(command, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_failure() { + showLeaveByPerson(model, TypicalPersons.BENSON); + + Index outofBoundsIndex = INDEX_SECOND_LEAVE; + assertTrue(outofBoundsIndex.getZeroBased() < model.getLeavesBook().getLeaveList().size()); + + DeleteLeaveCommand deleteLeaveCommand = new DeleteLeaveCommand(outofBoundsIndex); + + assertCommandFailure(deleteLeaveCommand, model, Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + + } + + @Test + public void equals() { + DeleteLeaveCommand deleteLeaveFirstCommand = new DeleteLeaveCommand(INDEX_FIRST_LEAVE); + DeleteLeaveCommand deleteLeaveSecondCommand = new DeleteLeaveCommand(INDEX_SECOND_LEAVE); + + assertTrue(deleteLeaveFirstCommand.equals(deleteLeaveFirstCommand)); + + DeleteLeaveCommand deleteLeaveFirstCommandCopy = new DeleteLeaveCommand(INDEX_FIRST_LEAVE); + assertTrue(deleteLeaveFirstCommand.equals(deleteLeaveFirstCommandCopy)); + + assertFalse(deleteLeaveFirstCommand.equals(1)); + + assertFalse(deleteLeaveFirstCommand.equals(null)); + + assertFalse(deleteLeaveFirstCommand.equals(deleteLeaveSecondCommand)); + } + + /** + * Updates {@code model}'s filtered list to show no one + */ + private void showNoLeave(Model model) { + model.updateFilteredLeaveList(l -> false); + + assertTrue(model.getFilteredLeaveList().isEmpty()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteTagCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteTagCommandTest.java new file mode 100644 index 00000000000..577fd423fd1 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteTagCommandTest.java @@ -0,0 +1,138 @@ +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.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; +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.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +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.LeavesBook; +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.tag.Tag; + +public class DeleteTagCommandTest { + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DeleteTagCommand(null, List.of(new Tag(VALID_TAG_FRIEND)))); + } + + @Test + public void constructor_nullTags_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DeleteTagCommand(INDEX_FIRST_PERSON, null)); + } + + @Test + public void execute_validIndexUnfilteredList_success() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new Person(firstPerson); + editedPerson.removeTags(firstPerson.getTags()); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(INDEX_FIRST_PERSON, firstPerson.getTags()); + String expectedMessage = String.format( + DeleteTagCommand.MESSAGE_REMOVE_TAG_SUCCESS, Messages.format(editedPerson)); + ModelManager expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + assertCommandSuccess(deleteTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_failure() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(outOfBoundIndex, List.of(new Tag(VALID_TAG_FRIEND))); + assertCommandFailure(deleteTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new Person(personInFilteredList); + editedPerson.removeTags(personInFilteredList.getTags()); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(INDEX_FIRST_PERSON, personInFilteredList.getTags()); + String expectedMessage = String.format( + DeleteTagCommand.MESSAGE_REMOVE_TAG_SUCCESS, Messages.format(editedPerson)); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setPerson(personInFilteredList, editedPerson); + assertCommandSuccess(deleteTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_failure() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(outOfBoundIndex, List.of(new Tag(VALID_TAG_FRIEND))); + assertCommandFailure(deleteTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_personDoesNotHaveAllTags_failure() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(INDEX_FIRST_PERSON, + List.of(new Tag(VALID_TAG_FRIEND), new Tag("random"))); + assertCommandFailure(deleteTagCommand, model, DeleteTagCommand.MESSAGE_MISSING_TAGS); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + Collection<Tag> tagsToAdd = new HashSet<>(List.of(new Tag(VALID_TAG_FRIEND))); + DeleteTagCommand deleteTagCommand = new DeleteTagCommand(index, tagsToAdd); + String expected = DeleteTagCommand.class.getCanonicalName() + "{index=" + index + ", tagsToAdd=" + + tagsToAdd + "}"; + assertEquals(expected, deleteTagCommand.toString()); + } + + @Test + public void equalsMethod() { + final DeleteTagCommand standardCommand = new DeleteTagCommand(INDEX_FIRST_PERSON, + List.of(new Tag(VALID_TAG_FRIEND))); + + // same values -> returns true + DeleteTagCommand commandWithSameValues = new DeleteTagCommand(INDEX_FIRST_PERSON, + List.of(new Tag(VALID_TAG_FRIEND))); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // 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 DeleteTagCommand(INDEX_SECOND_PERSON, + List.of(new Tag(VALID_TAG_FRIEND))))); + + // different tags -> returns false + assertFalse(standardCommand.equals(new DeleteTagCommand(INDEX_FIRST_PERSON, + List.of(new Tag("different"))))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/EditCommandIntegrationTest.java new file mode 100644 index 00000000000..fd63971d5f6 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditCommandIntegrationTest.java @@ -0,0 +1,92 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.EditCommand.MESSAGE_DUPLICATE_PERSON; +import static seedu.address.logic.commands.EditCommand.MESSAGE_EDIT_PERSON_SUCCESS; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.PersonEntry; +import seedu.address.model.person.Person; +import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.PersonBuilder; + + +public class EditCommandIntegrationTest { + private Model model; + + @BeforeEach + public void setUp() { + AddressBook ab = new AddressBook(); + LeavesBook lb = new LeavesBook(); + Person person = new PersonBuilder() + .withName(ALICE.getName().toString()) + .withAddress(ALICE.getAddress().toString()) + .withEmail(ALICE.getEmail().toString()) + .withPhone(ALICE.getPhone().toString()).build(); + ab.addPerson(person); + + + // personEntry used to ensure that leaves are associated to employees via ComparablePerson::isSamePerson + // rather than equals + Leave loadedLeave = new LeaveBuilder().withEmployee(new PersonEntry(ALICE.getName().toString())).build(); + lb.addLeave(loadedLeave); + + model = new ModelManager(ab, lb, new UserPrefs()); + } + + @Test + public void execute_renamePerson_success() { + // Checks that editing a person's name changes both addressBook and leaveBook entries, even for leaves + // that were not created in the current session + EditPersonDescriptor editDescriptor = new EditPersonDescriptorBuilder() + .withName(BOB.getName().toString()).build(); + + Index editCommandIndex = Index.fromOneBased(1); + EditCommand command = new EditCommand(editCommandIndex, editDescriptor); + + AddressBook expectedAb = new AddressBook(); + Person editedPerson = new PersonBuilder() + .withName(BOB.getName().toString()) + .withAddress(ALICE.getAddress().toString()) + .withEmail(ALICE.getEmail().toString()) + .withPhone(ALICE.getPhone().toString()).build(); + expectedAb.addPerson(editedPerson); + + LeavesBook expectedLb = new LeavesBook(); + Leave expectedLeave = new LeaveBuilder().withEmployee(new PersonEntry(BOB.getName().toString())).build(); + expectedLb.addLeave(expectedLeave); + + Model expectedModel = new ModelManager(expectedAb, expectedLb, new UserPrefs()); + + assertCommandSuccess(command, model, + String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)), + expectedModel); + } + + @Test + public void execute_duplicatePerson_throwsCommandException() { + model.addPerson(BOB); + + EditPersonDescriptor editDescriptor = new EditPersonDescriptorBuilder() + .withName(BOB.getName().toString()).build(); + Index editCommandIndex = Index.fromOneBased(1); + EditCommand command = new EditCommand(editCommandIndex, editDescriptor); + + assertCommandFailure(command, model, MESSAGE_DUPLICATE_PERSON); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 469dd97daa7..597021fab56 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -13,6 +13,7 @@ import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.Test; @@ -21,6 +22,7 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; @@ -33,7 +35,7 @@ */ public class EditCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); @Test public void execute_allFieldsSpecifiedUnfilteredList_success() { @@ -43,7 +45,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()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -64,7 +67,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()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); expectedModel.setPerson(lastPerson, editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); @@ -77,7 +81,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()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -93,7 +98,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()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/EditLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/EditLeaveCommandTest.java new file mode 100644 index 00000000000..dddc0130abc --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditLeaveCommandTest.java @@ -0,0 +1,284 @@ + +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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +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.showLeaveByPerson; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_LEAVE; +import static seedu.address.testutil.TypicalLeaves.DEFAULT_END; +import static seedu.address.testutil.TypicalLeaves.DEFAULT_START; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.model.leave.exceptions.EndBeforeStartException; +import seedu.address.testutil.EditLeaveDescriptorBuilder; +import seedu.address.testutil.LeaveBuilder; + +public class EditLeaveCommandTest { + private static final String DEFAULT_TITLE = "New Title"; + private static final String DEFAULT_DESCRIPTION = "New Leave Description"; + private static final String DEFAULT_EMPTY_DESCRIPTION = ""; + private static final StatusType DEFAULT_STATUS = StatusType.REJECTED; + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), + new UserPrefs()); + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + Leave editedLeave = new LeaveBuilder().build(); + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder(editedLeave).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, descriptor); + + String expectedMessage = String.format(EditLeaveCommand.MESSAGE_EDIT_LEAVE_SUCCESS, + Messages.format(editedLeave)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setLeave(model.getFilteredLeaveList().get(0), editedLeave); + + assertCommandSuccess(editLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() { + Index indexLastLeave = Index.fromOneBased(model.getFilteredLeaveList().size()); + Leave lastLeave = model.getFilteredLeaveList().get(indexLastLeave.getZeroBased()); + + LeaveBuilder leaveInList = new LeaveBuilder(lastLeave); + Leave editedLeave = leaveInList.withTitle(DEFAULT_TITLE).withDescription(DEFAULT_DESCRIPTION) + .withStatus(DEFAULT_STATUS).build(); + + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(DEFAULT_TITLE) + .withDescription(DEFAULT_DESCRIPTION).withStatus(DEFAULT_STATUS).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(indexLastLeave, descriptor); + + String expectedMessage = String.format(EditLeaveCommand.MESSAGE_EDIT_LEAVE_SUCCESS, + Messages.format(editedLeave)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setLeave(lastLeave, editedLeave); + + assertCommandSuccess(editLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_noFieldSpecifiedUnfilteredList_success() { + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, new EditLeaveDescriptor()); + Leave editedLeave = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + + String expectedMessage = String.format(EditLeaveCommand.MESSAGE_EDIT_LEAVE_SUCCESS, + Messages.format(editedLeave)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + + assertCommandSuccess(editLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_filteredList_success() { + showLeaveByPerson(model, model.getFilteredPersonList().get(0)); + + Leave leaveInFilteredList = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + Leave editedLeave = new LeaveBuilder(leaveInFilteredList).withTitle(DEFAULT_TITLE).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, new EditLeaveDescriptorBuilder() + .withTitle(DEFAULT_TITLE).build()); + + String expectedMessage = String.format(EditLeaveCommand.MESSAGE_EDIT_LEAVE_SUCCESS, + Messages.format(editedLeave)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setLeave(model.getFilteredLeaveList().get(0), editedLeave); + showLeaveByPerson(expectedModel, expectedModel.getFilteredPersonList().get(0)); + + assertCommandSuccess(editLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateLeaveUnfilteredList_failure() { + Leave firstLeave = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder(firstLeave).build(); + // third index has same employee as first index + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_THIRD_LEAVE, descriptor); + assertCommandFailure(editLeaveCommand, model, EditLeaveCommand.MESSAGE_DUPLICATED_LEAVE); + } + + @Test + public void execute_duplicateLeaveFilteredList_failure() { + showLeaveByPerson(model, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + + Leave firstLeave = model.getFilteredLeaveList().get(INDEX_FIRST_LEAVE.getZeroBased()); + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder(firstLeave).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_SECOND_LEAVE, descriptor); + assertCommandFailure(editLeaveCommand, model, EditLeaveCommand.MESSAGE_DUPLICATED_LEAVE); + } + + @Test + public void execute_invalidLeaveIndexUnfilteredList_failure() { + Index outOfBoundsIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(DEFAULT_TITLE).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(outOfBoundsIndex, descriptor); + + assertCommandFailure(editLeaveCommand, model, Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidLeaveIndexFilteredList_failure() { + showLeaveByPerson(model, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + Index outOfBoundsIndex = INDEX_THIRD_LEAVE; + + assertTrue(outOfBoundsIndex.getZeroBased() < model.getLeavesBook().getLeaveList().size()); + + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(outOfBoundsIndex, new EditLeaveDescriptorBuilder() + .withTitle(DEFAULT_TITLE).build()); + assertCommandFailure(editLeaveCommand, model, Messages.MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidLeaveDateRange_failure() { + assertFalse(DEFAULT_END.getDate().isBefore(DEFAULT_START.getDate())); + // both start and end supplied, new end is before new start + // process start then end + assertThrows(EndBeforeStartException.class, () -> new EditLeaveDescriptorBuilder() + .withStart(DEFAULT_END) + .withEnd(DEFAULT_START) + .build()); + + // process end then start + assertThrows(EndBeforeStartException.class, () -> new EditLeaveDescriptorBuilder() + .withEnd(DEFAULT_START) + .withStart(DEFAULT_END) + .build()); + + // only start supplied, start is after existing end + Date lateStart = Date.of(DEFAULT_END.getDate().plusDays(1)); + EditLeaveDescriptor lateStartDescriptor = new EditLeaveDescriptorBuilder().withStart(lateStart).build(); + EditLeaveCommand lateStartCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, lateStartDescriptor); + assertCommandFailure(lateStartCommand, model, Range.MESSAGE_END_BEFORE_START_ERROR); + + + // only end supplied, end is after existing start + Date earlyEnd = Date.of(DEFAULT_START.getDate().minusDays(1)); + EditLeaveDescriptor earlyEndDescriptor = new EditLeaveDescriptorBuilder().withEnd(earlyEnd).build(); + EditLeaveCommand earlyEndCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, earlyEndDescriptor); + assertCommandFailure(earlyEndCommand, model, Range.MESSAGE_END_BEFORE_START_ERROR); + } + + @Test + public void editLeaveDescriptor_copyConstructor() { + Leave editedLeave = new LeaveBuilder().build(); + EditLeaveDescriptor expectedLeaveDescriptor = new EditLeaveDescriptorBuilder(editedLeave).build(); + EditLeaveDescriptor actualLeaveDescriptor = new EditLeaveDescriptor(expectedLeaveDescriptor); + assertEquals(expectedLeaveDescriptor, actualLeaveDescriptor); + } + + @Test + public void editLeaveDescriptor_isAnyFieldEdited() { + // no fields edited -> returns false + EditLeaveDescriptor notEditedLeaveDescriptor = new EditLeaveDescriptorBuilder().build(); + assertFalse(notEditedLeaveDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor titleEditedDescriptor = new EditLeaveDescriptorBuilder().withTitle(DEFAULT_TITLE).build(); + assertTrue(titleEditedDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor descriptionNonEmptyDescriptor = new EditLeaveDescriptorBuilder() + .withDescription(DEFAULT_DESCRIPTION).build(); + assertTrue(descriptionNonEmptyDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor descriptionEmptyDescriptor = new EditLeaveDescriptorBuilder() + .withDescription(DEFAULT_EMPTY_DESCRIPTION).build(); + assertTrue(descriptionEmptyDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor startEditedDescriptor = new EditLeaveDescriptorBuilder().withStart(DEFAULT_START).build(); + assertTrue(startEditedDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor endEditedDescriptor = new EditLeaveDescriptorBuilder().withEnd(DEFAULT_END).build(); + assertTrue(endEditedDescriptor.isAnyFieldEdited()); + + EditLeaveDescriptor statusEditedDescriptor = new EditLeaveDescriptorBuilder() + .withStatus(DEFAULT_STATUS).build(); + assertTrue(statusEditedDescriptor.isAnyFieldEdited()); + } + + @Test + public void editLeaveDescriptor_equals() { + EditLeaveDescriptor emptyDescriptor = new EditLeaveDescriptorBuilder().build(); + assertEquals(emptyDescriptor, emptyDescriptor); + + assertFalse(emptyDescriptor.equals(1)); + + EditLeaveDescriptor secondEmptyDescriptor = new EditLeaveDescriptorBuilder().build(); + assertEquals(emptyDescriptor, secondEmptyDescriptor); + + EditLeaveDescriptor descriptorWithTitle = new EditLeaveDescriptorBuilder().withTitle(DEFAULT_TITLE).build(); + assertNotEquals(emptyDescriptor, descriptorWithTitle); + + EditLeaveDescriptor descriptorWithNonEmptyDescription = new EditLeaveDescriptorBuilder() + .withDescription(DEFAULT_DESCRIPTION).build(); + assertNotEquals(emptyDescriptor, descriptorWithNonEmptyDescription); + + EditLeaveDescriptor descriptorWithEmptyDescription = new EditLeaveDescriptorBuilder() + .withDescription(DEFAULT_EMPTY_DESCRIPTION).build(); + assertNotEquals(emptyDescriptor, descriptorWithEmptyDescription); + + EditLeaveDescriptor descriptorWithStart = new EditLeaveDescriptorBuilder() + .withStart(DEFAULT_START).build(); + assertNotEquals(emptyDescriptor, descriptorWithStart); + + EditLeaveDescriptor descriptorWithEnd = new EditLeaveDescriptorBuilder() + .withEnd(DEFAULT_END).build(); + assertNotEquals(emptyDescriptor, descriptorWithEnd); + + EditLeaveDescriptor descriptorWithStatus = new EditLeaveDescriptorBuilder() + .withStatus(DEFAULT_STATUS).build(); + assertNotEquals(emptyDescriptor, descriptorWithStatus); + } + + @Test + public void equals() { + Leave leave = new LeaveBuilder().build(); + EditLeaveDescriptor defaultDescriptor = new EditLeaveDescriptorBuilder(leave).build(); + EditLeaveCommand editLeaveCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, defaultDescriptor); + + // same command instance + assertEquals(editLeaveCommand, editLeaveCommand); + + // diff types + assertFalse(editLeaveCommand.equals(1)); + + // same index and descriptor + EditLeaveCommand sameCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, defaultDescriptor); + assertEquals(editLeaveCommand, sameCommand); + + // different index + EditLeaveCommand diffIndexCommand = new EditLeaveCommand(INDEX_SECOND_LEAVE, defaultDescriptor); + assertNotEquals(editLeaveCommand, diffIndexCommand); + + // diff descriptor + EditLeaveDescriptor diffDescriptor = new EditLeaveDescriptorBuilder().build(); + EditLeaveCommand diffDescriptorCommand = new EditLeaveCommand(INDEX_FIRST_LEAVE, diffDescriptor); + assertNotEquals(editLeaveCommand, diffDescriptorCommand); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditLeaveDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditLeaveDescriptorTest.java new file mode 100644 index 00000000000..f84da17e962 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditLeaveDescriptorTest.java @@ -0,0 +1,67 @@ +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.VALID_LEAVE_DATE_END; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_START; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_STATUS_APPROVED; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_STATUS_REJECTED; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Status; +import seedu.address.testutil.EditLeaveDescriptorBuilder; + +public class EditLeaveDescriptorTest { + + private static EditLeaveDescriptor desc = new EditLeaveDescriptorBuilder().withTitle(VALID_LEAVE_TITLE) + .withDescription(VALID_LEAVE_DESCRIPTION).withStart(Date.of(VALID_LEAVE_DATE_START)) + .withEnd(Date.of(VALID_LEAVE_DATE_END)).withStatus(Status.of(VALID_LEAVE_STATUS_APPROVED)).build(); + + @Test + public void equals() { + EditLeaveDescriptor descriptorWithSameValues = new EditLeaveDescriptor(desc); + assertTrue(desc.equals(descriptorWithSameValues)); + + assertTrue(desc.equals(desc)); + + assertFalse(desc.equals(null)); + + assertFalse(desc.equals(5)); + + EditLeaveDescriptor editDesc = new EditLeaveDescriptorBuilder(desc).withTitle("Other title").build(); + assertFalse(desc.equals(editDesc)); + + editDesc = new EditLeaveDescriptorBuilder(desc).withDescription("Other description").build(); + assertFalse(desc.equals(editDesc)); + + // check that start date and end date is not the same + assertFalse(VALID_LEAVE_DATE_START.equals(VALID_LEAVE_DATE_END)); + + editDesc = new EditLeaveDescriptorBuilder(desc).withStart(Date.of(VALID_LEAVE_DATE_END)).build(); + assertFalse(desc.equals(editDesc)); + + editDesc = new EditLeaveDescriptorBuilder(desc).withEnd(Date.of(VALID_LEAVE_DATE_START)).build(); + assertFalse(desc.equals(editDesc)); + + editDesc = new EditLeaveDescriptorBuilder(desc).withStatus(Status.of(VALID_LEAVE_STATUS_REJECTED)).build(); + assertFalse(desc.equals(editDesc)); + } + + @Test + public void toStringMethod() { + EditLeaveDescriptor editPersonDescriptor = new EditLeaveDescriptor(); + String expected = EditLeaveDescriptor.class.getCanonicalName() + "{title=" + + editPersonDescriptor.getTitle().orElse(null) + ", description=" + + editPersonDescriptor.getDescription().orElse(null) + ", start=" + + editPersonDescriptor.getStart().orElse(null) + ", end=" + + editPersonDescriptor.getEnd().orElse(null) + ", status=" + + editPersonDescriptor.getStatus().orElse(null) + "}"; + assertEquals(expected, editPersonDescriptor.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ExportContactCommandTest.java b/src/test/java/seedu/address/logic/commands/ExportContactCommandTest.java new file mode 100644 index 00000000000..9314a9e812b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ExportContactCommandTest.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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +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.FileAndPathUtil; + +public class ExportContactCommandTest { + + private static final Path TEST_DATA_FOLDER = Paths.get( + "src", "test", "data", "sandbox", "ExportContactCommandTest"); + + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + private Path addToTestDataPathIfNotNull(String filename) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filename); + } + + @Test + public void execute_missingModel_throwsException() { + Path testFilePath = addToTestDataPathIfNotNull("testFile.csv"); + ExportContactCommand command = new ExportContactCommand(testFilePath); + assertThrows(NullPointerException.class, () -> command.execute(null)); + } + @Test + public void execute_validFilePath_success() { + Path testFilePath = addToTestDataPathIfNotNull("testFile.csv"); + ExportContactCommand command = new ExportContactCommand(testFilePath); + String expectedMessage = String.format(ExportCommand.MESSAGE_SUCCESS, "Employee", testFilePath); + assertCommandSuccess(command, model, expectedMessage, model); + assertTrue(Files.exists(testFilePath)); + } + + @Test + public void equals() { + Path sameFilePath = addToTestDataPathIfNotNull("sameFile.csv"); + Path diffFilePath = addToTestDataPathIfNotNull("diffFile.csv"); + ExportContactCommand exportFirstCommand = new ExportContactCommand(sameFilePath); + ExportContactCommand exportSecondCommand = new ExportContactCommand(sameFilePath); + ExportContactCommand exportDiffCommand = new ExportContactCommand(diffFilePath); + + + // An export command is equal to itself + assertEquals(exportFirstCommand, exportFirstCommand); + // Two export commands with the same file path are equal + assertEquals(exportFirstCommand, exportSecondCommand); + // An export command is not equal to a different type + assertNotEquals(exportFirstCommand, 1); + // Two export commands with diff file paths are different + assertNotEquals(exportFirstCommand, exportDiffCommand); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ExportLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/ExportLeaveCommandTest.java new file mode 100644 index 00000000000..e548855205d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ExportLeaveCommandTest.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +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.FileAndPathUtil; + +public class ExportLeaveCommandTest { + private static final Path TEST_DATA_FOLDER = Paths.get( + "src", "test", "data", "sandbox", "ExportLeaveCommandTest"); + + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + private Path addToTestDataPathIfNotNull(String filename) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filename); + } + + @Test + public void execute_missingModel_throwsException() { + Path testFilePath = addToTestDataPathIfNotNull("testFile.csv"); + ExportLeaveCommand command = new ExportLeaveCommand(testFilePath); + assertThrows(NullPointerException.class, () -> command.execute(null)); + } + @Test + public void execute_validFilePath_success() { + Path testFilePath = addToTestDataPathIfNotNull("testFile.csv"); + ExportLeaveCommand command = new ExportLeaveCommand(testFilePath); + String expectedMessage = String.format(ExportLeaveCommand.MESSAGE_SUCCESS, "Leaves", testFilePath); + assertCommandSuccess(command, model, expectedMessage, model); + assertTrue(Files.exists(testFilePath)); + } + + @Test + public void equals() { + Path sameFilePath = addToTestDataPathIfNotNull("sameFile.csv"); + Path diffFilePath = addToTestDataPathIfNotNull("diffFile.csv"); + ExportLeaveCommand exportFirstCommand = new ExportLeaveCommand(sameFilePath); + ExportLeaveCommand exportSecondCommand = new ExportLeaveCommand(sameFilePath); + ExportLeaveCommand exportDiffCommand = new ExportLeaveCommand(diffFilePath); + + + // An export command is equal to itself + assertEquals(exportFirstCommand, exportFirstCommand); + // Two export commands with the same file path are equal + assertEquals(exportFirstCommand, exportSecondCommand); + // An export command is not equal to a different type + assertNotEquals(exportFirstCommand, 1); + // Two export commands with diff file paths are different + assertNotEquals(exportFirstCommand, exportDiffCommand); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindAllLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/FindAllLeaveCommandTest.java new file mode 100644 index 00000000000..0b224dbe03b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindAllLeaveCommandTest.java @@ -0,0 +1,50 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +/** + * Contains integration tests (interaction with the Model) and unit tests for FindAllLeaveCommand. + */ +public class FindAllLeaveCommandTest { + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); + } + + @Test + public void execute_findAllLeave_success() { + FindAllLeaveCommand findAllLeaveCommand = new FindAllLeaveCommand(); + CommandResult expectedCommandResult = new CommandResult( + String.format(FindAllLeaveCommand.MESSAGE_LEAVE_COUNT, model.getFilteredLeaveList().size())); + assertCommandSuccess(findAllLeaveCommand, model, expectedCommandResult, expectedModel); + + // test for empty leaves book + Model emptyModel = new ModelManager(getTypicalAddressBook(), new LeavesBook(), new UserPrefs()); + Model expectedEmptyModel = new ModelManager( + emptyModel.getAddressBook(), emptyModel.getLeavesBook(), new UserPrefs()); + CommandResult expectedEmptyCommandResult = new CommandResult(FindAllLeaveCommand.MESSAGE_FIND_LEAVE_NONE); + assertCommandSuccess(findAllLeaveCommand, emptyModel, expectedEmptyCommandResult, expectedEmptyModel); + } + + @Test + public void toStringMethod() { + FindAllLeaveCommand findAllLeaveCommand = new FindAllLeaveCommand(); + String expected = FindAllLeaveCommand.class.getCanonicalName() + "{}"; + assertEquals(expected, findAllLeaveCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindAllTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindAllTagCommandTest.java new file mode 100644 index 00000000000..23e73829bdc --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindAllTagCommandTest.java @@ -0,0 +1,106 @@ +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.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.MICHAEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +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.TagsContainAllTagsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Contains integration tests (interaction with the Model) for {@code FindSomeTagCommand }. + */ +public class FindAllTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void equals() { + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("full time")); + tagList1.add(new Tag("remote")); + tagList2.add(new Tag("part time")); + tagList2.add(new Tag("remote")); + TagsContainAllTagsPredicate firstPredicate = + new TagsContainAllTagsPredicate(tagList1); + TagsContainAllTagsPredicate secondPredicate = + new TagsContainAllTagsPredicate(tagList2); + + FindAllTagCommand findFirstCommand = new FindAllTagCommand(firstPredicate); + FindAllTagCommand findSecondCommand = new FindAllTagCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindAllTagCommand findFirstCommandCopy = new FindAllTagCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different tags -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_noTagsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + TagsContainAllTagsPredicate predicate = preparePredicate(new String[]{"test for tag"}); + FindAllTagCommand command = new FindAllTagCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void execute_twoKeywords_onePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1); + TagsContainAllTagsPredicate predicate = preparePredicate(new String[]{"remote", "full time"}); + FindAllTagCommand command = new FindAllTagCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(MICHAEL), model.getFilteredPersonList()); + } + + @Test + public void toStringMethod() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + TagsContainAllTagsPredicate predicate = new TagsContainAllTagsPredicate(tagList); + FindAllTagCommand findAllTagCommand = new FindAllTagCommand(predicate); + String expected = FindAllTagCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, findAllTagCommand.toString()); + } + + /** + * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. + */ + private TagsContainAllTagsPredicate preparePredicate(String[] args) { + List<Tag> tagList = new ArrayList<>(); + for (String keyword : args) { + Tag tag = new Tag(keyword); + tagList.add(tag); + } + return new TagsContainAllTagsPredicate(tagList); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index b8b7dbba91a..27157429e9b 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -5,6 +5,7 @@ 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.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.CARL; import static seedu.address.testutil.TypicalPersons.ELLE; import static seedu.address.testutil.TypicalPersons.FIONA; @@ -24,8 +25,8 @@ * 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(), getTypicalLeavesBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); @Test public void equals() { diff --git a/src/test/java/seedu/address/logic/commands/FindLeaveByPeriodCommandTest.java b/src/test/java/seedu/address/logic/commands/FindLeaveByPeriodCommandTest.java new file mode 100644 index 00000000000..950afe2a94c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindLeaveByPeriodCommandTest.java @@ -0,0 +1,86 @@ +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.assertNotEquals; +import static seedu.address.logic.Messages.MESSAGE_LEAVES_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BENSON_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; + +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.leave.Date; +import seedu.address.model.leave.LeaveInPeriodPredicate; +import seedu.address.model.leave.Range; + +public class FindLeaveByPeriodCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void equals() { + Date firstDate = Date.of("2023-10-30"); + Date secondDate = Date.of("2023-10-31"); + + LeaveInPeriodPredicate firstPredicate = new LeaveInPeriodPredicate( + Range.createNonNullRange(firstDate, firstDate)); + LeaveInPeriodPredicate secondPredicate = new LeaveInPeriodPredicate( + Range.createNonNullRange(secondDate, secondDate)); + + FindLeaveByPeriodCommand firstCommand = new FindLeaveByPeriodCommand(firstPredicate); + FindLeaveByPeriodCommand secondCommand = new FindLeaveByPeriodCommand(secondPredicate); + + // same command + assertEquals(firstCommand, firstCommand); + // diff types + assertFalse(firstCommand.equals("1")); + // same predicate + FindLeaveByPeriodCommand firstCommandCopy = new FindLeaveByPeriodCommand(firstPredicate); + assertEquals(firstCommand, firstCommandCopy); + // diff predicate + assertNotEquals(firstCommand, secondCommand); + } + + @Test + public void execute_noLeavesFound() { + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, 0); + LeaveInPeriodPredicate predicate = new LeaveInPeriodPredicate( + Range.createNullableRange(null, Date.of("2019-12-31")) + ); + FindLeaveByPeriodCommand command = new FindLeaveByPeriodCommand(predicate); + expectedModel.updateFilteredLeaveList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredLeaveList()); + } + + @Test + public void execute_twoPeopleFound() { + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, 2); + LeaveInPeriodPredicate predicate = new LeaveInPeriodPredicate( + Range.createNullableRange(Date.of("2020-01-01"), Date.of("2020-01-02")) + ); + FindLeaveByPeriodCommand command = new FindLeaveByPeriodCommand(predicate); + expectedModel.updateFilteredLeaveList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE_LEAVE, BENSON_LEAVE), model.getFilteredLeaveList()); + } + + @Test + public void toStringMethod() { + LeaveInPeriodPredicate predicate = new LeaveInPeriodPredicate( + Range.createNullableRange(Date.of("2020-01-01"), Date.of("2020-01-02")) + ); + FindLeaveByPeriodCommand command = new FindLeaveByPeriodCommand(predicate); + String expected = FindLeaveByPeriodCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, command.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindLeaveByStatusCommandTest.java b/src/test/java/seedu/address/logic/commands/FindLeaveByStatusCommandTest.java new file mode 100644 index 00000000000..b74782e6461 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindLeaveByStatusCommandTest.java @@ -0,0 +1,85 @@ +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.assertNotEquals; +import static seedu.address.logic.Messages.MESSAGE_LEAVES_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE_2; +import static seedu.address.testutil.TypicalLeaves.BENSON_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; + +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.leave.LeaveHasStatusPredicate; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; + +public class FindLeaveByStatusCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void equals() { + Status approvedStatus = Status.of(StatusType.APPROVED); + Status pendingStatus = Status.of(StatusType.PENDING); + Status rejectedStatus = Status.of(StatusType.REJECTED); + + LeaveHasStatusPredicate approvedPredicate = new LeaveHasStatusPredicate(approvedStatus); + LeaveHasStatusPredicate pendingPredicate = new LeaveHasStatusPredicate(pendingStatus); + LeaveHasStatusPredicate rejectedPredicate = new LeaveHasStatusPredicate(rejectedStatus); + + FindLeaveByStatusCommand approvedCommand = new FindLeaveByStatusCommand(approvedPredicate); + FindLeaveByStatusCommand pendingCommand = new FindLeaveByStatusCommand(pendingPredicate); + FindLeaveByStatusCommand rejectedCommand = new FindLeaveByStatusCommand(rejectedPredicate); + + // same command + assertEquals(approvedCommand, approvedCommand); + // diff types + assertFalse(approvedCommand.equals("1")); + // same predicate + FindLeaveByStatusCommand approvedCommandCopy = new FindLeaveByStatusCommand( + approvedPredicate); + assertEquals(approvedCommand, approvedCommandCopy); + // diff predicate + assertNotEquals(approvedCommand, pendingCommand); + assertNotEquals(approvedCommand, rejectedCommand); + assertNotEquals(pendingCommand, rejectedCommand); + } + + @Test + public void execute_noLeavesFound() { + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, 0); + LeaveHasStatusPredicate predicate = new LeaveHasStatusPredicate(Status.of(StatusType.APPROVED)); + FindLeaveByStatusCommand command = new FindLeaveByStatusCommand(predicate); + expectedModel.updateFilteredLeaveList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredLeaveList()); + } + + @Test + public void execute_threePeopleFound() { + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, 3); + LeaveHasStatusPredicate predicate = new LeaveHasStatusPredicate(Status.of(StatusType.PENDING)); + FindLeaveByStatusCommand command = new FindLeaveByStatusCommand(predicate); + expectedModel.updateFilteredLeaveList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE_LEAVE, BENSON_LEAVE, ALICE_LEAVE_2), model.getFilteredLeaveList()); + } + + @Test + public void toStringMethod() { + LeaveHasStatusPredicate predicate = new LeaveHasStatusPredicate(Status.of(StatusType.PENDING)); + FindLeaveByStatusCommand command = new FindLeaveByStatusCommand(predicate); + String expected = FindLeaveByStatusCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, command.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/FindLeaveCommandTest.java new file mode 100644 index 00000000000..9eaa3acd46c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindLeaveCommandTest.java @@ -0,0 +1,90 @@ +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_INVALID_PERSON_DISPLAYED_INDEX; +import static seedu.address.logic.Messages.MESSAGE_LEAVES_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.LeaveContainsPersonPredicate; +import seedu.address.model.person.Person; +import seedu.address.testutil.TestUtil; + +public class FindLeaveCommandTest { + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + @Test + public void execute_findLeave_success() { + Index index = Index.fromOneBased(1); + FindLeaveCommand findLeaveCommand = new FindLeaveCommand(index); + Person employee = TestUtil.getPerson(model, index); + LeaveContainsPersonPredicate predicate = new LeaveContainsPersonPredicate(employee); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel.updateFilteredLeaveList(predicate); + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, + expectedModel.getFilteredLeaveList().size()); + assertCommandSuccess(findLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_findNoLeave_success() { + Index index = Index.fromOneBased(3); + FindLeaveCommand findLeaveCommand = new FindLeaveCommand(index); + Person employee = TestUtil.getPerson(model, index); + LeaveContainsPersonPredicate predicate = new LeaveContainsPersonPredicate(employee); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel.updateFilteredLeaveList(predicate); + String expectedMessage = String.format(MESSAGE_LEAVES_LISTED_OVERVIEW, + expectedModel.getFilteredLeaveList().size()); + assertCommandSuccess(findLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_indexOutOfBounds_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + FindLeaveCommand findLeaveCommand = new FindLeaveCommand(outOfBoundIndex); + assertCommandFailure(findLeaveCommand, model, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(3); + FindLeaveCommand findLeaveCommand = new FindLeaveCommand(index); + String expectedMessage = FindLeaveCommand.class.getCanonicalName() + "{index=" + index + "}"; + assertEquals(expectedMessage, findLeaveCommand.toString()); + } + + @Test + public void equals() { + Index indexOne = Index.fromOneBased(1); + Index indexTwo = Index.fromOneBased(2); + + FindLeaveCommand firstCommand = new FindLeaveCommand(indexOne); + FindLeaveCommand secondCommand = new FindLeaveCommand(indexTwo); + + // same object -> returns true + assertTrue(firstCommand.equals(firstCommand)); + + // same values -> returns true + FindLeaveCommand firstCommandCopy = new FindLeaveCommand(indexOne); + assertTrue(firstCommand.equals(firstCommandCopy)); + + // different types -> returns false + assertFalse(firstCommand.equals(1)); + + // null -> returns false + assertFalse(firstCommand.equals(null)); + + // different tags -> returns false + assertFalse(firstCommand.equals(secondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindSomeTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindSomeTagCommandTest.java new file mode 100644 index 00000000000..1951f8e45d0 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindSomeTagCommandTest.java @@ -0,0 +1,107 @@ +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.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.DAVID; +import static seedu.address.testutil.TypicalPersons.MICHAEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +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.TagsContainSomeTagsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Contains integration tests (interaction with the Model) for {@code FindSomeTagCommand }. + */ +public class FindSomeTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + @Test + public void equals() { + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("full time")); + tagList1.add(new Tag("remote")); + tagList2.add(new Tag("part time")); + tagList2.add(new Tag("remote")); + TagsContainSomeTagsPredicate firstPredicate = + new TagsContainSomeTagsPredicate(tagList1); + TagsContainSomeTagsPredicate secondPredicate = + new TagsContainSomeTagsPredicate(tagList2); + + FindSomeTagCommand findFirstCommand = new FindSomeTagCommand(firstPredicate); + FindSomeTagCommand findSecondCommand = new FindSomeTagCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindSomeTagCommand findFirstCommandCopy = new FindSomeTagCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different tags -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_noTagsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + TagsContainSomeTagsPredicate predicate = preparePredicate(new String[]{"tagForTest"}); + FindSomeTagCommand command = new FindSomeTagCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void execute_oneKeywords_multiplePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 2); + TagsContainSomeTagsPredicate predicate = preparePredicate(new String[]{"remote"}); + FindSomeTagCommand command = new FindSomeTagCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(MICHAEL, DAVID), model.getFilteredPersonList()); + } + + @Test + public void toStringMethod() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + TagsContainSomeTagsPredicate predicate = new TagsContainSomeTagsPredicate(tagList); + FindSomeTagCommand findSomeTagCommand = new FindSomeTagCommand(predicate); + String expected = FindSomeTagCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, findSomeTagCommand.toString()); + } + + /** + * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. + */ + private TagsContainSomeTagsPredicate preparePredicate(String[] args) { + List<Tag> tagList = new ArrayList<>(); + for (String keyword : args) { + Tag tag = new Tag(keyword); + tagList.add(tag); + } + return new TagsContainSomeTagsPredicate(tagList); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ImportContactCommandTest.java b/src/test/java/seedu/address/logic/commands/ImportContactCommandTest.java new file mode 100644 index 00000000000..273d4c4053d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ImportContactCommandTest.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.FileAndPathUtil.MockSuccessfulFileDialogHandler; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import javafx.stage.FileChooser.ExtensionFilter; +import seedu.address.commons.controllers.FileDialogHandler; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.testutil.FileAndPathUtil; + +public class ImportContactCommandTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "CsvFiles"); + private static final Path INVALID_ADDRESS_BOOK_PATH = Paths.get("src", "test", "data", + "CsvAddressBookStorageTest", "validAndInvalidPersonAddressBook.csv"); + private final Model model = new ModelManager(getTypicalAddressBook(), new LeavesBook(), new UserPrefs()); + + private Path addToTestDataPathIfNotNull(String filename) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filename); + } + @Test + public void execute_fileChosen_success() { + Path filepath = addToTestDataPathIfNotNull("typicalPersonsAddressBook.csv"); + ImportContactCommand command = new ImportContactCommand( + new MockSuccessfulFileDialogHandler(filepath.toString())); + Model actualModel = new ModelManager(); + String expectedMessage = String.format(ImportContactCommand.MESSAGE_SUCCESS, filepath.getFileName()); + assertCommandSuccess(command, actualModel, expectedMessage, model); + } + + @Test + public void execute_fileNotChosen_failed() { + ImportContactCommand command = new ImportContactCommand(new MockUnsuccessfulFileDialogHandler()); + String expectedMessage = ImportContactCommand.MESSAGE_NO_FILE_SELECTED; + assertCommandSuccess(command, model, expectedMessage, model); + } + + private static class MockUnsuccessfulFileDialogHandler implements FileDialogHandler { + @Override + public Optional<File> openFile(String title, ExtensionFilter... extensions) { + return Optional.empty(); + } + } + + @Test + public void execute_invalidAddressBook_throwsException() { + ImportContactCommand command = new ImportContactCommand( + new MockSuccessfulFileDialogHandler(INVALID_ADDRESS_BOOK_PATH.toString())); + Model actualModel = new ModelManager(); + String expectedMessage = String.format(ImportContactCommand.MESSAGE_FAILED, + INVALID_ADDRESS_BOOK_PATH.getFileName()); + assertCommandFailure(command, actualModel, expectedMessage); + } + + @Test + public void execute_emptyAddressBook_throwsException() { + Path filePath = addToTestDataPathIfNotNull("emptyAddressBook.csv"); + ImportContactCommand command = new ImportContactCommand( + new MockSuccessfulFileDialogHandler(filePath.toString())); + Model actualModel = new ModelManager(); + String expectedMessage = String.format(ImportContactCommand.MESSAGE_EMPTY_ADDRESS_BOOK, + filePath.getFileName()); + assertCommandFailure(command, actualModel, expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ImportLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/ImportLeaveCommandTest.java new file mode 100644 index 00000000000..a3f40d0735c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ImportLeaveCommandTest.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.FileAndPathUtil.MockSuccessfulFileDialogHandler; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import javafx.stage.FileChooser; +import seedu.address.commons.controllers.FileDialogHandler; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.testutil.FileAndPathUtil; + +public class ImportLeaveCommandTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "CsvFiles"); + private static final Path INVALID_ADDRESS_BOOK_PATH = Paths.get("src", "test", "data", + "CsvLeavesBookStorageTest", "validAndInvalidLeavesBook.csv"); + private final Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + + private Path addToTestDataPathIfNotNull(String filename) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filename); + } + @Test + public void execute_fileChosen_success() { + Path filepath = addToTestDataPathIfNotNull("typicalLeavesBook.csv"); + ImportLeaveCommand command = new ImportLeaveCommand( + new MockSuccessfulFileDialogHandler(filepath.toString())); + Model actualModel = new ModelManager(getTypicalAddressBook(), new LeavesBook(), new UserPrefs()); + String expectedMessage = String.format(ImportLeaveCommand.MESSAGE_SUCCESS, filepath.getFileName()); + assertCommandSuccess(command, actualModel, expectedMessage, model); + } + + @Test + public void execute_fileNotChosen_failed() { + ImportContactCommand command = new ImportContactCommand(new MockUnsuccessfulFileDialogHandler()); + String expectedMessage = ImportContactCommand.MESSAGE_NO_FILE_SELECTED; + assertCommandSuccess(command, model, expectedMessage, model); + } + + private static class MockUnsuccessfulFileDialogHandler implements FileDialogHandler { + @Override + public Optional<File> openFile(String title, FileChooser.ExtensionFilter... extensions) { + return Optional.empty(); + } + } + + @Test + public void execute_invalidLeavesBook_throwsException() { + ImportLeaveCommand command = new ImportLeaveCommand( + new MockSuccessfulFileDialogHandler(INVALID_ADDRESS_BOOK_PATH.toString())); + Model actualModel = new ModelManager(getTypicalAddressBook(), new LeavesBook(), new UserPrefs()); + String expectedMessage = String.format(ImportLeaveCommand.MESSAGE_FAILED, + INVALID_ADDRESS_BOOK_PATH.getFileName()); + assertCommandFailure(command, actualModel, expectedMessage); + } + + @Test + public void execute_emptyAddressBook_throwsException() { + Path filePath = addToTestDataPathIfNotNull("emptyLeavesBook.csv"); + ImportLeaveCommand command = new ImportLeaveCommand( + new MockSuccessfulFileDialogHandler(filePath.toString())); + Model actualModel = new ModelManager(getTypicalAddressBook(), new LeavesBook(), new UserPrefs()); + String expectedMessage = String.format(ImportLeaveCommand.MESSAGE_EMPTY_LEAVES_BOOK, + filePath.getFileName()); + assertCommandFailure(command, actualModel, expectedMessage); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index 435ff1f7275..49345c6977b 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -3,6 +3,7 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.BeforeEach; @@ -22,8 +23,8 @@ public class ListCommandTest { @BeforeEach public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), model.getLeavesBook(), new UserPrefs()); } @Test diff --git a/src/test/java/seedu/address/logic/commands/RejectLeaveCommandTest.java b/src/test/java/seedu/address/logic/commands/RejectLeaveCommandTest.java new file mode 100644 index 00000000000..49fec9522ee --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/RejectLeaveCommandTest.java @@ -0,0 +1,98 @@ +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_INVALID_LEAVE_DISPLAYED_INDEX; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.LeavesBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.TestUtil; + +public class RejectLeaveCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + private final StatusType rejectedStatus = StatusType.REJECTED; + @Test + public void execute_rejectLeave_success() { + Index indexLastLeave = TestUtil.getLastLeaveIndex(model); + Leave originalLeave = TestUtil.getLeave(model, indexLastLeave); + LeaveBuilder leaveInList = new LeaveBuilder(originalLeave); + Leave rejectedLeave = leaveInList.withStatus(rejectedStatus).build(); + RejectLeaveCommand rejectLeaveCommand = new RejectLeaveCommand(indexLastLeave); + String expectedMessage = String.format( + RejectLeaveCommand.MESSAGE_REJECT_LEAVE_SUCCESS, + Messages.format(rejectedLeave)); + Model expectedModel = new ModelManager( + getTypicalAddressBook(), + new LeavesBook(model.getLeavesBook()), new UserPrefs()); + expectedModel.setLeave(originalLeave, rejectedLeave); + assertCommandSuccess(rejectLeaveCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateRejectLeave_failure() { + Index indexLastLeave = TestUtil.getLastLeaveIndex(model); + Leave originalLeave = TestUtil.getLeave(model, indexLastLeave); + LeaveBuilder leaveInList = new LeaveBuilder(originalLeave); + Leave rejectedLeave = leaveInList.withStatus(rejectedStatus).build(); + model.setLeave(originalLeave, rejectedLeave); + RejectLeaveCommand rejectLeaveCommand = new RejectLeaveCommand(indexLastLeave); + String expectedMessage = String.format( + RejectLeaveCommand.MESSAGE_DUPLICATE_LEAVE_REJECT, + Messages.format(rejectedLeave)); + assertCommandFailure(rejectLeaveCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndex_failure() { + Index outOfBoundIndex = TestUtil.getInvalidLeaveIndex(model); + RejectLeaveCommand rejectLeaveCommand = new RejectLeaveCommand(outOfBoundIndex); + String expectedMessage = MESSAGE_INVALID_LEAVE_DISPLAYED_INDEX; + assertCommandFailure(rejectLeaveCommand, model, expectedMessage); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + RejectLeaveCommand rejectLeaveCommand = new RejectLeaveCommand(index); + String expected = RejectLeaveCommand.class.getCanonicalName() + "{index=" + index + "}"; + assertEquals(expected, rejectLeaveCommand.toString()); + } + + @Test + public void equalsMethod() { + final Index index = Index.fromOneBased(1); + final RejectLeaveCommand standardCommand = new RejectLeaveCommand(index); + + // same values -> returns true + RejectLeaveCommand commandWithSameValues = new RejectLeaveCommand(index); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // 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 + Index differentIndex = Index.fromOneBased(2); + assertFalse(standardCommand.equals(new RejectLeaveCommand(differentIndex))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ViewTagCommandTest.java b/src/test/java/seedu/address/logic/commands/ViewTagCommandTest.java new file mode 100644 index 00000000000..bde81f500cd --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ViewTagCommandTest.java @@ -0,0 +1,38 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.ViewTagCommand.MESSAGE_VIEW_TAG_NONE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class ViewTagCommandTest { + @Test + public void execute_viewTagNone_success() { + Model model = new ModelManager(); + Model expectedModel = new ModelManager(); + assertCommandSuccess(new ViewTagCommand(), model, MESSAGE_VIEW_TAG_NONE, expectedModel); + } + + @Test + public void execute_viewTag_success() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + ViewTagCommand viewTagCommand = new ViewTagCommand(); + CommandResult expectedCommandResult = viewTagCommand.execute(model); + assertCommandSuccess(viewTagCommand, model, expectedCommandResult, expectedModel); + } + + @Test + public void toStringMethod() { + ViewTagCommand viewTagCommand = new ViewTagCommand(); + String expected = ViewTagCommand.class.getCanonicalName() + "{}"; + assertEquals(expected, viewTagCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5bc11d3cdaa..88fbd39d5d7 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -24,10 +24,10 @@ 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.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_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import static seedu.address.testutil.TypicalPersons.AMY; @@ -72,61 +72,62 @@ public void parse_repeatedNonTagValue_failure() { // multiple names assertParseFailure(parser, NAME_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_NAME)); // multiple phones assertParseFailure(parser, PHONE_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE)); // multiple emails assertParseFailure(parser, EMAIL_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_EMAIL)); // multiple addresses assertParseFailure(parser, ADDRESS_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_ADDRESS)); // multiple fields repeated assertParseFailure(parser, validExpectedPersonString + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_NAME, PREFIX_PERSON_ADDRESS, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_PHONE)); // invalid value followed by valid value // invalid name assertParseFailure(parser, INVALID_NAME_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_NAME)); // invalid email assertParseFailure(parser, INVALID_EMAIL_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_EMAIL)); // invalid phone assertParseFailure(parser, INVALID_PHONE_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE)); // invalid address assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_ADDRESS)); // valid value followed by invalid value // invalid name assertParseFailure(parser, validExpectedPersonString + INVALID_NAME_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_NAME)); // invalid email assertParseFailure(parser, validExpectedPersonString + INVALID_EMAIL_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_EMAIL)); // invalid phone assertParseFailure(parser, validExpectedPersonString + INVALID_PHONE_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE)); // invalid address assertParseFailure(parser, validExpectedPersonString + INVALID_ADDRESS_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_ADDRESS)); } @Test diff --git a/src/test/java/seedu/address/logic/parser/AddLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddLeaveCommandParserTest.java new file mode 100644 index 00000000000..d816a1ee099 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddLeaveCommandParserTest.java @@ -0,0 +1,202 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_NO_STATUS_PREFIX; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_END_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_START_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DESCRIPTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_EARLY_DATE_END_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_LATE_DATE_START_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_TITLE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_END; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_START; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_END_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_START_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE_DESC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.AddLeaveCommand; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Title; +import seedu.address.testutil.LeaveBuilder; + +public class AddLeaveCommandParserTest { + private final AddLeaveCommandParser parser = new AddLeaveCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + // without description + Leave expectedLeave = new LeaveBuilder().withTitle(VALID_LEAVE_TITLE) + .withStart(Date.of(VALID_LEAVE_DATE_START)).withEnd(Date.of(VALID_LEAVE_DATE_END)) + .withDescription("NONE").build(); + Range dateRange = Range.createNonNullRange(expectedLeave.getStart(), expectedLeave.getEnd()); + + assertParseSuccess(parser, " 1" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC, new AddLeaveCommand(INDEX_FIRST_LEAVE, expectedLeave.getTitle(), + dateRange, expectedLeave.getDescription())); + + + // with description + Leave expectedLeaveWithDescription = new LeaveBuilder().withTitle(VALID_LEAVE_TITLE) + .withStart(Date.of(VALID_LEAVE_DATE_START)).withEnd(Date.of(VALID_LEAVE_DATE_END)) + .withDescription(VALID_LEAVE_DESCRIPTION).build(); + Range dateRangeWithDescription = Range.createNonNullRange( + expectedLeaveWithDescription.getStart(), expectedLeaveWithDescription.getEnd()); + + assertParseSuccess(parser, " 2" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, + new AddLeaveCommand(INDEX_SECOND_LEAVE, expectedLeaveWithDescription.getTitle(), + dateRangeWithDescription , expectedLeaveWithDescription.getDescription())); + } + + @Test + public void parse_extraStatusPrefix_failure() { + + String expectedMessageWithoutDescription = String.format(MESSAGE_NO_STATUS_PREFIX, + AddLeaveCommand.MESSAGE_USAGE); + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC + " " + PREFIX_LEAVE_STATUS, + expectedMessageWithoutDescription); + } + + @Test + public void parse_duplicatedFieldPrefix_failure() { + String validExpectedLeaveString = " 3" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC; + + // multiple title + assertParseFailure(parser, VALID_LEAVE_TITLE_DESC + validExpectedLeaveString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE)); + + // multiple startDate + assertParseFailure(parser, VALID_LEAVE_START_DATE_DESC + validExpectedLeaveString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_START)); + + // multiple endDate + assertParseFailure(parser, VALID_LEAVE_END_DATE_DESC + validExpectedLeaveString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_END)); + + // multiple description + assertParseFailure(parser, VALID_LEAVE_DESCRIPTION_DESC + validExpectedLeaveString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DESCRIPTION)); + + // multiple fields repeated + assertParseFailure(parser, VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC + validExpectedLeaveString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE, PREFIX_LEAVE_DATE_START, + PREFIX_LEAVE_DATE_END, PREFIX_LEAVE_DESCRIPTION)); + + // invalid value followed by valid value + + // invalid title + assertParseFailure(parser, validExpectedLeaveString + INVALID_LEAVE_TITLE_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE)); + + // invalid startDate + assertParseFailure(parser, validExpectedLeaveString + INVALID_LEAVE_DATE_START_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_START)); + + // invalid endDate + assertParseFailure(parser, validExpectedLeaveString + INVALID_LEAVE_DATE_END_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_END)); + + // invalid description + assertParseFailure(parser, validExpectedLeaveString + INVALID_LEAVE_DESCRIPTION_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DESCRIPTION)); + + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLeaveCommand.MESSAGE_USAGE); + + // missing index + assertParseFailure(parser, VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, expectedMessage); + + // missing title prefix + assertParseFailure(parser, " 3" + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, expectedMessage); + + // missing startDate prefix + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, expectedMessage); + + // missing endDate prefix + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_DESCRIPTION_DESC, expectedMessage); + + // all prefixes missing + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE + VALID_LEAVE_DATE_START + + VALID_LEAVE_DATE_END + VALID_LEAVE_DESCRIPTION, expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + + // invalid title + assertParseFailure(parser, " 3" + INVALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, Title.MESSAGE_CONSTRAINTS); + + // invalid startDate + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + INVALID_LEAVE_DATE_START_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC, Date.MESSAGE_CONSTRAINTS); + + // invalid endDate + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + INVALID_LEAVE_DATE_END_DESC + VALID_LEAVE_DESCRIPTION_DESC, Date.MESSAGE_CONSTRAINTS); + + // invalid endDate which is earlier than the startDate + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + INVALID_LEAVE_LATE_DATE_START_DESC + + INVALID_LEAVE_EARLY_DATE_END_DESC + VALID_LEAVE_DESCRIPTION_DESC, + Range.MESSAGE_END_BEFORE_START_ERROR); + + // invalid description + assertParseFailure(parser, " 3" + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + INVALID_LEAVE_DESCRIPTION_DESC, Description.MESSAGE_CONSTRAINTS); + + // two invalid values, only first invalid value reported + assertParseFailure(parser, " 3" + INVALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + INVALID_LEAVE_DESCRIPTION_DESC, + Title.MESSAGE_CONSTRAINTS); + } + @Test + public void parse_invalidPreamble_failure() { + String validExpectedLeaveString = VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC; + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLeaveCommand.MESSAGE_USAGE); + + // negative index + assertParseFailure(parser, "-5" + validExpectedLeaveString, expectedMessage); + + // zero index + assertParseFailure(parser, "0" + validExpectedLeaveString, expectedMessage); + + // duplicated index + // assertParseFailure(parser, "1 1" + validExpectedLeaveString, expectedMessage); + + // invalid arguments being parsed as preamble + // assertParseFailure(parser, "1 some random string", expectedMessage); + + // invalid prefix being parsed as preamble + // assertParseFailure(parser, "1 i/ string", expectedMessage); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java new file mode 100644 index 00000000000..de0280e30a1 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java @@ -0,0 +1,102 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +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.TAG_EMPTY; +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 static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddTagCommand; +import seedu.address.model.tag.Tag; + +public class AddTagCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE); + + private AddTagCommandParser parser = new AddTagCommandParser(); + + + @Test + public void parse_missingParts_failure() { + // no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + + // no index specified + assertParseFailure(parser, VALID_TAG_FRIEND, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", AddTagCommand.MESSAGE_NO_TAGS_ADDED); + + // invalid index and no tag + assertParseFailure(parser, "1abc", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // non-numeric index + assertParseFailure(parser, "abc" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "10a" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // out-of-bound index + String intMaxPlusOne = Long.toString((long) Integer.MAX_VALUE + 1); + assertParseFailure(parser, intMaxPlusOne + TAG_DESC_FRIEND, 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_invalidValue_failure() { + // invalid tag + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); + + // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, + // parsing it together with a valid tag results in error + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + + // empty tag + assertParseFailure(parser, "1" + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_oneTagDf_success() { + Index targetIndex = INDEX_FIRST_PERSON; + + String userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; + AddTagCommand expectedCommand = new AddTagCommand(targetIndex, List.of(new Tag(VALID_TAG_FRIEND))); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleTags_success() { + Index targetIndex = INDEX_THIRD_PERSON; + String userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND + TAG_DESC_HUSBAND; + AddTagCommand expectedCommand = new AddTagCommand( + targetIndex, List.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..563e80e894e 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -4,9 +4,24 @@ 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.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FULL_TIME; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_REMOTE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_END; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_START; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_END_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_START_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -14,18 +29,49 @@ import org.junit.jupiter.api.Test; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddLeaveCommand; +import seedu.address.logic.commands.AddTagCommand; +import seedu.address.logic.commands.ApproveLeaveCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLeaveCommand; +import seedu.address.logic.commands.DeleteTagCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditLeaveCommand; +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportContactCommand; +import seedu.address.logic.commands.ExportLeaveCommand; +import seedu.address.logic.commands.FindAllLeaveCommand; +import seedu.address.logic.commands.FindAllTagCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindLeaveByPeriodCommand; +import seedu.address.logic.commands.FindLeaveByStatusCommand; +import seedu.address.logic.commands.FindLeaveCommand; +import seedu.address.logic.commands.FindSomeTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportContactCommand; +import seedu.address.logic.commands.ImportLeaveCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.RejectLeaveCommand; +import seedu.address.logic.commands.ViewTagCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.LeaveHasStatusPredicate; +import seedu.address.model.leave.LeaveInPeriodPredicate; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.TagsContainAllTagsPredicate; +import seedu.address.model.person.TagsContainSomeTagsPredicate; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditLeaveDescriptorBuilder; import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.LeaveBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -40,6 +86,19 @@ public void parseCommand_add() throws Exception { assertEquals(new AddCommand(person), command); } + @Test + public void parseCommand_addLeave() throws Exception { + Leave leave = new LeaveBuilder().withTitle(VALID_LEAVE_TITLE) + .withStart(Date.of(VALID_LEAVE_DATE_START)).withEnd(Date.of(VALID_LEAVE_DATE_END)) + .withDescription(VALID_LEAVE_DESCRIPTION).build(); + AddLeaveCommand command = (AddLeaveCommand) parser.parseCommand(AddLeaveCommand.COMMAND_WORD + " " + + INDEX_FIRST_LEAVE.getOneBased() + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_DESCRIPTION_DESC); + Range dateRange = Range.createNonNullRange(leave.getStart(), leave.getEnd()); + assertEquals(new AddLeaveCommand(INDEX_FIRST_LEAVE, leave.getTitle(), + dateRange, leave.getDescription()), command); + } + @Test public void parseCommand_clear() throws Exception { assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); @@ -62,12 +121,38 @@ public void parseCommand_edit() throws Exception { assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); } + @Test + public void parseCommand_editLeave() throws Exception { + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(VALID_LEAVE_TITLE).build(); + EditLeaveCommand command = (EditLeaveCommand) parser.parseCommand(EditLeaveCommand.COMMAND_WORD + " " + + INDEX_FIRST_LEAVE.getOneBased() + VALID_LEAVE_TITLE_DESC); + assertEquals(new EditLeaveCommand(INDEX_FIRST_LEAVE, 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_exportContact() throws Exception { + String testFileName = "testExportFile"; + Path testFilePath = Paths.get(ExportContactCommand.EXPORT_DEST, "testExportFile.csv"); + ExportContactCommand command = (ExportContactCommand) parser.parseCommand( + ExportContactCommand.COMMAND_WORD + " " + testFileName); + assertEquals(new ExportContactCommand(testFilePath), command); + } + + @Test + public void parseCommand_exportLeave() throws Exception { + String testFileName = "testExportFile"; + Path testFilePath = Paths.get(ExportLeaveCommand.EXPORT_DEST, "testExportFile.csv"); + ExportLeaveCommand command = (ExportLeaveCommand) parser.parseCommand( + ExportLeaveCommand.COMMAND_WORD + " " + testFileName); + assertEquals(new ExportLeaveCommand(testFilePath), command); + } + @Test public void parseCommand_find() throws Exception { List<String> keywords = Arrays.asList("foo", "bar", "baz"); @@ -76,26 +161,123 @@ public void parseCommand_find() throws Exception { assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); } + @Test + public void parseCommand_findAllTag() throws Exception { + List<Tag> keywords = Arrays.asList(new Tag("remote"), new Tag("full time")); + FindAllTagCommand command = (FindAllTagCommand) parser.parseCommand( + FindAllTagCommand.COMMAND_WORD + " " + TAG_DESC_REMOTE + TAG_DESC_FULL_TIME); + assertEquals(new FindAllTagCommand(new TagsContainAllTagsPredicate(keywords)), command); + } + + @Test + public void parseCommand_findSomeTag() throws Exception { + List<Tag> keywords = Arrays.asList(new Tag("remote"), new Tag("full time")); + FindSomeTagCommand command = (FindSomeTagCommand) parser.parseCommand( + FindSomeTagCommand.COMMAND_WORD + " " + TAG_DESC_REMOTE + TAG_DESC_FULL_TIME); + assertEquals(new FindSomeTagCommand(new TagsContainSomeTagsPredicate(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_import() throws Exception { + assertTrue(parser.parseCommand(ImportContactCommand.COMMAND_WORD) instanceof ImportContactCommand); + assertTrue(parser.parseCommand(ImportContactCommand.COMMAND_WORD + " 3") instanceof ImportContactCommand); + } + + @Test + public void parseCommand_importLeave() throws Exception { + assertTrue(parser.parseCommand(ImportLeaveCommand.COMMAND_WORD) instanceof ImportLeaveCommand); + assertTrue(parser.parseCommand(ImportLeaveCommand.COMMAND_WORD + " 3") instanceof ImportLeaveCommand); + } + @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_addTag() throws Exception { + AddTagCommand command = (AddTagCommand) parser.parseCommand( + AddTagCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " " + TAG_DESC_FRIEND); + assertEquals(new AddTagCommand(INDEX_FIRST_PERSON, List.of(new Tag(VALID_TAG_FRIEND))), command); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); + -> parser.parseCommand("")); } @Test public void parseCommand_unknownCommand_throwsParseException() { assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); } + + @Test + public void parseCommand_deleteTag() throws Exception { + assertTrue(parser.parseCommand(DeleteTagCommand.COMMAND_WORD + " 1 t/friends") instanceof DeleteTagCommand); + assertTrue(parser.parseCommand(DeleteTagCommand.COMMAND_WORD + " 1 t/friends t/colleagues") + instanceof DeleteTagCommand); + assertTrue(parser.parseCommand(DeleteTagCommand.COMMAND_WORD + " 1 t/friends t/colleagues t/family") + instanceof DeleteTagCommand); + } + + @Test + public void parseCommand_viewTag() throws Exception { + assertTrue(parser.parseCommand(ViewTagCommand.COMMAND_WORD) instanceof ViewTagCommand); + } + + @Test + public void parseCommand_approveLeave() throws Exception { + assertTrue(parser.parseCommand(ApproveLeaveCommand.COMMAND_WORD + " 1") instanceof ApproveLeaveCommand); + } + + @Test + public void parseCommand_deleteLeave() throws Exception { + DeleteLeaveCommand deleteLeaveCommand = (DeleteLeaveCommand) parser.parseCommand( + DeleteLeaveCommand.COMMAND_WORD + " 1"); + assertEquals(deleteLeaveCommand, new DeleteLeaveCommand(INDEX_FIRST_LEAVE)); + } + + @Test + public void parseCommand_findAllLeaveCommand() throws Exception { + assertTrue(parser.parseCommand(FindAllLeaveCommand.COMMAND_WORD) instanceof FindAllLeaveCommand); + assertTrue(parser.parseCommand(FindAllLeaveCommand.COMMAND_WORD + " 3") instanceof FindAllLeaveCommand); + } + @Test + public void parseCommand_findLeave() throws Exception { + assertTrue(parser.parseCommand(FindLeaveCommand.COMMAND_WORD + " 1") instanceof FindLeaveCommand); + } + @Test + public void parseCommand_findLeaveByPeriod() throws Exception { + String startDate = "2023-10-30"; + String endDate = "2023-10-31"; + + LeaveInPeriodPredicate expectedPredicate = new LeaveInPeriodPredicate( + Range.createNonNullRange(Date.of(startDate), Date.of(endDate))); + String userInput = FindLeaveByPeriodCommand.COMMAND_WORD + VALID_LEAVE_START_DATE_DESC + + VALID_LEAVE_END_DATE_DESC; + assertTrue(parser.parseCommand(userInput) instanceof FindLeaveByPeriodCommand); + assertEquals(parser.parseCommand(userInput), new FindLeaveByPeriodCommand(expectedPredicate)); + } + + @Test + public void parseCommand_findLeaveByStatus() throws Exception { + LeaveHasStatusPredicate expectedPredicate = new LeaveHasStatusPredicate( + Status.of(StatusType.APPROVED)); + String userInput = FindLeaveByStatusCommand.COMMAND_WORD + " " + + StatusType.APPROVED; + assertTrue(parser.parseCommand(userInput) instanceof FindLeaveByStatusCommand); + assertEquals(parser.parseCommand(userInput), new FindLeaveByStatusCommand(expectedPredicate)); + } + + @Test + public void parseCommand_rejectLeave() throws Exception { + assertTrue(parser.parseCommand(RejectLeaveCommand.COMMAND_WORD + " 1") instanceof RejectLeaveCommand); + } } diff --git a/src/test/java/seedu/address/logic/parser/ApproveLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ApproveLeaveCommandParserTest.java new file mode 100644 index 00000000000..b7d23d2a300 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ApproveLeaveCommandParserTest.java @@ -0,0 +1,49 @@ +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.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ApproveLeaveCommand; + +public class ApproveLeaveCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ApproveLeaveCommand.MESSAGE_USAGE); + + private ApproveLeaveCommandParser parser = new ApproveLeaveCommandParser(); + + @Test + public void parse_validIndex_success() { + String userInput = String.valueOf(INDEX_FIRST_LEAVE.getOneBased()); + ApproveLeaveCommand expectedCommand = new ApproveLeaveCommand(INDEX_FIRST_LEAVE); + assertParseSuccess(parser, userInput, expectedCommand); + } + @Test + public void parse_missingIndex_failure() { + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_nonNumericIndex_failure() { + // non-numeric indices + assertParseFailure(parser, "1a", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_outOfBoundIndex_failure() { + // negative index + assertParseFailure(parser, "-5", MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0", MESSAGE_INVALID_FORMAT); + + // exceed Integer.MAX_VALUE + assertParseFailure(parser, "2147483648", MESSAGE_INVALID_FORMAT); + } +} diff --git a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java index 9bf1ccf1cef..ae1e71c44b9 100644 --- a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java +++ b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java @@ -34,6 +34,8 @@ public static void assertParseFailure(Parser<? extends Command> parser, String u throw new AssertionError("The expected ParseException was not thrown."); } catch (ParseException pe) { assertEquals(expectedMessage, pe.getMessage()); + } catch (IllegalArgumentException ie) { + assertEquals(expectedMessage, ie.getMessage()); } } } diff --git a/src/test/java/seedu/address/logic/parser/DeleteLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteLeaveCommandParserTest.java new file mode 100644 index 00000000000..cf5be096aa7 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteLeaveCommandParserTest.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.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.DeleteLeaveCommand; + +public class DeleteLeaveCommandParserTest { + + private DeleteLeaveCommandParser parser = new DeleteLeaveCommandParser(); + + @Test + public void parse_validArgs_returnsDeleteLeaveCommand() { + assertParseSuccess(parser, "1", new DeleteLeaveCommand(INDEX_FIRST_LEAVE)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteLeaveCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/DeleteTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteTagCommandParserTest.java new file mode 100644 index 00000000000..fc56fa6fe65 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteTagCommandParserTest.java @@ -0,0 +1,100 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +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.TAG_EMPTY; +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 static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteTagCommand; +import seedu.address.model.tag.Tag; + + + +public class DeleteTagCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTagCommand.MESSAGE_USAGE); + + private DeleteTagCommandParser parser = new DeleteTagCommandParser(); + + + @Test + public void parse_missingParts_failure() { + // no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + + // no index specificed + assertParseFailure(parser, VALID_TAG_FRIEND, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", DeleteTagCommand.MESSAGE_NO_TAGS_REMOVED); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // non-numeric index + assertParseFailure(parser, "abc" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "10a" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // out-of-bound index + String intMaxPlusOne = Long.toString((long) Integer.MAX_VALUE + 1); + assertParseFailure(parser, intMaxPlusOne + TAG_DESC_FRIEND, 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_invalidValue_failure() { + // invalid tag + assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); + + // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, + // parsing it together with a valid tag results in error + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + + // empty tag + assertParseFailure(parser, "1" + TAG_EMPTY, DeleteTagCommand.MESSAGE_NO_TAGS_REMOVED); + } + + @Test + public void parse_oneTag_success() { + Index targetIndex = INDEX_FIRST_PERSON; + String userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; + DeleteTagCommand expectedCommand = new DeleteTagCommand(targetIndex, List.of(new Tag(VALID_TAG_FRIEND))); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleTags_success() { + Index targetIndex = INDEX_THIRD_PERSON; + String userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND + TAG_DESC_HUSBAND; + DeleteTagCommand expectedCommand = new DeleteTagCommand( + targetIndex, List.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index cc7175172d4..8fadd4dbae8 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -15,6 +15,7 @@ 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.TAG_EMPTY; 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; @@ -22,10 +23,9 @@ 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.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; 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; @@ -47,8 +47,6 @@ public class EditCommandParserTest { - private static final String TAG_EMPTY = " " + PREFIX_TAG; - private static final String MESSAGE_INVALID_FORMAT = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); @@ -74,11 +72,20 @@ public void parse_invalidPreamble_failure() { // zero index assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + // non-numeric index + assertParseFailure(parser, "abc" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "10a" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // out-of-bound index + String intMaxPlusOne = Long.toString((long) Integer.MAX_VALUE + 1); + assertParseFailure(parser, intMaxPlusOne + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + // invalid arguments being parsed as preamble - assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "1 some random string" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); // invalid prefix being parsed as preamble - assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "1 i/ string" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); } @Test @@ -94,12 +101,16 @@ public void parse_invalidValue_failure() { // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, // parsing it together with a valid tag results in error - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, + Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, + Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, + Tag.MESSAGE_CONSTRAINTS); // multiple invalid values, but only the first invalid value is captured - assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + + VALID_PHONE_AMY, Name.MESSAGE_CONSTRAINTS); } @@ -172,27 +183,29 @@ public void parse_multipleRepeatedFields_failure() { Index targetIndex = INDEX_FIRST_PERSON; String userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + PHONE_DESC_BOB; - assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE)); // invalid followed by valid userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + INVALID_PHONE_DESC; - assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE)); - // mulltiple valid fields repeated + // multiple valid fields repeated userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND; assertParseFailure(parser, userInput, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_ADDRESS)); // multiple invalid values userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + INVALID_ADDRESS_DESC + INVALID_EMAIL_DESC + INVALID_PHONE_DESC + INVALID_ADDRESS_DESC + INVALID_EMAIL_DESC; assertParseFailure(parser, userInput, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PERSON_PHONE, + PREFIX_PERSON_EMAIL, PREFIX_PERSON_ADDRESS)); } @Test diff --git a/src/test/java/seedu/address/logic/parser/EditLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditLeaveCommandParserTest.java new file mode 100644 index 00000000000..d7a1dfc66ec --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditLeaveCommandParserTest.java @@ -0,0 +1,210 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.DESCRIPTION_EMPTY; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_END_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_END_EARLY_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_START_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DATE_START_LATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_DESCRIPTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_STATUS_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_LEAVE_TITLE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_END; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DATE_START; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_DESCRIPTION_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_END_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_START_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_STATUS_APPROVED; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_STATUS_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LEAVE_TITLE_DESC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_TITLE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_LEAVE; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.EditLeaveCommand; +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.testutil.EditLeaveDescriptorBuilder; + +public class EditLeaveCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditLeaveCommand.MESSAGE_USAGE); + + private final EditLeaveCommandParser parser = new EditLeaveCommandParser(); + + @Test + public void parse_missingParts_failure() { + assertParseFailure(parser, VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + + assertParseFailure(parser, "1", EditLeaveCommand.MESSAGE_NOT_EDITED); + + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + + assertParseFailure(parser, "abc", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "10a", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10", MESSAGE_INVALID_FORMAT); + } + + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + + // non-numeric index + assertParseFailure(parser, "abc" + VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "10a" + VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10" + VALID_LEAVE_STATUS_DESC, MESSAGE_INVALID_FORMAT); + + // out-of-bound index + String intMaxPlusOne = Long.toString((long) Integer.MAX_VALUE + 1); + assertParseFailure(parser, intMaxPlusOne + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string" + TAG_DESC_FRIEND, MESSAGE_INVALID_FORMAT); + } + + @Test public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_LEAVE_DESCRIPTION_DESC, Description.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + INVALID_LEAVE_STATUS_DESC, Status.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + INVALID_LEAVE_DATE_START_DESC, Date.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + INVALID_LEAVE_DATE_END_DESC, Date.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + INVALID_LEAVE_TITLE_DESC, Title.MESSAGE_CONSTRAINTS); + + assertParseFailure(parser, "1" + INVALID_LEAVE_TITLE_DESC + VALID_LEAVE_STATUS_DESC, + Title.MESSAGE_CONSTRAINTS); + + } + + @Test + public void parse_allFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND_LEAVE; + String userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_DESCRIPTION_DESC + + VALID_LEAVE_START_DATE_DESC + VALID_LEAVE_END_DATE_DESC + VALID_LEAVE_STATUS_DESC; + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(VALID_LEAVE_TITLE) + .withDescription(VALID_LEAVE_DESCRIPTION).withStart(Date.of(VALID_LEAVE_DATE_START)) + .withEnd(Date.of(VALID_LEAVE_DATE_END)).withStatus(Status.of(VALID_LEAVE_STATUS_APPROVED)).build(); + EditLeaveCommand expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_someFieldsSpecified_success() { + Index targetIndex = INDEX_FIRST_LEAVE; + String userInput = targetIndex.getOneBased() + VALID_LEAVE_DESCRIPTION_DESC + VALID_LEAVE_TITLE_DESC; + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(VALID_LEAVE_TITLE) + .withDescription(VALID_LEAVE_DESCRIPTION).build(); + EditLeaveCommand expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_oneFieldSpecified_success() { + Index targetIndex = INDEX_THIRD_LEAVE; + String userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC; + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder().withTitle(VALID_LEAVE_TITLE).build(); + EditLeaveCommand expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + VALID_LEAVE_DESCRIPTION_DESC; + descriptor = new EditLeaveDescriptorBuilder().withDescription(VALID_LEAVE_DESCRIPTION).build(); + expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + VALID_LEAVE_START_DATE_DESC; + descriptor = new EditLeaveDescriptorBuilder().withStart(Date.of(VALID_LEAVE_DATE_START)).build(); + expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + VALID_LEAVE_END_DATE_DESC; + descriptor = new EditLeaveDescriptorBuilder().withEnd(Date.of(VALID_LEAVE_DATE_END)).build(); + expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + VALID_LEAVE_STATUS_DESC; + descriptor = new EditLeaveDescriptorBuilder().withStatus(Status.of(VALID_LEAVE_STATUS_APPROVED)).build(); + expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + + } + + @Test + public void parse_multipleRepeatedFields_failure() { + Index targetIndex = INDEX_FIRST_LEAVE; + String userInput = targetIndex.getOneBased() + INVALID_LEAVE_TITLE_DESC + VALID_LEAVE_TITLE_DESC; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE)); + + // invalid followed by valid + userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC + INVALID_LEAVE_TITLE_DESC; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE)); + + // multiple valid fields repeated + userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_STATUS_DESC + + VALID_LEAVE_START_DATE_DESC + VALID_LEAVE_TITLE_DESC + VALID_LEAVE_STATUS_DESC + + VALID_LEAVE_START_DATE_DESC; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE, + PREFIX_LEAVE_STATUS, PREFIX_LEAVE_DATE_START)); + + // multiple invalid values + userInput = targetIndex.getOneBased() + INVALID_LEAVE_TITLE_DESC + INVALID_LEAVE_STATUS_DESC + + INVALID_LEAVE_DATE_START_DESC + INVALID_LEAVE_TITLE_DESC + INVALID_LEAVE_STATUS_DESC + + INVALID_LEAVE_DATE_START_DESC; + + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_TITLE, + PREFIX_LEAVE_STATUS, PREFIX_LEAVE_DATE_START)); + } + + @Test + public void parse_endBeforeStart_throwsError() { + Index targetIndex = INDEX_FIRST_LEAVE; + // ensure that order is thrown regardless of whether start or end is processed first + String userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC + INVALID_LEAVE_DATE_END_EARLY_DESC + + INVALID_LEAVE_DATE_START_LATE_DESC; + assertParseFailure(parser, userInput, Range.MESSAGE_END_BEFORE_START_ERROR); + + userInput = targetIndex.getOneBased() + VALID_LEAVE_TITLE_DESC + INVALID_LEAVE_DATE_START_LATE_DESC + + INVALID_LEAVE_DATE_END_EARLY_DESC; + assertParseFailure(parser, userInput, Range.MESSAGE_END_BEFORE_START_ERROR); + } + + @Test + public void parse_emptyDescription_success() { + Index targetIndex = INDEX_FIRST_LEAVE; + String userInput = targetIndex.getOneBased() + DESCRIPTION_EMPTY; + EditLeaveDescriptor descriptor = new EditLeaveDescriptorBuilder() + .withDescription(Description.DESCRIPTION_PLACEHOLDER).build(); + EditLeaveCommand expectedCommand = new EditLeaveCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} + + diff --git a/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java new file mode 100644 index 00000000000..14f94074387 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java @@ -0,0 +1,53 @@ +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 java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ExportContactCommand; + +/** + * ExportContactCommandParser implementation is used to test ExportCommandParser + */ +public class ExportCommandParserTest { + private final ExportContactCommandParser parser = new ExportContactCommandParser(); + + @Test + public void parse_validFilePaths_success() { + Path targetFilePath = Paths.get(ExportContactCommand.EXPORT_DEST, "TestFile.csv"); + + assertParseSuccess(parser, "TestFile", new ExportContactCommand(targetFilePath)); + // parser should not append csv more than once + assertParseSuccess(parser, "TestFile.csv", new ExportContactCommand(targetFilePath)); + // parser should replace invalid file extension provided by user with valid file extension + assertParseSuccess(parser, "TestFile.txt", new ExportContactCommand(targetFilePath)); + // parser should recognise the supplied extension to be the suffix that follows the last period + assertParseSuccess(parser, "TestFile.tar.gz.txt", new ExportContactCommand( + Paths.get(ExportContactCommand.EXPORT_DEST, "TestFile.tar.gz.csv") + )); + // parser should extract file name from user input if user provides a path + assertParseSuccess(parser, ExportContactCommand.EXPORT_DEST + "/" + "TestFile", + new ExportContactCommand(targetFilePath)); + // parser should ignore parent directories + assertParseSuccess(parser, "../TestFile", new ExportContactCommand(targetFilePath)); + assertParseSuccess(parser, "fakeParentDir/TestFile", new ExportContactCommand(targetFilePath)); + assertParseSuccess(parser, "fakeGrandparentDir/fakeParentDir/TestFile.txt", + new ExportContactCommand(targetFilePath)); + } + + @Test + public void parse_invalidFilePaths_failure() { + String usageErrorMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportContactCommand.MESSAGE_USAGE); + + // parser should reject empty inputs, or inputs with only spaces + assertParseFailure(parser, "", usageErrorMessage); + assertParseFailure(parser, " ", usageErrorMessage); + // parser should reject directories + assertParseFailure(parser, "parentDir/", usageErrorMessage); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindAllTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindAllTagCommandParserTest.java new file mode 100644 index 00000000000..d8ef31f251e --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindAllTagCommandParserTest.java @@ -0,0 +1,49 @@ +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.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FindAllTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagsContainAllTagsPredicate; +import seedu.address.model.tag.Tag; + +public class FindAllTagCommandParserTest { + + private FindAllTagCommandParser parser = new FindAllTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindAllTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindCommand() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + tagList.add(new Tag("remote")); + // no leading and trailing whitespaces + FindAllTagCommand expectedFindCommand = + new FindAllTagCommand(new TagsContainAllTagsPredicate(tagList)); + assertParseSuccess(parser, " t/full time t/remote ", expectedFindCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n t/full time \n \t t/remote \t", expectedFindCommand); + } + + @Test + public void parse_invalidTags_throwsParseException() { + assertThrows(ParseException.class, () -> parser.parse("full time")); + assertThrows(ParseException.class, () -> parser.parse("t/full time a/remote")); + } + + +} diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java index d92e64d12f9..82c7a8b5db3 100644 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java @@ -13,7 +13,7 @@ public class FindCommandParserTest { - private FindCommandParser parser = new FindCommandParser(); + private final FindCommandParser parser = new FindCommandParser(); @Test public void parse_emptyArg_throwsParseException() { diff --git a/src/test/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParserTest.java new file mode 100644 index 00000000000..ec30315cd12 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindLeaveByPeriodCommandParserTest.java @@ -0,0 +1,78 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_END; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEAVE_DATE_START; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.model.leave.LeaveInPeriodPredicateTest.generatePredicate; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.FindLeaveByPeriodCommand; +import seedu.address.model.leave.Date; + +public class FindLeaveByPeriodCommandParserTest { + private static final String START_DATE_INPUT = "2023-10-30"; + private static final String END_DATE_INPUT = "2023-10-31"; + + private static final Date START_DATE = Date.of(START_DATE_INPUT); + private static final Date END_DATE = Date.of(END_DATE_INPUT); + private final FindLeaveByPeriodCommandParser parser = new FindLeaveByPeriodCommandParser(); + + @Test + public void parse_validArgs_returnsFindLeaveByPeriodCommand() { + + // both start and end dates supplied + String startAndEndInput = generateUserInput(true, true); + FindLeaveByPeriodCommand expectedStartAndEndCommand = new FindLeaveByPeriodCommand( + generatePredicate(START_DATE, END_DATE)); + assertParseSuccess(parser, startAndEndInput, expectedStartAndEndCommand); + + // only start date supplied + String startInput = generateUserInput(true, false); + FindLeaveByPeriodCommand expectedStartCommand = new FindLeaveByPeriodCommand( + generatePredicate(START_DATE, null)); + assertParseSuccess(parser, startInput, expectedStartCommand); + + // only end date supplied + String endInput = generateUserInput(false, true); + FindLeaveByPeriodCommand expectedEndCommand = new FindLeaveByPeriodCommand( + generatePredicate(null, END_DATE)); + assertParseSuccess(parser, endInput, expectedEndCommand); + + // no dates supplied + String noDateInput = generateUserInput(false, false); + FindLeaveByPeriodCommand expectedNoDateCommand = new FindLeaveByPeriodCommand( + generatePredicate(null, null)); + assertParseSuccess(parser, noDateInput, expectedNoDateCommand); + + // multiple whitespaces between characters + String whitespaceInput = " \n " + PREFIX_LEAVE_DATE_START + START_DATE_INPUT + " \n " + + " \t " + PREFIX_LEAVE_DATE_END + END_DATE_INPUT + " \t "; + FindLeaveByPeriodCommand expectedWhitespaceCommand = new FindLeaveByPeriodCommand( + generatePredicate(START_DATE, END_DATE)); + assertParseSuccess(parser, whitespaceInput, expectedWhitespaceCommand); + } + + private String generateUserInput(boolean hasStartDate, boolean hasEndDate) { + String startDateStr = hasStartDate ? " " + PREFIX_LEAVE_DATE_START + START_DATE_INPUT + : ""; + String endDateStr = hasEndDate ? " " + PREFIX_LEAVE_DATE_END + END_DATE_INPUT + : ""; + return startDateStr + endDateStr; + } + + @Test + public void parse_duplicateArgs_throwsParseException() { + String startDateStr = " " + PREFIX_LEAVE_DATE_START + START_DATE_INPUT; + String duplicateStartInput = startDateStr + startDateStr; + assertParseFailure(parser, duplicateStartInput, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_START)); + + String endDateStr = " " + PREFIX_LEAVE_DATE_END + END_DATE_INPUT; + String duplicateEndInput = endDateStr + endDateStr; + assertParseFailure(parser, duplicateEndInput, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_LEAVE_DATE_END)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindLeaveByStatusCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindLeaveByStatusCommandParserTest.java new file mode 100644 index 00000000000..cda1d181392 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindLeaveByStatusCommandParserTest.java @@ -0,0 +1,68 @@ +package seedu.address.logic.parser; + +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.FindLeaveByStatusCommand; +import seedu.address.model.leave.LeaveHasStatusPredicate; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; + +public class FindLeaveByStatusCommandParserTest { + private final FindLeaveByStatusCommandParser parser = new FindLeaveByStatusCommandParser(); + + @Test + public void parse_validArgs_returnsFindLeaveByStatusCommand() { + // approved + String approvedInput = "APPROVED"; + FindLeaveByStatusCommand expectedApprovedCommand = new FindLeaveByStatusCommand( + generatePredicate(StatusType.APPROVED)); + assertParseSuccess(parser, approvedInput, expectedApprovedCommand); + // approved in lowercase + String approvedLowercaseInput = "approved"; + assertParseSuccess(parser, approvedLowercaseInput, expectedApprovedCommand); + // approved with whitespace + String approvedWhitespaceInput = "\tAPPROVED\t"; + assertParseSuccess(parser, approvedWhitespaceInput, expectedApprovedCommand); + + // pending + String pendingInput = "PENDING"; + FindLeaveByStatusCommand expectedPendingCommand = new FindLeaveByStatusCommand( + generatePredicate(StatusType.PENDING)); + assertParseSuccess(parser, pendingInput, expectedPendingCommand); + // approved in lowercase + String pendingLowercaseInput = "pending"; + assertParseSuccess(parser, pendingLowercaseInput, expectedPendingCommand); + // approved with whitespace + String pendingWhitespaceInput = "\tPENDING\t"; + assertParseSuccess(parser, pendingWhitespaceInput, expectedPendingCommand); + + // rejected + String rejectedInput = "REJECTED"; + FindLeaveByStatusCommand expectedRejectedCommand = new FindLeaveByStatusCommand( + generatePredicate(StatusType.REJECTED)); + assertParseSuccess(parser, rejectedInput, expectedRejectedCommand); + // approved in lowercase + String rejectedLowercaseInput = "rejected"; + assertParseSuccess(parser, rejectedLowercaseInput, expectedRejectedCommand); + // approved with whitespace + String rejectedWhitespaceInput = "\tREJECTED\t"; + assertParseSuccess(parser, rejectedWhitespaceInput, expectedRejectedCommand); + } + + private LeaveHasStatusPredicate generatePredicate(StatusType status) { + return new LeaveHasStatusPredicate(Status.of(status)); + } + + @Test + public void parse_invalidArgs_throwsException() { + // empty string + assertParseFailure(parser, "", FindLeaveByStatusCommand.MESSAGE_FAILED); + // non-status word + assertParseFailure(parser, "badStatus", FindLeaveByStatusCommand.MESSAGE_FAILED); + // multiple statuses + assertParseFailure(parser, "APPROVED APPROVED", FindLeaveByStatusCommand.MESSAGE_FAILED); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindLeaveCommandParserTest.java new file mode 100644 index 00000000000..945840d3711 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindLeaveCommandParserTest.java @@ -0,0 +1,53 @@ +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.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.FindLeaveCommand; + + +public class FindLeaveCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindLeaveCommand.MESSAGE_USAGE); + private FindLeaveCommandParser parser = new FindLeaveCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindLeaveCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindCommand() { + Index targetIndex = INDEX_FIRST_PERSON; + String userInput = String.valueOf(targetIndex.getOneBased()); + FindLeaveCommand expectedCommand = new FindLeaveCommand(targetIndex); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_invalidTags_throwsParseException() { + // negative index + assertParseFailure(parser, "-5", MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0", MESSAGE_INVALID_FORMAT); + + // non-numeric indices + assertParseFailure(parser, "abc", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "1abc", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10", MESSAGE_INVALID_FORMAT); + + // out-of-bound index + String intMaxPlusOne = Long.toString((long) Integer.MAX_VALUE + 1); + assertParseFailure(parser, intMaxPlusOne, MESSAGE_INVALID_FORMAT); + + // 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/FindSomeTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindSomeTagCommandParserTest.java new file mode 100644 index 00000000000..3284194ae39 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindSomeTagCommandParserTest.java @@ -0,0 +1,49 @@ +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.testutil.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FindSomeTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagsContainSomeTagsPredicate; +import seedu.address.model.tag.Tag; + +public class FindSomeTagCommandParserTest { + + private FindSomeTagCommandParser parser = new FindSomeTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindSomeTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindCommand() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + tagList.add(new Tag("remote")); + // no leading and trailing whitespaces + FindSomeTagCommand expectedFindCommand = + new FindSomeTagCommand(new TagsContainSomeTagsPredicate(tagList)); + assertParseSuccess(parser, " t/full time t/remote", expectedFindCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n t/full time \n \t t/remote \t", expectedFindCommand); + } + + @Test + public void parse_invalidTags_throwsParseException() { + assertThrows(ParseException.class, () -> parser.parse("full time")); + assertThrows(ParseException.class, () -> parser.parse("t/full time a/remote")); + } + + +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..8ab4e4b8367 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; @@ -13,7 +12,14 @@ import org.junit.jupiter.api.Test; +import seedu.address.logic.parser.exceptions.InvalidIndexException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.model.leave.Title; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -34,17 +40,58 @@ public class ParserUtilTest { private static final String VALID_TAG_1 = "friend"; private static final String VALID_TAG_2 = "neighbour"; + private static final String VALID_TITLE = "Leave Title"; + private static final String VALID_DESCRIPTION = "Leave Description"; + private static final String VALID_START_DATE = "2020-01-01"; + private static final String VALID_END_DATE = "2020-01-02"; + private static final String VALID_STATUS = StatusType.APPROVED.toString(); + + private static final String INVALID_TITLE = "B@d T!tl3"; + + private static final String INVALID_START_DATE = "2020/01/01"; + private static final String INVALID_END_DATE = "2020/01/02"; + + private static final String START_DATE_LATE = VALID_END_DATE; + private static final String END_DATE_EARLY = VALID_START_DATE; + + private static final String INVALID_DESCRIPTION = "*** ***"; + private static final String INVALID_STATUS = "STATUS LOST"; + private static final String WHITESPACE = " \t\r\n"; + @Test + public void parseIndex_nullInput_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseIndex(null)); + } @Test public void parseIndex_invalidInput_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseIndex("10 a")); + // no numeric index at all + assertThrows(ParseException.class, ParserUtil.MESSAGE_INVALID_INDEX, () -> + ParserUtil.parseIndex("abc")); + + // "10a" is not numeric; even though we could potentially extract "10" out of it + // but it's better not to, to avoid dealing with cases like "1a2b3c4a" + assertThrows(ParseException.class, ParserUtil.MESSAGE_INVALID_INDEX, () -> + ParserUtil.parseIndex("10a")); + + // numeric index must come at the start + assertThrows(ParseException.class, ParserUtil.MESSAGE_INVALID_INDEX, () -> + ParserUtil.parseIndex("abc 10")); } @Test - public void parseIndex_outOfRangeInput_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_INVALID_INDEX, () - -> ParserUtil.parseIndex(Long.toString(Integer.MAX_VALUE + 1))); + public void parseIndex_outOfRangeInput_throwsInvalidIndexException() { + // reject negative indices + assertThrows(InvalidIndexException.class, InvalidIndexException.MESSAGE_INVALID_INDEX, () + -> ParserUtil.parseIndex("-1")); + // reject zero index + assertThrows(InvalidIndexException.class, InvalidIndexException.MESSAGE_INVALID_INDEX, () + -> ParserUtil.parseIndex("0")); + + // reject indices beyond Integer.MAX_VALUE + String exceedIntMaxInput = Long.toString((long) Integer.MAX_VALUE + 1); + assertThrows(ParseException.class, ParserUtil.MESSAGE_INVALID_INDEX, () + -> ParserUtil.parseIndex(exceedIntMaxInput)); } @Test @@ -53,12 +100,12 @@ public void parseIndex_validInput_success() throws Exception { assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex("1")); // Leading and trailing whitespaces - assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex(" 1 ")); + assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex(WHITESPACE + "1" + WHITESPACE)); } @Test public void parseName_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseName((String) null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseName(null)); } @Test @@ -81,7 +128,7 @@ public void parseName_validValueWithWhitespace_returnsTrimmedName() throws Excep @Test public void parsePhone_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parsePhone((String) null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parsePhone(null)); } @Test @@ -104,7 +151,7 @@ public void parsePhone_validValueWithWhitespace_returnsTrimmedPhone() throws Exc @Test public void parseAddress_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseAddress((String) null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseAddress(null)); } @Test @@ -127,7 +174,7 @@ public void parseAddress_validValueWithWhitespace_returnsTrimmedAddress() throws @Test public void parseEmail_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseEmail((String) null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseEmail(null)); } @Test @@ -189,8 +236,152 @@ public void parseTags_emptyCollection_returnsEmptySet() throws Exception { @Test public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { Set<Tag> actualTagSet = ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, VALID_TAG_2)); - Set<Tag> expectedTagSet = new HashSet<Tag>(Arrays.asList(new Tag(VALID_TAG_1), new Tag(VALID_TAG_2))); + Set<Tag> expectedTagSet = new HashSet<>(Arrays.asList(new Tag(VALID_TAG_1), new Tag(VALID_TAG_2))); assertEquals(expectedTagSet, actualTagSet); } + + @Test + public void parseTitle_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseTitle(null)); + } + + @Test + public void parseTitle_validTitleNoWhitespace_returnsTitle() throws ParseException { + Title expectedTitle = new Title(VALID_TITLE); + assertEquals(expectedTitle, ParserUtil.parseTitle(VALID_TITLE)); + } + + @Test + public void parseTitle_validTitleWithWhitespace_returnsTitle() throws ParseException { + Title expectedTitle = new Title(VALID_TITLE); + assertEquals(expectedTitle, ParserUtil.parseTitle(WHITESPACE + VALID_TITLE + WHITESPACE)); + } + + @Test + public void parseTitle_invalidTitle_throwsParseException() throws ParseException { + assertThrows(ParseException.class, Title.MESSAGE_CONSTRAINTS, () -> ParserUtil.parseTitle(INVALID_TITLE)); + } + + @Test + public void parseNonNullRange_nullDate_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseNonNullRange(null, VALID_START_DATE)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseNonNullRange(VALID_START_DATE, null)); + } + + @Test + public void parseNonNullRange_invalidDate_throwsException() { + assertThrows(ParseException.class, () -> ParserUtil.parseNonNullRange(INVALID_START_DATE, VALID_END_DATE)); + assertThrows(ParseException.class, () -> ParserUtil.parseNonNullRange(VALID_START_DATE, INVALID_END_DATE)); + } + + @Test + public void parseNonNullRange_endBeforeStart_throwsException() { + assertThrows(ParseException.class, () -> ParserUtil.parseNonNullRange(START_DATE_LATE, END_DATE_EARLY)); + } + + @Test + public void parseNonNullRange_validDates_returnRange() throws Exception { + Range expected = Range.createNonNullRange(Date.of(VALID_START_DATE), Date.of(VALID_END_DATE)); + assertEquals(ParserUtil.parseNonNullRange(VALID_START_DATE, VALID_END_DATE), expected); + } + + @Test + public void parseDate_nullDate_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseDate(null)); + } + + @Test + public void parseDate_validDateNoWhitespace_returnsDate() throws Exception { + Date expected = Date.of(VALID_START_DATE); + assertEquals(ParserUtil.parseDate(VALID_START_DATE), expected); + } + + @Test + public void parseDate_validDateWithWhitespace_returnsDate() throws Exception { + Date expected = Date.of(VALID_START_DATE); + assertEquals(ParserUtil.parseDate(WHITESPACE + VALID_START_DATE + WHITESPACE), expected); + } + + @Test + public void parseDate_invalidDate_throwsParseException() throws Exception { + assertThrows(ParseException.class, Date.MESSAGE_CONSTRAINTS, () -> ParserUtil.parseDate(INVALID_START_DATE)); + } + + @Test + public void parseNullableRange_invalidDate_throwsException() { + assertThrows(ParseException.class, () -> ParserUtil.parseNullableRange(INVALID_START_DATE, VALID_END_DATE)); + assertThrows(ParseException.class, () -> ParserUtil.parseNullableRange(VALID_START_DATE, INVALID_END_DATE)); + } + + @Test + public void parseNullableRange_endBeforeStart_throwsException() { + assertThrows(ParseException.class, () -> ParserUtil.parseNullableRange(START_DATE_LATE, END_DATE_EARLY)); + } + + @Test + public void parseNullableRange_validDates_returnRange() throws Exception { + Range expected; + + // start and end date present + expected = Range.createNullableRange(Date.of(VALID_START_DATE), Date.of(VALID_END_DATE)); + assertEquals(ParserUtil.parseNullableRange(VALID_START_DATE, VALID_END_DATE), expected); + + // start date present + expected = Range.createNullableRange(Date.of(VALID_START_DATE), null); + assertEquals(ParserUtil.parseNullableRange(VALID_START_DATE, null), expected); + + // end date present + expected = Range.createNullableRange(null, Date.of(VALID_END_DATE)); + assertEquals(ParserUtil.parseNullableRange(null, VALID_END_DATE), expected); + + // start and end date not present + expected = Range.createNullableRange(null, null); + assertEquals(ParserUtil.parseNullableRange(null, null), expected); + } + + @Test + public void parseDescription_nullDescription_throwsException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseDescription(null)); + } + + @Test + public void parseDescription_validDescriptionNoWhitespace_returnsDescription() throws Exception { + Description expected = new Description(VALID_DESCRIPTION); + assertEquals(ParserUtil.parseDescription(VALID_DESCRIPTION), expected); + } + + @Test + public void parseDescription_validDescriptionWithWhitespace_returnsDescription() throws Exception { + Description expected = new Description(VALID_DESCRIPTION); + assertEquals(ParserUtil.parseDescription(WHITESPACE + VALID_DESCRIPTION + WHITESPACE), expected); + } + + @Test + public void parseDescription_invalidDescription_throwsParseException() { + assertThrows(ParseException.class, Description.MESSAGE_CONSTRAINTS, () -> + ParserUtil.parseDescription(INVALID_DESCRIPTION)); + } + + @Test + public void parseStatus_nullStatus_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseStatus(null)); + } + + @Test + public void parseStatus_validStatusNoWhitespace_returnsStatus() throws Exception { + Status expected = Status.of(VALID_STATUS); + assertEquals(ParserUtil.parseStatus(VALID_STATUS), expected); + } + + @Test + public void parseStatus_validStatusWithWhitespace_returnsStatus() throws Exception { + Status expected = Status.of(VALID_STATUS); + assertEquals(ParserUtil.parseStatus(WHITESPACE + VALID_STATUS + WHITESPACE), expected); + } + + @Test + public void parseStatus_invalidStatus_throwsParseException() { + assertThrows(ParseException.class, Status.MESSAGE_CONSTRAINTS, () -> ParserUtil.parseStatus(INVALID_STATUS)); + } } diff --git a/src/test/java/seedu/address/logic/parser/RejectLeaveCommandParserTest.java b/src/test/java/seedu/address/logic/parser/RejectLeaveCommandParserTest.java new file mode 100644 index 00000000000..23739e21228 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/RejectLeaveCommandParserTest.java @@ -0,0 +1,56 @@ +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.testutil.TypicalIndexes.INDEX_FIRST_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.RejectLeaveCommand; + +public class RejectLeaveCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RejectLeaveCommand.MESSAGE_USAGE); + + private final RejectLeaveCommandParser parser = new RejectLeaveCommandParser(); + + @Test + public void parse_emptyIndex_failure() { + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, " ", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_validArgs_returnsFindCommand() { + Index targetIndex = INDEX_FIRST_LEAVE; + String userInput = String.valueOf(targetIndex.getOneBased()); + RejectLeaveCommand expectedCommand = new RejectLeaveCommand(targetIndex); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_nonNumericIndex_failure() { + // non-numeric indices + assertParseFailure(parser, "1a", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc", MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "abc 10", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidIndex_failure() { + // negative index + assertParseFailure(parser, "-5", MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0", MESSAGE_INVALID_FORMAT); + + // exceed Integer.MAX_VALUE + assertParseFailure(parser, "2147483648", MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + } +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 68c8c5ba4d5..38d53a63f64 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -20,6 +20,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; import seedu.address.testutil.PersonBuilder; public class AddressBookTest { @@ -70,6 +71,22 @@ public void hasPerson_personInAddressBook_returnsTrue() { assertTrue(addressBook.hasPerson(ALICE)); } + @Test + public void getPerson_personDoesNotExist_throwsPersonNotFoundException() { + assertThrows(PersonNotFoundException.class, () -> addressBook.getPerson(ALICE)); + } + + @Test + public void getPerson_nullPerson_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> addressBook.getPerson(null)); + } + + @Test + public void getPerson_validPerson_success() { + addressBook.addPerson(ALICE); + assertEquals(ALICE, addressBook.getPerson(ALICE)); + } + @Test public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() { addressBook.addPerson(ALICE); @@ -89,6 +106,38 @@ public void toStringMethod() { assertEquals(expected, addressBook.toString()); } + @Test + public void equalsMethod() { + // same object + assertTrue(addressBook.equals(addressBook)); + + // different class + assertFalse(addressBook.equals(new Object())); + + // same class, different person list + AddressBook addressBook2 = new AddressBook(); + addressBook2.addPerson(ALICE); + assertFalse(addressBook.equals(addressBook2)); + + // same class, same person list + addressBook.addPerson(ALICE); + assertTrue(addressBook.equals(addressBook2)); + } + + @Test + public void hashcodeMethod() { + // same object + assertTrue(addressBook.hashCode() == addressBook.hashCode()); + + // different class + assertFalse(addressBook.hashCode() == new Object().hashCode()); + + // same class, different person list + AddressBook addressBook2 = new AddressBook(); + addressBook2.addPerson(ALICE); + assertFalse(addressBook.hashCode() == addressBook2.hashCode()); + } + /** * A stub ReadOnlyAddressBook whose persons list can violate interface constraints. */ @@ -104,5 +153,4 @@ public ObservableList<Person> getPersonList() { return persons; } } - } diff --git a/src/test/java/seedu/address/model/LeavesBookTest.java b/src/test/java/seedu/address/model/LeavesBookTest.java new file mode 100644 index 00000000000..e422b141a49 --- /dev/null +++ b/src/test/java/seedu/address/model/LeavesBookTest.java @@ -0,0 +1,185 @@ +package seedu.address.model; + +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.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BOB_LEAVE; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.exceptions.DuplicateLeaveException; +import seedu.address.model.leave.exceptions.LeaveNotFoundException; +import seedu.address.testutil.LeaveBuilder; + +public class LeavesBookTest { + + private final LeavesBook leavesBook = new LeavesBook(); + + @Test + public void constructor() { + assertEquals(Collections.emptyList(), leavesBook.getLeaveList()); + } + + @Test + public void resetData_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> leavesBook.resetData(null)); + } + + @Test + public void resetData_withValidReadOnlyLeavesBook_replacesData() { + LeavesBook newData = getTypicalLeavesBook(); + leavesBook.resetData(newData); + assertEquals(newData, leavesBook); + } + + @Test + public void resetData_withDuplicateLeaves_throwsDuplicateLeaveException() { + // Two leaves with the same identity fields + Leave editedAlice = new LeaveBuilder(ALICE_LEAVE).withDescription( + "Alice's Maternity Leave Description").build(); + List<Leave> newLeaves = Arrays.asList(ALICE_LEAVE, editedAlice); + LeavesBookStub newData = new LeavesBookStub(newLeaves); + + assertThrows(DuplicateLeaveException.class, () -> leavesBook.resetData(newData)); + } + + @Test + public void hasLeave_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> leavesBook.hasLeave(null)); + } + + @Test + public void hasLeave_leaveNotInLeavesBook_returnsFalse() { + assertFalse(leavesBook.hasLeave(ALICE_LEAVE)); + } + + @Test + public void hasLeave_leaveInLeavesBook_returnsTrue() { + leavesBook.addLeave(ALICE_LEAVE); + assertTrue(leavesBook.hasLeave(ALICE_LEAVE)); + } + + @Test + public void hasLeave_leaveWithSameIdentityFieldsInLeavesBook_returnsTrue() { + leavesBook.addLeave(ALICE_LEAVE); + Leave editedAlice = new LeaveBuilder(ALICE_LEAVE).withDescription( + "Alice's Maternity Leave Description").build(); + assertTrue(leavesBook.hasLeave(editedAlice)); + } + + @Test + public void addLeave_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> leavesBook.addLeave(null)); + } + + @Test + public void addLeave_duplicateLeave_throwsDuplicateLeaveException() { + leavesBook.addLeave(ALICE_LEAVE); + assertThrows(DuplicateLeaveException.class, () -> leavesBook.addLeave(ALICE_LEAVE)); + } + + @Test + public void setLeave_nullTargetLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> leavesBook.setLeave(null, ALICE_LEAVE)); + } + + @Test + public void setLeave_targetNotInBook_throwsLeaveNotFoundException() { + assertThrows(LeaveNotFoundException.class, () -> leavesBook.setLeave(ALICE_LEAVE, ALICE_LEAVE)); + } + + @Test + public void setLeave_targetAlreadyExistInBook_throwsDuplicateLeaveException() { + leavesBook.addLeave(ALICE_LEAVE); + leavesBook.addLeave(BOB_LEAVE); + assertThrows(DuplicateLeaveException.class, () -> leavesBook.setLeave(ALICE_LEAVE, BOB_LEAVE)); + } + + @Test + public void setLeave_editedLeaveIsSameLeave_success() { + leavesBook.addLeave(ALICE_LEAVE); + leavesBook.setLeave(ALICE_LEAVE, ALICE_LEAVE); + LeavesBook expectedLeavesBook = new LeavesBook(); + expectedLeavesBook.addLeave(ALICE_LEAVE); + assertEquals(expectedLeavesBook, leavesBook); + } + + @Test + public void removeLeave_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> leavesBook.removeLeave(null)); + } + + @Test + public void removeLeave_leaveDoesNotExist_throwsLeaveNotFoundException() { + assertThrows(LeaveNotFoundException.class, () -> leavesBook.removeLeave(ALICE_LEAVE)); + } + + @Test + public void removeLeave_existingLeave_removesLeave() { + leavesBook.addLeave(ALICE_LEAVE); + leavesBook.removeLeave(ALICE_LEAVE); + LeavesBook expectedLeavesBook = new LeavesBook(); + assertEquals(expectedLeavesBook, leavesBook); + } + + @Test + public void getLeaveList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> leavesBook.getLeaveList().remove(0)); + } + + @Test + public void equalsMethod() { + // same object -> returns true + assertTrue(leavesBook.equals(leavesBook)); + + // null -> returns false + assertFalse(leavesBook.equals(null)); + + // different types -> returns false + assertFalse(leavesBook.equals(5)); + + // different leaves book -> returns false + LeavesBook differentLeavesBook = new LeavesBook(); + differentLeavesBook.addLeave(ALICE_LEAVE); + assertFalse(leavesBook.equals(differentLeavesBook)); + + // same unique leave list -> return true + LeavesBook differentLeavesBook2 = new LeavesBook(); + differentLeavesBook2.addLeave(ALICE_LEAVE); + assertTrue(differentLeavesBook.equals(differentLeavesBook2)); + } + + @Test + public void toStringMethod() { + String expected = LeavesBook.class.getCanonicalName() + "{leaves=" + leavesBook.getLeaveList() + "}"; + assertEquals(expected, leavesBook.toString()); + } + + /** + * A stub ReadOnlyLeavesBook whose leaves list can violate interface constraints. + */ + private static class LeavesBookStub implements ReadOnlyLeavesBook { + private final ObservableList<Leave> leaves = FXCollections.observableArrayList(); + + LeavesBookStub(Collection<Leave> leaves) { + this.leaves.setAll(leaves); + } + + @Override + public ObservableList<Leave> getLeaveList() { + return leaves; + } + } + +} diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..662c30a8ab2 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -3,8 +3,11 @@ 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.Model.PREDICATE_SHOW_ALL_LEAVES; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BENSON_LEAVE; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BENSON; @@ -15,6 +18,7 @@ import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.leave.LeaveContainsPersonPredicate; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.testutil.AddressBookBuilder; @@ -27,6 +31,7 @@ public void constructor() { assertEquals(new UserPrefs(), modelManager.getUserPrefs()); assertEquals(new GuiSettings(), modelManager.getGuiSettings()); assertEquals(new AddressBook(), new AddressBook(modelManager.getAddressBook())); + assertEquals(new LeavesBook(), new LeavesBook(modelManager.getLeavesBook())); } @Test @@ -97,11 +102,17 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException public void equals() { AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); AddressBook differentAddressBook = new AddressBook(); + + LeavesBook leavesBook = new LeavesBook(); + leavesBook.addLeave(ALICE_LEAVE); + leavesBook.addLeave(BENSON_LEAVE); + LeavesBook differentLeavesBook = new LeavesBook(); + UserPrefs userPrefs = new UserPrefs(); // same values -> returns true - modelManager = new ModelManager(addressBook, userPrefs); - ModelManager modelManagerCopy = new ModelManager(addressBook, userPrefs); + modelManager = new ModelManager(addressBook, leavesBook, userPrefs); + ModelManager modelManagerCopy = new ModelManager(addressBook, leavesBook, userPrefs); assertTrue(modelManager.equals(modelManagerCopy)); // same object -> returns true @@ -114,19 +125,29 @@ public void equals() { assertFalse(modelManager.equals(5)); // different addressBook -> returns false - assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs))); + assertFalse(modelManager.equals(new ModelManager(differentAddressBook, leavesBook, userPrefs))); // different filteredList -> returns false - String[] keywords = ALICE.getName().fullName.split("\\s+"); + String[] keywords = ALICE.getName().toString().split("\\s+"); modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords))); - assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); + assertFalse(modelManager.equals(new ModelManager(addressBook, leavesBook, userPrefs))); // resets modelManager to initial state for upcoming tests modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + // different leavesBook -> returns false + assertFalse(modelManager.equals(new ModelManager(addressBook, differentLeavesBook, userPrefs))); + + // different filteredList -> returns false + modelManager.updateFilteredLeaveList(new LeaveContainsPersonPredicate(BENSON)); + assertFalse(modelManager.equals(new ModelManager(addressBook, leavesBook, userPrefs))); + + // resets modelManager to initial state for upcoming tests + modelManager.updateFilteredLeaveList(PREDICATE_SHOW_ALL_LEAVES); + // 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, leavesBook, differentUserPrefs))); } } diff --git a/src/test/java/seedu/address/model/ReadOnlyFilteredAddressBookTest.java b/src/test/java/seedu/address/model/ReadOnlyFilteredAddressBookTest.java new file mode 100644 index 00000000000..db313a26091 --- /dev/null +++ b/src/test/java/seedu/address/model/ReadOnlyFilteredAddressBookTest.java @@ -0,0 +1,23 @@ +package seedu.address.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalPersons.getTypicalPersons; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Person; + +public class ReadOnlyFilteredAddressBookTest { + @Test + public void getPersonList() { + Model model = new ModelManager(getTypicalAddressBook(), getTypicalLeavesBook(), new UserPrefs()); + ReadOnlyFilteredAddressBook addressBook = new ReadOnlyFilteredAddressBook(model); + + List<Person> persons = addressBook.getPersonList(); + assertEquals(persons, getTypicalPersons()); + } +} diff --git a/src/test/java/seedu/address/model/UserPrefsTest.java b/src/test/java/seedu/address/model/UserPrefsTest.java index b1307a70d52..2c045014674 100644 --- a/src/test/java/seedu/address/model/UserPrefsTest.java +++ b/src/test/java/seedu/address/model/UserPrefsTest.java @@ -1,5 +1,7 @@ package seedu.address.model; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; @@ -18,4 +20,13 @@ public void setAddressBookFilePath_nullPath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> userPrefs.setAddressBookFilePath(null)); } + @Test + public void equalMethod() { + // same object + UserPrefs userPrefs = new UserPrefs(); + assertTrue(userPrefs.equals(userPrefs)); + + // different class + assertFalse(userPrefs.equals(new Object())); + } } diff --git a/src/test/java/seedu/address/model/leave/DateTest.java b/src/test/java/seedu/address/model/leave/DateTest.java new file mode 100644 index 00000000000..77b92deda6d --- /dev/null +++ b/src/test/java/seedu/address/model/leave/DateTest.java @@ -0,0 +1,90 @@ +package seedu.address.model.leave; + +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.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import org.junit.jupiter.api.Test; + +public class DateTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> Date.of((String) null)); + assertThrows(NullPointerException.class, () -> Date.of((LocalDate) null)); + } + + @Test + public void constructor_incorrectStringFormat_throwsIllegalArgumentException() { + assertThrows(DateTimeParseException.class, () -> Date.of("2020-01-01-01")); + assertThrows(DateTimeParseException.class, () -> Date.of("2020/01/01")); + } + + @Test + public void constructor_leapYear_throwsDateTimeException() { + assertThrows(DateTimeException.class, () -> Date.of("2020-02-30")); + } + + @Test + public void constructor_invalidDate_throwsDateTimeException() { + assertThrows(DateTimeException.class, () -> Date.of("2020-02-31")); + assertThrows(DateTimeException.class, () -> Date.of("2020-04-31")); + assertThrows(DateTimeException.class, () -> Date.of("2020-06-31")); + assertThrows(DateTimeException.class, () -> Date.of("2020-09-31")); + assertThrows(DateTimeException.class, () -> Date.of("2020-11-31")); + } + + @Test + public void constructor_validDate() { + assertTrue(Date.of("2020-01-01").getDate().toString().equals("2020-01-01")); + assertTrue(Date.of(LocalDate.of(2020, 1, 1)).getDate().toString().equals("2020-01-01")); + } + + @Test + public void isBeforeMethod() { + assertTrue(Date.of("2020-01-01").isBefore(Date.of("2020-01-02"))); + assertFalse(Date.of("2020-01-01").isBefore(Date.of("2020-01-01"))); + assertFalse(Date.of("2020-01-02").isBefore(Date.of("2020-01-01"))); + } + + @Test + public void isAfterMethod() { + assertTrue(Date.of("2020-01-02").isAfter(Date.of("2020-01-01"))); + assertFalse(Date.of("2020-01-01").isAfter(Date.of("2020-01-02"))); + assertFalse(Date.of("2020-01-01").isAfter(Date.of("2020-01-01"))); + } + + @Test + public void toStringMethod() { + assertTrue(Date.of("2020-01-01").toString().equals("2020-01-01")); + } + + @Test + public void equalsMethod() { + Date date = Date.of("2020-01-01"); + + // same values -> returns true + assertTrue(date.equals(Date.of("2020-01-01"))); + + // same object -> returns true + assertTrue(date.equals(date)); + + // null -> returns false + assertFalse(date.equals(null)); + + // different types -> returns false + assertFalse(date.equals(5.0f)); + + // different values -> returns false + assertFalse(date.equals(Date.of("2020-01-02"))); + } + + @Test + public void hashcodeMethod() { + assertTrue(Date.of("2020-01-01").hashCode() == Date.of("2020-01-01").hashCode()); + assertFalse(Date.of("2020-01-01").hashCode() == Date.of("2020-01-02").hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/leave/DescriptionTest.java b/src/test/java/seedu/address/model/leave/DescriptionTest.java new file mode 100644 index 00000000000..87034cbfb68 --- /dev/null +++ b/src/test/java/seedu/address/model/leave/DescriptionTest.java @@ -0,0 +1,71 @@ +package seedu.address.model.leave; + +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 org.junit.jupiter.api.Test; + +public class DescriptionTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Description(null)); + } + + @Test + public void constructor_invalidDescription_throwsIllegalArgumentException() { + String invalidDescription = "testing#*"; + assertThrows(IllegalArgumentException.class, Description.MESSAGE_CONSTRAINTS, () + -> new Description(invalidDescription)); + } + + @Test + public void constructor_emptyDescription_valid() { + String emptyDescription = ""; + assertEquals(new Description(emptyDescription).toString(), emptyDescription); + } + + @Test + public void factoryConstructor_getDefault() { + assertEquals(Description.getDefault().toString(), ""); + } + + @Test + public void constructor_validDescription_success() { + String validDescription = "testing"; + assertEquals(new Description(validDescription).toString(), validDescription); + + validDescription = "Alice's maternity leave. Please, refer to attachment or something"; + assertEquals(new Description(validDescription).toString(), validDescription); + } + + @Test + public void isEmpty() { + String nonEmptyDescription = "testing"; + assertFalse(new Description(nonEmptyDescription).isEmpty()); + + String emptyDescription = ""; + assertTrue(new Description(emptyDescription).isEmpty()); + } + + @Test + public void equalsMethod() { + Description description = new Description("testing"); + Description descriptionCopy = new Description("testing"); + assertTrue(description.equals(descriptionCopy)); + + // different description + Description differentDescription = new Description("testing123"); + assertFalse(description.equals(differentDescription)); + + // different object + assertFalse(description.equals(new Object())); + + // null + assertFalse(description.equals(null)); + + // same object + assertTrue(description.equals(description)); + } +} diff --git a/src/test/java/seedu/address/model/leave/LeaveHasStatusPredicateTest.java b/src/test/java/seedu/address/model/leave/LeaveHasStatusPredicateTest.java new file mode 100644 index 00000000000..26ad4310a67 --- /dev/null +++ b/src/test/java/seedu/address/model/leave/LeaveHasStatusPredicateTest.java @@ -0,0 +1,61 @@ +package seedu.address.model.leave; + +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.leave.Status.StatusType.APPROVED; +import static seedu.address.model.leave.Status.StatusType.PENDING; +import static seedu.address.model.leave.Status.StatusType.REJECTED; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.LeaveBuilder; + +public class LeaveHasStatusPredicateTest { + private static final LeaveHasStatusPredicate PENDING_PRED = new LeaveHasStatusPredicate(Status.of(PENDING)); + private static final LeaveHasStatusPredicate APPROVED_PRED = new LeaveHasStatusPredicate(Status.of(APPROVED)); + private static final LeaveHasStatusPredicate REJECTED_PRED = new LeaveHasStatusPredicate(Status.of(REJECTED)); + + @Test + public void constructor_nullRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new LeaveHasStatusPredicate(null)); + } + + @Test + public void equals() { + // same predicate + assertEquals(PENDING_PRED, PENDING_PRED); + // diff type + assertFalse(PENDING_PRED.equals("1")); + + // same status + LeaveHasStatusPredicate pendingPredCopy = new LeaveHasStatusPredicate(Status.of(PENDING)); + assertEquals(PENDING_PRED, pendingPredCopy); + + // diff status + assertNotEquals(PENDING_PRED, APPROVED_PRED); + assertNotEquals(PENDING_PRED, REJECTED_PRED); + assertNotEquals(APPROVED_PRED, REJECTED_PRED); + } + + @Test + public void test() { + Leave approvedLeave = new LeaveBuilder().withStatus(APPROVED).build(); + Leave pendingLeave = new LeaveBuilder().withStatus(PENDING).build(); + Leave rejectedLeave = new LeaveBuilder().withStatus(REJECTED).build(); + + assertTrue(APPROVED_PRED.test(approvedLeave)); + assertFalse(APPROVED_PRED.test(pendingLeave)); + assertFalse(APPROVED_PRED.test(rejectedLeave)); + + assertTrue(PENDING_PRED.test(pendingLeave)); + assertFalse(PENDING_PRED.test(approvedLeave)); + assertFalse(PENDING_PRED.test(rejectedLeave)); + + assertTrue(REJECTED_PRED.test(rejectedLeave)); + assertFalse(REJECTED_PRED.test(approvedLeave)); + assertFalse(REJECTED_PRED.test(pendingLeave)); + } +} diff --git a/src/test/java/seedu/address/model/leave/LeaveInPeriodPredicateTest.java b/src/test/java/seedu/address/model/leave/LeaveInPeriodPredicateTest.java new file mode 100644 index 00000000000..3728fe8a846 --- /dev/null +++ b/src/test/java/seedu/address/model/leave/LeaveInPeriodPredicateTest.java @@ -0,0 +1,295 @@ +package seedu.address.model.leave; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.leave.exceptions.EndBeforeStartException; +import seedu.address.testutil.LeaveBuilder; + + +public class LeaveInPeriodPredicateTest { + private static final Date FIRST_DATE = Date.of("2023-10-20"); + private static final Date SECOND_DATE = Date.of("2023-10-27"); + private static final Date THIRD_DATE = Date.of("2023-11-03"); + private static final Date FOURTH_DATE = Date.of("2023-11-10"); + + private static final Date FIFTH_DATE = Date.of("2023-11-17"); + private static final Date SIXTH_DATE = Date.of("2023-11-24"); + + @Test + public void constructor_nullRange_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new LeaveInPeriodPredicate(null)); + } + @Test + public void equals_startAndEndDateNonNull() { + LeaveInPeriodPredicate firstPredicate = generatePredicate(FIRST_DATE, SECOND_DATE); + LeaveInPeriodPredicate secondPredicate = generatePredicate(FIRST_DATE, THIRD_DATE); + LeaveInPeriodPredicate thirdPredicate = generatePredicate(SECOND_DATE, THIRD_DATE); + + // same object -> returns true + assertEquals(firstPredicate, firstPredicate); + + // different types -> returns false + assertFalse(firstPredicate.equals("1")); + + // same start and end date -> returns true + LeaveInPeriodPredicate firstPredicateCopy = generatePredicate(FIRST_DATE, SECOND_DATE); + assertEquals(firstPredicate, firstPredicateCopy); + + // same start, different end -> returns false + assertNotEquals(secondPredicate, firstPredicate); + + // different start, same end -> returns false + assertNotEquals(thirdPredicate, secondPredicate); + } + + /** + * Generates a LeaveInPeriodPredicate that can take in null start and end dates + * @param startDate Start date of period / null + * @param endDate End date of period / null + * @return LeaveInPeriodPredicate containing specified date range + */ + public static LeaveInPeriodPredicate generatePredicate(Date startDate, Date endDate) { + Range dateRange = Range.createNullableRange(startDate, endDate); + return new LeaveInPeriodPredicate(dateRange); + } + + @Test + public void equals_startNull() { + LeaveInPeriodPredicate defaultPredicate = generatePredicate(null, SECOND_DATE); + LeaveInPeriodPredicate nonNullStartPredicate = generatePredicate(FIRST_DATE, SECOND_DATE); + LeaveInPeriodPredicate diffEndPredicate = generatePredicate(null, FIRST_DATE); + + // same end date -> returns true + LeaveInPeriodPredicate defaultPredicateCopy = generatePredicate(null, SECOND_DATE); + assertEquals(defaultPredicate, defaultPredicateCopy); + + // compare with non-null start date -> returns false + assertNotEquals(nonNullStartPredicate, defaultPredicate); + + // diff end date -> returns false + assertNotEquals(diffEndPredicate, defaultPredicate); + } + + @Test + public void equals_endNull() { + LeaveInPeriodPredicate defaultPredicate = generatePredicate(FIRST_DATE, null); + LeaveInPeriodPredicate nonNullEndPredicate = generatePredicate(FIRST_DATE, SECOND_DATE); + LeaveInPeriodPredicate diffStartPredicate = generatePredicate(SECOND_DATE, null); + + // same start date -> returns true + LeaveInPeriodPredicate defaultPredicateCopy = generatePredicate(FIRST_DATE, null); + assertEquals(defaultPredicate, defaultPredicateCopy); + + // compare with non-null end date -> returns false + assertNotEquals(nonNullEndPredicate, defaultPredicate); + + // diff start date -> returns false + assertNotEquals(diffStartPredicate, defaultPredicate); + } + + @Test + public void equals_startAndEndNull() { + LeaveInPeriodPredicate defaultPredicate = generatePredicate(null, null); + LeaveInPeriodPredicate nonNullStartPredicate = generatePredicate(FIRST_DATE, null); + LeaveInPeriodPredicate nonNullEndPredicate = generatePredicate(null, SECOND_DATE); + + // start and end both null -> returns true + LeaveInPeriodPredicate defaultPredicateCopy = generatePredicate(null, null); + assertEquals(defaultPredicate, defaultPredicateCopy); + + // compare with non-null start date -> returns false + assertNotEquals(nonNullStartPredicate, defaultPredicate); + + // compare with non-null end date -> returns false + assertNotEquals(nonNullEndPredicate, defaultPredicate); + } + + @Test + public void constructor_validInputs_doesNotThrow() { + // no dates supplied + assertDoesNotThrow(() -> generatePredicate(null, null)); + // only start date supplied + assertDoesNotThrow(() -> generatePredicate(FIRST_DATE, null)); + // only end date supplied + assertDoesNotThrow(() -> generatePredicate(null, SIXTH_DATE)); + // start and end date supplied, start before end + assertDoesNotThrow(() -> generatePredicate(FIRST_DATE, SIXTH_DATE)); + // start and end date supplied, start equals end + assertDoesNotThrow(() -> generatePredicate(FIRST_DATE, FIRST_DATE)); + } + + @Test + public void constructor_endBeforeStart_throwsException() { + assertThrows(EndBeforeStartException.class, () -> + generatePredicate(SECOND_DATE, FIRST_DATE)); + } + + @Test + public void test_noStartAndEndDate_returnsTrue() { + LeaveInPeriodPredicate predicate = generatePredicate(null, null); + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(SECOND_DATE) + .build())); + } + + @Test + public void test_hasStartAndEndDate_returnsTrue() { + LeaveInPeriodPredicate predicate = generatePredicate( + SECOND_DATE, FIFTH_DATE); + + // start before period, end within period -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(FOURTH_DATE) + .build())); + // start before period, end date is same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(FIFTH_DATE) + .build())); + // start before period, end after period -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(SIXTH_DATE) + .build())); + + // start date is same, end date within range -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(SECOND_DATE).withEnd(FOURTH_DATE) + .build())); + // start date is same, end date is same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(SECOND_DATE).withEnd(FIFTH_DATE) + .build())); + // start date is same, end after period -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(SECOND_DATE).withEnd(SIXTH_DATE) + .build())); + + // start within range, end within range -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(THIRD_DATE).withEnd(FOURTH_DATE) + .build())); + // start date within range, end date is same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(THIRD_DATE).withEnd(FIFTH_DATE) + .build())); + // start within period, end after period -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(THIRD_DATE).withEnd(SIXTH_DATE) + .build())); + + // Boundary tests - we only require an intersection of at least one day + // start before period, end date same as query start date -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(SECOND_DATE) + .build())); + // start and end date and query start date are the same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(SECOND_DATE).withEnd(SECOND_DATE) + .build())); + // start date same as query end date, end after period -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIFTH_DATE).withEnd(SIXTH_DATE) + .build())); + // start and end and query end date are the same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIFTH_DATE).withEnd(FIFTH_DATE) + .build())); + } + + @Test + public void test_hasStartAndEndDate_returnsFalse() { + LeaveInPeriodPredicate latePredicate = generatePredicate(THIRD_DATE, SIXTH_DATE); + // end date before query start date -> return false + assertFalse(latePredicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(SECOND_DATE) + .build())); + + LeaveInPeriodPredicate earlyPredicate = generatePredicate(FIRST_DATE, FOURTH_DATE); + // start date after query end date -> returns false + assertFalse(earlyPredicate.test(new LeaveBuilder() + .withStart(FIFTH_DATE).withEnd(SIXTH_DATE) + .build())); + } + + @Test + public void test_hasStartDate_returnsTrue() { + LeaveInPeriodPredicate predicate = generatePredicate(THIRD_DATE, null); + + // start date before query start, end date on query start -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(THIRD_DATE) + .build())); + // start date before query start, end date after query start -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(FOURTH_DATE) + .build())); + // start date on query start, end date after query start -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(THIRD_DATE).withEnd(SIXTH_DATE) + .build())); + // start date after query start -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FOURTH_DATE).withEnd(SIXTH_DATE) + .build())); + + // Boundary test + // start date and end date same as query start date -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(THIRD_DATE).withEnd(THIRD_DATE) + .build())); + } + + @Test + public void test_hasStartDate_returnsFalse() { + LeaveInPeriodPredicate predicate = generatePredicate(THIRD_DATE, null); + + // end date is before query start -> returns false + assertFalse(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(SECOND_DATE) + .build())); + } + + @Test + public void test_hasEndDate_returnsTrue() { + LeaveInPeriodPredicate predicate = generatePredicate(null, FOURTH_DATE); + + // end date before query end -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(THIRD_DATE) + .build())); + // start date before query end, end date same as query end -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(FOURTH_DATE) + .build())); + // start date before query end, end date after query end -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FIRST_DATE).withEnd(FIFTH_DATE) + .build())); + // start date on query end, end date after query end -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FOURTH_DATE).withEnd(SIXTH_DATE) + .build())); + + // Boundary test + // start date and end date and query end are the same -> returns true + assertTrue(predicate.test(new LeaveBuilder() + .withStart(FOURTH_DATE).withEnd(FOURTH_DATE) + .build())); + } + + @Test + public void test_hasEndDate_returnsFalse() { + LeaveInPeriodPredicate predicate = generatePredicate(null, FOURTH_DATE); + + // start date after query end -> returns false + assertFalse(predicate.test(new LeaveBuilder() + .withStart(FIFTH_DATE).withEnd(SIXTH_DATE) + .build())); + } +} diff --git a/src/test/java/seedu/address/model/leave/LeaveTest.java b/src/test/java/seedu/address/model/leave/LeaveTest.java new file mode 100644 index 00000000000..c9c0f031b9c --- /dev/null +++ b/src/test/java/seedu/address/model/leave/LeaveTest.java @@ -0,0 +1,219 @@ +package seedu.address.model.leave; + +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.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BOB_LEAVE; +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.ComparablePerson; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.testutil.LeaveBuilder; +import seedu.address.testutil.PersonBuilder; + +public class LeaveTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Leave(null, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()))); + assertThrows(NullPointerException.class, () -> new Leave(new MockPersonWithNullName(), ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()))); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, null, + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()))); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + null)); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), null))); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(null, ALICE_LEAVE.getEnd()))); + + assertThrows(NullPointerException.class, () -> new Leave(null, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()), ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(new MockPersonWithNullName(), ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()), ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, null, + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()), ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + null, ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), null), ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(null, ALICE_LEAVE.getEnd()), ALICE_LEAVE.getDescription())); + assertThrows(NullPointerException.class, () -> new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getEnd()), null)); + } + + /** + * A class implementing ComparablePerson but with null as its name field + */ + private class MockPersonWithNullName implements ComparablePerson { + @Override + public boolean isSamePerson(ComparablePerson otherPerson) { + return false; + } + + @Override + public Name getName() { + return null; + } + } + + private Range constructRange(Date start, Date end) { + return Range.createNonNullRange(start, end); + } + + @Test + public void constructor_startSameAsEnd_success() { + Leave leave = new Leave(ALICE, ALICE_LEAVE.getTitle(), + constructRange(ALICE_LEAVE.getStart(), ALICE_LEAVE.getStart()), ALICE_LEAVE.getDescription()); + assertEquals(leave.getStart(), leave.getEnd()); + } + + @Test + public void copyWithNewPerson_nullPerson_throwsNullExceptionPointer() { + assertThrows(NullPointerException.class, () -> new LeaveBuilder().build().copyWithNewPerson(null)); + } + + @Test + public void copyWithNewPerson_success() { + Leave existingLeave = new LeaveBuilder().withEmployee(ALICE).build(); + Leave newLeave = existingLeave.copyWithNewPerson(BOB); + assertEquals(newLeave.getEmployee(), BOB); + assertEquals(newLeave.getDescription(), existingLeave.getDescription()); + assertEquals(newLeave.getStart(), existingLeave.getStart()); + assertEquals(newLeave.getEnd(), existingLeave.getEnd()); + assertEquals(newLeave.getTitle(), existingLeave.getTitle()); + assertEquals(newLeave.getStatus(), existingLeave.getStatus()); + } + + @Test + public void equalsMethod() { + // same values -> returns true + Leave aliceCopy = new LeaveBuilder(ALICE_LEAVE).build(); + assertTrue(ALICE_LEAVE.equals(aliceCopy)); + + // same object -> returns true + assertTrue(ALICE_LEAVE.equals(ALICE_LEAVE)); + + // null -> returns false + assertFalse(ALICE_LEAVE.equals(null)); + + // different type -> returns false + assertFalse(ALICE_LEAVE.equals(5)); + + // different leave -> returns false + assertFalse(ALICE_LEAVE.equals(BOB_LEAVE)); + + // different employee -> returns false + Leave editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).withEmployee(BOB).build(); + assertFalse(ALICE_LEAVE.equals(editedAliceLeave)); + + // different title -> returns false + editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).withTitle("Bob's Paternity Leave").build(); + assertFalse(ALICE_LEAVE.equals(editedAliceLeave)); + + // different description -> returns false + editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).withDescription("Bob's Paternity Leave Description").build(); + assertFalse(ALICE_LEAVE.equals(editedAliceLeave)); + + // different start date -> returns false + editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).withStart( + Date.of(ALICE_LEAVE.getStart().getDate().plusDays(1))).build(); + assertFalse(ALICE_LEAVE.equals(editedAliceLeave)); + + // different end date -> returns false + editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).withEnd( + Date.of(ALICE_LEAVE.getEnd().getDate().plusDays(1))).build(); + assertFalse(ALICE_LEAVE.equals(editedAliceLeave)); + } + + @Test + public void isSameLeaveMethod() { + // same object -> returns true + assertTrue(ALICE_LEAVE.isSameLeave(ALICE_LEAVE)); + + // null -> returns false + assertFalse(ALICE_LEAVE.isSameLeave(null)); + + // different leave -> returns false + assertFalse(ALICE_LEAVE.isSameLeave(BOB_LEAVE)); + + // different employee -> returns false + assertFalse(ALICE_LEAVE.isSameLeave(new LeaveBuilder(ALICE_LEAVE).withEmployee(BOB).build())); + + // different start date -> returns false + assertFalse(ALICE_LEAVE.isSameLeave(new LeaveBuilder(ALICE_LEAVE).withStart( + Date.of(ALICE_LEAVE.getStart().getDate().plusDays(1))).build())); + + // different end date -> returns false + assertFalse(ALICE_LEAVE.isSameLeave(new LeaveBuilder(ALICE_LEAVE).withEnd( + Date.of(ALICE_LEAVE.getEnd().getDate().plusDays(1))).build())); + + // same employee, same start date, same end date -> returns true + assertTrue(ALICE_LEAVE.isSameLeave(new LeaveBuilder(ALICE_LEAVE).withTitle("Alice's Maternity Leave 2") + .withDescription("Alice's Maternity Leave 2 Description").build())); + + // different status -> return true + assertTrue(ALICE_LEAVE.isSameLeave(new LeaveBuilder(ALICE_LEAVE) + .withStatus(Status.StatusType.APPROVED).build())); + } + + @Test + public void toStringMethod() { + assertEquals(ALICE_LEAVE.toString(), "Employee: " + ALICE.getName() + + " Title: " + ALICE_LEAVE.getTitle() + + " Start: " + ALICE_LEAVE.getStart() + + " End: " + ALICE_LEAVE.getEnd() + + " Status: " + ALICE_LEAVE.getStatus()); + } + + @Test + public void belongsToMethod() { + // same employee -> returns true + assertTrue(ALICE_LEAVE.belongsTo(ALICE)); + + // different employee -> returns false + assertFalse(ALICE_LEAVE.belongsTo(BOB)); + + // different employee instance but share the same name -> returns true + // This test is needed as leaves loaded from storage will not share the same employee + // instance as an existing employee instance in the address book + Person aliceDuplicate = new PersonBuilder().withName(ALICE.getName().toString()).build(); + assertTrue(ALICE_LEAVE.belongsTo(aliceDuplicate)); + } + + @Test + public void hashCodeMethod() { + // same object -> returns same hashcode + assertEquals(ALICE_LEAVE.hashCode(), ALICE_LEAVE.hashCode()); + + // different leave -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == BOB_LEAVE.hashCode()); + + // different employee -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == new LeaveBuilder(ALICE_LEAVE).withEmployee(BOB).build().hashCode()); + + // different title -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == new LeaveBuilder(ALICE_LEAVE).withTitle("Alice's Maternity Leave 2") + .build().hashCode()); + + // different description -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == new LeaveBuilder(ALICE_LEAVE) + .withDescription("Alice's Maternity Leave 2 Description").build().hashCode()); + + // different start date -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == new LeaveBuilder(ALICE_LEAVE).withStart( + Date.of(ALICE_LEAVE.getStart().getDate().plusDays(1))).build().hashCode()); + + // different end date -> returns different hashcode + assertFalse(ALICE_LEAVE.hashCode() == new LeaveBuilder(ALICE_LEAVE).withEnd( + Date.of(ALICE_LEAVE.getEnd().getDate().plusDays(1))).build().hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/leave/PersonEntryTest.java b/src/test/java/seedu/address/model/leave/PersonEntryTest.java new file mode 100644 index 00000000000..f9abcfd2505 --- /dev/null +++ b/src/test/java/seedu/address/model/leave/PersonEntryTest.java @@ -0,0 +1,53 @@ +package seedu.address.model.leave; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PersonEntryTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new PersonEntry(null)); + } + + @Test + public void constructor_invalidName_throwsIllegalArgumentException() { + String invalidName = ""; + assertThrows(IllegalArgumentException.class, () -> new PersonEntry(invalidName)); + } + + @Test + public void constructor_success() { + String validName = "Valid Name"; + PersonEntry personEntry = new PersonEntry(validName); + assertTrue(personEntry.getName().toString().equals(validName)); + } + + @Test + public void toStringMethod() { + String validName = "Valid Name"; + PersonEntry personEntry = new PersonEntry(validName); + assertTrue(personEntry.toString().equals(validName)); + } + + @Test + public void isSamePersonMethod() { + // same person object + PersonEntry personEntry = new PersonEntry("Valid Name"); + assertTrue(personEntry.isSamePerson(personEntry)); + + // null person object + assertFalse(personEntry.isSamePerson(null)); + + // different person object, same name + PersonEntry personEntry2 = new PersonEntry("Valid Name"); + assertTrue(personEntry.isSamePerson(personEntry2)); + + // different person object, different name + PersonEntry personEntry3 = new PersonEntry("Other Valid Name"); + assertFalse(personEntry.isSamePerson(personEntry3)); + + } +} diff --git a/src/test/java/seedu/address/model/leave/RangeTest.java b/src/test/java/seedu/address/model/leave/RangeTest.java new file mode 100644 index 00000000000..a52cea71c4e --- /dev/null +++ b/src/test/java/seedu/address/model/leave/RangeTest.java @@ -0,0 +1,124 @@ +package seedu.address.model.leave; + +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 seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.leave.exceptions.EndBeforeStartException; + +public class RangeTest { + private static final Date FIRST_DATE = Date.of("2020-10-30"); + private static final Date SECOND_DATE = Date.of("2020-10-31"); + @Test + public void createNonNullRange_invalidLeave_throwsEndBeforeStartException() { + assertThrows(EndBeforeStartException.class, () -> Range.createNonNullRange( + SECOND_DATE, FIRST_DATE)); + } + + @Test + public void createNonNullRange_nullStartOrEnd_throwsNullPointerException() { + // null start date + assertThrows(NullPointerException.class, () -> Range.createNonNullRange(null, SECOND_DATE)); + // null end date + assertThrows(NullPointerException.class, () -> Range.createNonNullRange(FIRST_DATE, null)); + } + + @Test + public void createNonNullRange_validLeave_returnsRange() { + // start date before end date + Range expectedDiffRange = Range.createNonNullRange(FIRST_DATE, SECOND_DATE); + assertEquals(expectedDiffRange.getStartDate().get(), FIRST_DATE); + assertEquals(expectedDiffRange.getEndDate().get(), SECOND_DATE); + + // start date same as end date + Range expectedSameRange = Range.createNonNullRange(FIRST_DATE, FIRST_DATE); + assertEquals(expectedSameRange.getStartDate().get(), FIRST_DATE); + assertEquals(expectedSameRange.getEndDate().get(), FIRST_DATE); + } + + @Test + public void createNullableRange_invalidLeave_throwsEndBeforeStartException() { + assertThrows(EndBeforeStartException.class, () -> Range.createNullableRange( + SECOND_DATE, FIRST_DATE)); + } + + @Test + public void createNullableRange_validLeave_returnsRange() { + // start date before end date + Range expectedDiffRange = Range.createNullableRange(FIRST_DATE, SECOND_DATE); + assertEquals(expectedDiffRange.getStartDate().get(), FIRST_DATE); + assertEquals(expectedDiffRange.getEndDate().get(), SECOND_DATE); + + // start date same as end date + Range expectedSameRange = Range.createNullableRange(FIRST_DATE, FIRST_DATE); + assertEquals(expectedSameRange.getStartDate().get(), FIRST_DATE); + assertEquals(expectedSameRange.getEndDate().get(), FIRST_DATE); + + // start date provided, null end date + Range expectedStartRange = Range.createNullableRange(FIRST_DATE, null); + assertEquals(expectedStartRange.getStartDate().get(), FIRST_DATE); + assertFalse(expectedStartRange.getEndDate().isPresent()); + + // end date provided, start date null + Range expectedEndRange = Range.createNullableRange(null, SECOND_DATE); + assertFalse(expectedEndRange.getStartDate().isPresent()); + assertEquals(expectedEndRange.getEndDate().get(), SECOND_DATE); + + // no start and end date provided + Range expectedNullRange = Range.createNullableRange(null, null); + assertFalse(expectedNullRange.getStartDate().isPresent()); + assertFalse(expectedNullRange.getEndDate().isPresent()); + } + + @Test + public void equals() { + Range defaultRange = Range.createNullableRange(FIRST_DATE, SECOND_DATE); + Range diffStart = Range.createNullableRange(SECOND_DATE, SECOND_DATE); + Range diffEnd = Range.createNullableRange(FIRST_DATE, FIRST_DATE); + + // same object -> return true + assertEquals(defaultRange, defaultRange); + // diff types -> return false + assertFalse(defaultRange.equals("1")); + + // same start and end -> return true + Range defaultRangeCopy = Range.createNullableRange(FIRST_DATE, SECOND_DATE); + assertEquals(defaultRange, defaultRangeCopy); + // diff start -> return false + assertNotEquals(defaultRange, diffStart); + // diff end -> return false + assertNotEquals(defaultRange, diffEnd); + + Range nullStart = Range.createNullableRange(null, SECOND_DATE); + Range nullStartDiffEnd = Range.createNullableRange(null, FIRST_DATE); + // null start vs non-null start -> return false + assertNotEquals(defaultRange, nullStart); + // null start, same end -> return true + Range nullStartCopy = Range.createNullableRange(null, SECOND_DATE); + assertEquals(nullStart, nullStartCopy); + // null start, diff end -> return false + assertNotEquals(nullStart, nullStartDiffEnd); + + Range nullEnd = Range.createNullableRange(FIRST_DATE, null); + Range nullEndDiffStart = Range.createNullableRange(SECOND_DATE, null); + // null end vs non-null end -> return false + assertNotEquals(defaultRange, nullEnd); + // null end, same start -> return true + Range nullEndCopy = Range.createNullableRange(FIRST_DATE, null); + assertEquals(nullEnd, nullEndCopy); + // null end, diff start -> return false + assertNotEquals(nullEnd, nullEndDiffStart); + + Range nullRange = Range.createNullableRange(null, null); + // null end vs non-null end -> return false + assertNotEquals(nullRange, nullStart); + // null start vs non-null start -> return false + assertNotEquals(nullRange, nullEnd); + // null start and null end -> return true + Range nullRangeCopy = Range.createNullableRange(null, null); + assertEquals(nullRange, nullRangeCopy); + } +} diff --git a/src/test/java/seedu/address/model/leave/StatusTest.java b/src/test/java/seedu/address/model/leave/StatusTest.java new file mode 100644 index 00000000000..2a4fd550c8f --- /dev/null +++ b/src/test/java/seedu/address/model/leave/StatusTest.java @@ -0,0 +1,80 @@ +package seedu.address.model.leave; + +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.leave.Status.StatusType; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class StatusTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> Status.of((StatusType) null)); + } + + @Test + public void constructor_validStatusType() { + assertEquals(Status.of("PENDING").getStatusType(), StatusType.PENDING); + assertEquals(Status.getDefault().getStatusType(), StatusType.PENDING); + assertEquals(Status.of("APPROVED").getStatusType(), StatusType.APPROVED); + assertEquals(Status.of("REJECTED").getStatusType(), StatusType.REJECTED); + assertEquals(Status.of(StatusType.APPROVED).getStatusType(), StatusType.APPROVED); + assertEquals(Status.of(StatusType.PENDING).getStatusType(), StatusType.PENDING); + assertEquals(Status.of(StatusType.REJECTED).getStatusType(), StatusType.REJECTED); + } + + @Test + public void toStringMethod() { + assertEquals("PENDING", Status.of("PENDING").toString()); + assertEquals("APPROVED", Status.of("APPROVED").toString()); + assertEquals("REJECTED", Status.of("REJECTED").toString()); + } + + @Test + public void equalsMethod() { + // Status status = new Status(StatusType.PENDING); + Status status = Status.getDefault(); + + // same values -> returns true + assertTrue(status.equals(Status.of("PENDING"))); + + // same object -> returns true + assertTrue(status.equals(status)); + + // null -> returns false + assertFalse(status.equals(null)); + + // different types -> returns false + assertFalse(status.equals(5.0f)); + + // different values -> returns false + assertFalse(status.equals(Status.of("APPROVED"))); + assertFalse(status.equals(Status.of("REJECTED"))); + } + + @Test + public void isValidStatus() { + assertTrue(Status.isValidStatus("APPROVED")); + assertTrue(Status.isValidStatus("PENDING")); + assertTrue(Status.isValidStatus("REJECTED")); + + assertFalse(Status.isValidStatus("")); + assertFalse(Status.isValidStatus(" ")); + assertFalse(Status.isValidStatus("INVALID")); + assertFalse(Status.isValidStatus("approved")); + } + + @Test + public void hashcodeMethod() { + Status status = Status.getDefault(); + + // same values -> returns true + assertTrue(status.hashCode() == Status.of("PENDING").hashCode()); + + // different values -> returns false + assertFalse(status.hashCode() == Status.of("APPROVED").hashCode()); + assertFalse(status.hashCode() == Status.of("REJECTED").hashCode()); + } +} diff --git a/src/test/java/seedu/address/model/leave/TitleTest.java b/src/test/java/seedu/address/model/leave/TitleTest.java new file mode 100644 index 00000000000..b684eb35c03 --- /dev/null +++ b/src/test/java/seedu/address/model/leave/TitleTest.java @@ -0,0 +1,59 @@ +package seedu.address.model.leave; + +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 org.junit.jupiter.api.Test; + +public class TitleTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Title(null)); + } + + @Test + public void constructor_emptyTitle_throwsIllegalArgumentException() { + String invalidTitle = ""; + assertThrows(IllegalArgumentException.class, Title.MESSAGE_CONSTRAINTS, () -> new Title(invalidTitle)); + } + + @Test + public void constructor_blankTitle_throwsIllegalArgumentException() { + String invalidTitle = " "; + assertThrows(IllegalArgumentException.class, Title.MESSAGE_CONSTRAINTS, () -> new Title(invalidTitle)); + } + + @Test + public void constructor_invalidTitle_throwsIllegalArgumentException() { + String invalidTitle = "title*#"; + assertThrows(IllegalArgumentException.class, Title.MESSAGE_CONSTRAINTS, () -> new Title(invalidTitle)); + } + + @Test + public void toStringMethod() { + String validTitle = "title"; + assertEquals(new Title(validTitle).toString(), validTitle); + } + + @Test + public void equalsMethod() { + Title title = new Title("title"); + Title titleCopy = new Title("title"); + assertTrue(title.equals(titleCopy)); + + // different title + Title differentTitle = new Title("title123"); + assertFalse(title.equals(differentTitle)); + + // different object + assertFalse(title.equals(new Object())); + + // null + assertFalse(title.equals(null)); + + // same object + assertTrue(title.equals(title)); + } +} diff --git a/src/test/java/seedu/address/model/leave/UniqueLeaveTest.java b/src/test/java/seedu/address/model/leave/UniqueLeaveTest.java new file mode 100644 index 00000000000..0f9a246976c --- /dev/null +++ b/src/test/java/seedu/address/model/leave/UniqueLeaveTest.java @@ -0,0 +1,256 @@ +package seedu.address.model.leave; + +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.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BOB_LEAVE; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.AMY; +import static seedu.address.testutil.TypicalPersons.BOB; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.leave.exceptions.DuplicateLeaveException; +import seedu.address.model.leave.exceptions.LeaveNotFoundException; +import seedu.address.testutil.LeaveBuilder; + +public class UniqueLeaveTest { + + private final UniqueLeaveList uniqueLeaveList = new UniqueLeaveList(); + + @Test + public void contains_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.contains(null)); + } + + @Test + public void contains_leaveInList_returnsTrue() { + uniqueLeaveList.add(ALICE_LEAVE); + assertTrue(uniqueLeaveList.contains(ALICE_LEAVE)); + } + + @Test + public void contains_leaveNotInList_returnsFalse() { + assertFalse(uniqueLeaveList.contains(ALICE_LEAVE)); + } + + @Test + public void contains_leaveWithSameIdentityFieldsInList_returnsTrue() { + uniqueLeaveList.add(ALICE_LEAVE); + Leave editedAliceLeave = new LeaveBuilder(ALICE_LEAVE).build(); + assertTrue(uniqueLeaveList.contains(editedAliceLeave)); + } + + @Test + public void add_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.add(null)); + } + + @Test + public void add_duplicateLeave_throwsDuplicateLeaveException() { + uniqueLeaveList.add(ALICE_LEAVE); + assertThrows(DuplicateLeaveException.class, () -> uniqueLeaveList.add(ALICE_LEAVE)); + } + + @Test + public void setLeave_nullTargetLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.setLeave(null, ALICE_LEAVE)); + } + + @Test + public void setLeave_nullEditedLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.setLeave(ALICE_LEAVE, null)); + } + + @Test + public void setLeave_targetLeaveNotInList_throwsLeaveNotFoundException() { + assertThrows(LeaveNotFoundException.class, () -> uniqueLeaveList.setLeave(ALICE_LEAVE, ALICE_LEAVE)); + } + + @Test + public void setLeave_setDuplicateLeave_throwsDuplicateLeaveException() { + uniqueLeaveList.add(ALICE_LEAVE); + uniqueLeaveList.add(BOB_LEAVE); + assertThrows(DuplicateLeaveException.class, () -> uniqueLeaveList.setLeave(ALICE_LEAVE, BOB_LEAVE)); + } + + @Test + public void setLeave_setEditedLeave_success() { + uniqueLeaveList.add(ALICE_LEAVE); + uniqueLeaveList.setLeave(ALICE_LEAVE, BOB_LEAVE); + UniqueLeaveList expectedUniqueLeaveList = new UniqueLeaveList(); + expectedUniqueLeaveList.add(BOB_LEAVE); + assertEquals(expectedUniqueLeaveList, uniqueLeaveList); + } + + @Test + public void remove_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.remove(null)); + } + + @Test + public void remove_leaveDoesNotExist_throwsLeaveNotFoundException() { + assertThrows(LeaveNotFoundException.class, () -> uniqueLeaveList.remove(ALICE_LEAVE)); + } + + @Test + public void remove_existingLeave_removesLeave() { + uniqueLeaveList.add(ALICE_LEAVE); + uniqueLeaveList.remove(ALICE_LEAVE); + UniqueLeaveList expectedUniqueLeaveList = new UniqueLeaveList(); + assertEquals(expectedUniqueLeaveList, uniqueLeaveList); + } + + @Test + public void setLeaves_nullUniqueLeaveList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.setLeaves((UniqueLeaveList) null)); + } + + @Test + public void setLeaves_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueLeaveList.setLeaves((List<Leave>) null)); + } + + @Test + public void setLeaves_leaveListNotUnique_throwsDuplicateLeaveException() { + List<Leave> listWithDuplicateLeaves = Arrays.asList(ALICE_LEAVE, ALICE_LEAVE); + assertThrows(DuplicateLeaveException.class, () -> uniqueLeaveList.setLeaves(listWithDuplicateLeaves)); + } + + @Test + public void setLeaves_uniqueLeaveList_replacesOwnListWithProvidedUniqueLeaveList() { + uniqueLeaveList.add(ALICE_LEAVE); + UniqueLeaveList expectedUniqueLeaveList = new UniqueLeaveList(); + expectedUniqueLeaveList.add(BOB_LEAVE); + uniqueLeaveList.setLeaves(expectedUniqueLeaveList); + assertEquals(expectedUniqueLeaveList, uniqueLeaveList); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> uniqueLeaveList.asUnmodifiableObservableList().remove(0)); + } + + @Test + public void toStringMethod() { + assertEquals(uniqueLeaveList.toString(), uniqueLeaveList.asUnmodifiableObservableList().toString()); + } + + @Test + public void hashCodeMethod() { + assertEquals(uniqueLeaveList.hashCode(), uniqueLeaveList.asUnmodifiableObservableList().hashCode()); + } + + @Test + public void equalsMethod() { + // same object -> returns true + assertTrue(uniqueLeaveList.equals(uniqueLeaveList)); + + // null -> returns false + assertFalse(uniqueLeaveList.equals(null)); + + // different type -> returns false + assertFalse(uniqueLeaveList.equals(5)); + + // different internal list -> returns false + UniqueLeaveList uniqueLeaveListCopy = new UniqueLeaveList(); + uniqueLeaveListCopy.add(ALICE_LEAVE); + assertFalse(uniqueLeaveList.equals(uniqueLeaveListCopy)); + + // same internal list -> returns true + uniqueLeaveList.add(ALICE_LEAVE); + assertTrue(uniqueLeaveList.equals(uniqueLeaveListCopy)); + } + + @Test + public void removePerson_nullPerson_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new UniqueLeaveList().removePerson(null)); + } + + @Test + public void removePerson_personInList_success() { + Leave aliceLeave = new LeaveBuilder().withEmployee( + new PersonEntry(ALICE.getName().toString())).build(); + Leave bobLeave = new LeaveBuilder().withEmployee( + new PersonEntry(BOB.getName().toString())).build(); + + // ensure we are not comparing based on same instance + assertNotEquals(aliceLeave.getEmployee(), ALICE); + assertNotEquals(bobLeave.getEmployee(), BOB); + + UniqueLeaveList ull = new UniqueLeaveList(); + ull.add(aliceLeave); + ull.add(bobLeave); + + ull.removePerson(ALICE); + assertTrue(ull.contains(bobLeave)); + assertFalse(ull.contains(aliceLeave)); + } + + @Test + public void removePerson_personNotInList_noChange() { + Leave aliceLeave = new LeaveBuilder().withEmployee( + new PersonEntry(ALICE.getName().toString())).build(); + + assertNotEquals(aliceLeave.getEmployee(), BOB); + + UniqueLeaveList ull = new UniqueLeaveList(); + ull.add(aliceLeave); + ull.removePerson(BOB); + assertTrue(ull.contains(aliceLeave)); + } + + @Test + public void setPerson_nullPerson_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new UniqueLeaveList().setPerson(null, BOB)); + assertThrows(NullPointerException.class, () -> new UniqueLeaveList().setPerson(ALICE, null)); + } + + @Test + public void setPerson_personInList_success() { + Leave aliceLeave = new LeaveBuilder().withEmployee( + new PersonEntry(ALICE.getName().toString())).build(); + Leave bobLeave = new LeaveBuilder().withEmployee( + new PersonEntry(BOB.getName().toString())).build(); + + // ensure we are not comparing based on same instance + assertNotEquals(aliceLeave.getEmployee(), ALICE); + assertNotEquals(bobLeave.getEmployee(), BOB); + + UniqueLeaveList ull = new UniqueLeaveList(); + ull.add(aliceLeave); + ull.add(bobLeave); + + Leave expectedChangedLeave = new LeaveBuilder().withEmployee( + new PersonEntry(AMY.getName().toString())).build(); + + assertFalse(ull.contains(expectedChangedLeave)); + + ull.setPerson(ALICE, AMY); + assertTrue(ull.contains(bobLeave)); + assertFalse(ull.contains(aliceLeave)); + assertTrue(ull.contains(expectedChangedLeave)); + } + + @Test + public void setPerson_personNotInList_noChange() { + Leave aliceLeave = new LeaveBuilder().withEmployee( + new PersonEntry(ALICE.getName().toString())).build(); + + // ensure we are not comparing based on same instance + assertNotEquals(aliceLeave.getEmployee(), ALICE); + + UniqueLeaveList ull = new UniqueLeaveList(); + ull.add(aliceLeave); + + ull.setPerson(BOB, AMY); + assertTrue(ull.contains(aliceLeave)); + } +} diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java index 94e3dd726bd..82469197aeb 100644 --- a/src/test/java/seedu/address/model/person/NameTest.java +++ b/src/test/java/seedu/address/model/person/NameTest.java @@ -29,13 +29,20 @@ public void isValidName() { assertFalse(Name.isValidName(" ")); // spaces only assertFalse(Name.isValidName("^")); // only non-alphanumeric characters assertFalse(Name.isValidName("peter*")); // contains non-alphanumeric characters + assertFalse(Name.isValidName("\\|*&^%#")); + assertFalse(Name.isValidName("(()))()()())()")); + assertFalse(Name.isValidName("1234")); // numbers only + assertFalse(Name.isValidName("-some-name")); // starts with non-alphabet characters + assertFalse(Name.isValidName("2starts-with-number")); // starts with number // valid name assertTrue(Name.isValidName("peter jack")); // alphabets only - assertTrue(Name.isValidName("12345")); // numbers only + assertTrue(Name.isValidName("alice-boberton")); // numbers only assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric characters assertTrue(Name.isValidName("Capital Tan")); // with capital letters assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names + assertTrue(Name.isValidName("Alice s/o Bob")); // with slash character + assertTrue(Name.isValidName("Alice (Tembusu)")); // with brackets } @Test diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index 31a10d156c9..103c1b1d634 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -12,6 +12,8 @@ import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import java.util.HashSet; + import org.junit.jupiter.api.Test; import seedu.address.testutil.PersonBuilder; @@ -96,4 +98,79 @@ public void toStringMethod() { + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}"; assertEquals(expected, ALICE.toString()); } + + @Test + public void hasAllTags_allTagsPresent_returnsTrue() { + assertTrue(ALICE.hasAllTags(ALICE.getTags())); + } + + @Test + public void hasAllTags_notAllTagsPresent_returnsFalse() { + assertFalse(ALICE.hasAllTags(BOB.getTags())); + } + + @Test + public void hasAllTags_emptyCollection_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.hasAllTags(new HashSet<>())); + } + + @Test + public void hasAllTags_null_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.hasAllTags(null)); + } + + @Test + public void hasTag_tagPresent_returnsTrue() { + assertTrue(ALICE.hasTag(ALICE.getTags().iterator().next())); + } + + @Test + public void hasTag_tagNotPresent_returnsFalse() { + assertFalse(ALICE.hasTag(BOB.getTags().iterator().next())); + } + + @Test + public void hasTag_null_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.hasTag(null)); + } + + @Test + public void removeTags_allTagsPresent_returnsTrue() { + Person person = new PersonBuilder(ALICE).build(); + person.removeTags(ALICE.getTags()); + assertTrue(person.getTags().isEmpty()); + } + + @Test + public void removeTags_notAllTagsPresent_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.removeTags(BOB.getTags())); + } + + @Test + public void removeTags_emptyCollection_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.removeTags(new HashSet<>())); + } + + @Test + public void removeTags_null_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.removeTags(null)); + } + + @Test + public void removeTag_tagPresent_returnsTrue() { + Person person = new PersonBuilder(ALICE).build(); + person.removeTag(ALICE.getTags().iterator().next()); + assertTrue(person.getTags().isEmpty()); + } + + @Test + public void removeTag_tagNotPresent_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.removeTag(BOB.getTags().iterator().next())); + } + + @Test + public void removeTag_null_throwsAssertionError() { + assertThrows(AssertionError.class, () -> ALICE.removeTag(null)); + } + } diff --git a/src/test/java/seedu/address/model/person/TagsContainAllTagsPredicateTest.java b/src/test/java/seedu/address/model/person/TagsContainAllTagsPredicateTest.java new file mode 100644 index 00000000000..622df64582b --- /dev/null +++ b/src/test/java/seedu/address/model/person/TagsContainAllTagsPredicateTest.java @@ -0,0 +1,96 @@ +package seedu.address.model.person; + +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 java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.Tag; +import seedu.address.testutil.PersonBuilder; + +public class TagsContainAllTagsPredicateTest { + + @Test + public void equals() { + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("full time")); + tagList2.add(new Tag("part time")); + tagList2.add(new Tag("remote")); + + TagsContainAllTagsPredicate firstPredicate = new TagsContainAllTagsPredicate(tagList1); + TagsContainAllTagsPredicate secondPredicate = new TagsContainAllTagsPredicate(tagList2); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + TagsContainAllTagsPredicate firstPredicateCopy = new TagsContainAllTagsPredicate(tagList1); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_nameContainsKeywords_returnsTrue() { + + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("full time")); + tagList2.add(new Tag("part time")); + tagList2.add(new Tag("remote")); + // One keyword + TagsContainAllTagsPredicate predicate = + new TagsContainAllTagsPredicate(tagList1); + assertTrue(predicate.test(new PersonBuilder() + .withTags("full time").build())); + + // Multiple keywords + predicate = new TagsContainAllTagsPredicate(tagList2); + assertTrue(predicate.test(new PersonBuilder() + .withTags("part time", "remote").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + + // Only one matching keyword + TagsContainAllTagsPredicate predicate = new TagsContainAllTagsPredicate(tagList); + assertTrue(predicate.test(new PersonBuilder() + .withTags("full time", "remote").build())); + + // Non-matching keyword + predicate = new TagsContainAllTagsPredicate(tagList); + assertFalse(predicate.test(new PersonBuilder() + .withTags("part time", "remote").build())); + + // Mixed-case keywords + predicate = new TagsContainAllTagsPredicate(tagList); + assertFalse(predicate.test(new PersonBuilder() + .withTags("Full Time").build())); + } + + @Test + public void toStringMethod() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + + TagsContainAllTagsPredicate predicate = new TagsContainAllTagsPredicate(tagList); + + String expected = TagsContainAllTagsPredicate.class.getCanonicalName() + "{tags=" + "[[full time]]" + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/TagsContainSomeTagsPredicateTest.java b/src/test/java/seedu/address/model/person/TagsContainSomeTagsPredicateTest.java new file mode 100644 index 00000000000..f65eb4cd6d5 --- /dev/null +++ b/src/test/java/seedu/address/model/person/TagsContainSomeTagsPredicateTest.java @@ -0,0 +1,103 @@ +package seedu.address.model.person; + +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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.Tag; +import seedu.address.testutil.PersonBuilder; + +public class TagsContainSomeTagsPredicateTest { + + @Test + public void equals() { + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("fullTime")); + tagList2.add(new Tag("partTime")); + tagList2.add(new Tag("remote")); + + TagsContainSomeTagsPredicate firstPredicate = new TagsContainSomeTagsPredicate(tagList1); + TagsContainSomeTagsPredicate secondPredicate = new TagsContainSomeTagsPredicate(tagList2); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + TagsContainSomeTagsPredicate firstPredicateCopy = new TagsContainSomeTagsPredicate(tagList1); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_nameContainsKeywords_returnsTrue() { + + List<Tag> tagList1 = new ArrayList<>(); + List<Tag> tagList2 = new ArrayList<>(); + tagList1.add(new Tag("full time")); + tagList2.add(new Tag("part time")); + tagList2.add(new Tag("remote")); + // One keyword + TagsContainSomeTagsPredicate predicate = + new TagsContainSomeTagsPredicate(tagList1); + assertTrue(predicate.test(new PersonBuilder() + .withTags("full time").build())); + + // Multiple keywords + predicate = new TagsContainSomeTagsPredicate(tagList2); + assertTrue(predicate.test(new PersonBuilder() + .withTags("part time", "remote").build())); + + // Only one matching keyword + predicate = new TagsContainSomeTagsPredicate(tagList2); + assertTrue(predicate.test(new PersonBuilder() + .withTags("full time", "remote").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + + // Zero keywords + TagsContainSomeTagsPredicate predicate = + new TagsContainSomeTagsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder() + .withTags("part time").build())); + + // Non-matching keyword + predicate = new TagsContainSomeTagsPredicate(tagList); + assertFalse(predicate.test(new PersonBuilder() + .withTags("part time", "remote").build())); + + // Mixed-case keywords + predicate = new TagsContainSomeTagsPredicate(tagList); + assertFalse(predicate.test(new PersonBuilder() + .withTags("Full Time").build())); + } + + @Test + public void toStringMethod() { + List<Tag> tagList = new ArrayList<>(); + tagList.add(new Tag("full time")); + + TagsContainSomeTagsPredicate predicate = new TagsContainSomeTagsPredicate(tagList); + + String expected = TagsContainSomeTagsPredicate.class.getCanonicalName() + "{tags=" + "[[full time]]" + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java index 64d07d79ee2..d6c635cac92 100644 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ b/src/test/java/seedu/address/model/tag/TagTest.java @@ -1,5 +1,7 @@ package seedu.address.model.tag; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; @@ -23,4 +25,22 @@ public void isValidTagName() { assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null)); } + @Test + public void equalsMethod() { + // same object + Tag tag = new Tag("Valid Tag"); + assertTrue(tag.equals(tag)); + + // different class + assertFalse(tag.equals(new Object())); + + // same class, different tag name + Tag tag2 = new Tag("Other Valid Tag"); + assertFalse(tag.equals(tag2)); + + // same class, same tag name + Tag tag3 = new Tag("Valid Tag"); + assertTrue(tag.equals(tag3)); + } + } diff --git a/src/test/java/seedu/address/storage/AdaptedLeaveTest.java b/src/test/java/seedu/address/storage/AdaptedLeaveTest.java new file mode 100644 index 00000000000..0f9915361e4 --- /dev/null +++ b/src/test/java/seedu/address/storage/AdaptedLeaveTest.java @@ -0,0 +1,179 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.storage.AdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; + +public class AdaptedLeaveTest { + private static final String VALID_START = ALICE_LEAVE.getStart().toString(); + private static final String VALID_END = ALICE_LEAVE.getEnd().toString(); + private static final String VALID_TITLE = ALICE_LEAVE.getTitle().toString(); + private static final String VALID_DESCRIPTION = ALICE_LEAVE.getDescription().toString(); + private static final String EMPTY_DESCRIPTION = ""; + private static final String VALID_STATUS = ALICE_LEAVE.getStatus().toString(); + private static final String VALID_EMPLOYEE = ALICE_LEAVE.getEmployee().getName().toString(); + private static final Leave VALID_LEAVE = ALICE_LEAVE; + + private static final String INVALID_START = "2020/01/01"; + private static final String INVALID_END = "2020/01/01"; + private static final String EMPTY_TITLE = ""; + private static final String INVALID_TITLE = "title*#"; + private static final String INVALID_STATUS = "invalid"; + private static final String INVALID_DESCRIPTION = "description*#"; + private static final String INVALID_EMPLOYEE = "R@chel"; + + static class MockAdaptedLeave extends AdaptedLeave { + public MockAdaptedLeave(String start, String end, String title, String description, + String status, String employee) { + super(start, end, title, description, status, employee); + } + + public MockAdaptedLeave(Leave source) { + super(source); + } + } + + @Test + public void constructor_nullLeave_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new MockAdaptedLeave(null)); + } + + @Test + public void constructor_nullEmployee_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new MockAdaptedLeave(null)); + } + + @Test + public void constructor_invalidName_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new MockAdaptedLeave( + VALID_START, VALID_END, VALID_TITLE, VALID_DESCRIPTION, VALID_STATUS, INVALID_EMPLOYEE)); + } + + @Test + public void toModelType_leaveParameterConstructor_returnsLeave() throws Exception { + AdaptedLeave leave = new MockAdaptedLeave(ALICE_LEAVE); + assertEquals(VALID_LEAVE, leave.toModelType()); + } + + @Test + public void toModelType_stringParameterConstructor_returnsLeave() throws Exception { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + assertEquals(ALICE_LEAVE, leave.toModelType()); + } + + @Test + public void toModelType_emptyDescription_returnsLeave() throws Exception { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + EMPTY_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + assertTrue(VALID_LEAVE.isSameLeave(leave.toModelType())); + } + + @Test + public void toModelType_invalidStart_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(INVALID_START, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Date.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_nullStart_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(null, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "start"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidEnd_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, INVALID_END, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Date.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_nullEnd_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, null, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "end"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_endBeforeStart_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_END, VALID_START, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Range.MESSAGE_END_BEFORE_START_ERROR; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidTitle_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, INVALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Title.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_emptyTitle_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, EMPTY_TITLE, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Title.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_nullTitle_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, null, + VALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "title"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidDescription_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + INVALID_DESCRIPTION, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Description.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_nullDescription_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + null, VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "description"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidStatus_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, INVALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Status.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_nullStatus_throwsIllegalValueException() { + AdaptedLeave leave = new MockAdaptedLeave(VALID_START, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, null, VALID_EMPLOYEE); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "status"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/AdaptedPersonTest.java b/src/test/java/seedu/address/storage/AdaptedPersonTest.java new file mode 100644 index 00000000000..80affb4b31f --- /dev/null +++ b/src/test/java/seedu/address/storage/AdaptedPersonTest.java @@ -0,0 +1,127 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalPersons.BENSON; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +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; + +public class AdaptedPersonTest { + private static final String INVALID_NAME = "R@chel"; + private static final String INVALID_PHONE = "+651234"; + 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 VALID_NAME = BENSON.getName().toString(); + private static final String VALID_PHONE = BENSON.getPhone().toString(); + private static final String VALID_EMAIL = BENSON.getEmail().toString(); + private static final String VALID_ADDRESS = BENSON.getAddress().toString(); + private static final List<AdaptedTag> VALID_TAGS = BENSON.getTags().stream() + .map(AdaptedTag::new) + .collect(Collectors.toList()); + + static class MockAdaptedPerson extends AdaptedPerson { + public MockAdaptedPerson(String name, String phone, String email, String address, List<AdaptedTag> tags) { + super(name, phone, email, address, tags); + } + + public MockAdaptedPerson(Person source) { + super(source); + } + } + + @Test + public void toModelType_personParameterConstructor_returnsPerson() throws Exception { + AdaptedPerson person = new MockAdaptedPerson(BENSON); + assertEquals(BENSON, person.toModelType()); + } + + @Test + public void toModelType_stringParameterConstructor_returnsPerson() throws Exception { + AdaptedPerson person = new MockAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS); + assertEquals(BENSON, person.toModelType()); + } + + @Test + public void toModelType_invalidName_throwsIllegalValueException() { + AdaptedPerson person = + new MockAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = Name.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() { + AdaptedPerson person = new MockAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidPhone_throwsIllegalValueException() { + AdaptedPerson person = + new MockAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = Phone.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullPhone_throwsIllegalValueException() { + AdaptedPerson person = new MockAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidEmail_throwsIllegalValueException() { + AdaptedPerson person = + new MockAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = Email.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullEmail_throwsIllegalValueException() { + AdaptedPerson person = new MockAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidAddress_throwsIllegalValueException() { + AdaptedPerson person = + new MockAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + String expectedMessage = Address.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullAddress_throwsIllegalValueException() { + AdaptedPerson person = new MockAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidTags_throwsIllegalValueException() { + List<AdaptedTag> invalidTags = new ArrayList<>(VALID_TAGS); + invalidTags.add(new AdaptedTag(INVALID_TAG)); + AdaptedPerson person = + new MockAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + assertThrows(IllegalValueException.class, person::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/AdaptedTagTest.java b/src/test/java/seedu/address/storage/AdaptedTagTest.java new file mode 100644 index 00000000000..ef435381c5f --- /dev/null +++ b/src/test/java/seedu/address/storage/AdaptedTagTest.java @@ -0,0 +1,80 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.tag.Tag; + +public class AdaptedTagTest { + + private static final String VALID_TAGNAME = "test"; + private static final String VALID_ALPHANUMERIC_TAGNAME = "testTag123"; + private static final String VALID_MULTIWORD_TAGNAME = "test tag"; + + private static final String ILLEGAL_CHAR_TAGNAME = "t@st3r!"; + private static final String ILLEGAL_CHAR_MULTIWORD_TAGNAME = "test t@g"; + + + private static final Tag expectedSingleWordTag = new Tag(VALID_TAGNAME); + private static final Tag expectedMultiWordTag = new Tag(VALID_MULTIWORD_TAGNAME); + + @Test + public void toModelType_validName_noExceptionsThrown() { + AdaptedTag singleWordTag = new AdaptedTag(VALID_TAGNAME); + assertDoesNotThrow(() -> { + singleWordTag.toModelType(); + }); + + AdaptedTag alphanumericTag = new AdaptedTag(VALID_ALPHANUMERIC_TAGNAME); + assertDoesNotThrow(() -> { + alphanumericTag.toModelType(); + }); + + AdaptedTag multiWordTag = new AdaptedTag(VALID_MULTIWORD_TAGNAME); + assertDoesNotThrow(() -> { + multiWordTag.toModelType(); + }); + } + + @Test + public void toModelType_validName_stringConstructor() throws Exception { + AdaptedTag singleWordTag = new AdaptedTag(VALID_TAGNAME); + assertEquals(singleWordTag.toModelType(), expectedSingleWordTag); + + AdaptedTag multiWordTag = new AdaptedTag(VALID_MULTIWORD_TAGNAME); + assertEquals(multiWordTag.toModelType(), expectedMultiWordTag); + } + + @Test + public void toModelType_validName_tagConstructor() throws Exception { + AdaptedTag singleWordTag = new AdaptedTag(expectedSingleWordTag); + assertEquals(singleWordTag.toModelType(), expectedSingleWordTag); + + AdaptedTag multiWordTag = new AdaptedTag(expectedMultiWordTag); + assertEquals(multiWordTag.toModelType(), expectedMultiWordTag); + } + + @Test + public void toModelType_invalidName_throwsException() { + AdaptedTag illegalCharTag = new AdaptedTag(ILLEGAL_CHAR_TAGNAME); + assertThrows(IllegalValueException.class, Tag.MESSAGE_CONSTRAINTS, + illegalCharTag::toModelType); + + AdaptedTag illegalCharMultiwordTag = new AdaptedTag(ILLEGAL_CHAR_MULTIWORD_TAGNAME); + assertThrows(IllegalValueException.class, Tag.MESSAGE_CONSTRAINTS, + illegalCharMultiwordTag::toModelType); + } + + @Test + public void getTagName() { + AdaptedTag singleWordTag = new AdaptedTag(VALID_TAGNAME); + assertEquals(singleWordTag.getTagName(), VALID_TAGNAME); + + AdaptedTag multiWordTag = new AdaptedTag(VALID_MULTIWORD_TAGNAME); + assertEquals(multiWordTag.getTagName(), VALID_MULTIWORD_TAGNAME); + } +} diff --git a/src/test/java/seedu/address/storage/CsvAdaptedLeaveTest.java b/src/test/java/seedu/address/storage/CsvAdaptedLeaveTest.java new file mode 100644 index 00000000000..5808a00b1cf --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvAdaptedLeaveTest.java @@ -0,0 +1,178 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.CsvAdaptedLeave.DESCRIPTION_HEADER; +import static seedu.address.storage.CsvAdaptedLeave.EMPLOYEE_HEADER; +import static seedu.address.storage.CsvAdaptedLeave.END_HEADER; +import static seedu.address.storage.CsvAdaptedLeave.START_HEADER; +import static seedu.address.storage.CsvAdaptedLeave.STATUS_HEADER; +import static seedu.address.storage.CsvAdaptedLeave.TITLE_HEADER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.util.GetValuer; + +public class CsvAdaptedLeaveTest { + private static final String VALID_TITLE = ALICE_LEAVE.getTitle().toString(); + private static final String VALID_EMPLOYEE = ALICE_LEAVE.getEmployee().getName().toString(); + private static final String VALID_START = ALICE_LEAVE.getStart().toFormattedString(); + private static final String VALID_END = ALICE_LEAVE.getEnd().toFormattedString(); + private static final String VALID_DESCRIPTION = ALICE_LEAVE.getDescription().toString(); + private static final String VALID_STATUS = ALICE_LEAVE.getStatus().toString(); + + @Test + public void getCsvValues_validFields() { + String[] expectedValues = {VALID_TITLE, VALID_EMPLOYEE, VALID_START, + VALID_END, VALID_DESCRIPTION, VALID_STATUS}; + + CsvAdaptedLeave leave = new CsvAdaptedLeave(ALICE_LEAVE); + assertEquals(leave.getCsvValues(), Arrays.asList(expectedValues)); + } + + @Test + public void getHeader() { + String[] expectedHeaders = {TITLE_HEADER, EMPLOYEE_HEADER, START_HEADER, + END_HEADER, DESCRIPTION_HEADER, STATUS_HEADER}; + assertEquals(CsvAdaptedLeave.getHeader(), Arrays.asList(expectedHeaders)); + } + + /** + * Mock CsvRow that returns ALICE_LEAVE's values when the corresponding headers are queried. + * All headers' values are present. + */ + class MockCsvRow implements GetValuer { + private final boolean hasTitle; + private final boolean hasEmployee; + private final boolean hasStart; + private final boolean hasEnd; + private final boolean hasDescription; + private final boolean hasStatus; + + /** + * Constructs a MockCsvRow object with all headers' values present. + */ + public MockCsvRow() { + this.hasTitle = true; + this.hasEmployee = true; + this.hasStart = true; + this.hasEnd = true; + this.hasDescription = true; + this.hasStatus = true; + } + + /** + * Constructs a MockCsvRow object. Boolean values are used to control whether the corresponding + * fields' values are present in the row + * @param hasTitle If the row contains the title + * @param hasEmployee If the row contains the employee name + * @param hasStart If the row contains the start date + * @param hasEnd If the row contains the end date + * @param hasDescription If the row contains the description + * @param hasStatus If the row contains the status + */ + public MockCsvRow(boolean hasTitle, boolean hasEmployee, boolean hasStart, + boolean hasEnd, boolean hasDescription, boolean hasStatus) { + this.hasTitle = hasTitle; + this.hasEmployee = hasEmployee; + this.hasStart = hasStart; + this.hasEnd = hasEnd; + this.hasDescription = hasDescription; + this.hasStatus = hasStatus; + } + + /** + * Returns values associated with the corresponding column. + * + * @param field Name of column to query for + * @return String value associated with column + * @throws CsvMissingFieldException if queried field has no associated value + */ + public String getValue(String field) throws CsvMissingFieldException { + if (hasTitle && field.equals(TITLE_HEADER)) { + return VALID_TITLE; + } + if (hasEmployee && field.equals(EMPLOYEE_HEADER)) { + return VALID_EMPLOYEE; + } + if (hasStart && field.equals(START_HEADER)) { + return VALID_START; + } + if (hasEnd && field.equals(END_HEADER)) { + return VALID_END; + } + if (hasDescription && field.equals(DESCRIPTION_HEADER)) { + return VALID_DESCRIPTION; + } + if (hasStatus && field.equals(STATUS_HEADER)) { + return VALID_STATUS; + } + throw new CsvMissingFieldException(field); + } + } + + @Test + public void deserialiseLeave_allFieldsPresent_returnsCsvAdaptedLeave() throws Exception { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow(); + CsvAdaptedLeave leave = CsvAdaptedLeave.deserialiseLeave(mockCsvRow); + assertEquals(leave.toModelType(), ALICE_LEAVE); + } + + @Test + public void deserialiseLeave_missingTitle_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + false, true, true, true, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(TITLE_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } + + @Test + public void deserialiseLeave_missingEmployee_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + true, false, true, true, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(EMPLOYEE_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } + + @Test + public void deserialiseLeave_missingStart_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + true, true, false, true, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(START_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } + + @Test + public void deserialiseLeave_missingEnd_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + true, true, true, false, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(END_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } + + @Test + public void deserialiseLeave_missingDescription_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + true, true, true, true, false, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(DESCRIPTION_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } + + @Test + public void deserialiseLeave_missingStatus_throwsException() { + GetValuer mockCsvRow = new CsvAdaptedLeaveTest.MockCsvRow( + true, true, true, true, true, false); + CsvMissingFieldException expectedError = new CsvMissingFieldException(STATUS_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedLeave.deserialiseLeave(mockCsvRow)); + } +} diff --git a/src/test/java/seedu/address/storage/CsvAdaptedPersonTest.java b/src/test/java/seedu/address/storage/CsvAdaptedPersonTest.java new file mode 100644 index 00000000000..c4d724aa03a --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvAdaptedPersonTest.java @@ -0,0 +1,165 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.storage.CsvAdaptedPerson.ADDRESS_HEADER; +import static seedu.address.storage.CsvAdaptedPerson.EMAIL_HEADER; +import static seedu.address.storage.CsvAdaptedPerson.NAME_HEADER; +import static seedu.address.storage.CsvAdaptedPerson.PHONE_HEADER; +import static seedu.address.storage.CsvAdaptedPerson.TAGS_HEADER; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalPersons.BENSON; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.CsvMissingFieldException; +import seedu.address.commons.util.GetValuer; + +public class CsvAdaptedPersonTest { + private static final String VALID_NAME = BENSON.getName().toString(); + private static final String VALID_PHONE = BENSON.getPhone().toString(); + private static final String VALID_EMAIL = BENSON.getEmail().toString(); + private static final String VALID_ADDRESS = BENSON.getAddress().toString(); + private static final List<AdaptedTag> VALID_TAGS = BENSON.getTags().stream() + .map(AdaptedTag::new) + .collect(Collectors.toList()); + + private static final List<String> VALID_TAG_NAMES = VALID_TAGS.stream() + .map(AdaptedTag::getTagName) + .collect(Collectors.toList()); + + private static final String VALID_TAGS_CSV_STRING = String.join( + CsvAdaptedPerson.TAG_DELIMITER, VALID_TAG_NAMES); + + @Test + public void getCsvValues_validFields() { + String tagRep = String.join(CsvAdaptedPerson.TAG_DELIMITER, VALID_TAG_NAMES); + String[] expectedValues = {VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, tagRep}; + + CsvAdaptedPerson person = new CsvAdaptedPerson(BENSON); + assertEquals(person.getCsvValues(), Arrays.asList(expectedValues)); + } + + @Test + public void getHeader() { + String[] expectedHeaders = {NAME_HEADER, CsvAdaptedPerson.PHONE_HEADER, + CsvAdaptedPerson.EMAIL_HEADER, CsvAdaptedPerson.ADDRESS_HEADER, CsvAdaptedPerson.TAGS_HEADER}; + assertEquals(CsvAdaptedPerson.getHeader(), Arrays.asList(expectedHeaders)); + } + + /** + * Mock CsvRow that returns Benson's values when the corresponding headers are queried. All headers' values are + * present. + */ + class MockCsvRow implements GetValuer { + private final boolean hasName; + private final boolean hasPhone; + private final boolean hasEmail; + private final boolean hasAddress; + private final boolean hasTags; + + /** + * Constructs a MockCsvRow object with all headers' values present. + */ + public MockCsvRow() { + this.hasName = true; + this.hasPhone = true; + this.hasEmail = true; + this.hasAddress = true; + this.hasTags = true; + } + + /** + * Constructs a MockCsvRow object. Boolean values are used to control whether the corresponding + * fields' values are present in the row + * @param hasName If the row contains the name + * @param hasPhone If the row contains the phone number + * @param hasEmail If the row contains the email address + * @param hasAddress If the row contains the address + * @param hasTags If the row contains the tags + */ + public MockCsvRow(boolean hasName, boolean hasPhone, boolean hasEmail, boolean hasAddress, + boolean hasTags) { + this.hasName = hasName; + this.hasPhone = hasPhone; + this.hasEmail = hasEmail; + this.hasAddress = hasAddress; + this.hasTags = hasTags; + } + + /** + * Returns values associated with the corresponding column. + * + * @param field Name of column to query for + * @return String value associated with column + * @throws CsvMissingFieldException if queried field has no associated value + */ + public String getValue(String field) throws CsvMissingFieldException { + if (hasName && field.equals(NAME_HEADER)) { + return VALID_NAME; + } + if (hasPhone && field.equals(PHONE_HEADER)) { + return VALID_PHONE; + } + if (hasEmail && field.equals(EMAIL_HEADER)) { + return VALID_EMAIL; + } + if (hasAddress && field.equals(ADDRESS_HEADER)) { + return VALID_ADDRESS; + } + if (hasTags && field.equals(TAGS_HEADER)) { + return VALID_TAGS_CSV_STRING; + } + throw new CsvMissingFieldException(field); + } + } + + @Test + public void deserialisePerson_allFieldsPresent_returnsCsvAdaptedPerson() throws Exception { + GetValuer mockCsvRow = new MockCsvRow(); + CsvAdaptedPerson person = CsvAdaptedPerson.deserialisePerson(mockCsvRow); + assertEquals(person.toModelType(), BENSON); + } + + @Test + public void deserialisePerson_missingName_throwsException() { + GetValuer mockCsvRow = new MockCsvRow(false, true, true, true, true); + assertThrows(CsvMissingFieldException.class, () -> + CsvAdaptedPerson.deserialisePerson(mockCsvRow)); + } + + @Test + public void deserialisePerson_missingPhone_throwsException() { + GetValuer mockCsvRow = new MockCsvRow(true, false, true, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(PHONE_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedPerson.deserialisePerson(mockCsvRow)); + } + + @Test + public void deserialisePerson_missingEmail_throwsException() { + GetValuer mockCsvRow = new MockCsvRow(true, true, false, true, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(EMAIL_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedPerson.deserialisePerson(mockCsvRow)); + } + + @Test + public void deserialisePerson_missingAddress_throwsException() { + GetValuer mockCsvRow = new MockCsvRow(true, true, true, false, true); + CsvMissingFieldException expectedError = new CsvMissingFieldException(ADDRESS_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedPerson.deserialisePerson(mockCsvRow)); + } + + @Test + public void deserialisePerson_missingTags_throwsException() { + GetValuer mockCsvRow = new MockCsvRow(true, true, true, true, false); + CsvMissingFieldException expectedError = new CsvMissingFieldException(TAGS_HEADER); + assertThrows(CsvMissingFieldException.class, expectedError.getMessage(), () -> + CsvAdaptedPerson.deserialisePerson(mockCsvRow)); + } +} diff --git a/src/test/java/seedu/address/storage/CsvAddressBookStorageTest.java b/src/test/java/seedu/address/storage/CsvAddressBookStorageTest.java new file mode 100644 index 00000000000..0c40d2017ef --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvAddressBookStorageTest.java @@ -0,0 +1,106 @@ +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.TypicalPersons.getTypicalAddressBook; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.testutil.FileAndPathUtil; + +public class CsvAddressBookStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get( + "src", "test", "data", "CsvAddressBookStorageTest"); + + private Optional<ReadOnlyAddressBook> readAddressBook(String filePath) throws Exception { + return new CsvAddressBookStorage(addToTestDataPathIfNotNull(filePath)) + .readAddressBook(); + } + + private Path addToTestDataPathIfNotNull(String filePath) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filePath); + } + + @Test + public void readAddressBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readAddressBook(null)); + } + + @Test + public void readAddressBook_invalidFilePath_emptyResult() throws Exception { + assertFalse(readAddressBook("nonExistentFile.csv").isPresent()); + } + + @Test + public void readAddressBook_notCsvFile_throwsException() { + assertThrows(DataLoadingException.class, () -> readAddressBook("notCsvFile.txt")); + } + + @Test + public void readAddressBook_invalidPersonAddressBook_throwsDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readAddressBook("invalidPersonAddressBook.csv")); + } + + @Test + public void readAddressBook_validAndInvalidPersonAddressBook_throwsDataLoadingException() { + assertThrows(DataLoadingException.class, () -> + readAddressBook("validAndInvalidPersonAddressBook.csv")); + } + + @Test + public void readAddressBook_missingField_emptyResult() throws Exception { + assertFalse(readAddressBook("missingFieldCsvFile.csv").isPresent()); + } + + @Test + public void readAndSaveAddressBook_validPersonAddressBook_success() throws Exception { + Path savePath = addToTestDataPathIfNotNull("testSaveFile.csv"); + AddressBook original = getTypicalAddressBook(); + CsvAddressBookStorage abStorage = new CsvAddressBookStorage(savePath); + + // save in new data and read back + abStorage.saveAddressBook(original, savePath); + ReadOnlyAddressBook readBack = abStorage.readAddressBook(savePath).get(); + assertEquals(readBack, original); + + FileAndPathUtil.cleanupCreatedFiles(savePath); + + // save and read again without specifying the file path + abStorage.saveAddressBook(original); + readBack = abStorage.readAddressBook().get(); + assertEquals(readBack, original); + + FileAndPathUtil.cleanupCreatedFiles(savePath); + } + + @Test + public void saveAddressBook_nullAddressBook_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveAddressBook(null, "shouldNotExist.csv")); + } + + private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { + new CsvAddressBookStorage(addToTestDataPathIfNotNull(filePath)) + .saveAddressBook(addressBook); + } + + @Test + public void saveAddressBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveAddressBook(getTypicalAddressBook(), null)); + } + + @Test + public void getAddressBookFilePath() { + Path testPath = addToTestDataPathIfNotNull("mockFile.csv"); + CsvAddressBookStorage abStorage = new CsvAddressBookStorage(testPath); + assertEquals(abStorage.getAddressBookFilePath(), testPath); + } +} diff --git a/src/test/java/seedu/address/storage/CsvLeavesBookStorageTest.java b/src/test/java/seedu/address/storage/CsvLeavesBookStorageTest.java new file mode 100644 index 00000000000..23ae3f68d9e --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvLeavesBookStorageTest.java @@ -0,0 +1,112 @@ +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.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.ReadOnlyLeavesBook; +import seedu.address.testutil.FileAndPathUtil; + + +public class CsvLeavesBookStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get( + "src", "test", "data", "CsvLeavesBookStorageTest"); + + private Optional<ReadOnlyLeavesBook> readLeavesBook(String filePath) throws Exception { + AddressBook addressBook = getTypicalAddressBook(); + return new CsvLeavesBookStorage(addToTestDataPathIfNotNull(filePath)) + .readLeavesBook(addressBook); + } + + private Path addToTestDataPathIfNotNull(String filePath) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, filePath); + } + + @Test + public void readLeavesBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readLeavesBook(null)); + } + + @Test + public void readLeavesBook_invalidFilePath_emptyResult() throws Exception { + assertFalse(readLeavesBook("nonExistentFile.csv").isPresent()); + } + + @Test + public void readLeavesBook_notCsvFile_throwsException() { + assertThrows(DataLoadingException.class, () -> readLeavesBook("notCsvFile.txt")); + } + + /* + @Test + public void readLeavesBook_invalidLeavesAddressBook_throwsDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readLeavesBook("invalidPersonAddressBook.csv")); + } + + @Test + public void readAddressBook_validAndInvalidPersonAddressBook_throwsDataLoadingException() { + assertThrows(DataLoadingException.class, () -> + readLeavesBook("validAndInvalidPersonAddressBook.csv")); + } + + @Test + public void readAddressBook_missingField_emptyResult() throws Exception { + assertFalse(readLeavesBook("missingFieldCsvFile.csv").isPresent()); + } + */ + + @Test + public void readAndSaveLeavesBook_validLeavesBook_success() throws Exception { + Path savePath = addToTestDataPathIfNotNull("testSaveFile.csv"); + LeavesBook original = getTypicalLeavesBook(); + CsvLeavesBookStorage lvStorage = new CsvLeavesBookStorage(savePath); + + // save in new data and read back + lvStorage.saveLeavesBook(original, savePath); + ReadOnlyLeavesBook readBack = lvStorage.readLeavesBook(savePath, getTypicalAddressBook()).get(); + assertEquals(readBack, original); + + FileAndPathUtil.cleanupCreatedFiles(savePath); + + // save and read again without specifying the file path + lvStorage.saveLeavesBook(original); + readBack = lvStorage.readLeavesBook(getTypicalAddressBook()).get(); + assertEquals(readBack, original); + + FileAndPathUtil.cleanupCreatedFiles(savePath); + } + + @Test + public void saveLeavesBook_nullLeavesBook_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveLeavesBook(null, "shouldNotExist.csv")); + } + + private void saveLeavesBook(ReadOnlyLeavesBook leavesBook, String filePath) throws IOException { + new CsvLeavesBookStorage(addToTestDataPathIfNotNull(filePath)) + .saveLeavesBook(leavesBook); + } + + @Test + public void saveLeavesBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveLeavesBook(getTypicalLeavesBook(), null)); + } + + @Test + public void getAddressBookFilePath() { + Path testPath = addToTestDataPathIfNotNull("mockFile.csv"); + CsvLeavesBookStorage lvStorage = new CsvLeavesBookStorage(testPath); + assertEquals(lvStorage.getLeavesBookFilePath(), testPath); + } +} diff --git a/src/test/java/seedu/address/storage/CsvSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/CsvSerializableAddressBookTest.java new file mode 100644 index 00000000000..07d609db8f5 --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvSerializableAddressBookTest.java @@ -0,0 +1,55 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; +import seedu.address.model.AddressBook; + +public class CsvSerializableAddressBookTest { + + private static final CsvAdaptedPerson aliceAP = new CsvAdaptedPerson(ALICE); + private static final CsvAdaptedPerson bensonAP = new CsvAdaptedPerson(BENSON); + @Test + public void constructor_containsPersons() throws Exception { + List<CsvAdaptedPerson> persons = generatePersonsList(); + + CsvSerializableAddressBook csvBook = new CsvSerializableAddressBook(persons); + AddressBook addressBook = csvBook.toModelType(); + + assertTrue(addressBook.hasPerson(ALICE)); + assertTrue(addressBook.hasPerson(BENSON)); + } + + private List<CsvAdaptedPerson> generatePersonsList() { + List<CsvAdaptedPerson> persons = new ArrayList<>(); + persons.add(aliceAP); + persons.add(bensonAP); + return persons; + } + + @Test + public void saveAddressBook_successfulCreateFile() { + List<CsvAdaptedPerson> persons = generatePersonsList(); + CsvSerializableAddressBook csvBook = new CsvSerializableAddressBook(persons); + CsvFile csvFile = csvBook.saveAddressBook(); + + List<String> rowStrings = csvFile.getRows() + .map(CsvFile.CsvRow::printRow) + .collect(Collectors.toList()); + + List<String> expectedStrings = persons.stream().map(person -> + String.join(CsvUtil.DELIMITER, person.getCsvValues())).collect(Collectors.toList()); + + assertEquals(rowStrings, expectedStrings); + } +} diff --git a/src/test/java/seedu/address/storage/CsvSerializableLeavesBookTest.java b/src/test/java/seedu/address/storage/CsvSerializableLeavesBookTest.java new file mode 100644 index 00000000000..1fbbfb9f28c --- /dev/null +++ b/src/test/java/seedu/address/storage/CsvSerializableLeavesBookTest.java @@ -0,0 +1,56 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BENSON_LEAVE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.util.CsvFile; +import seedu.address.commons.util.CsvUtil; +import seedu.address.model.LeavesBook; + +public class CsvSerializableLeavesBookTest { + private static final CsvAdaptedLeave aliceAL = new CsvAdaptedLeave(ALICE_LEAVE); + private static final CsvAdaptedLeave bensonAL = new CsvAdaptedLeave(BENSON_LEAVE); + + @Test + public void constructor_containsLeaves() throws Exception { + List<CsvAdaptedLeave> leaves = generateLeavesList(); + + CsvSerializableLeavesBook csvBook = new CsvSerializableLeavesBook(leaves); + LeavesBook leavesBook = csvBook.toModelType(getTypicalAddressBook()); + + assertTrue(leavesBook.hasLeave(ALICE_LEAVE)); + assertTrue(leavesBook.hasLeave(BENSON_LEAVE)); + } + + private List<CsvAdaptedLeave> generateLeavesList() { + List<CsvAdaptedLeave> leaves = new ArrayList<>(); + leaves.add(aliceAL); + leaves.add(bensonAL); + return leaves; + } + + @Test + public void saveAddressBook_successfulCreateFile() { + List<CsvAdaptedLeave> leaves = generateLeavesList(); + CsvSerializableLeavesBook csvBook = new CsvSerializableLeavesBook(leaves); + CsvFile csvFile = csvBook.saveLeavesBook(); + + List<String> rowStrings = csvFile.getRows() + .map(CsvFile.CsvRow::printRow) + .collect(Collectors.toList()); + + List<String> expectedStrings = leaves.stream().map(leave -> + String.join(CsvUtil.DELIMITER, leave.getCsvValues())).collect(Collectors.toList()); + + assertEquals(rowStrings, expectedStrings); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedLeaveTest.java b/src/test/java/seedu/address/storage/JsonAdaptedLeaveTest.java new file mode 100644 index 00000000000..29ee50d9f3e --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedLeaveTest.java @@ -0,0 +1,143 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalPersons.ALICE; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Title; +import seedu.address.storage.JsonAdaptedLeave.Employee; +import seedu.address.storage.JsonAdaptedLeave.Name; + +public class JsonAdaptedLeaveTest { + + private static final String VALID_START = ALICE_LEAVE.getStart().toString(); + private static final String VALID_END = ALICE_LEAVE.getEnd().toString(); + private static final String VALID_TITLE = ALICE_LEAVE.getTitle().toString(); + private static final String VALID_DESCRIPTION = ALICE_LEAVE.getDescription().toString(); + private static final String EMPTY_DESCRIPTION = ""; + private static final String VALID_STATUS = ALICE_LEAVE.getStatus().toString(); + private static final Employee VALID_EMPLOYEE = new Employee(new Name(ALICE.getName().toString())); + private static final Leave VALID_LEAVE = ALICE_LEAVE; + + private static final String INVALID_START = "2020/01/01"; + private static final String INVALID_END = "2020/01/01"; + private static final String EMPTY_TITLE = ""; + private static final String INVALID_TITLE = "title*#"; + private static final String INVALID_STATUS = "invalid"; + private static final String INVALID_DESCRIPTION = "description*#"; + + + @Test + public void toModelType_createsEqualLeaveObject_success() throws Exception { + JsonAdaptedLeave leave = new JsonAdaptedLeave(VALID_LEAVE); + assertEquals(VALID_LEAVE, leave.toModelType()); + + JsonAdaptedLeave leave2 = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertEquals(VALID_LEAVE, leave2.toModelType()); + } + + @Test + public void toModelType_nullStart_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(null, VALID_END, VALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(JsonAdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT, "start"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidStart_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(INVALID_START, VALID_END, VALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertThrows(IllegalValueException.class, leave::toModelType); + } + + @Test + public void toModelType_nullEnd_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, null, VALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(JsonAdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT, "end"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidEnd_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, INVALID_END, VALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertThrows(IllegalValueException.class, leave::toModelType); + } + + @Test + public void toModelType_nullTitle_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, null, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(JsonAdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT, "title"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_emptyTitle_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, EMPTY_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertThrows(IllegalValueException.class, Title.MESSAGE_CONSTRAINTS, leave::toModelType); + } + + @Test + public void toModelType_invalidTitle_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, INVALID_TITLE, VALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertThrows(IllegalValueException.class, Title.MESSAGE_CONSTRAINTS, leave::toModelType); + } + + @Test + public void toModelType_nullDescription_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, null, + VALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = String.format(JsonAdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT, "description"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_emptyDescription_success() throws Exception { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, EMPTY_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertTrue(VALID_LEAVE.isSameLeave(leave.toModelType())); + } + + @Test + public void toModelType_invalidDescription_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, INVALID_DESCRIPTION, + VALID_STATUS, VALID_EMPLOYEE); + assertThrows(IllegalValueException.class, Description.MESSAGE_CONSTRAINTS, leave::toModelType); + } + + @Test + public void toModelType_nullEmployee_throwsIllegalValueException() { + assertThrows(NullPointerException.class, () -> JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, + VALID_DESCRIPTION, VALID_STATUS, null)); + } + + @Test + public void toModelType_nullStatus_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, VALID_DESCRIPTION, + null, VALID_EMPLOYEE); + String expectedMessage = String.format(JsonAdaptedLeave.MISSING_FIELD_MESSAGE_FORMAT, "status"); + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } + + @Test + public void toModelType_invalidStatus_throwsIllegalValueException() { + JsonAdaptedLeave leave = JsonAdaptedLeave.of(VALID_START, VALID_END, VALID_TITLE, VALID_DESCRIPTION, + INVALID_STATUS, VALID_EMPLOYEE); + String expectedMessage = Status.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, leave::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..35107bd53cb 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -1,110 +1,32 @@ package seedu.address.storage; import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; -import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.BENSON; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; - public class JsonAdaptedPersonTest { - private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; - 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 VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); private static final String VALID_EMAIL = BENSON.getEmail().toString(); private static final String VALID_ADDRESS = BENSON.getAddress().toString(); - private static final List<JsonAdaptedTag> VALID_TAGS = BENSON.getTags().stream() - .map(JsonAdaptedTag::new) + private static final List<AdaptedTag> VALID_TAGS = BENSON.getTags().stream() + .map(AdaptedTag::new) .collect(Collectors.toList()); @Test - public void toModelType_validPersonDetails_returnsPerson() throws Exception { - JsonAdaptedPerson person = new JsonAdaptedPerson(BENSON); + public void toModelType_personParameterConstructor_returnsPerson() throws Exception { + AdaptedPerson person = new JsonAdaptedPerson(BENSON); assertEquals(BENSON, person.toModelType()); } @Test - public void toModelType_invalidName_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - 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); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - 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); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - 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); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); - 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); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidTags_throwsIllegalValueException() { - List<JsonAdaptedTag> invalidTags = new ArrayList<>(VALID_TAGS); - invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); - assertThrows(IllegalValueException.class, person::toModelType); + public void toModelType_stringParameterConstructor_returnsPerson() throws Exception { + AdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS); + assertEquals(BENSON, person.toModelType()); } - } diff --git a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java index 4e5ce9200c8..c6727e0602c 100644 --- a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java @@ -18,9 +18,11 @@ import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.testutil.FileAndPathUtil; public class JsonAddressBookStorageTest { - private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonAddressBookStorageTest"); + private static final Path TEST_DATA_FOLDER = Paths.get( + "src", "test", "data", "JsonAddressBookStorageTest"); @TempDir public Path testFolder; @@ -30,14 +32,13 @@ public void readAddressBook_nullFilePath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> readAddressBook(null)); } - private java.util.Optional<ReadOnlyAddressBook> readAddressBook(String filePath) throws Exception { - return new JsonAddressBookStorage(Paths.get(filePath)).readAddressBook(addToTestDataPathIfNotNull(filePath)); + private java.util.Optional<ReadOnlyAddressBook> readAddressBook(String fileName) throws Exception { + Path filePath = addToTestDataPathIfNotNull(fileName); + return new JsonAddressBookStorage(filePath).readAddressBook(); } - private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { - return prefsFileInTestDataFolder != null - ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) - : null; + private Path addToTestDataPathIfNotNull(String fileName) { + return FileAndPathUtil.addToTestDataPathIfNotNull(TEST_DATA_FOLDER, fileName); } @Test @@ -94,10 +95,10 @@ public void saveAddressBook_nullAddressBook_throwsNullPointerException() { /** * Saves {@code addressBook} at the specified {@code filePath}. */ - private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) { + private void saveAddressBook(ReadOnlyAddressBook addressBook, String fileName) { + Path filePath = addToTestDataPathIfNotNull(fileName); try { - new JsonAddressBookStorage(Paths.get(filePath)) - .saveAddressBook(addressBook, addToTestDataPathIfNotNull(filePath)); + new JsonAddressBookStorage(filePath).saveAddressBook(addressBook); } catch (IOException ioe) { throw new AssertionError("There should not be an error writing to the file.", ioe); } @@ -107,4 +108,11 @@ private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) { public void saveAddressBook_nullFilePath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> saveAddressBook(new AddressBook(), null)); } + + @Test + public void getAddressBookFilePath() { + Path filePath = addToTestDataPathIfNotNull("tempFile.csv"); + JsonAddressBookStorage abStorage = new JsonAddressBookStorage(filePath); + assertEquals(abStorage.getAddressBookFilePath(), filePath); + } } diff --git a/src/test/java/seedu/address/storage/JsonLeavesBookStorageTest.java b/src/test/java/seedu/address/storage/JsonLeavesBookStorageTest.java new file mode 100644 index 00000000000..344128941c2 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonLeavesBookStorageTest.java @@ -0,0 +1,129 @@ +package seedu.address.storage; + +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.TypicalLeaves.ALICE_LEAVE; +import static seedu.address.testutil.TypicalLeaves.BENSON_LEAVE_2; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +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.AddressBook; +import seedu.address.model.LeavesBook; +import seedu.address.model.ReadOnlyLeavesBook; + +public class JsonLeavesBookStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonLeavesBookStorageTest"); + + @TempDir + public Path testFolder; + + @Test + public void readLeavesBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readLeavesBook(null)); + } + + private java.util.Optional<ReadOnlyLeavesBook> readLeavesBook(String filePath) throws Exception { + AddressBook addressBook = getTypicalAddressBook(); + return new JsonLeavesBookStorage(Paths.get(filePath)).readLeavesBook( + addToTestDataPathIfNotNull(filePath), addressBook); + } + + private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { + return prefsFileInTestDataFolder != null + ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) + : null; + } + + @Test + public void readLeavesBook_missingFile_emptyResult() throws Exception { + assertFalse(readLeavesBook("NonExistentFile.json").isPresent()); + } + + @Test + public void readLeavesBook_success() { + Path filePath = Paths.get("src", "test", "data", "JsonLeavesBookStorageTest", "ValidLeaves.json"); + try { + AddressBook addressBook = getTypicalAddressBook(); + ReadOnlyLeavesBook leavesBook = new JsonLeavesBookStorage(filePath).readLeavesBook(addressBook).get(); + assertEquals(leavesBook, getTypicalLeavesBook()); + } catch (DataLoadingException dle) { + throw new AssertionError("This should not happen.", dle); + } + } + + @Test + public void readLeavesBook_invalidPerson_skipsRow() throws Exception { + assertTrue(readLeavesBook("InvalidEmployeeName.json").get().getLeaveList().isEmpty()); + } + + @Test + public void readLeavesBook_invalidDateFields_throwsDataLoadingException() { + assertThrows(DataLoadingException.class, () -> readLeavesBook("InvalidDateFields.json")); + } + + @Test + public void readLeavesBook_notJsonFormat_throwsDataConversionException() { + assertThrows(DataLoadingException.class, () -> readLeavesBook("NotJsonFormat.json")); + } + + @Test + public void readAndSaveLeavesBook_allInOrder_success() throws Exception { + Path filePath = testFolder.resolve("TempLeavesBook.json"); + LeavesBook original = getTypicalLeavesBook(); + AddressBook addressBook = getTypicalAddressBook(); + JsonLeavesBookStorage jsonLeavesBookStorage = new JsonLeavesBookStorage(filePath); + + // Save in new file and read back + jsonLeavesBookStorage.saveLeavesBook(original, filePath); + ReadOnlyLeavesBook readBack = jsonLeavesBookStorage.readLeavesBook(addressBook).get(); + assertEquals(original, new LeavesBook(readBack)); + + // Modify data, overwrite exiting file, and read back + original.addLeave(BENSON_LEAVE_2); + original.removeLeave(ALICE_LEAVE); + jsonLeavesBookStorage.saveLeavesBook(original, filePath); + readBack = jsonLeavesBookStorage.readLeavesBook(addressBook).get(); + assertEquals(original, new LeavesBook(readBack)); + + // Save and read without specifying file path + original.addLeave(ALICE_LEAVE); + jsonLeavesBookStorage.saveLeavesBook(original); // file path not specified + readBack = jsonLeavesBookStorage.readLeavesBook(addressBook).get(); + assertEquals(original, new LeavesBook(readBack)); + } + + @Test + public void saveLeavesBook_nullLeavesBook_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveLeavesBook(null, "SomeFile.json")); + } + + @Test + public void saveLeavesBook_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveLeavesBook(new LeavesBook(), null)); + } + + /** + * Saves {@code leavesBook} at the specified {@code filePath}. + * + * @param leavesBook + * @param filePath + */ + private void saveLeavesBook(ReadOnlyLeavesBook leavesBook, String filePath) { + try { + new JsonLeavesBookStorage(Paths.get(filePath)) + .saveLeavesBook(leavesBook, addToTestDataPathIfNotNull(filePath)); + } catch (IOException ioe) { + throw new AssertionError("This should not happen.", ioe); + } + } +} diff --git a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java index 188c9058d20..cdfbde5cec5 100644 --- a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java +++ b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java @@ -1,47 +1,36 @@ package seedu.address.storage; import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; 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.AddressBook; -import seedu.address.testutil.TypicalPersons; +import seedu.address.model.ReadOnlyAddressBook; public class JsonSerializableAddressBookTest { private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableAddressBookTest"); private static final Path TYPICAL_PERSONS_FILE = TEST_DATA_FOLDER.resolve("typicalPersonsAddressBook.json"); - private static final Path INVALID_PERSON_FILE = TEST_DATA_FOLDER.resolve("invalidPersonAddressBook.json"); - private static final Path DUPLICATE_PERSON_FILE = TEST_DATA_FOLDER.resolve("duplicatePersonAddressBook.json"); @Test - public void toModelType_typicalPersonsFile_success() throws Exception { + public void constructor_typicalPersonsFile_success() throws Exception { JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, JsonSerializableAddressBook.class).get(); AddressBook addressBookFromFile = dataFromFile.toModelType(); - AddressBook typicalPersonsAddressBook = TypicalPersons.getTypicalAddressBook(); + AddressBook typicalPersonsAddressBook = getTypicalAddressBook(); assertEquals(addressBookFromFile, typicalPersonsAddressBook); } @Test - public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, dataFromFile::toModelType); - } - - @Test - public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, - dataFromFile::toModelType); + public void constructor_addressBookParameter_success() throws Exception { + ReadOnlyAddressBook typicalAddressBook = getTypicalAddressBook(); + JsonSerializableAddressBook jsonAddressBook = + new JsonSerializableAddressBook(typicalAddressBook); + assertEquals(jsonAddressBook.toModelType(), typicalAddressBook); } - } diff --git a/src/test/java/seedu/address/storage/JsonSerializableLeavesBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableLeavesBookTest.java new file mode 100644 index 00000000000..c4de3e29471 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonSerializableLeavesBookTest.java @@ -0,0 +1,58 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +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.AddressBook; +import seedu.address.model.ReadOnlyLeavesBook; + +public class JsonSerializableLeavesBookTest { + + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableLeavesBookTest"); + private static final Path VALID_DATA_FILE = TEST_DATA_FOLDER.resolve("ValidLeaves.json"); + private static final Path INVALID_PERSON_DATA_FILE = TEST_DATA_FOLDER.resolve("invalidPersonLeaves.json"); + private static final Path DUPLICATE_DATA_FILE = TEST_DATA_FOLDER.resolve("duplicateLeaves.json"); + private static final Path INVALID_FIELD_DATA_FILE = TEST_DATA_FOLDER.resolve("invalidFieldLeaves.json"); + + @Test + public void toModelType_typicalPersonsFile_success() throws Exception { + AddressBook addressBook = getTypicalAddressBook(); + JsonSerializableLeavesBook dataFromFile = JsonUtil.readJsonFile( + VALID_DATA_FILE, JsonSerializableLeavesBook.class).get(); + ReadOnlyLeavesBook addressBookFromFile = dataFromFile.toModelType(addressBook); + assertEquals(addressBookFromFile, getTypicalLeavesBook()); + } + + @Test + public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { + JsonSerializableLeavesBook dataFromFile = JsonUtil.readJsonFile( + INVALID_PERSON_DATA_FILE, JsonSerializableLeavesBook.class).get(); + assertTrue(dataFromFile.toModelType(getTypicalAddressBook()).getLeaveList().isEmpty()); + } + + @Test + public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { + JsonSerializableLeavesBook dataFromFile = JsonUtil.readJsonFile( + DUPLICATE_DATA_FILE, JsonSerializableLeavesBook.class).get(); + assertThrows(IllegalValueException.class, SerializableLeavesBook.MESSAGE_DUPLICATE_LEAVE, () -> + dataFromFile.toModelType(getTypicalAddressBook())); + } + + @Test + public void toModelType_invalidFieldFile_throwsIllegalValueException() throws Exception { + JsonSerializableLeavesBook dataFromFile = JsonUtil.readJsonFile( + INVALID_FIELD_DATA_FILE, JsonSerializableLeavesBook.class).get(); + assertThrows(IllegalValueException.class, () -> dataFromFile.toModelType(getTypicalAddressBook())); + } + +} diff --git a/src/test/java/seedu/address/storage/SerializableAddressBookTest.java b/src/test/java/seedu/address/storage/SerializableAddressBookTest.java new file mode 100644 index 00000000000..4813091a435 --- /dev/null +++ b/src/test/java/seedu/address/storage/SerializableAddressBookTest.java @@ -0,0 +1,49 @@ +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.AddressBook; +import seedu.address.testutil.TypicalPersons; + +public class SerializableAddressBookTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableAddressBookTest"); + private static final Path TYPICAL_PERSONS_FILE = TEST_DATA_FOLDER.resolve("typicalPersonsAddressBook.json"); + private static final Path INVALID_PERSON_FILE = TEST_DATA_FOLDER.resolve("invalidPersonAddressBook.json"); + private static final Path DUPLICATE_PERSON_FILE = TEST_DATA_FOLDER.resolve("duplicatePersonAddressBook.json"); + + // JsonSerializableAddressBooks are used as they are a child class of SerializableAddressBook, + // and helps us avoid having to create a child class of AdaptedPerson to be the type parameter + // of AdaptedPerson + + @Test + public void toModelType_typicalPersonsFile_success() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, + JsonSerializableAddressBook.class).get(); + AddressBook addressBookFromFile = dataFromFile.toModelType(); + AddressBook typicalPersonsAddressBook = TypicalPersons.getTypicalAddressBook(); + assertEquals(addressBookFromFile, typicalPersonsAddressBook); + } + + @Test + public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_PERSON_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, dataFromFile::toModelType); + } + + @Test + public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, + dataFromFile::toModelType); + } +} diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java index 99a16548970..7af67a95352 100644 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ b/src/test/java/seedu/address/storage/StorageManagerTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static seedu.address.testutil.TypicalLeaves.getTypicalLeavesBook; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.nio.file.Path; @@ -12,7 +13,9 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.model.AddressBook; +import seedu.address.model.LeavesBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLeavesBook; import seedu.address.model.UserPrefs; public class StorageManagerTest { @@ -26,7 +29,8 @@ public class StorageManagerTest { public void setUp() { JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs")); - storageManager = new StorageManager(addressBookStorage, userPrefsStorage); + JsonLeavesBookStorage leavesBookStorage = new JsonLeavesBookStorage(getTempFilePath("lb")); + storageManager = new StorageManager(addressBookStorage, userPrefsStorage, leavesBookStorage); } private Path getTempFilePath(String fileName) { @@ -60,9 +64,28 @@ public void addressBookReadSave() throws Exception { assertEquals(original, new AddressBook(retrieved)); } + @Test + public void leavesBookReadSave() throws Exception { + /* + * Note: This is an integration test that verifies the StorageManager is properly wired to the + * {@link JsonLeavesBookStorage} class. + * More extensive testing of UserPref saving/reading is done in {@link JsonLeavesBookStorageTest} class. + */ + LeavesBook original = getTypicalLeavesBook(); + AddressBook addressBook = getTypicalAddressBook(); + storageManager.saveLeavesBook(original); + ReadOnlyLeavesBook retrieved = storageManager.readLeavesBook(addressBook).get(); + assertEquals(original, new LeavesBook(retrieved)); + } + @Test public void getAddressBookFilePath() { assertNotNull(storageManager.getAddressBookFilePath()); } + @Test + public void getLeavesBookFilePath() { + assertNotNull(storageManager.getLeavesBookFilePath()); + } + } diff --git a/src/test/java/seedu/address/testutil/EditLeaveDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditLeaveDescriptorBuilder.java new file mode 100644 index 00000000000..69d9e55a9ab --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditLeaveDescriptorBuilder.java @@ -0,0 +1,89 @@ +package seedu.address.testutil; + +import seedu.address.logic.commands.EditLeaveCommand.EditLeaveDescriptor; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.model.leave.Title; + +/** + * A utility class to help with building EditLeaveDescriptor objects. + */ +public class EditLeaveDescriptorBuilder { + + private EditLeaveDescriptor descriptor; + + public EditLeaveDescriptorBuilder() { + descriptor = new EditLeaveDescriptor(); + } + + public EditLeaveDescriptorBuilder(EditLeaveDescriptor descriptor) { + this.descriptor = new EditLeaveDescriptor(descriptor); + } + + /** + * Returns an {@code EditLeaveDescriptor} with fields containing {@code person}'s details + */ + public EditLeaveDescriptorBuilder(Leave leave) { + descriptor = new EditLeaveDescriptor(); + descriptor.setTitle(leave.getTitle()); + descriptor.setStart(leave.getStart()); + descriptor.setEnd(leave.getEnd()); + descriptor.setStatus(leave.getStatus()); + descriptor.setDescription(leave.getDescription()); + } + + /** + * Sets the {@code Title} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withTitle(String title) { + descriptor.setTitle(new Title(title)); + return this; + } + + /** + * Sets the {@code Start} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withStart(Date start) { + descriptor.setStart(start); + return this; + } + /** + * Sets the {@code End} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withEnd(Date end) { + descriptor.setEnd(end); + return this; + } + + + /** + * Sets the {@code Status} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withStatus(Status status) { + descriptor.setStatus(status); + return this; + } + + /** + * Sets the {@code Status} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withStatus(StatusType status) { + descriptor.setStatus(Status.of(status)); + return this; + } + + /** + * Sets the {@code Start} of the {@code EditLeaveDescriptor} that we are building + */ + public EditLeaveDescriptorBuilder withDescription(String description) { + descriptor.setDescription(new Description(description)); + return this; + } + + public EditLeaveDescriptor build() { + return descriptor; + } +} diff --git a/src/test/java/seedu/address/testutil/FileAndPathUtil.java b/src/test/java/seedu/address/testutil/FileAndPathUtil.java new file mode 100644 index 00000000000..ae93617572f --- /dev/null +++ b/src/test/java/seedu/address/testutil/FileAndPathUtil.java @@ -0,0 +1,61 @@ +package seedu.address.testutil; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import javafx.stage.FileChooser; +import seedu.address.commons.controllers.FileDialogHandler; + +/** + * Contains methods for handling data files in tests. + */ +public class FileAndPathUtil { + /** + * Returns the path of the file given the path of its enclosing folder + * @param testDataFolder Folder where file is located + * @param testFileInTestDataFolder Name of file + * @return Path to file + */ + public static Path addToTestDataPathIfNotNull(Path testDataFolder, String testFileInTestDataFolder) { + return testFileInTestDataFolder != null + ? testDataFolder.resolve(testFileInTestDataFolder) + : null; + } + + /** + * Deletes file specified by the file path, if the file exists. Used to clean up test files to ensure that + * a new file is created every time the test is run. + * @param filePath Path of file to delete + */ + public static void cleanupCreatedFiles(Path filePath) { + if (Files.exists(filePath)) { + File fileToDelete = new File(filePath.toString()); + if (!fileToDelete.delete()) { + fail(String.format("Could not clean up test file: %s", filePath)); + } + } + } + + /** + * Provides a mock implementation of a FileDialogHandler so that user input is not required + * during testing + */ + public static class MockSuccessfulFileDialogHandler implements FileDialogHandler { + + private final String pathname; + + public MockSuccessfulFileDialogHandler(String filename) { + this.pathname = filename; + } + + @Override + public Optional<File> openFile(String title, FileChooser.ExtensionFilter...extensions) { + File outputFile = new File(pathname); + return Optional.of(outputFile); + } + } +} diff --git a/src/test/java/seedu/address/testutil/LeaveBuilder.java b/src/test/java/seedu/address/testutil/LeaveBuilder.java new file mode 100644 index 00000000000..2349ff99d4c --- /dev/null +++ b/src/test/java/seedu/address/testutil/LeaveBuilder.java @@ -0,0 +1,115 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalPersons.ALICE; + +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Status; +import seedu.address.model.leave.Status.StatusType; +import seedu.address.model.leave.Title; +import seedu.address.model.leave.exceptions.EndBeforeStartException; +import seedu.address.model.person.ComparablePerson; +import seedu.address.model.person.Person; + +/** + * A utility class to help with building Leave objects. + */ +public class LeaveBuilder { + public static final Title DEFAULT_TITLE = new Title("Alice's Maternity Leave"); + public static final Description DEFAULT_DESCRIPTION = new Description("Alice's Maternity Leave Description"); + public static final Person DEFAULT_PERSON = ALICE; + public static final Status DEFAULT_STATUS = Status.getDefault(); + public static final Date DEFAULT_START = Date.of("2020-02-01"); + public static final Date DEFAULT_END = Date.of("2020-02-02"); + + private Title title; + private Description description; + private ComparablePerson employee; + private Date start; + private Date end; + private Status status; + /** + * Creates a {@code LeaveBuilder} with the default details. + */ + public LeaveBuilder() { + title = DEFAULT_TITLE; + description = DEFAULT_DESCRIPTION; + employee = DEFAULT_PERSON; + status = DEFAULT_STATUS; + start = DEFAULT_START; + end = DEFAULT_END; + } + + /** + * Initializes the LeaveBuilder with the data of {@code leaveToCopy}. + */ + public LeaveBuilder(Leave leaveToCopy) { + employee = leaveToCopy.getEmployee(); + title = leaveToCopy.getTitle(); + description = leaveToCopy.getDescription(); + start = leaveToCopy.getStart(); + end = leaveToCopy.getEnd(); + status = leaveToCopy.getStatus(); + } + + /** + * Sets the {@code start} of the {@code Leave} that we are building. + */ + public LeaveBuilder withStart(Date start) { + this.start = start; + return this; + } + + /** + * Sets the {@code end} of the {@code Leave} that we are building. + */ + public LeaveBuilder withEnd(Date end) { + this.end = end; + return this; + } + + /** + * Sets the {@code employee} of the {@code Leave} that we are building. + */ + public LeaveBuilder withEmployee(ComparablePerson employee) { + this.employee = employee; + return this; + } + + /** + * Sets the {@code title} of the {@code Leave} that we are building. + */ + public LeaveBuilder withTitle(String title) { + this.title = new Title(title); + return this; + } + + /** + * Sets the {@code description} of the {@code Leave} that we are building. + */ + public LeaveBuilder withDescription(String description) { + this.description = new Description(description); + return this; + } + + /** + * Sets the {@code status} of the {@code Leave} that we are building. + */ + public LeaveBuilder withStatus(StatusType status) { + this.status = Status.of(status); + return this; + } + + /** + * Builds Leave object based on attributes set previously + * @return Leave object with given attributes + * @throws EndBeforeStartException if end date is before start date + * @throws NullPointerException if start or end date is null + */ + public Leave build() throws EndBeforeStartException, NullPointerException { + Range dateRange = Range.createNonNullRange(start, end); + return new Leave(employee, title, dateRange, description, status); + } +} diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..18de3d67fdc 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -1,10 +1,10 @@ package seedu.address.testutil; -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.CliSyntax.PREFIX_PERSON_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON_TAG; import java.util.Set; @@ -30,12 +30,12 @@ public static String getAddCommand(Person person) { */ public static String getPersonDetails(Person person) { StringBuilder sb = new StringBuilder(); - sb.append(PREFIX_NAME + person.getName().fullName + " "); - sb.append(PREFIX_PHONE + person.getPhone().value + " "); - sb.append(PREFIX_EMAIL + person.getEmail().value + " "); - sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); + sb.append(PREFIX_PERSON_NAME + person.getName().toString() + " "); + sb.append(PREFIX_PERSON_PHONE + person.getPhone().value + " "); + sb.append(PREFIX_PERSON_EMAIL + person.getEmail().value + " "); + sb.append(PREFIX_PERSON_ADDRESS + person.getAddress().value + " "); person.getTags().stream().forEach( - s -> sb.append(PREFIX_TAG + s.tagName + " ") + s -> sb.append(PREFIX_PERSON_TAG + s.tagName + " ") ); return sb.toString(); } @@ -45,16 +45,17 @@ public static String getPersonDetails(Person person) { */ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descriptor) { StringBuilder sb = new StringBuilder(); - descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); - descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); - descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); - descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getName().ifPresent(name -> sb.append(PREFIX_PERSON_NAME).append(name.toString()).append(" ")); + descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PERSON_PHONE).append(phone.value).append(" ")); + descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_PERSON_EMAIL).append(email.value).append(" ")); + descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_PERSON_ADDRESS) + .append(address.value).append(" ")); if (descriptor.getTags().isPresent()) { Set<Tag> tags = descriptor.getTags().get(); if (tags.isEmpty()) { - sb.append(PREFIX_TAG); + sb.append(PREFIX_PERSON_TAG); } else { - tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); + tags.forEach(s -> sb.append(PREFIX_PERSON_TAG).append(s.tagName).append(" ")); } } return sb.toString(); diff --git a/src/test/java/seedu/address/testutil/TestUtil.java b/src/test/java/seedu/address/testutil/TestUtil.java index 896d103eb0b..97348b76110 100644 --- a/src/test/java/seedu/address/testutil/TestUtil.java +++ b/src/test/java/seedu/address/testutil/TestUtil.java @@ -7,6 +7,7 @@ import seedu.address.commons.core.index.Index; import seedu.address.model.Model; +import seedu.address.model.leave.Leave; import seedu.address.model.person.Person; /** @@ -52,4 +53,25 @@ public static Index getLastIndex(Model model) { public static Person getPerson(Model model, Index index) { return model.getFilteredPersonList().get(index.getZeroBased()); } + + /** + * Returns the last index of the leave in the {@code model}'s leave list. + */ + public static Index getLastLeaveIndex(Model model) { + return Index.fromOneBased(model.getLeavesBook().getLeaveList().size()); + } + + /** + * Returns an out of bound index of the {@code model}'s leave list. + */ + public static Index getInvalidLeaveIndex(Model model) { + return Index.fromOneBased(model.getLeavesBook().getLeaveList().size() + 1); + } + + /** + * Returns the leave in the {@code model}'s leave list at {@code index}. + */ + public static Leave getLeave(Model model, Index index) { + return model.getLeavesBook().getLeaveList().get(index.getZeroBased()); + } } diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..16be24c43ef 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -9,4 +9,8 @@ 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 INDEX_FIRST_LEAVE = Index.fromOneBased(1); + public static final Index INDEX_SECOND_LEAVE = Index.fromOneBased(2); + public static final Index INDEX_THIRD_LEAVE = Index.fromOneBased(3); } diff --git a/src/test/java/seedu/address/testutil/TypicalLeaves.java b/src/test/java/seedu/address/testutil/TypicalLeaves.java new file mode 100644 index 00000000000..f165ed90e4f --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalLeaves.java @@ -0,0 +1,50 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.BOB; + +import seedu.address.model.LeavesBook; +import seedu.address.model.leave.Date; +import seedu.address.model.leave.Description; +import seedu.address.model.leave.Leave; +import seedu.address.model.leave.Range; +import seedu.address.model.leave.Title; + +/** + * A utility class containing a list of {@code Leave} objects to be used in tests. + */ +public class TypicalLeaves { + + public static final Date DEFAULT_START = Date.of("2020-01-01"); + public static final Date DEFAULT_END = Date.of("2020-01-05"); + public static final Date DEFAULT_START_2 = Date.of("2020-01-03"); + public static final Date DEFAULT_END_2 = Date.of("2020-01-04"); + public static final Leave ALICE_LEAVE = new Leave(ALICE, new Title("Alice's Maternity Leave"), + Range.createNonNullRange(DEFAULT_START, DEFAULT_END), + new Description("Alice's Maternity Leave Description")); + public static final Leave BOB_LEAVE = new Leave(BOB, new Title("Bob's Paternity Leave"), + Range.createNonNullRange(DEFAULT_START, DEFAULT_END), + new Description("Bob's Paternity Leave Description")); + public static final Leave ALICE_LEAVE_2 = new Leave(ALICE, new Title("Alice's Maternity Leave 2"), + Range.createNonNullRange(DEFAULT_START_2, DEFAULT_END_2)); + public static final Leave BENSON_LEAVE = new Leave(BENSON, new Title("Benson's Paternity Leave"), + Range.createNonNullRange(DEFAULT_START, DEFAULT_END), + new Description("Benson's Paternity Leave Description")); + public static final Leave BENSON_LEAVE_2 = new Leave(BENSON, new Title("Benson's Paternity Leave 2"), + Range.createNonNullRange(DEFAULT_START_2, DEFAULT_END_2)); + + private TypicalLeaves() {} // prevents instantiation + + public static Leave[] getTypicalLeaves() { + return new Leave[] {ALICE_LEAVE, BENSON_LEAVE, ALICE_LEAVE_2}; + } + + public static LeavesBook getTypicalLeavesBook() { + LeavesBook lb = new LeavesBook(); + for (Leave leave : getTypicalLeaves()) { + lb.addLeave(leave); + } + return lb; + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..a2c958ec832 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -47,7 +47,12 @@ public class TypicalPersons { .withEmail("stefan@example.com").withAddress("little india").build(); public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") .withEmail("hans@example.com").withAddress("chicago ave").build(); - + public static final Person GEORGIA = new PersonBuilder().withName("Georgia Best").withPhone("9484442") + .withEmail("georgia@example.com").withAddress("4th street").withTags("full time").build(); + public static final Person MICHAEL = new PersonBuilder().withName("Michael Miller").withPhone("9482242") + .withEmail("michael@example.com").withAddress("2th street").withTags("full time", "remote").build(); + public static final Person DAVID = new PersonBuilder().withName("David Chan").withPhone("9484342") + .withEmail("david@example.com").withAddress("7th street").withTags("part time", "remote").build(); // Manually added - Person's details found in {@code CommandTestUtil} public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); @@ -71,6 +76,7 @@ public static AddressBook getTypicalAddressBook() { } public static List<Person> getTypicalPersons() { - return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); + return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, + ELLE, FIONA, GEORGE, GEORGIA, MICHAEL, DAVID)); } }