diff --git a/README.md b/README.md index 13f5c77403f..55cc05af893 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,25 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2122S2-CS2103T-T12-2/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103T-T12-2/tp/actions/workflows/gradle.yml) ![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. +# d'Intérieur - address book for interior designers +An address book app built with Java and JavaFX (see acknowledgements), enhanced for interior designers. + +## Introduction +* This is **an address book application designed for interior designers**.
+ * quick-add functionality, add your contact details quickly on-the-fly + * filter and search through your contacts + * perfect for getting your contacts based on type of contact (ongoing clients, favourite contractors) + * add notes to keep track of the minor details + +## User Guide +- If you are interested to use our app, head over to the [_quick start_ section of our **User Guide**](https://ay2122s2-cs2103t-t12-2.github.io/tp/UserGuide.html#quick-start). + +## Developer Guide +- If you are interested to develop the app, head over to the [**Developer Guide**](https://ay2122s2-cs2103t-t12-2.github.io/tp/DeveloperGuide.html) to get started. + +## About Us +- To learn more about the _awesome_ developers for this project, head over to our [**About Us** page](https://ay2122s2-cs2103t-t12-2.github.io/tp/AboutUs.html). + +## Acknowledgements +* 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 be2d2905dde..c4e3bbb50bb 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,11 @@ test { finalizedBy jacocoTestReport } +run { + enableAssertions = true +} + + task coverage(type: JacocoReport) { sourceDirectories.from files(sourceSets.main.allSource.srcDirs) classDirectories.from files(sourceSets.main.output) diff --git a/copyright.txt b/copyright.txt index 93aa2a39ce2..bfa16ff81a2 100644 --- a/copyright.txt +++ b/copyright.txt @@ -7,3 +7,6 @@ Copyright by Susumu Yoshida - http://www.mcdodesign.com/ Copyright by Jan Jan Kovařík - http://glyphicons.com/ - calendar.png - edit.png + +Copyright by https://www.rawpixel.com/image/2512749/free-illustration-png-font-handwritten-abstract-alphabet +- d_interieur.png diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..2f4dc7d2488 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,55 @@ title: 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` ## Project team -### John Doe +### David Limantara - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/xSaints19x)] +[[portfolio](team/xsaints19x.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: UI -### Jane Doe +### Ezekiel Toh Fun Kai - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/takufunkai)] +[[portfolio](team/takufunkai.md)] -* Role: Team Lead +* Role: Developer * Responsibilities: UI -### Johnny Doe +### Glenn Lim Jun Wei - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/glennljw)] +[[portfolio](team/glennljw.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: UI -### Jean Doe +### Teh Kok Hoe - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/tehkokhoe)] +[[portfolio](team/tehkokhoe.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Deadline and Sort feature -### James Doe +### Yoong Wei Jue - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/weijuey)] +[[portfolio](team/weijuey.md)] * Role: Developer * Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..25746ed5c05 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,50 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} +Table of Contents + +* [Acknowledgements](#acknowledgements) +* [Setting up, getting started](#setting-up-getting-started) +* [Design](#design) + * [Architecture](#architecture) + * [UI component](#ui-component) + * [Logic component](#logic-component) + * [Model component](#model-component) + * [Storage component](#storage-component) + * [Common classes](#common-classes) +* [Implementations](#implementations) + * [Adding attributes to contacts](#adding-attributes-to-contacts) + * [Notes feature](#notes-feature) + * [Favourite feature](#favourite-feature) + * [Deadline feature](#deadline-feature) + * [Add contact with address as optional field feature](#add-contact-with-address-as-optional-field-feature) + * [High importance flag feature](#high-importance-flag-feature) + * [Images feature](#images-feature) + * [Adding features to Model](#adding-features-to-model) + * [Find tag feature](#find-tag-feature) + * [Assimilating new UI components](#assimilating-new-ui-components) + * [Contact view feature](#contact-view-feature) + * [Enhancing data storage](#enhancing-data-storage) + * [\[Proposed\] Undo/Redo feature](#proposed-undoredo-feature) +* [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +* [Appendix: Requirements](#appendix-requirements) + * [Product scope](#product-scope) + * [User stories](#user-stories) + * [Use cases](#use-cases) + * [Non-functional requirements](#non-functional-requirements) + * [Glossary](#glossary) +* [Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing) + * [Launch and shutdown](#launch-and-shutdown) + * [Deleting a person](#deleting-a-person) + * [Commands in detailed view](#commands-in-detailed-view) + * [Creating a tag](#creating-a-tag) -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Star used to represent a favourited person + * Reused from https://zetcode.com/gui/javafx/canvas/ with minor modifications to the points and fill, for suitable colour and size. -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +59,7 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/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. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/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 @@ -36,7 +72,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** has two classes called [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, +**`Main`** has two classes called [`Main`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. @@ -69,13 +105,15 @@ The sections below give more details of each component. ### UI 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) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) +![Information Panels](images/InformationPanels.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` 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) +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/AY2122S2-CS2103T-T12-2/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2122S2-CS2103T-T12-2/tp/blob/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -86,7 +124,7 @@ The `UI` component, ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -114,7 +152,7 @@ How the parsing works: * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/model/Model.java) @@ -126,16 +164,9 @@ The `Model` component, * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- - - -
- - ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2122S2-CS2103T-T12-2/tp/tree/master/src/main/java/seedu/address/storage/Storage.java) @@ -150,10 +181,433 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa -------------------------------------------------------------------------------------------------------------------- -## **Implementation** +## **Implementations** This section describes some noteworthy details on how certain features are implemented. +### Adding attributes to contacts + +### Notes feature + +#### Implementation + +The Notes class encapsulates a List that contains the notes added to a Person. The main feature of the class is that updating a Notes object, be it adding or deleting a note, returns a new Notes object that contains a new list with the result. + +In the execution of a command that modify a contact's notes, for instance, the NoteCommand which adds a given note to a contact, a new Person object is created with the new Notes object. Thus, this implementation of Notes avoids the problem of aliasing between different Person objects, and reduces coupling. + +The sequence diagram below illustrates this behaviour. + +![NoteCommandExecution](images/NoteSequenceExecution.png) + +
:information_source: **Note:** The lifelines for `personToEdit` and `oldNotes` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifelines reaches the end of diagram. + +
+ +This implementation is in line with other forms of editing a contact, and will facilitate new features you may plan to implement, such as the planned undo/redo feature. + +Another reason for this implementation is the ease of testing. You will find that the TypicalPersons contains Person objects as the baseline to create a list of contacts. Writing tests for Notes is easier without having to intently reverse any changes to the Notes of these Persons. + +### Favourite feature + +#### Implementation + +The favourites feature is implemented by identifying the user's favourite contacts through their `favouriteStatus` state, which is stored as a `Persons` field. The favourite-status of a person is represented by a `Favourite`, which can be either `IS_FAVOURITE`, or `NOT_FAVOURITE` singletons. + +Although it is a `Persons` field, it cannot be instantiated in an add command, nor modified through an edit command, unlike other fields such as "name" or "address". It should be thought of as metadata for the contact, and is set as `NOT_FAVOURITE` by default. + +As such, a contact's favourite-status is handled through the `FavouritesCommand`, which extends `Command`. Toggles the contact's favourite status based on their current favourite-status. Its execution behaves rather similarly to `edit`, and has its own `FavouritesCommand#createFavouritedPerson(target, newPerson)` operation. + +As part of the favourites feature, a user can also list all their favourite contacts through the `ListFavouritesCommand`, which extends `Command` as well. It leverages on the `PersonIsFavouriteContactPredicate`, which can be passed as an argument to `Model#updateFilteredPersonList(predicate)` + +Given below is an example scenario of the favourites feature in operation. + +Step 1. When a user is created, they are automatically assigned `NOT_FAVOURITE` as their favourite-status. + +![FavouriteState0](images/favourite/FavouriteState0.png) + +Step 2. The user executes `fav 3` command to favourite the 3rd person in the address book. The `fav` command executes and determines the current `favouriteStatus` of the contact (which in this case is `NOT_FAVOURITE`). It then calls the private helper method `createFavouritedPerson` to create a copy of the target contact, except with their `favouriteStatus` now pointing to the opposite `Favourite` value (which will be the `IS_FAVOURITE` singleton). + +![FavouriteState1](images/favourite/FavouriteState1.png) + +Step 3. The command execution continues on to replace the existing person object with the newly favourited contact through `Model#setPerson`. After the execution successfully terminates, the user can visually identify their favourite contacts through the yellow star right beside their contact names. + +![FavouriteState2](images/favourite/FavouriteState2.png) + +Step 4. The user decides that they want to see only their favourite contacts. The `favourites` command will then execute, passing the `PersonIsFavouriteContactPredicate` into the `Model#updateFilteredPersonList`. + +![FavouriteState3](images/favourite/FavouriteState3.png) + +The following sequence diagram shows how the favourite operation works: + +![FavouriteSequenceDiagram](images/favourite/FavouriteSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `FavouriteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +If the user runs `fav 3` again, or commands `fav` on an existing favourited contact, the only difference in the execution would be that the `NOT_FAVOURITE` reference will be passed into the `FavouriteCommand#createFavouritePerson` method instead. + +From the user experience point of view, not having an un-favourite command is intuitive. In order to favourite a contact, they will need to look-up their current index number, in which case, they will already know if they are a favourite contact or not. +This makes toggling instead of hard-setting the favourite status of a contact feasible, and reduces the number of commands the user has to remember. + +#### Design considerations: + +**Aspect: How favourite and list favourite commands execute:** + +* **Alternative 1 (current choice):** Each person responsible for their own favourite state + * Pros: + * Less coupling between favourite and other commands such as edit and delete, as well as the model. + * Easy to reference to check favourite status of contact. + * Easy to implement. + * Cons: + * Adds more fields to the person contact, increasing the complexity of the contact in the long run. + * Need to have a `Favourite` class to represent a contact's status. +* **Alternative 2:** Store favourited contacts into a `FavouritePersonList`. + * Pros: + * No need to add field into `Persons` class. + * No need to have `Favourite` class, which basically just acts as a boolean. + * `List` command will be easy to implement. + * Cons: + * Have to update the `FavouritePersonList` if a favourited contact has been modified. + * Needs more work, specially within the storage. + +### Deadline feature + +#### Implementation +The `deadline` mechanism borrows from the current `edit` mechanism. The main idea of the mechanism is creating a new `Person` object with added `deadline`, then replace existing `Person` in list with the new `Person`. + +Step 1. The user is currently on the screen displaying the list of `Person`. + +Diagram below represents the list of `Person` + + +Step 2. The user executes `deadline 1 /d return book 1/1/2023` command to add the deadline `1/1/2023` with description `return book` to the first person in the address book. + +
:information_source: **Note:** Adding multiple `deadline` is supported, for example: `deadline 1 /d return book 1/1/2023 /d write report 5/1/2023`. +
+ +Step 3. The `DeadlineCommand#execute()` method will find the `Person` at the index specified. + + +Step 4. After getting `personToAddDeadline`, `DeadlineCommand#execute()` method creates a new `Person` called `editedPerson` with added `Deadline` and all the other attributes of `personToAddDeadline`. + + +Step 5. The `editedPerson` replaces the `personToAddDeadline` in the list. + + +Step 6. The `UI` displays the updated list. + +Diagram below shows the execution of `deadline 1 /d return book 1/1/2023` command + + +
:information_source: **Note:** The lifeline for `DeadlineCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram +
+ +#### Limitations and proposed solutions +Currently, `deadline` command allows duplicate deadlines to be added, it will be changed in a later version + +**Solution**: Check `DeadlineList` before adding `deadline`. + +(more limitations and solutions to be discovered...) + +### Add contact with address as optional field feature + +#### Implementation + +The implemented enhanced add mechanism is facilitated by the `add` command. + +Classes changed for this feature: +- `AddCommand` +- `AddCommandParser` + +Given below is a code snippet implemented for `AddCommandParser`: + +![AddCommandParserEnhancement](images/AddCommandParserEnhancement.png) + +
:information_source: **Note:** A string value of `*No Address Specified*` is used for `Address.EMPTY_ADDRESS` instead of an empty string. +
+ +#### Design considerations: + +**Aspect: Values to accept in address field:** + +* **Alternative 1 (current choice):** Having a string `*No Address Specified*` in the address field + * Pros: Easy to implement. + * Cons: UI may look less pleasing with repetitive words for each contact. + +* **Alternative 2:** Accepting empty string in address field + * Pros: UI may look neater and cleaner. + * Cons: Will need to change many components for address field to be an empty string. + +### High importance flag feature + +#### Implementation + +The implemented high importance flag feature is facilitated by using the `execute()` method in `HighImportanceCommand` with the keyword `impt` to execute the command. This feature is stored as an additional attribute `highImportanceStatus` for each person within the application and is implemented as a separate class within the `Person` package. + +The `highImportanceStatus` contains an attribute `value` which takes on the values of either `"true"` or `"false"`. +- `"true"` denotes that the `Person` is a person of high importance. +- `"false"` denotes that the `Person` is a person not of high importance. + +Classes added for this feature: +- `HighImportance` +- `HighImportanceCommand` +- `HighImportanceParser` + +Given below is an example scenario of the high importance flag feature. + +Step 1. When a user is created, they are automatically assigned `NOT_HIGH_IMPORTANCE` as their importance status. + +![ImportanceState0](images/high-importance-flag/ImportanceState0.png) + +Step 2. The user executes `impt 1` command to change the high importance status of the 1st person in the address book. The `impt` command executes and determines the current `highImportanceStatus` of the contact (which in this case is `NOT_HIGH_IMPORTANCE`). It then calls the private helper method `createHighImportancePerson` to create a copy of the target contact, except with their `highImportanceStatus` now pointing to the opposite `highImportanceStatus` value (which will be `HIGH_IMPORTANCE`). + +![ImportanceState1](images/high-importance-flag/ImportanceState1.png) + +Step 3. The command execution continues on to replace the existing person object with the contact with the newly updated high importance status through `Model#setPerson`. After the successful execution, the user can visually identify their high importance contacts through the red flag beside their contact names. + +![ImportanceState2](images/high-importance-flag/ImportanceState2.png) + +Step 4. The user decides that they want to see only their high importance contacts. The `impts` command will execute, passing the `PersonHasHighImportancePredicate` into the `Model#updateFilteredPersonList`. + +The following sequence diagram shows how the feature works: + +![HighImportanceSequenceDiagram](images/high-importance-flag/HighImportanceSequenceDiagram.png) + +#### Design considerations: + +**Aspect: How important and list important commands execute:** + +* **Alternative 1 (current choice):** Each person responsible for their own high importance state + * Pros: + * Easy to implement. + * Easy to check high importance status of contact. + * Less coupling between `HighImportance` command and other commands such as edit and delete, as well as the model. + * Cons: + * Adds more fields to the person contact, increasing the complexity of the contact in the long run. + * Need to have a `HighImportance` class to represent a contact's status. +* **Alternative 2:** Store contacts into a `HighImportancePersonList`. + * Pros: + * No need to add a field into `Person` class. + * No need to have `HighImportance` class, which acts like a boolean value. + * Cons: + * Have to update the `HighImportancePersonList` if a contact of high importance has been modified. + * Quite a fair bit of changes requires, especially within the storage. + +### Images feature + +#### Implementation + +All images added by the user must be associated to some contact. This is done by storing a `ImageDetailsList` within +the contact's `Person` object. The `ImageDetailsList` encapsulates the information and methods of an iterable list of +`ImageDetails`. `ImageDetails` encapsulates the information of images added to the contact, such as the file name +and the filepath of the image. + +It is worth noting that `ImageDetailsList` and `ImageDetails` do not actually hold the image files, but only the +file and path names. This means that an `ImageDetails` object could exist for a non-existent image file. This is resolved +by using the `ImageUtil` helper class' methods. + +Apart from the `ImageDetailsList` and `ImageDetails`, the `ImageUtil` helper class can be seen as the bridge between +the `ImageDetails` object and the actual directory/image file. It contains methods to check the existence of files at +a specified filepath, copying an image into a directory, and sanitizing `ImageDetailList`s (removing `ImageDetails` +objects from the list which do not have an existing image in the specified filepath). + +Users first interact with images by adding them to a specific contact through the `AddImagesCommand`. It leverages +on the `JFileChooser` class provided by `JavaFX`. This is the interface that users will encounter when trying to +select image(s) to add into the application. Note that only `.jpg` and `.png` files are allowed by default, and there +is no existing way for the user to customize it. + +The `ImagesCommand` will identify the contact whose images are of interest to the user, based on the given index. +The `Logic` will then pass the `ImageDetailsList` of this `Person` into the `ImagesToView` field of the `Model`. +It will return a `SpecialCommandResult` which is `VIEW_IMAGES`, and the `UI` will then push the user to the `ViewImagesPanel`, +where the images stored in the `ImagesToView` field will be shown to the user. + +The user can delete images by running the `DeleteImageCommand`, with parameters: 1. The index of the contact whose images +are to be deleted, 2. The index of the image belonging to the contact. The logic will then identify the person and the +image of choice. It will remove the `ImageDetails` object from the `Person`'s `ImageDetailsList`, _and_ delete the +image associated with it. + +Given below is an example scenario of the add images feature in operation + +Step 1. When a user types `addimg INDEX`, `Logic` looks for the specified person at `INDEX` based on `SortedPersonList`. + +Step 2. `Logic` will then open the `JFileChooser` interface for the user to select any amount of images they wish to +add into the selected contact. + +Step 3. After selecting the image(s), they will be copied through the help of the `ImageUtil` class, which leverages on the +`ImageIO` class provided by the `javax` `imageio` library, into the specified `contact_images_path` specified in the +`UserPrefs`. By default, this is `/data/images/` directory. + +Step 4. Each of these newly copied images will have their filepaths stored into their own newly created `ImageDetails` objects. +The existing images of the contact, if any, will be added together with the new `ImageDetails` into a new `ImageDetailsList` + +Step 5. The `ImageDetailsList` will be added to the `Person` object. + +#### Design considerations + +Aspect: How the images should be saved +* Alternative 1 (current choice): Image details associated with a contact is stored directly as a `Person`'s field. + * Pros: Easier to search for a contact's images, as we only have to search for the given contact. + * Cons: Not necessarily part of a contact's personal information. Eventually starts to bloat up the `Person` class. +* Alternative 2: Image details are stored in a list in the `Model`. + * Pros: Easier to maintain/sanitize the list of images for outdated/corrupted images. + * Cons: Hard to associate the images with the persons, since there is no unique identifying number/value associated + with `Person`. + + +### Adding features to Model + +### Find tag feature + +#### Implementation + +The find tag feature is used when a user is interested in finding contacts who have a certain `Tag`. Each `Person` has a set of `Tags` which contains unique `Tags` since each `Person` should not have more than 1 of the same tag. + +This feature is facilitated by `FindTagCommand`, which makes use of a `List` of keywords that the `TagContainsKeywordPredicate` uses to checks if the `Tag` set of a `Person` contains all the tag names in the current `ActivatedTagList` which contains all selected keywords used as filter (case-insensitive). + +Each use of the find tag feature adds the given keyword(s) into the `ActivatedTagList` that serves as a filter for selected tags. The keywords in the `ActivatedTagList` are then used to create the `TagContainsKeywordPredicate` to check the `Tag` set of each `Person`. The keywords in the `ActivatedTagList` is only cleared when `list` command is called. + +The `FindTagCommand#execute()` method looks through the `Tag` set of each `Person` and updates the `Model#filteredPersons` using `Model#updateFilteredPersonsList()` which uses the `TagContainsKeywordPredicate`. The `Model` then displays the currently most updated filtered person list which reflects contacts with the specified tags in the list. + +As such, `FindTagCommand` extends `Command` and executes the command by calling `FindTagCommand#execute()`. + +
:information_source: **Note:** If there are multiple keywords, e.g.`findtag friends colleagues`, then the contact should have both tags `friends` and `colleagues` in order for the contact to be reflected on the updated filtered person list. + +
+ +Given below is an example usage scenario and how the find tag feature behaves at each step. + +Step 1. The user launches the address book for the first time, initialized with the initial address book state. + +Step 2. The user executes the command `findtag friends` to find contacts with the tag `friends` in their set of tags. + +Step 3. The AddressBookParser parses the command `findtag friends` and creates a `FindTagCommandParser` to parse the keyword `friends`. + +Step 4. The `FindTagCommand` object is created with the given keyword `friends` as a `Singleton List`. + +Step 5. The current tag names in the `ActivatedTagList` are then retrieved, creating a new `TagContainsKeywordPredicate`. + +Step 6. The `LogicManager` then calls `FindTagCommand#execute()`, calling `Model#updateFilteredPersonList()` which updates the list of persons to be displayed. + +Step 7. The `CommandResult` created from `FindTagCommand#execute()` is returned to the `LogicManager` which will be reflected in the `ResultDisplay` of the `GUI`. + +![FindTagSequenceDiagram](images/FindTagSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `FindTagCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +
+ +#### Design considerations: + +**Aspect: Should contacts have all the keywords searched for:** + +* **Alternative 1 (current choice):** Contacts containing strictly the same tags as all the keywords searched for. + * Pros: More intuitive, as only contacts with strictly the same tags as all the keywords searched will be listed. + * Cons: Have to ensure the contacts are tagged correctly, or they will not be found with this feature. + +* **Alternative 2:** Contacts containing any tags of the keywords searched for. + * Pros: Can find contacts who have any tags the keywords searched. + * Cons: Unintuitive, as we usually narrow down the scope for filtering. + +### Assimilating new UI components + +### Contact view feature + +The detailed contact view feature is a new component added to the UI. The detailed view would replace the list of +contacts when it is requested. While in this view, commands to edit the contact no longer need to specify an index, +since it is implied that the contact to edit is the one being displayed. Thus, implementing the feature required +resolving the following two challenges: + +1. How to tell the UI whether to show the list panel or the detailed view panel. +2. How to support alternative command formats for modifying the contact in detailed view + +#### Implementation + +The first challenge is particularly complex, as switching between panel views is done through commands, which are +executed by `Logic`, and modify `Model`. In the current design, commands are not aware of UI. + +However, the `MainWindow` class does have some responsibilities to fulfill in the process of command execution. It +handles the special cases where the command is either `ExitCommand` or `HelpCommand`, and closes the main window or +displays the help window respectively. Building upon this behaviour, the `CommandResult`, produced by every `Command` +after its execution, can be responsible for signalling to the `MainWindow` to change view. The main draw of this +implementation is that `MainWindow` continues to solely handle UI-related responsibilities, while `Command` will handle +the logic of what should be displayed, hence preserving the Single-Responsibility Principle. + +For the second challenge, since the `MainWindow` knows which panel it is displaying, the `LogicManager` can be +informed to parse the command differently. + +To illustrate the behaviour of the implementation, let's examine the process where the user passes +`note r/red`, while the MainWindow is displaying the detailed contact view. + +
:information_source: **Note:** In the follwing sequence diagrams, the +lifelines for deleted objects should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline +reaches the end of diagram. + +
+ +Step 1. The user gives the command `note r/red`. `MainWindow` realises it is in detailed view, and calls +`LogicManager::executeInDetailedViewMode`. + +![DetailedViewExecutionState1](images/detailedview/DetailedViewExecutionState1.png) + +Step 2. `LogicManager` calls `AddressBookParser::parseInDetailedViewContext` to parse the user input in a different +way. The method returns a `NoteCommand`, which is a `DetailedViewExecutable`. Note that `NoteCommandParser` +successfully parsed a `NoteCommand` without an index. + +![DetailedViewExecutionState2](images/detailedview/DetailedViewExecutionState2.png) + +Step 3. `LogicManager` calls `NoteCommand::executeInDetailedView`. `NoteCommand` executes a slightly different logic, +as it can get `personToEdit` from `model::getDetailedContactViewPerson`. It replaces `personToEdit` in both the person +list as well as in the detailed contact view. Finally, the `CommandResult` produced has the field +`SpecialCommandResult.DETAILED_VIEW`, and is returned to `LogicManager`. + +![DetailedViewExecutionState3](images/detailedview/DetailedViewExecutionState3.png) + +Step 4. `LogicManager` returns the same `CommandResult` to `MainWindow`. `MainWindow` checks and finds that it has +to call its `handleDetailedView` method. It continues to show the detailed view panel. + +![DetailedViewExecutionState4](images/detailedview/DetailedViewExecutionState4.png) + +To summarise, here is a generalised sequence diagram for a command execution in detailed view mode, with some details +omitted. + +![DetailedViewGeneralExecution](images/detailedview/DetailedViewGeneralExecution.png) + +#### Design Considerations + +To support this implementation, on top of the Command design pattern that was in place, the `DetailedViewExecutable` +interface was added to enforce which commands can run in detailed view mode. This implementation does not preserve the +Command design pattern. Notice that commands have to implement two separate kinds of execute method, when the ideal +case is that commands are simply executed by `LogicManager`, without having to intently call two different kinds of +`execute`. + +This can be resolved by having an intermediate abstract class `DetailedViewExecutableCommand`, that extends `Command` +and is inherited by concrete `Command` classes. `LogicManager` will parse for a `DetailedViewExecutableCommand`, and +call its `execute` method. + +However, there are also cons for such a method. The following table gives some comparisons. + +| Aspect | Interface forcing additional `execute` methods (current choice) | Abstract Class allowing singular `execute` method | +|---------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| Effort to change | More lines of code required implementing multiple methods with some repetition | Less lines of code as some code is common between different execution and can be placed in one method | +| Extensibility | Easy to extend to include more types of command using a new interface and implementing a new method | Difficult to extend as adding existing commands to a new type requires rewriting existing code and regression testing | +| Simplicity | Easy to reason as command execution is isolated to each method and depends on which is called | Difficult to reason as command execution depends on potentially many external factors | + +However, one area that both implementations can improve is interacting with Model. Due to the pre-existing requirements, +commands only needed `Index` as a way to retrieve a `Person` object from `Model`. For commands running in detailed view +mode, this `Index` was not required, but retrieving a `Person` explicitly called `getDetailedContactViewPerson` of the +`ModelManager`. + +A more ideal solution is for commands to have an `Identifier` field, which can retrieve a `Person` from a `Model`. The +`Identifier` can be an `Index`, which will be a superclass, or a different superclass that will retrieve the `Person` +in detailed view. Do note that `Index` is a utility class for indexing and is used in places other than identifying +`Person` in the contact list. You should implement a different class to behave like an index for this usage so as to +preserve Single Responsibility Principle. + +This feature has potential to be even more useful. If commands are enhanced to support operating on multiple `Person` +at once, a new `Identifier` can be implemented to support this, thus changes to commands will be minimal. + +### Enhancing data storage + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -224,20 +678,13 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** * **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. * **Alternative 2:** Individual command knows how to undo/redo by itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. - -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - + * 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. -------------------------------------------------------------------------------------------------------------------- @@ -257,44 +704,70 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types +d'Intérieur is designed for interior designers who: + +* Have a preference for CLI apps +* have many clients and projects to keep track of * can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +* prefer typing to mouse interactions -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: organise information on client and projects, giving a one-stop overview of current work progress and schedule. ### 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 | look at a basic tutorial for adding contacts | understand what details I can add to a contact | +| `* * *` | busy interior designer | add a new contact quickly | reduce time on adding new contacts | +| `* * *` | user | delete a contact | remove entries that I no longer need | +| `* * *` | user | find a contact by name | locate details of contacts without having to go through the entire list | +| `* * *` | interior designer | label my contacts with some fixed labels | categorise my clients based on the stage of design I am at | +| `* * *` | interior designer with many ongoing leads | filter my contacts list based on label | easily keep track of who I am working with at a specific point of time without having to remember | +| `* * *` | interior designer | add notes under a contact | keep track of the demands and requirements of each individual project | +| `* * *` | busy interior designer | add a deadline under a contact | keep track of my deadlines easily | +| `* * *` | user | favourite certain contacts | look up favourites and contact them easily | +| `* * *` | interior designer | add images under a client | keep track of images such as floor plans and inspirational designs relevant to the client | +| `* *` | interior designer | check upcoming deadlines in chronological order | keep track of deadlines automatically | +| `* *` | long-time user | separate past and current clients | avoid contacting clients with similar names or old clients that I am not presently working with | +| `* *` | new user | view a list of commands | know what commands are available and the right commands to use | +| `* *` | interior designer | add a high importance flag to a client | take note of pressing issues regarding a client, such as mobility issues | +| `*` | interior designer | track and calculate costs accumulated for a client | at a glance, know how much money has been spent on them for a project | +| `*` | interior designer | generate invoices | easily generate, store and print invoices for my clients | +| `*` | interior designer | send out automated messages/emails to clients to wish them well on festive occasions | maintain good rapport with clients | +| `*` | user | create contacts by importing information from other apps | create contacts more 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 `d'Intérieur` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +**UC01: Add a contact** **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 adds a person with the required fields. +2. d'Intérieur shows the new contact added with the details entered. - Use case ends. + Use case ends. + +**Extensions** + +* 1a. User includes address field. + + * Use case resumes at step 2. + +**UC02: Delete a contact** + +**MSS** + +1. User requests to list contacts. +2. d'Intérieur shows a list of contacts. +3. User requests to delete a specific contact in the list. +4. d'Intérieur shows contact deleted and updates the list. + + Use case ends. **Extensions** @@ -304,22 +777,240 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. d'Intérieur shows an error message. Use case resumes at step 2. -*{More to be added}* +**UC03: Add a label to a contact** -### Non-Functional Requirements +**MSS** + +1. User requests to list contacts. +2. d'Intérieur shows a list of contacts. +3. User requests to add a label to a specific contact in the list. +4. d'Intérieur adds the label to the contact and shows the contact. + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. d'Intérieur shows an error message. + + Use case resumes at step 2. + +* 3b. The label requested does not currently exist in d'Intérieur. + + * 3b1. Add a label to d'Intérieur (UC08) + + Use case ends. + +**UC04: Filter contacts using labels** + +**MSS** + +1. User requests list of contacts filtered to those containing the given label. +2. d'Intérieur shows a list of contacts who have the given label. + + Use case ends. + +**Extensions** + +* 1a. The given label does not exist. + + * Add a label to d'Intérieur (UC08) + + Use case ends. + +**UC05: Adding a note to a contact** + +**MSS** + +1. User requests to add a note to a specific contact in the list. +2. d'Intérieur adds the note to the contact and shows the contact. + + Use case ends. + +**Extensions** + +* 1a. User enters only whitespaces or nothing as a note. + + * 1a1. d'Intérieur alerts the user that no changes have been made to notes + + Use case ends. + +* 1b. The given index is invalid. + + * 1b1. d'Intérieur shows an error message. + + Use case resumes at step 1. + +**UC06: Adding a contact to favourites** + +**MSS** + +1. User requests to add a specific contact in the list to favourites. +2. d'Intérieur adds the contact to favourites and shows the contact. + + Use case ends. + +**Extensions** + +* 1a. The given index is invalid. + + * 1a1. d'Intérieur shows an error message. + + Use case resumes at step 1. -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. +**UC07: Add a deadline** -*{More to be added}* +1. User requests to add a deadline under a contact. +2. d'Intérieur adds the deadline under the contact and shows the contact. + + Use case ends. + +**Extensions** + +* 1a. The deadline given does not contain a valid date or time. + + * 1a1. d'Intérieur shows an error message. + + Use case ends + +* 1b. The given index is invalid. + + * 1b1. d'Intérieur shows an error message. + + Use case ends. + +**UC08: Add a label to d'Intérieur** + +**MSS** + +1. User requests to create a label to label contacts in the contact list. +2. d'Intérieur adds the label to the contact list. + +Use case ends. + +**Extensions** + +* 1a. User enters non-alphanumeric name or nothing as the name for the label. + + * 1a1. d'Intérieur alerts the user that the given name is invalid. + + Use case ends. + +**UC09: Deleting a label in d'Intérieur** + +**MSS** + +1. User requests to delete one or more labels in the contact list. +2. d'Intérieur deletes the given labels and unassign (UC10) the labels from every contact. + +Use case ends. + +**Extensions** + +* 1a. User enters all labels that do not exist. + + * 1a1. d'Intérieur alerts the user that the given labels do not exist. + + Use case ends. + +* 1b. Users enter some labels that do not exist. + + * 1b1. d'Intérieur alerts the user that only some labels will be deleted. + + Use case resumes at step 2. + +**UC10: Unassign a label from a contact** + +**MSS** + +1. User requests to unassign a label from a specified contact. +2. d'Intérieur removes the given label from the specified contact. + +Use case ends. + +**Extensions** + +* 1a. The given label does not exist. + + * Add a label to d'Intérieur (UC08) + + Use case ends. + +**UC11: Updating contact information in detailed view** + +**MSS** + +1. User requests to view a contact's full information. +2. d'Intérieur shows the contact's full information. +3. User edits the contact's information. +4. d'Intérieur updates the contact's information and displays it.
+ Steps 3 and 4 repeats until the user has updated the contact as they have needed. +5. User requests to list contacts. +6. d'Intérieur shows the list of contacts + +Use case ends. + +**UC12: Adding an image to a contact** + +**MSS** + +1. User requests to add image(s) to a contact. +2. d'Intérieur adds the images to the given contact. +3. d'Intérieur shows the newly added images in the images view. + +Use case ends. + +**Extensions** + +* 1a. User closes the interface without selecting any image(s). + + * 1a1. d'Intérieur alerts the user that no images have been added. + + Use case ends. + +**UC13: Deleting an image from a contact** + +**MSS** + +1. User requests to delete an image from a contact. +2. d'Intérieur removes the image from the contact. + +Use case ends. + +**UC14: Listing all images of a contact** + +**MSS** + +1. User requests to view all images of a contact. +2. d'Intérieur lists all their image(s) in the images view panel. + +Use case ends. + +### 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 contacts 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. +4. Should work on both 32-bit and 64-bit environments. +5. The system should boot up within five seconds. +6. The system is not required to handle a non-text input. +7. The system should be able to have up to 5000 contacts. +8. The response to any action should become visible within five seconds. ### Glossary +* **User** and **Interior Designer**: Both are used interchangeably as d'Intérieur is enhanced for interior designers +* **Contact**: An entry in the d'Intérieur app +* **Client**: A specific type of contact that interior designers will most likely keep track of * **Mainstream OS**: Windows, Linux, Unix, OS-X * **Private contact detail**: A contact detail that is not meant to be shared with others @@ -349,8 +1040,6 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ - ### Deleting a person 1. Deleting a person while all persons are being shown @@ -366,12 +1055,43 @@ testers are expected to do more *exploratory* testing. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Commands in detailed view + +*Refer to the command summary table in the User Guide for all the valid commands to test.* + +1. Commands that work in both list view and detailed view + + 1. Prerequisite: Use `view` on a contact. + + 1. Test case: `note r/Likes dark wood`
+ Expected: Note `Likes dark wood` is added to the contact's list of notes. + + 1. Test case: `note 2 r/Likes dark wood`
+ Expected: Same outcome as above test case where no index is specified + + 1. Test case: `note Likes dark wood`
+ Expected: No note added. Error details shown. + + 1. Repeat above three test cases for other commands, using the correct detailed view command format, followed by the list view command format, and lastly erroneous command formats, if any. + +1. Commands that do not work in detailed view -### Saving data + 1. Prerequisite: Use `view` on a contact. + + 1. Test case: `clear`
+ Expected: Error message shown, using `list` will show that the list of contacts were not cleared. + + 1. Repeat the test for other commands that do not work in detailed view. + +### Creating a tag -1. Dealing with missing/corrupted data files +1. Creating a tag after clearing sample data - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Test case: `tag Friends`
+ Expected: Friends tag is created. + + 1. Test case: `tag Friends & Colleagues`
+ Expected: No tag is created. Error details shown in the status message. -1. _{ more test cases …​ }_ + 1. Other incorrect commands to try: `tag`, `tag _`, `tag -1`, `tag foo Bar`, `tag TAGNAME`, `...` (where TAGNAME is non-alphanumeric)
+ Expected: Similar to previous. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..e0d0ce7d864 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,12 +1,58 @@ --- -layout: page -title: User Guide +layout: page 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. - -* Table of Contents -{:toc} +d'Intérieur is a **desktop app for interior designers to manage their contacts and projects efficiently**. +If you can type fast, d'Intérieur can get your contact management tasks done faster than traditional GUI apps. + +If you utilize other online applications for interior designing and lack a centralized, robust, and efficient customer +management tool, this application will be able to meet your needs. Designed with modern, tech-savvy interior designers +in mind, the app is minimalistic, practical, and greatly rewards users with basic computer proficiency, such as +quick typing speed and general know-how. + +In this user guide, we will walk you through the basic features and commands of the application, so that you can +quickly get started and make full of use of what the application has to offer. + +## Table of Contents + +* [Quick start](#quick-start) +* [Features](#features) + * [Command format](#command-format) + * [The UI](#the-ui) + * [Listing all contacts](#listing-all-contacts--list) + * [Viewing a contact's full details](#viewing-a-contacts-full-details--view) + * [Viewing help](#viewing-help--help) + * [Exiting the program](#exiting-the-program--exit) + * [Updating Contacts](#updating-contacts) + * [Adding a contact](#adding-a-contact--add) + * [Editing a contact](#editing-a-contact--edit) + * [Deleting a contact](#deleting-a-contact--delete) + * [Creating a tag](#creating-a-tag--tag) + * [Assigning a tag to a contact](#assigning-a-tag-to-a-contact--assign) + * [Unassigning a tag from a contact](#unassigning-a-tag-from-a-contact--unassign) + * [Deleting a tag](#deleting-a-tag--deltag) + * [Adding favourites](#adding-favourites--fav) + * [Adding high importance flag](#adding-high-importance-flag--impt) + * [Adding deadlines to meet in relation to a contact](#adding-deadlines-to-meet-in-relation-to-a-contact--deadline) + * [Deleting a deadline from a contact](#deleting-a-deadline-from-a-contact--deldl) + * [Adding additional notes to a contact](#adding-additional-notes-to-a-contact--note) + * [Deleting notes from a contact](#deleting-notes-from-a-contact--delnote) + * [Adding images](#adding-images--addimg) + * [List contact's images](#list-contacts-images--images) + * [Deleting images](#deleting-images--delimg) + * [Clearing all entries](#clearing-all-entries--clear) + * [Navigating your contact list](#navigating-your-contact-list) + * [Listing favourites](#listing-favourites--favourites) + * [Listing contacts with high importance](#listing-contacts-with-high-importance--impts) + * [Prioritising relevant contacts to you](#prioritising-relevant-contacts-to-you--sort) + * [Locating contacts by name](#locating-contacts-by-name--find) + * [Locating contacts by tag](#locating-contacts-by-tag--findtag) + * [Saving the data](#saving-the-data) + * [Editing the data file](#editing-the-data-file) + * [Cycling through input history](#cycling-through-input-history) + * [Archiving data files](#archiving-data-files-coming-in-v20) +* [FAQ](#faq) +* [Command summary](#command-summary) -------------------------------------------------------------------------------------------------------------------- @@ -14,158 +60,687 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `dinterieur.jar` from [here](https://github.com/AY2122S2-CS2103T-T12-2/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for your d'Intérieur. -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +4. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app + contains some sample data.
+![Sample List View](images/Sample1.png) +![Sample Detailed View](images/Sample2.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will + open the help window.
Some example commands you can try: - * **`list`** : Lists all contacts. + * **`list`** : Lists all contacts. + + * **`add n/Mary Jane p/12345678 e/maryJ@example.com`** : Adds a contact named Mary Jane to the Address Book. - * **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * **`delete`** `3` : Deletes the 3rd contact shown in the current list. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + * **`fav`** `2` : Adds the 2nd contact shown in the current list to your list of favourite contacts - * **`clear`** : Deletes all contacts. + * **`view`** `2` : Brings the 2nd contact into detailed view. - * **`exit`** : Exits the app. + * **`exit`** : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. + +-------------------------------------------------------------------------------------------------------------------- + +## Tutorial + +1. This is the **list view**, the primary view you will work with when using the app. +![Default list view](images/Sample1.png) +2. In list view, all commands requiring an `INDEX` will reference the index provided on the left side of the contact's +name. For example, typing the command `fav 2` will favourite the 2nd contact in the list. +![Tutorial fav](images/TutorialFav.png) + +3. Add a new contact by running `add n/ p/ e/`. These fields are +mandatory when creating a new contact. However, you can add more optional information in the same add command, or in a +future edit command. See [adding a contact](#adding-a-contact--add) and [editing a contact](#editing-a-contact--edit) +for more information. +![Tutorial add](images/TutorialAdd.png) + +4. To bring the second contact into **detailed view**, input `view 2`. +![Tutorial view](images/TutorialView.png) + +5. In **detailed view**, some commands available to you in **list view** are no longer available, however, your commands +will now automatically reference the current contact you are viewing in **detailed view**. For example, you can type +`impt` to simply label the current contact you are viewing to set them as an important contact. +![Tutorial impt](images/TutorialImpt.png) + +6. To exit **detailed view**, simply input `list` to return to the **list view**. + +These are just the basic features to get you ready to start using the app. There are many more features for you to +explore, it is recommended for you to have a read through our User Guide to familiarize yourself with the functionalities +of the app. You can always input the `help` command whenever you need help or want to reference the User Guide again! -------------------------------------------------------------------------------------------------------------------- ## Features -
+### Command Format -**:information_source: Notes about the command format:**
+Before going into the commands, take note of how the command format is given in the guide: -* 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`. +* Words in `UPPER_CASE` are the values that you will enter in the command.
+ e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/Mary Jane`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g `n/NAME [t/TAG]` can be used as `n/Mary Jane t/friend` or as `n/Mary Jane`. * 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. -* Parameters can be in any order.
+* Values can be given in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
+* If a type of value is expected only once in the command but you specified it multiple times, only the last occurrence of + the type will be taken.
e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Extraneous values for commands that do not take in values (such as `help`, `list`, `exit` and `clear`) will be + ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. -
+### The UI + +The UI consists of the command line to enter your commands, the feedback box which displays textual information about +the result of the command execution, and the contact display. + +There are 2 main ways to view contacts. + +### Listing all contacts : `list` + +Shows the list view with all contacts in the address book. + +Format: `list` + +### Viewing a contact's full details : `view` + +Allows you to view the full details of the contact, as some are hidden in the contact list. This command **only works in +list view**. + +Format `view INDEX` + +#### Commands in detailed view + +Some commands may work differently in the detailed view from in the list view. + +In general, commands for modifying a contact will work, and will modify the contact currently displayed. As such, there is no need to give an index for those commands anymore, and **they will be ignored** if the command is called in this view. + +If the command does not work in the current view, the app will inform you. To return to list view, use `list`. + +You may check out the summary table of commands for the overview. ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. You may use this frequently when you first begin using d'Intérieur. -![help message](images/helpMessage.png) +When you're feeling overwhelmed by the number of commands available, refer to the [command summary](#command-summary) and the examples! Format: `help` +![helpMessage](images/helpMessage.png) -### Adding a person: `add` +
-Adds a person to the address book. +**:information_source: More information about the help window:**
-Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +After you successfully clicked on the `Copy URL` button, you will see the following window: -
:bulb: **Tip:** -A person can have any number of tags (including 0) +![copyUrlSuccessMessage](images/copyLinkSuccessMessage.png) + +You're now ready to paste the link into your browser!
-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` +### Exiting the program : `exit` + +Exits the program. -### Listing all persons : `list` +Format: `exit` -Shows a list of all persons in the address book. +### Updating contacts -Format: `list` +There is a set of information that you can save for a contact. This section contains the commands for +adding contacts and modifying their information. -### Editing a person : `edit` +### Adding a contact : `add` -Edits an existing person in the address book. +You can add a contact to the address book with the **address as an optional field**. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Your contacts are uniquely identified by their `NAME` which is **case-sensitive**.
+e.g. `Alex Yeoh` is different from `alex yeoh` -* 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. +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL [a/ADDRESS] [t/TAG]…​` + +
:bulb: **Tip:** +A contact can have 0 or 1 address +
+
:bulb: **Tip:** +A contact can have any number of tags (including 0) +
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. -### Locating persons by name: `find` +* `add n/John Doe p/98765432 e/johnd@example.com` +* `add n/Mary Jane p/12345678 e/maryJ@example.com a/Bukit Timah t/completed` -Finds persons whose names contain any of the given keywords. +### Editing a contact : `edit` -Format: `find KEYWORD [MORE_KEYWORDS]` +Edits an existing contact in the address book. This command can be used in detailed view. -* 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` +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS]…​` + +* Edits the contact at the specified `INDEX`. The index refers to the index number shown in the displayed contact 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. + +
:bulb: **Tip:** +To edit tags, use assign and unassign commands +
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` +* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st contact to be `91234567` + and `johndoe@example.com` respectively. +* `edit 1 n/John` Edits the name of the 1st contact to be `John`. + +Format in detailed view: `edit [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS]…​` + +Example: + +* `edit p/88438809 e/alex_yeoh@example.com` Edits the phone number and email address of the contact in detailed view to + be `88438809` and `alex_yeoh@example.com` respectively. -Deletes the specified person from the address book. +### Deleting a contact : `delete` + +Deletes the specified contact from the address book. This command **only works in list view**. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* Deletes the contact at the specified `INDEX`. +* The index refers to the index number shown in the displayed contact list. * The index **must be a positive integer** 1, 2, 3, …​ 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. + +* `list` followed by `delete 2` deletes the 2nd contact in the address book. +* `find Betsy` followed by `delete 1` deletes the 1st contact in the results of the `find` command. + +### Creating a tag : `tag` + +Creates a tag that can be assigned to any contact. This command can be used in detailed view with the same format. + +Format: `tag TAGNAME` + +* A tag with the same `TAGNAME` can **only be created once**. +* The `TAGNAME` is case-insensitive. e.g. creating the tag `friends` will not allow `Friends` to be created. +* `TAGNAME` must be alphanumeric. e.g. `Hello`, `Friends`, `Colleagues` +* `TAGNAME` such as `-1`, `Sub Contractors` are not allowed. i.e. non-alphanumeric characters, including spaces. +* To create a tag named `Sub Contractors`, eliminate the whitespace in between in order for it to be a valid tag. + e.g. `SubContractors` + +
:bulb: **Tip:** +You can create meaningful tags to assign your contacts with! With tags, you can search for contacts assigned to that particular tag! +Tags must be created first before you can perform any tag related features. +
+ +List of tag related features: +* [Assign Tag](#assigning-a-tag-to-a-contact--assign) +* [Unassign Tag](#unassigning-a-tag-from-a-contact--unassign) +* [Find Tag](#locating-contacts-by-tag--findtag) +* [Delete Tag](#deleting-a-tag--deltag) + +Example: + +* `tag Friends` creates a tag `Friends` to be stored in the address book. + ![result for create tag friends](images/create-tag/create-tag-friends.png) + +### Assigning a tag to a contact : `assign` + +Assigns a created tag to a contact. This command can be used in detailed view. + +Format: `assign INDEX TAGNAME` + +* Assigns a `TAG` with a given `TAGNAME` to a contact at the specified `INDEX` +* The `TAG` given by the `TAGNAME` must be created first. +* The `TAGNAME` is case-insensitive. +* The index refers to the index number shown in the displayed contact list. +* The index **must be a positive integer** 1, 2, 3, ...​ +* The contact should have **at most one** `TAG` with a given unique `TAGNAME`. +* The contact assigned to the given `TAGNAME` cannot be assigned to the same `TAGNAME` again. + e.g. assigning the tag `friends` to Alice at index 1 will not allow `Friends` to be assigned to the same contact. + +Example: + +* `assign 1 Friends` assigns a tag `Friends` to the contact at index `1`. + + * Before using the command `assign 1 Friends`: + ![before assign 1 friends](images/assign-tag/before-assign-1-friends.png) + + * `assign 1 Friends`: + ![result for assign 1 friends](images/assign-tag/assign-1-friends.png) + +* `assign 3 Colleagues` assigns a tag `Colleagues` to the contact at index `3`. + + * Before using the command `assign 3 Colleagues`: + ![before assign 3 colleagues](images/assign-tag/before-assign-3-colleagues.png) + + * `assign 3 Colleagues`: + ![result for assign 3 colleagues](images/assign-tag/assign-3-colleagues.png) + +Format in detailed view: `assign TAGNAME` + +Example: + +* `assign client` assigns a tag `client` to the currently viewed contact. + + * `view 1` + ![result for view 1](images/assign-tag/view-1.png) + + * `assign client` + ![result for assign client](images/assign-tag/view-assign-client.png) + + +### Unassigning a tag from a contact : `unassign` + +Unassigns a created tag from a contact. This command can be used in detailed view. + +Format: `unassign INDEX TAGNAME` + +* Removes a `TAG` with a given `TAGNAME` from a contact at the specified `INDEX` +* The `TAG` given by the `TAGNAME` must be created first. +* The `TAGNAME` is case-insensitive. +* The index refers to the index number shown in the displayed contact list. +* The index **must be a positive integer** 1, 2, 3, ...​ +* The contact must have been assigned to this `TAG` previously. + +Example: + +* `unassign 1 Friends` removes the tag `Friends` from the contact at index `1`. + * Before using the command `unassign 1 Friends` + ![before unassign 1 Friends](images/unassign-tag/before-unassign-1-friends.png) + + * `unassign 1 Friends` + ![result for unassign 1 Friends](images/unassign-tag/unassign-1-friends.png) + + +Format in detailed view: `unassign TAGNAME` + +Example: + +* `unassign client` removes the tag `client` from the currently viewed contact. + * `view 1` + ![result for view 1](images/unassign-tag/view-1.png) + + * `unassign client` + ![result for unassign client](images/unassign-tag/view-unassign-client.png) + +### Deleting a tag : `deltag` + +Deletes the specified tag(s) + +Format: `deltag TAGNAME [MORE_TAGNAME]` + +* Deletes the tag(s) identified by the given `TAGNAME`. +* Unassigns the deleted tags from all contacts who were previously assigned to the `tag` with given `TAGNAME`. +* If the multiple `TAGNAME` specified has more than 1 `tag` that cannot be identified , the identifiable tag(s) will be deleted. + +Examples: + +* `deltag friends` deletes the tag `friends` + + * Before using the command `deltag friends`: + ![before deltag friends](images/del-tag/before-deltag-friends.png) + + * `deltag Friends` + ![result for deltag friends](images/del-tag/deltag-friends.png) + +* `deltag friends colleagues` deletes the tag `friends` and `colleagues` + + * Before using the command `deltag friends colleagues` + ![before deltag friends colleagues](images/del-tag/before-deltag-friends-colleagues.png) + + * `deltag friends colleagues` + ![result for deltag friends colleagues](images/del-tag/deltag-friends-colleagues.png) + +* `deltag friends colleagues` when the tag `colleagues` does not exist will delete the tag `friends` and unassign the tag `friends` from every contact + * Before using the command `deltag friends colleagues` + ![before deltag friends colleagues not exist](images/del-tag/before-deltag-friends-colleagues-not-exist.png) + + * `deltag friends colleagues` + ![result for deltag friends colleagues, colleagues not exist](images/del-tag/deltag-friends-colleagues-not-exist.png) + +* `deltag colleagues` when the tag `colleagues` does not exist will not change the data. +![result for deltag colleagues](images/del-tag/deltag-colleagues-not-exist.png) + +### Adding favourites : `fav` + +Toggles the favourite status of your contacts. This command can be used in detailed view. +Favourited contacts **can be un-favourited** by running the same command on the contact again. + +Format: `fav INDEX` + +
:bulb: **Tip:** +You can run `fav INDEX` where `INDEX` is the index of a contact that currently belongs in your favourites list to remove them. +
+ +Examples: + +* `fav 1` — Adds contact at index 1 to your list of favourites + +![favourite](images/favourite_command.png) + +* `fav 1` - Run the command for the same contact, and the favourite status will be toggled off. + +![unfavourited](images/after_unfavourite_command.png) + +Format in detailed view: `fav` + +Example: + +* `fav` Adds the currently viewed contact to your list of favourites. + +### Adding high importance flag : `impt` + +Adds the contact to your list of contacts with high importance and a red flag will appear beside the contact's name to indicate that. +This command can be used in detailed view. + +Format: `impt INDEX` + +
:bulb: **Tip:** +When a red flag appears beside the contact's name, you can run `impt INDEX` again where `INDEX` is the index of a contact that currently belongs in your list of contacts with high importance to remove them. +
+
:bulb: **Tip:** +You may wish to use the `note` command to add a note to indicate why the contact is important. E.g. Mobility Issues. +
+ +Examples: + +* `impt 1` - Adds contact at index 1 to your list of contacts with high importance, indicated by the red flag + +![important](images/high-importance-flag/add_importance_flag.png) + +You will remove the red flag beside the contact's name after entering `impt 1` again. + +![not_important](images/high-importance-flag/before_command.png) + +`note 1 r/Mobility Issues` - Adds a note for your contact at index 1 indicating he/she has mobility issues + +![note_usage](images/high-importance-flag/add_note_for_reason.png) + +Format in detailed view: `impt` + +Example: + +* `impt` Adds the currently viewed contact to your list of contacts with high importance. + +### Adding deadlines to meet in relation to a contact : `deadline` + +Creates a deadline that is placed under the profile of a contact. This command can be used in detailed view. + +Format: `deadline INDEX d/DESCRIPTION DATE [d/DESCRIPTION DATE]...` + +* Deadline must have description. +* The given date is added to the contact as deadline. +* Date should be dd/mm/yyyy + +Example: + +* `deadline 1 d/windows 01/01/2022` adds a deadline with description `windows` and date `01/01/2022` to the contact in index `1`. + +List before `deadline` command: + +![before 'deadline 1 d/windows 01/01/2022'](images/BeforeDeadlineCommand.png) + +List after `deadline` command: + +![after 'deadline 1 d/windows 01/01/2022'](images/AfterDeadlineCommand.png) + +Format in detailed view: `deadline d/DESCRIPTION DATE [d/DESCRIPTION DATE]...` + +Example: + +* `deadline d/Lunch meeting 03/06/2022` adds a deadline with description `Lunch meeting` and date `03/06/2022` to the + currently viewed contact. + +Interface before `deadline` command in detailed view mode: + +![before 'deadline d/Lunch meeting 03/06/2022'](images/BeforeDetailedViewDeadlineCommand.png) + +Interface after `deadline` command in detailed view mode: + +![after 'deadline d/Lunch meeting 03/06/2022'](images/AfterDetailedViewDeadlineCommand.png) + +### Deleting a deadline from a contact : `deldl` + +Deletes the deadline under the contact in detailed view. This command cannot be used in list view. + +Format: `deldl INDEX` + +* Deletes the note at the index of the list of deadlines displayed. + +Example: + +* `view 2` shows you the detailed view of the contact at index 2, then using `deldl 2` will delete the second deadline in the + notes list of the contact + +Interface before `deldl` command: + +![before 'deldl 2'](images/BeforeDeleteDeadlineCommand.png) + +Interface after `deldl` command: + +![after 'deldl 2'](images/AfterDeleteDeadlineCommand.png) + +### Adding additional notes to a contact : `note` + +Adds the given note under the contact. This command can be used in detailed view. + +Format: `note INDEX r/NOTES` + +* Notes are displayed in a list. +* The given note is appended to the existing list of notes at the end. + +
:bulb: **Tip:** +Notes store good-to-know information about the user. To classify contacts so that you can search for them, use tags instead. +
+ +Example: + +* `note 2 r/loves green` will add a note under the contact at index 2 that reads `loves green`. + +Format in detailed view: `note r/NOTES` + +Example: + +* `note r/Likes wood furniture` will add a note to currently viewed contact that reads `Likes wood furniture`. + +### Deleting notes from a contact : `delnote` + +Deletes the note under the contact in detailed view. This command cannot be used in list view. + +Format: `delnote INDEX` + +* Deletes the note at the index of the list of notes displayed. + +Example: + +* `view 1` shows you the detailed view of the contact at index 1, then using `delnote 2` will delete the second note in the + notes list of the contact + +### Adding images : `addimg` + +Add image(s) to a contact. + +
:bulb: **Tip:** +Take note that images uploaded are duplicated and stored in the data folder of the app. +If your computer's storage space is a concern for you, please delete the original image +after you have uploaded it! +
+ +Format: `addimg INDEX` + +![file chooser for images](images/images_file_chooser.png) + +* Upon running the command, a file chooser will appear for you to select images from. +* Images can be in `.png` or `.jpg` formats. +* Images uploaded cannot have duplicate names. + +### List contact's images : `images` + +Lists the contact's image(s). + +Format: `images INDEX` + +* You can click on the images to get a focused view of the image. + +### Deleting images : `delimg` + +Deletes the image(s) associated with a contact. + +Format: `delimg INDEX i/IMAGE_INDEX` + +![identify image index from images command](images/image_index.png) + +* An image's index is relative to the person it belongs to. +* You can identify it by running the images command for a given user + (as seen in the above image). The `IMAGE_INDEX` of the image will be + directly above the image itself. + +Example: + +`delimg 1 i/2` will delete the second image of the contact ### Clearing all entries : `clear` -Clears all entries from the address book. +Clears all entries from the address book. This command can **only be used in list view**. + +
:exclamation: **Caution:** +There is no warning if you run this command, and there is **no way to recover the deleted data**. Ensure you intend to +run this command. + +It is recommended to use this command **only for clearing the sample data provided in the beginning**. +
Format: `clear` -### Exiting the program : `exit` +### Navigating your contact list -Exits the program. +As your contact list grows larger, you may start having trouble finding the contact you are looking for. +However, with these commands to aid you, finding contacts will still be easy and intuitive. -Format: `exit` +### Listing Favourites : `favourites` + +Lists all your favourite contacts to the list of displayed contacts. This command **only works in list view**. + +Format: `favourites` + +### Listing contacts with high importance : `impts` + +Shows you all contact(s) with high importance, tagged with the red flag. This command **only works in list view**. + +Format: `impts` + +Examples: + +* You can enter `impt 1` followed by `impt 3` to add a red flag beside contacts at index 1 and index 3. + + ![command_result](images/high-importance-flag/command_result.png) + +* Use `impts` to list all of your contacts that has been tagged with the red flag. + +![impts_command_result](images/high-importance-flag/impts_command_result.png) + +### Prioritising relevant contacts to you : `sort` + +Sort contacts by given criteria. This command **only works in list view**. + +Format: `sort CRITERIA` + +* `CRITERIA` should be written in lower-case. + +| `CRITERIA` | Order | +|------------|------------------------------------------------------| +| `address` | Alphabetically sorted | +| `deadline` | Early dates to later dates | +| `email` | Alphabetically sorted | +| `fav` | Favourited contacts to not favourited contacts | +| `impt` | HighImportant contacts to not HighImportant contacts | +| `name` | Alphabetically sorted | +| `phone` | Numerically sorted | + +### Locating contacts by name : `find` + +Find contacts whose names contain any of the given keywords. This command **only works in list view**. + +Format: `find KEYWORD [MORE_KEYWORDS]` + +* 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` +* Contacts matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber` + , `Bo Yang` + +Examples: + +* `find John` returns `john` and `John Doe` +* `find alex david` returns `Alex Yeoh`, `David Li`
+ ![result for 'find alex david'](images/findAlexDavidResult.png) + +### Locating contacts by tag : `findtag` + +Find contacts based on the selected tags given by keywords to search for. This command **only works in list view**. + +Format: `findtag KEYWORD [MORE_KEYWORDS]` + +* Each `findtag` command selects a `TAG` to be found by using the given `KEYWORD` + e.g. + + * `findtag friends` adds `friends` as a tag to be searched for + * `findtag colleagues` adds `colleagues` to pre-existing search, now containing both colleagues and friends +* The search is case-insensitive. e.g `tag` will match `Tag` +* **Only the tag is searched** +* **Only full words will be matched** e.g. `Ta` will not match `Tag` +* List of contacts matching at least the searched tag\(s\) will be returned. e.g. `Tag1` will return `Contact` A with + tags `Tag1` and `Tag2`. + +
:bulb: **Tip:** +Use `list` to clear currently selected tags! +
+ +Examples: + +* `findtag Friends` returns contacts with tag `Friends` + ![result findtag Friends](images/findtag/findtag-friends.png) +* `findtag Friends` followed by `findtag InProgress AlmostFinished` returns contacts tagged by at least `Friends`, `InProgress` and `AlmostFinished` + ![result findtag InProgress AlmostFinished](images/findtag/findtag-friends-inprogress-almostfinished.png) ### 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. +d'Intérieur 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 as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +d'Intérieur data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to +update data directly by editing that data file.
: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. +If your changes to the data file makes its format invalid, d'Intérieur will discard all data and start with an empty data file at the next run.
+### Cycling through input history + +You can cycle through your input history by hitting the up arrow key to go back to older inputs, +and the down arrow key to your latest inputs. You can save time on typing repeat inputs, or re-writing erroneous +inputs! + ### Archiving data files `[coming in v2.0]` _Details coming soon ..._ @@ -175,18 +750,39 @@ _Details coming soon ..._ ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains +the data of your previous d'Intérieur home folder. -------------------------------------------------------------------------------------------------------------------- ## 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` +Empty entries mean the commands cannot be used in the view. + +| Action | Format, Examples in List View | Format, Examples in Detailed View | +|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL [a/ADDRESS] [t/TAG]…​`
e.g., `add n/Mary Jane p/12345678 e/maryJ@example.com a/Bukit Timah t/completed` | Same as list view | +| **Add Image** | `addimg INDEX`
e.g., `addimg 1` | `addimg` | +| **Assign Tag** | `assign INDEX TAGNAME`
e.g., `assign 1 Friends` | `assign TAGNAME`
e.g., `assign client` | +| **Clear** | `clear` | - | +| **Create Tag** | `tag TAGNAME`
e.g., `tag Friends` | Same as list view | +| **Deadline** | `deadline INDEX d/DESCRIPTION DATE [d/DESCRIPTION DATE]...`
e.g., `deadline 1 d/windows 01/01/2022` | `deadline d/DESCRIPTION DATE [d/DESCRIPTION DATE]...`
e.g., `deadline d/Lunch meeting 03/06/2022` | +| **Delete** | `delete INDEX`
e.g., `delete 3` | - | +| **Delete Deadline** | - | `deldl INDEX`
e.g. `deldl 2` | +| **Delete Image** | `delimg INDEX i/IMAGE_INDEX`
e.g., `delimg 1 i/2` | `delimg i/IMAGE_INDEX`
e.g., `delimg i/1` | +| **Delete Note** | - | `delnote INDEX`
e.g. `delnote 2` | +| **Delete Tag** | `deltag TAGNAME`
e.g., `delete friends` | - | +| **Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] …​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` | `edit [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS]…​`
e.g., `edit p/88438809 e/alex_yeoh@example.com` | +| **Fav** | `fav INDEX`
e.g., `fav 1` | `fav` | +| **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` | - | +| **Find Tag** | `findtag KEYWORD [MORE_KEYWORDS}`
e.g., `findtag Friends` | - | +| **Help** | `help` | Same as list view | +| **Impt** | `impt INDEX`
e.g., `impt 1` | `impt` | +| **Impts** | `impts` | - | +| **Sort** | `sort CRITERIA`
e.g., `sort address` | - | +| **List** | `list` | Same as list view | +| **List Favourites** | `favourites` | - | +| **List Images** | `images INDEX`
e.g., `images 1` | `images` | +| **Note** | `note INDEX r/NOTES`
e.g. `note 2 r/loves green` | `note r/NOTES`
e.g., `note r/Likes wood furniture` | +| **Unassign Tag** | `unassign INDEX TAGNAME`
e.g., `unassign 1 Friends` | `unassign TAGNAME`
e.g., `unassign client` | +| **View** | `view INDEX`
e.g., `view 1` | - | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..8eb7c2e3b6c 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "d'Intérieur" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S2-CS2103T-T12-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..2287ac5ba6f 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "d'Intérieur"; font-size: 32px; } } diff --git a/docs/diagrams/DeadlineSequenceDiagram.puml b/docs/diagrams/DeadlineSequenceDiagram.puml new file mode 100644 index 00000000000..a1e04456857 --- /dev/null +++ b/docs/diagrams/DeadlineSequenceDiagram.puml @@ -0,0 +1,92 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeadlineCommandParser" as DeadlineCommandParser LOGIC_COLOR +participant "command:DeadlineCommand" as DeadlineCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("deadline 1 /d return book 1/1/2023") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deadline 1 /d return book 1/1/2023") +activate AddressBookParser + +create DeadlineCommandParser +AddressBookParser -> DeadlineCommandParser +activate DeadlineCommandParser + +DeadlineCommandParser --> AddressBookParser +deactivate DeadlineCommandParser + +AddressBookParser -> DeadlineCommandParser : parse("1 /d return book 1/1/2023") +activate DeadlineCommandParser + +DeadlineCommandParser -> ParserUtil : parseIndex("1") +activate ParserUtil + +ParserUtil --> DeadlineCommandParser : index +deactivate ParserUtil + +DeadlineCommandParser -> ParserUtil : parseDeadlines([" return book 1/1/2023"]) +activate ParserUtil + +ParserUtil --> DeadlineCommandParser : deadlines +deactivate ParserUtil + +create DeadlineCommand +DeadlineCommandParser -> DeadlineCommand : DeadlineCommand(index, deadlines) +activate DeadlineCommand + +DeadlineCommand --> DeadlineCommandParser : command +deactivate DeadlineCommand + +DeadlineCommandParser --> AddressBookParser : command +deactivate DeadlineCommandParser + +DeadlineCommandParser -[hidden]-> AddressBookParser +destroy DeadlineCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> DeadlineCommand : execute(model) +activate DeadlineCommand + +DeadlineCommand -> Model : getFilteredPersonList() +activate Model + +Model --> DeadlineCommand : lastShownList +deactivate Model + +DeadlineCommand -> Model : setPerson(personToAddDeadline, editedPerson) +activate Model + +deactivate Model + +DeadlineCommand -> Model : updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS) +activate Model + +deactivate Model + +create CommandResult +DeadlineCommand -> CommandResult +activate CommandResult + +CommandResult --> DeadlineCommand +deactivate CommandResult + +DeadlineCommand --> LogicManager : CommandResult(feedbackToUser) +deactivate DeadlineCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeadlineState1.puml b/docs/diagrams/DeadlineState1.puml new file mode 100644 index 00000000000..e0ed99a7aec --- /dev/null +++ b/docs/diagrams/DeadlineState1.puml @@ -0,0 +1,15 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package List { +Class "Person1" as Person1 +Class "Person2" as Person2 +Class "Person3" as Person3 +} + +Person1 - Person2 +Person2 - Person3 +@enduml diff --git a/docs/diagrams/DeadlineState2.puml b/docs/diagrams/DeadlineState2.puml new file mode 100644 index 00000000000..8102cef1ee8 --- /dev/null +++ b/docs/diagrams/DeadlineState2.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package List { +Class "personToAddDeadline" as Person1 +Class "Person2" as Person2 +Class "Person3" as Person3 +} + +Person1 - Person2 +Person2 - Person3 + +class Pointer as "DeadlineCommand" +Pointer -up-> Person1 +@enduml diff --git a/docs/diagrams/DeadlineState3.puml b/docs/diagrams/DeadlineState3.puml new file mode 100644 index 00000000000..4748423e8c7 --- /dev/null +++ b/docs/diagrams/DeadlineState3.puml @@ -0,0 +1,21 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package List { +Class "personToAddDeadline" as Person1 +Class "Person2" as Person2 +Class "Person3" as Person3 +} + +Class "editedPerson" as Person4 + +Person1 - Person2 +Person2 - Person3 + +class Pointer as "DeadlineCommand" +Pointer -up-> Person1 +Pointer -up-> Person4 +@enduml diff --git a/docs/diagrams/DeadlineState4.puml b/docs/diagrams/DeadlineState4.puml new file mode 100644 index 00000000000..e0b43c4ad76 --- /dev/null +++ b/docs/diagrams/DeadlineState4.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package List { +Class "editedPerson" as Person1 +Class "Person2" as Person2 +Class "Person3" as Person3 +} + +Person1 - Person2 +Person2 - Person3 + +class Pointer as "DeadlineCommand" +Pointer -up-> Person1 +@enduml diff --git a/docs/diagrams/FindTagSequenceDiagram.puml b/docs/diagrams/FindTagSequenceDiagram.puml new file mode 100644 index 00000000000..837dd04cf07 --- /dev/null +++ b/docs/diagrams/FindTagSequenceDiagram.puml @@ -0,0 +1,83 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindTagCommandParser" as FindTagCommandParser LOGIC_COLOR +participant "f:FindTagCommand" as FindTagCommand LOGIC_COLOR +participant "p:TagContainsKeywordPredicate" as TagContainsKeywordPredicate LOGIC_COLOR +participant "result:CommandResult" as CommandResult LOGIC_COLOR + +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("findtag friends") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("findtag friends") +activate AddressBookParser + +create FindTagCommandParser +AddressBookParser -> FindTagCommandParser +activate FindTagCommandParser + +FindTagCommandParser --> AddressBookParser +deactivate FindTagCommandParser + +AddressBookParser -> FindTagCommandParser : parse("friends") +activate FindTagCommandParser + +create FindTagCommand +FindTagCommandParser -> FindTagCommand +activate FindTagCommand + +FindTagCommand --> FindTagCommandParser : f +deactivate FindTagCommand + +FindTagCommandParser --> AddressBookParser : f +deactivate FindTagCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindTagCommandParser -[hidden]-> AddressBookParser +destroy FindTagCommandParser + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> FindTagCommand : execute() +activate FindTagCommand + +FindTagCommand -> Model : getActivatedTagList() +activate Model + +Model --> FindTagCommand : activatedTagList + +create TagContainsKeywordPredicate +FindTagCommand -> TagContainsKeywordPredicate : {"friends"} + +activate TagContainsKeywordPredicate +TagContainsKeywordPredicate --> FindTagCommand : p +deactivate TagContainsKeywordPredicate + + +FindTagCommand -> Model : updateFilteredPersonList(p) + +Model --> FindTagCommand +deactivate Model + +create CommandResult +FindTagCommand -> CommandResult +activate CommandResult + +CommandResult --> FindTagCommand +deactivate CommandResult + +FindTagCommand --> LogicManager : result +deactivate FindTagCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/InformationPanels.puml b/docs/diagrams/InformationPanels.puml new file mode 100644 index 00000000000..351d9112d97 --- /dev/null +++ b/docs/diagrams/InformationPanels.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +package InformationPanels <> { +Class PersonListPanel +Class PersonCard +Class DetailedContactPanel +Class DetailedPersonCard +Class ImageViewPanel +Class ImageCard +} + +package Model <> { +Class HiddenModel #FFFFFF +} + +Class MainWindow +Class "{abstract}\nUiPart" as UiPart + +MainWindow*-down-> PersonListPanel +MainWindow*-down-> DetailedContactPanel +MainWindow*-down-> ImageViewPanel + +PersonListPanel -down-> "*" PersonCard +DetailedContactPanel -down-> "0...1" DetailedPersonCard +ImageViewPanel -down-> "*" ImageCard + +PersonListPanel --|> UiPart +PersonCard -right-|> UiPart +DetailedContactPanel --|> UiPart +DetailedPersonCard -right-|> UiPart +ImageViewPanel --|> UiPart +ImageCard -right-|> UiPart + +PersonCard ..> Model +DetailedPersonCard ..> Model +ImageCard ..> Model + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..fcf8b1dae22 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -5,6 +5,7 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR Package Model <>{ +Class Positioning #FFFFFF Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs Class "<>\nModel" as Model @@ -12,6 +13,8 @@ Class AddressBook Class ModelManager Class UserPrefs +Class UniqueTagList +Class ActivatedTagList Class UniquePersonList Class Person Class Address @@ -25,7 +28,9 @@ Class Tag Class HiddenOutside #FFFFFF HiddenOutside ..> Model + AddressBook .up.|> ReadOnlyAddressBook +ReadOnlyAddressBook -[hidden]left-> Positioning ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs @@ -34,13 +39,19 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs +AddressBook*-left->UniqueTagList +AddressBook*-->ActivatedTagList AddressBook *--> "1" UniquePersonList +ActivatedTagList-[hidden]right-> UniquePersonList + UniquePersonList --> "~* all" Person +UniqueTagList*--> Tag +ActivatedTagList*--> Tag +Person *--> "*" Tag Person *--> Name Person *--> Phone Person *--> Email Person *--> Address -Person *--> "*" Tag Name -[hidden]right-> Phone Phone -[hidden]right-> Address diff --git a/docs/diagrams/NoteSequenceExecution.puml b/docs/diagrams/NoteSequenceExecution.puml new file mode 100644 index 00000000000..29e5919b13e --- /dev/null +++ b/docs/diagrams/NoteSequenceExecution.puml @@ -0,0 +1,52 @@ +@startuml +!include style.puml + +Box Logic LOGIC_COLOR_T1 +participant ":NoteCommand" as NoteCommand LOGIC_COLOR +end box + +Box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +participant "personToEdit:Person" as OldPerson MODEL_COLOR +participant "oldNotes:Notes" as OldNotes MODEL_COLOR +participant "newNotes:Notes" as NewNotes MODEL_COLOR +participant "editedPerson:Person" as NewPerson MODEL_COLOR +end box + +[-> NoteCommand : execute(model) +activate NoteCommand + +NoteCommand -> Model : getFilteredPersonList() + +NoteCommand -> NoteCommand : updateNotes(personToEdit, note) +activate NoteCommand + +NoteCommand -> OldPerson : getNotes() + +NoteCommand -> OldNotes : updateNotes(note) +activate OldNotes +create NewNotes +OldNotes -> NewNotes +activate NewNotes +NewNotes -> OldNotes +deactivate NewNotes +OldNotes -> NoteCommand : newNotes +deactivate OldNotes +'Hidden arrow to move destroy marker lower' +OldNotes -[hidden]-> OldNotes +destroy OldNotes + +create NewPerson +NoteCommand -> NewPerson +activate NewPerson +NewPerson -> NoteCommand +deactivate NewPerson +NoteCommand -> NoteCommand : editedPerson +deactivate NoteCommand + +NoteCommand -> Model : setPerson(personToEdit, editedPerson) + +Model -> OldPerson +destroy OldPerson + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..b81efcc5fbf 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,10 +11,11 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard Class StatusBarFooter Class CommandBox +package InformationPanels <> { +class HiddenPanels #FFFFFF +} } package Model <> { @@ -30,31 +31,29 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow +UiManager -[hidden]down-> MainWindow + +MainWindow *-right-> InformationPanels MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow - -PersonListPanel -down-> "*" PersonCard - -MainWindow -left-|> UiPart +MainWindow -down-> "0..1" HelpWindow +MainWindow --|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +InformationPanels --|> UiPart -PersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox +HiddenModel -[hidden]up- HiddenLogic +InformationPanels .right.> Model + CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter -MainWindow -[hidden]-|> UiPart + @enduml diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml index 96e30744d24..34885420931 100644 --- a/docs/diagrams/UndoRedoState0.puml +++ b/docs/diagrams/UndoRedoState0.puml @@ -15,6 +15,6 @@ State2 -[hidden]right-> State3 hide State2 hide State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State1 @end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml index 01fcb9b2b96..0e2c8c72d33 100644 --- a/docs/diagrams/UndoRedoState1.puml +++ b/docs/diagrams/UndoRedoState1.puml @@ -16,7 +16,7 @@ State2 -[hidden]right-> State3 hide State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State2 @end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index bccc230a5d1..0ce7073e187 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -14,7 +14,7 @@ package States <> { State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State3 @end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml index ea29c9483e4..50bf43b3f34 100644 --- a/docs/diagrams/UndoRedoState3.puml +++ b/docs/diagrams/UndoRedoState3.puml @@ -14,7 +14,7 @@ package States <> { State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State2 @end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 1b784cece80..83cbe4c740c 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -14,7 +14,7 @@ package States <> { State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State2 @end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml index 88927be32bc..fc89dd99d2d 100644 --- a/docs/diagrams/UndoRedoState5.puml +++ b/docs/diagrams/UndoRedoState5.puml @@ -14,7 +14,7 @@ package States <> { State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 -class Pointer as "Current State" #FFFFF +class Pointer as "Current State" #FFFFFF Pointer -up-> State3 note right on link: State ab2 deleted. diff --git a/docs/diagrams/detailedview/DetailedViewExecutionState1.puml b/docs/diagrams/detailedview/DetailedViewExecutionState1.puml new file mode 100644 index 00000000000..0862bd8cd10 --- /dev/null +++ b/docs/diagrams/detailedview/DetailedViewExecutionState1.puml @@ -0,0 +1,18 @@ +@startuml +!include ../style.puml + +Box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +end box + +Box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +end box + +[-> MainWindow : executeCommand("note r/red") +activate MainWindow + +MainWindow -> LogicManager : executeInDetailedViewMode("note r/red") +activate LogicManager + +@enduml diff --git a/docs/diagrams/detailedview/DetailedViewExecutionState2.puml b/docs/diagrams/detailedview/DetailedViewExecutionState2.puml new file mode 100644 index 00000000000..ba2ba123530 --- /dev/null +++ b/docs/diagrams/detailedview/DetailedViewExecutionState2.puml @@ -0,0 +1,40 @@ +@startuml +!include ../style.puml + +Box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as ABParser LOGIC_COLOR +participant ":NoteCommandParser" as NoteCommandParser LOGIC_COLOR +participant "noteCommand:NoteCommand" as NoteCommand LOGIC_COLOR +end box + +activate LogicManager + +LogicManager -> ABParser : parseInDetailedViewContext("note r/red") +activate ABParser + +create NoteCommandParser +ABParser -> NoteCommandParser +activate NoteCommandParser +NoteCommandParser -> ABParser +deactivate NoteCommandParser + +ABParser -> NoteCommandParser : parseInDetailedViewContext("r/red") +activate NoteCommandParser + +create NoteCommand +NoteCommandParser -> NoteCommand +activate NoteCommand +NoteCommand -> NoteCommandParser +deactivate NoteCommand + +NoteCommandParser -> ABParser : noteCommand +deactivate NoteCommandParser +'Hidden arrow to move destroy marker lower' +NoteCommandParser -[hidden]-> NoteCommandParser +destroy NoteCommandParser + +ABParser -> LogicManager : noteCommand +deactivate ABParser + +@enduml diff --git a/docs/diagrams/detailedview/DetailedViewExecutionState3.puml b/docs/diagrams/detailedview/DetailedViewExecutionState3.puml new file mode 100644 index 00000000000..62f083387df --- /dev/null +++ b/docs/diagrams/detailedview/DetailedViewExecutionState3.puml @@ -0,0 +1,54 @@ +@startuml +!include ../style.puml + +Box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant "noteCommand:NoteCommand" as NoteCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +Box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +participant "personToEdit:Person" as OldPerson MODEL_COLOR +participant "editedPerson:Person" as NewPerson MODEL_COLOR +end box + +activate LogicManager + +LogicManager -> NoteCommand : executeInDetailedView +activate NoteCommand + +NoteCommand -> Model : getDetailedContactViewPerson +activate Model +Model -> NoteCommand : personToEdit +deactivate Model + +create NewPerson +NoteCommand -> NewPerson +activate NewPerson +NewPerson -> NoteCommand +deactivate NewPerson + +NoteCommand -> Model : setPerson(personToEdit, editedPerson) +Model -> NoteCommand + +NoteCommand -> Model : setDetailedContactView(editedPerson) +Model -> OldPerson +OldPerson -[hidden]-> OldPerson +destroy OldPerson +Model -> NoteCommand + +create CommandResult +NoteCommand -> CommandResult : SpecialCommandResult.DETAILED_VIEW +activate CommandResult +CommandResult -> NoteCommand +deactivate CommandResult + +NoteCommand -> LogicManager : commandResult +deactivate NoteCommand + +'Hidden arrow to move destroy marker lower' +NoteCommand -[hidden]-> NoteCommand +destroy NoteCommand + +@enduml diff --git a/docs/diagrams/detailedview/DetailedViewExecutionState4.puml b/docs/diagrams/detailedview/DetailedViewExecutionState4.puml new file mode 100644 index 00000000000..7c9e8aa678b --- /dev/null +++ b/docs/diagrams/detailedview/DetailedViewExecutionState4.puml @@ -0,0 +1,29 @@ +@startuml +!include ../style.puml + +Box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +end box + +Box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +activate MainWindow + +LogicManager -> MainWindow : commandResult + +MainWindow -> CommandResult : getSpecialCommandResult + +CommandResult -> MainWindow : DETAILED_VIEW + +MainWindow -> MainWindow : handleDetailedView +activate MainWindow +deactivate MainWindow +'Hidden arrow to move return arrow lower' +MainWindow -[hidden]-> MainWindow +[<- MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/detailedview/DetailedViewGeneralExecution.puml b/docs/diagrams/detailedview/DetailedViewGeneralExecution.puml new file mode 100644 index 00000000000..d28c7cca27d --- /dev/null +++ b/docs/diagrams/detailedview/DetailedViewGeneralExecution.puml @@ -0,0 +1,84 @@ +@startuml +!include ../style.puml + +Box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +end box + +Box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as ABParser LOGIC_COLOR +participant ":DetailedViewExecutableParser" as DVEParser LOGIC_COLOR +participant "command:DetailedViewExecutable" as DVECommand LOGIC_COLOR +participant "result:CommandResult" as CommandResult LOGIC_COLOR +end box + +Box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand +activate MainWindow + +MainWindow -> LogicManager : executeInDetailedViewMode +activate LogicManager + +LogicManager -> ABParser : parseInDetailedViewContext +activate ABParser + +create DVEParser +ABParser -> DVEParser +activate DVEParser +DVEParser -> ABParser +deactivate DVEParser + +ABParser -> DVEParser : parseInDetailedViewContext +activate DVEParser + +create DVECommand +DVEParser -> DVECommand +activate DVECommand +DVECommand -> DVEParser +deactivate DVECommand + +DVEParser -> ABParser : command +deactivate DVEParser +'Hidden arrow to move destroy marker lower' +DVEParser -[hidden]-> DVEParser +destroy DVEParser + +ABParser -> LogicManager : command +deactivate ABParser + +LogicManager -> DVECommand : executeInDetailedView +activate DVECommand + +DVECommand -> Model +'Hidden arrow to extend activation box' +DVECommand -[hidden]-> DVECommand +create CommandResult +DVECommand -> CommandResult +activate CommandResult +CommandResult -> DVECommand +deactivate CommandResult +DVECommand -> LogicManager : result +deactivate DVECommand +'Hidden arrow to move destroy marker lower' +DVECommand -[hidden]-> DVECommand +destroy DVECommand + +LogicManager -> MainWindow : result +deactivate LogicManager + +MainWindow -> CommandResult : getSpecialCommandResult + +alt NONE SpecialCommandResult +MainWindow -> MainWindow : handleListView +else DETAILED_VIEW SpecialCommandResult +MainWindow -> MainWindow : handleDetailedView +end + +[<- MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/favourite/FavouriteSequenceDiagram.puml b/docs/diagrams/favourite/FavouriteSequenceDiagram.puml new file mode 100644 index 00000000000..c862eb59b1b --- /dev/null +++ b/docs/diagrams/favourite/FavouriteSequenceDiagram.puml @@ -0,0 +1,79 @@ +@startuml +!include ../style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FavouriteCommandParser" as FavouriteCommandParser LOGIC_COLOR +participant "f:FavouriteCommand" as FavouriteCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "pl:List" as PersonList MODEL_COLOR +end box +[-> LogicManager : execute(fav 3) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(fav 3) +activate AddressBookParser + +create FavouriteCommandParser +AddressBookParser -> FavouriteCommandParser +activate FavouriteCommandParser + +FavouriteCommandParser --> AddressBookParser +deactivate FavouriteCommandParser + +AddressBookParser -> FavouriteCommandParser : parse(3) +activate FavouriteCommandParser + +create FavouriteCommand +FavouriteCommandParser -> FavouriteCommand +activate FavouriteCommand + +FavouriteCommand --> FavouriteCommandParser +deactivate FavouriteCommand + +FavouriteCommandParser --> AddressBookParser +deactivate FavouriteCommandParser + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +destroy FavouriteCommandParser + +LogicManager -> FavouriteCommand : execute() +activate FavouriteCommand + +FavouriteCommand -> Model : getFilteredPersonList() +activate Model + +Model --> FavouriteCommand : pl +deactivate Model + +FavouriteCommand -> PersonList : get(2) +activate PersonList + +PersonList --> FavouriteCommand : targetPerson +deactivate PersonList + +FavouriteCommand -> FavouriteCommand : createFavouritePerson(targetPerson, IS_FAVOURITE) +activate FavouriteCommand + +FavouriteCommand --> FavouriteCommand : favouritedPerson +deactivate FavouriteCommand + +FavouriteCommand -> Model : setPerson(targetPerson, favouritedPerson) +activate Model + +Model --> FavouriteCommand +deactivate Model + +FavouriteCommand --> LogicManager +deactivate FavouriteCommand + +[<--LogicManager +deactivate LogicManager +destroy FavouriteCommand +@enduml diff --git a/docs/diagrams/favourite/FavouriteState0.puml b/docs/diagrams/favourite/FavouriteState0.puml new file mode 100644 index 00000000000..f7ed7837b07 --- /dev/null +++ b/docs/diagrams/favourite/FavouriteState0.puml @@ -0,0 +1,25 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title Newly created person +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class P1 as "__p1:Person__" + class P2 as "__p2:Person__" + class Person as "__p3:Person__" + class NotFavourite as "__NOT_FAVOURITE:Favourite__" +} + +PersonList -> Person +PersonList -up-> P1 +PersonList -> P2 +Person -up-> NotFavourite +P1 -> NotFavourite +P2 -right-> NotFavourite + +P1 -[hidden]-> P2 +P2 -[hidden]-> Person + +@enduml diff --git a/docs/diagrams/favourite/FavouriteState1.puml b/docs/diagrams/favourite/FavouriteState1.puml new file mode 100644 index 00000000000..4a74b24bcb2 --- /dev/null +++ b/docs/diagrams/favourite/FavouriteState1.puml @@ -0,0 +1,20 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title During execution (other contacts omitted) +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class Person as "__p3:Person__" + class NotFavourite as "__NOT_FAVOURITE:Favourite__" + class PersonFavourited as "__p3Favourited:Person__" + class IsFavourite as "__IS_FAVOURITE:Favourite__" +} + +PersonList -> Person +Person -> NotFavourite +Person -[hidden]-> PersonFavourited +PersonFavourited -> IsFavourite + +@enduml diff --git a/docs/diagrams/favourite/FavouriteState2.puml b/docs/diagrams/favourite/FavouriteState2.puml new file mode 100644 index 00000000000..3876fa4e9d6 --- /dev/null +++ b/docs/diagrams/favourite/FavouriteState2.puml @@ -0,0 +1,27 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title After execution +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class NotFavourite as "__NOT_FAVOURITE:Favourite__" + class PersonFavourited as "__p3Favourited:Person__" + class P1 as "__p1:Person__" + class P2 as "__p2:Person__" + class IsFavourite as "__IS_FAVOURITE:Favourite__" +} + +PersonList -> PersonFavourited +PersonList -up-> P1 +PersonList -> P2 +PersonFavourited -right-> IsFavourite +P1 -> NotFavourite +P2 -right-> NotFavourite + +P1 -[hidden]-> P2 +P2 -[hidden]-> PersonFavourited +NotFavourite -[hidden]-> IsFavourite + +@enduml diff --git a/docs/diagrams/favourite/FavouriteState3.puml b/docs/diagrams/favourite/FavouriteState3.puml new file mode 100644 index 00000000000..5a2d149ccbf --- /dev/null +++ b/docs/diagrams/favourite/FavouriteState3.puml @@ -0,0 +1,18 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title List Favourites +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class PersonFavourited as "__p3Favourited:Person__" + class IsFavourite as "__IS_FAVOURITE:Favourite__" + class Predicate as "__:PersonIsFavouriteContactPredicate__" +} + +PersonList -> PersonFavourited +PersonFavourited -right-> IsFavourite +PersonList ..> Predicate + +@enduml diff --git a/docs/diagrams/high-importance-flag/HighImportanceSequenceDiagram.puml b/docs/diagrams/high-importance-flag/HighImportanceSequenceDiagram.puml new file mode 100644 index 00000000000..db2df934cba --- /dev/null +++ b/docs/diagrams/high-importance-flag/HighImportanceSequenceDiagram.puml @@ -0,0 +1,64 @@ +@startuml +!include ../style.puml + +box Logic +participant ":LogicManager" as LogicManager LOGIC_COLOR_T5 +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR_T5 +participant ":HighImportanceCommandParser" as HighImportanceCommandParser LOGIC_COLOR_T5 +participant "h :HighImportanceCommand" as HighImportanceCommand LOGIC_COLOR_T5 +participant ":CommandResult" as CommandResult LOGIC_COLOR_T5 + +-> LogicManager : execute("impt 1") +activate LogicManager + +LogicManager -> AddressBookParser: parseCommand("impt 1") +activate AddressBookParser + +create HighImportanceCommandParser +AddressBookParser -> HighImportanceCommandParser ++ + +HighImportanceCommandParser --> AddressBookParser -- + +AddressBookParser -> HighImportanceCommandParser ++ : \nparse("1") + +create HighImportanceCommand +HighImportanceCommandParser -> HighImportanceCommand ++ + +HighImportanceCommand --> HighImportanceCommandParser -- : h +HighImportanceCommandParser --> AddressBookParser -- : h +destroy HighImportanceCommandParser + +AddressBookParser --> LogicManager -- : h +LogicManager -> HighImportanceCommand ++ : execute() + +box Model MODEL_COLOR_T1 +participant ":Model" as model MODEL_COLOR_T4 +participant "personList:List" as personList MODEL_COLOR_T4 +end box + +HighImportanceCommand -> model ++ : getSortedPersonList() +model --> HighImportanceCommand : personList +deactivate model + +HighImportanceCommand -> personList ++ : get(2) +personList --> HighImportanceCommand : targetPerson +deactivate personList + +HighImportanceCommand -> HighImportanceCommand ++ : createHighImportancePerson(targetPerson, HIGH_IMPORTANCE) +HighImportanceCommand --> HighImportanceCommand : editedPerson +deactivate HighImportanceCommand + +HighImportanceCommand -> model ++ : setPerson(targetPerson, editedPerson) +model --> HighImportanceCommand +deactivate model + +create CommandResult +HighImportanceCommand -> CommandResult ++ +CommandResult --> HighImportanceCommand -- + +HighImportanceCommand --> LogicManager +deactivate HighImportanceCommand +destroy HighImportanceCommand +<-- LogicManager + +@enduml diff --git a/docs/diagrams/high-importance-flag/ImportanceState0.puml b/docs/diagrams/high-importance-flag/ImportanceState0.puml new file mode 100644 index 00000000000..19d813f8261 --- /dev/null +++ b/docs/diagrams/high-importance-flag/ImportanceState0.puml @@ -0,0 +1,21 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title Newly created persons (2 persons as example) +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class P1 as "__p1:Person__" + class P2 as "__p2:Person__" + class NotImportant as "__NOT_HIGH_IMPORTANCE:HighImportance__" +} + +PersonList -right-> P1 +P1 -right-> NotImportant +PersonList --> P2 +P2 -up-> NotImportant + +P1 -[hidden]down-> P2 + +@enduml diff --git a/docs/diagrams/high-importance-flag/ImportanceState1.puml b/docs/diagrams/high-importance-flag/ImportanceState1.puml new file mode 100644 index 00000000000..4b7325bf1c0 --- /dev/null +++ b/docs/diagrams/high-importance-flag/ImportanceState1.puml @@ -0,0 +1,21 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title During Execution (p2 omitted) +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class P1 as "__p1:Person__" + class P1HighImportance as "__p1HighImportance:Person__" + class NotImportant as "__NOT_HIGH_IMPORTANCE:HighImportance__" + class Important as "__HIGH_IMPORTANCE:HighImportance__" +} + +PersonList -right-> P1 +P1 -right-> NotImportant +P1HighImportance -right-> Important + +P1HighImportance -[hidden]up-> P1 + +@enduml diff --git a/docs/diagrams/high-importance-flag/ImportanceState2.puml b/docs/diagrams/high-importance-flag/ImportanceState2.puml new file mode 100644 index 00000000000..1e87ca31996 --- /dev/null +++ b/docs/diagrams/high-importance-flag/ImportanceState2.puml @@ -0,0 +1,22 @@ +@startuml +!include ../style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title After execution +package ContactState { + class PersonList as "__personList:FilteredPersonList__" + class P1 as "__p1:Person__" + class P2 as "__p2:Person__" + class Important as "__HIGH_IMPORTANCE:HighImportance__" + class NotImportant as "__NOT_HIGH_IMPORTANCE:HighImportance__" +} + +PersonList -right-> P1 +P1 -right-> Important +PersonList --> P2 +P2 -right-> NotImportant + +P1 -[hidden]down-> P2 + +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..c0639daac10 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -13,11 +13,14 @@ !define UI_COLOR_T3 #166800 !define UI_COLOR_T4 #0E4100 +!define LOGIC_COLOR_HEADING #FFF !define LOGIC_COLOR #3333C4 !define LOGIC_COLOR_T1 #C8C8FA !define LOGIC_COLOR_T2 #6A6ADC !define LOGIC_COLOR_T3 #1616B0 !define LOGIC_COLOR_T4 #101086 +!define LOGIC_COLOR_T5 #7777DB +!define LOGIC_COLOR_T6 #5252CE !define MODEL_COLOR #9D0012 !define MODEL_COLOR_T1 #F97181 diff --git a/docs/images/AddCommandParserEnhancement.png b/docs/images/AddCommandParserEnhancement.png new file mode 100644 index 00000000000..966ca444c29 Binary files /dev/null and b/docs/images/AddCommandParserEnhancement.png differ diff --git a/docs/images/AfterDeadlineCommand.png b/docs/images/AfterDeadlineCommand.png new file mode 100644 index 00000000000..633189f482d Binary files /dev/null and b/docs/images/AfterDeadlineCommand.png differ diff --git a/docs/images/AfterDeleteDeadlineCommand.png b/docs/images/AfterDeleteDeadlineCommand.png new file mode 100644 index 00000000000..014bbafe767 Binary files /dev/null and b/docs/images/AfterDeleteDeadlineCommand.png differ diff --git a/docs/images/AfterDetailedViewDeadlineCommand.png b/docs/images/AfterDetailedViewDeadlineCommand.png new file mode 100644 index 00000000000..c4ffaa8ffd9 Binary files /dev/null and b/docs/images/AfterDetailedViewDeadlineCommand.png differ diff --git a/docs/images/BeforeDeadlineCommand.png b/docs/images/BeforeDeadlineCommand.png new file mode 100644 index 00000000000..46540bbbe0b Binary files /dev/null and b/docs/images/BeforeDeadlineCommand.png differ diff --git a/docs/images/BeforeDeleteDeadlineCommand.png b/docs/images/BeforeDeleteDeadlineCommand.png new file mode 100644 index 00000000000..b4075b35a28 Binary files /dev/null and b/docs/images/BeforeDeleteDeadlineCommand.png differ diff --git a/docs/images/BeforeDetailedViewDeadlineCommand.png b/docs/images/BeforeDetailedViewDeadlineCommand.png new file mode 100644 index 00000000000..d382a09605f Binary files /dev/null and b/docs/images/BeforeDetailedViewDeadlineCommand.png differ diff --git a/docs/images/DeadlineSequenceDiagram.png b/docs/images/DeadlineSequenceDiagram.png new file mode 100644 index 00000000000..d149d96f291 Binary files /dev/null and b/docs/images/DeadlineSequenceDiagram.png differ diff --git a/docs/images/DeadlineState1.png b/docs/images/DeadlineState1.png new file mode 100644 index 00000000000..610c78ce444 Binary files /dev/null and b/docs/images/DeadlineState1.png differ diff --git a/docs/images/DeadlineState2.png b/docs/images/DeadlineState2.png new file mode 100644 index 00000000000..68dd3d3e8c7 Binary files /dev/null and b/docs/images/DeadlineState2.png differ diff --git a/docs/images/DeadlineState3.png b/docs/images/DeadlineState3.png new file mode 100644 index 00000000000..c73b0fe8506 Binary files /dev/null and b/docs/images/DeadlineState3.png differ diff --git a/docs/images/DeadlineState4.png b/docs/images/DeadlineState4.png new file mode 100644 index 00000000000..4a7177c1c11 Binary files /dev/null and b/docs/images/DeadlineState4.png differ diff --git a/docs/images/FindTagSequenceDiagram.png b/docs/images/FindTagSequenceDiagram.png new file mode 100644 index 00000000000..51db52e4750 Binary files /dev/null and b/docs/images/FindTagSequenceDiagram.png differ diff --git a/docs/images/InformationPanels.png b/docs/images/InformationPanels.png new file mode 100644 index 00000000000..029152a4b88 Binary files /dev/null and b/docs/images/InformationPanels.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..c0026239b65 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/NoteSequenceExecution.png b/docs/images/NoteSequenceExecution.png new file mode 100644 index 00000000000..018843f9c63 Binary files /dev/null and b/docs/images/NoteSequenceExecution.png differ diff --git a/docs/images/Sample1.png b/docs/images/Sample1.png new file mode 100644 index 00000000000..6ec76658ef6 Binary files /dev/null and b/docs/images/Sample1.png differ diff --git a/docs/images/Sample2.png b/docs/images/Sample2.png new file mode 100644 index 00000000000..6b261b89037 Binary files /dev/null and b/docs/images/Sample2.png differ diff --git a/docs/images/TutorialAdd.png b/docs/images/TutorialAdd.png new file mode 100644 index 00000000000..d3c98084b48 Binary files /dev/null and b/docs/images/TutorialAdd.png differ diff --git a/docs/images/TutorialFav.png b/docs/images/TutorialFav.png new file mode 100644 index 00000000000..2c4791c37ac Binary files /dev/null and b/docs/images/TutorialFav.png differ diff --git a/docs/images/TutorialImpt.png b/docs/images/TutorialImpt.png new file mode 100644 index 00000000000..e5e85da6236 Binary files /dev/null and b/docs/images/TutorialImpt.png differ diff --git a/docs/images/TutorialView.png b/docs/images/TutorialView.png new file mode 100644 index 00000000000..7f2395c5815 Binary files /dev/null and b/docs/images/TutorialView.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..2c85a1c5d74 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 index 785e04dbab4..4f14d89cefc 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/after_unfavourite_command.png b/docs/images/after_unfavourite_command.png new file mode 100644 index 00000000000..9b792cd8efb Binary files /dev/null and b/docs/images/after_unfavourite_command.png differ diff --git a/docs/images/assign-tag/assign-1-friends.png b/docs/images/assign-tag/assign-1-friends.png new file mode 100644 index 00000000000..ca093dcf3d7 Binary files /dev/null and b/docs/images/assign-tag/assign-1-friends.png differ diff --git a/docs/images/assign-tag/assign-3-colleagues.png b/docs/images/assign-tag/assign-3-colleagues.png new file mode 100644 index 00000000000..f8f22441a3f Binary files /dev/null and b/docs/images/assign-tag/assign-3-colleagues.png differ diff --git a/docs/images/assign-tag/before-assign-1-friends.png b/docs/images/assign-tag/before-assign-1-friends.png new file mode 100644 index 00000000000..4307cd2eee2 Binary files /dev/null and b/docs/images/assign-tag/before-assign-1-friends.png differ diff --git a/docs/images/assign-tag/before-assign-3-colleagues.png b/docs/images/assign-tag/before-assign-3-colleagues.png new file mode 100644 index 00000000000..c15d014c939 Binary files /dev/null and b/docs/images/assign-tag/before-assign-3-colleagues.png differ diff --git a/docs/images/assign-tag/view-1.png b/docs/images/assign-tag/view-1.png new file mode 100644 index 00000000000..ebd1d62399c Binary files /dev/null and b/docs/images/assign-tag/view-1.png differ diff --git a/docs/images/assign-tag/view-assign-client.png b/docs/images/assign-tag/view-assign-client.png new file mode 100644 index 00000000000..cb145ffe66d Binary files /dev/null and b/docs/images/assign-tag/view-assign-client.png differ diff --git a/docs/images/copyLinkSuccessMessage.png b/docs/images/copyLinkSuccessMessage.png new file mode 100644 index 00000000000..cc4bbe7a398 Binary files /dev/null and b/docs/images/copyLinkSuccessMessage.png differ diff --git a/docs/images/create-tag/create-tag-friends.png b/docs/images/create-tag/create-tag-friends.png new file mode 100644 index 00000000000..8a7421f3b55 Binary files /dev/null and b/docs/images/create-tag/create-tag-friends.png differ diff --git a/docs/images/del-tag/before-deltag-friends-colleagues-not-exist.png b/docs/images/del-tag/before-deltag-friends-colleagues-not-exist.png new file mode 100644 index 00000000000..9a3a4471b2b Binary files /dev/null and b/docs/images/del-tag/before-deltag-friends-colleagues-not-exist.png differ diff --git a/docs/images/del-tag/before-deltag-friends-colleagues.png b/docs/images/del-tag/before-deltag-friends-colleagues.png new file mode 100644 index 00000000000..9b23ea71c89 Binary files /dev/null and b/docs/images/del-tag/before-deltag-friends-colleagues.png differ diff --git a/docs/images/del-tag/before-deltag-friends.png b/docs/images/del-tag/before-deltag-friends.png new file mode 100644 index 00000000000..4d4d1151be5 Binary files /dev/null and b/docs/images/del-tag/before-deltag-friends.png differ diff --git a/docs/images/del-tag/deltag-colleagues-not-exist.png b/docs/images/del-tag/deltag-colleagues-not-exist.png new file mode 100644 index 00000000000..179a3865f2e Binary files /dev/null and b/docs/images/del-tag/deltag-colleagues-not-exist.png differ diff --git a/docs/images/del-tag/deltag-friends-colleagues-not-exist.png b/docs/images/del-tag/deltag-friends-colleagues-not-exist.png new file mode 100644 index 00000000000..0f80381dbd9 Binary files /dev/null and b/docs/images/del-tag/deltag-friends-colleagues-not-exist.png differ diff --git a/docs/images/del-tag/deltag-friends-colleagues.png b/docs/images/del-tag/deltag-friends-colleagues.png new file mode 100644 index 00000000000..94a77056223 Binary files /dev/null and b/docs/images/del-tag/deltag-friends-colleagues.png differ diff --git a/docs/images/del-tag/deltag-friends.png b/docs/images/del-tag/deltag-friends.png new file mode 100644 index 00000000000..58edebb7f20 Binary files /dev/null and b/docs/images/del-tag/deltag-friends.png differ diff --git a/docs/images/detailedview/DetailedViewExecutionState1.png b/docs/images/detailedview/DetailedViewExecutionState1.png new file mode 100644 index 00000000000..500b3a46c37 Binary files /dev/null and b/docs/images/detailedview/DetailedViewExecutionState1.png differ diff --git a/docs/images/detailedview/DetailedViewExecutionState2.png b/docs/images/detailedview/DetailedViewExecutionState2.png new file mode 100644 index 00000000000..76530676007 Binary files /dev/null and b/docs/images/detailedview/DetailedViewExecutionState2.png differ diff --git a/docs/images/detailedview/DetailedViewExecutionState3.png b/docs/images/detailedview/DetailedViewExecutionState3.png new file mode 100644 index 00000000000..6a8491e8b96 Binary files /dev/null and b/docs/images/detailedview/DetailedViewExecutionState3.png differ diff --git a/docs/images/detailedview/DetailedViewExecutionState4.png b/docs/images/detailedview/DetailedViewExecutionState4.png new file mode 100644 index 00000000000..37978d00dc0 Binary files /dev/null and b/docs/images/detailedview/DetailedViewExecutionState4.png differ diff --git a/docs/images/detailedview/DetailedViewGeneralExecution.png b/docs/images/detailedview/DetailedViewGeneralExecution.png new file mode 100644 index 00000000000..4af836f28e8 Binary files /dev/null and b/docs/images/detailedview/DetailedViewGeneralExecution.png differ diff --git a/docs/images/favourite/FavouriteSequenceDiagram.png b/docs/images/favourite/FavouriteSequenceDiagram.png new file mode 100644 index 00000000000..168e78b5e19 Binary files /dev/null and b/docs/images/favourite/FavouriteSequenceDiagram.png differ diff --git a/docs/images/favourite/FavouriteState0.png b/docs/images/favourite/FavouriteState0.png new file mode 100644 index 00000000000..df80d2bab83 Binary files /dev/null and b/docs/images/favourite/FavouriteState0.png differ diff --git a/docs/images/favourite/FavouriteState1.png b/docs/images/favourite/FavouriteState1.png new file mode 100644 index 00000000000..7367346fe65 Binary files /dev/null and b/docs/images/favourite/FavouriteState1.png differ diff --git a/docs/images/favourite/FavouriteState2.png b/docs/images/favourite/FavouriteState2.png new file mode 100644 index 00000000000..cbb53a0acd6 Binary files /dev/null and b/docs/images/favourite/FavouriteState2.png differ diff --git a/docs/images/favourite/FavouriteState3.png b/docs/images/favourite/FavouriteState3.png new file mode 100644 index 00000000000..c5d817a21d2 Binary files /dev/null and b/docs/images/favourite/FavouriteState3.png differ diff --git a/docs/images/favourite_command.png b/docs/images/favourite_command.png new file mode 100644 index 00000000000..a126f6c6edb Binary files /dev/null and b/docs/images/favourite_command.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..44eb2d8040f 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/findtag/findtag-friends-inprogress-almostfinished.png b/docs/images/findtag/findtag-friends-inprogress-almostfinished.png new file mode 100644 index 00000000000..893847a406c Binary files /dev/null and b/docs/images/findtag/findtag-friends-inprogress-almostfinished.png differ diff --git a/docs/images/findtag/findtag-friends.png b/docs/images/findtag/findtag-friends.png new file mode 100644 index 00000000000..f69b9dfb6b4 Binary files /dev/null and b/docs/images/findtag/findtag-friends.png differ diff --git a/docs/images/glennljw.png b/docs/images/glennljw.png new file mode 100644 index 00000000000..ecb8e1fcc75 Binary files /dev/null and b/docs/images/glennljw.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..879e662e4e4 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/high-importance-flag/HighImportanceSequenceDiagram.png b/docs/images/high-importance-flag/HighImportanceSequenceDiagram.png new file mode 100644 index 00000000000..295ec203e04 Binary files /dev/null and b/docs/images/high-importance-flag/HighImportanceSequenceDiagram.png differ diff --git a/docs/images/high-importance-flag/ImportanceState0.png b/docs/images/high-importance-flag/ImportanceState0.png new file mode 100644 index 00000000000..9071e2b0028 Binary files /dev/null and b/docs/images/high-importance-flag/ImportanceState0.png differ diff --git a/docs/images/high-importance-flag/ImportanceState1.png b/docs/images/high-importance-flag/ImportanceState1.png new file mode 100644 index 00000000000..0f52f4f4274 Binary files /dev/null and b/docs/images/high-importance-flag/ImportanceState1.png differ diff --git a/docs/images/high-importance-flag/ImportanceState2.png b/docs/images/high-importance-flag/ImportanceState2.png new file mode 100644 index 00000000000..a1bf6ea4fcd Binary files /dev/null and b/docs/images/high-importance-flag/ImportanceState2.png differ diff --git a/docs/images/high-importance-flag/add_importance_flag.png b/docs/images/high-importance-flag/add_importance_flag.png new file mode 100644 index 00000000000..59c0cd1d17e Binary files /dev/null and b/docs/images/high-importance-flag/add_importance_flag.png differ diff --git a/docs/images/high-importance-flag/add_note_for_reason.png b/docs/images/high-importance-flag/add_note_for_reason.png new file mode 100644 index 00000000000..98ff93189ed Binary files /dev/null and b/docs/images/high-importance-flag/add_note_for_reason.png differ diff --git a/docs/images/high-importance-flag/before_command.png b/docs/images/high-importance-flag/before_command.png new file mode 100644 index 00000000000..882924e4d17 Binary files /dev/null and b/docs/images/high-importance-flag/before_command.png differ diff --git a/docs/images/high-importance-flag/command_result.png b/docs/images/high-importance-flag/command_result.png new file mode 100644 index 00000000000..20558e2da33 Binary files /dev/null and b/docs/images/high-importance-flag/command_result.png differ diff --git a/docs/images/high-importance-flag/impts_command_result.png b/docs/images/high-importance-flag/impts_command_result.png new file mode 100644 index 00000000000..f6f47b0b469 Binary files /dev/null and b/docs/images/high-importance-flag/impts_command_result.png differ diff --git a/docs/images/image_index.png b/docs/images/image_index.png new file mode 100644 index 00000000000..70bf6883d1c Binary files /dev/null and b/docs/images/image_index.png differ diff --git a/docs/images/images_file_chooser.png b/docs/images/images_file_chooser.png new file mode 100644 index 00000000000..a276d9184fa Binary files /dev/null and b/docs/images/images_file_chooser.png differ diff --git a/docs/images/takufunkai.png b/docs/images/takufunkai.png new file mode 100644 index 00000000000..db3d82e30c4 Binary files /dev/null and b/docs/images/takufunkai.png differ diff --git a/docs/images/tehkokhoe.png b/docs/images/tehkokhoe.png new file mode 100644 index 00000000000..3ce3be63c9f Binary files /dev/null and b/docs/images/tehkokhoe.png differ diff --git a/docs/images/unassign-tag/before-unassign-1-friends.png b/docs/images/unassign-tag/before-unassign-1-friends.png new file mode 100644 index 00000000000..367d16becc9 Binary files /dev/null and b/docs/images/unassign-tag/before-unassign-1-friends.png differ diff --git a/docs/images/unassign-tag/unassign-1-friends.png b/docs/images/unassign-tag/unassign-1-friends.png new file mode 100644 index 00000000000..111907592a7 Binary files /dev/null and b/docs/images/unassign-tag/unassign-1-friends.png differ diff --git a/docs/images/unassign-tag/view-1.png b/docs/images/unassign-tag/view-1.png new file mode 100644 index 00000000000..e41e438cb89 Binary files /dev/null and b/docs/images/unassign-tag/view-1.png differ diff --git a/docs/images/unassign-tag/view-unassign-client.png b/docs/images/unassign-tag/view-unassign-client.png new file mode 100644 index 00000000000..0fdd272efd9 Binary files /dev/null and b/docs/images/unassign-tag/view-unassign-client.png differ diff --git a/docs/images/weijuey.png b/docs/images/weijuey.png new file mode 100644 index 00000000000..46c9a505878 Binary files /dev/null and b/docs/images/weijuey.png differ diff --git a/docs/images/xsaints19x.png b/docs/images/xsaints19x.png new file mode 100644 index 00000000000..b84a71351f8 Binary files /dev/null and b/docs/images/xsaints19x.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..a8c663ef09e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: d'Intérieur --- -[![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) +[![CI Status](https://github.com/AY2122S2-CS2103T-T12-2/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103T-T12-2/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2122S2-CS2103T-T12-2/tp/branch/master/graph/badge.svg?token=LTPAH44EQN)](https://codecov.io/gh/AY2122S2-CS2103T-T12-2/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). +**d'Intérieur is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using d'Intérieur, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested in developing d'Intérieur, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Icons used: [Red Flag by Flaticon](https://www.flaticon.com/free-icon/finish_2164620), [White Flag by Flaticon](https://www.flaticon.com/free-icon/finish_2164598), [Check mark by Flaticon](https://www.flaticon.com/free-icon/checked_190411) diff --git a/docs/team/glennljw.md b/docs/team/glennljw.md new file mode 100644 index 00000000000..edd25b7483b --- /dev/null +++ b/docs/team/glennljw.md @@ -0,0 +1,69 @@ +--- +layout: page +title: Glenn Lim's Project Portfolio Page +--- + +### Project: d'Intérieur + +d'Intérieur is a desktop address book application, designed with interior designers in mind. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 26 kLoC. + +Interior designers can use d'Intérieur to improve contact management and take on the needs of an ever-growing client base, so that they can focus on what matters most - delivering quality service for clients. + +Given below are my contributions to the project. + +* **New Feature**: `Assign Tag` + * What it does: Assigns a tag to a contact. + * Justification: This feature allows different contacts to be grouped at different points of time, such as labeling them as `InProgress` and `Completed` according to which stages they are at in the design cycle.' + * Highlights: + * Credits: This feature can be done when a contact profile is currently in view, which was done by [Wei Jue](weijuey.md) + +* **New Feature**: `Create Tag` + * What it does: Creates a tag with a meaningful name to categorise and group relevant contacts. + * Justification: This feature is essential as it allows tags to be created, then assigned to contacts whenever they reach a certain stage in the design cycle, or for personal use. + +* **New Feature**: `Delete Tag` + * What it does: Deletes a tag stored in the data, and unassigns any contact that are assigned to the tag to be deleted. + * Justification: This feature helps interior designers manage unused tags, or do a mass unassignment at once. +
+ +* **New Feature**: `Find Tag` + * What it does: Acts as a filter to locate contacts with a certain tag. + * Justification: This feature helps interior designers locate contacts that are grouped by their tags. + * Highlights: This enhancement was made through the use of an `ActivatedTagList` which acts as a list of selected criteria/category just like how an eCommerce store would select filter to apply. + +* **New Feature**: `Unassign Tag` + * What it does: Unassigns a tag from a contact. + * Justification: This feature allows contacts who reach different stages of the design cycle can have their older tags (which specifies their older stage) removed from them. + * Credits: This feature can be done when a contact profile is currently in view, which was done by [Wei Jue](weijuey.md) + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=glennljw&breakdown=true) + +* **Project management**: + * Managed releases `1.2` - `1.4` (4 releases) on GitHub + * Added photos and details of team members [\#11](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/11) + +* **Enhancements to existing features**: + * Enhanced existing `Tag` to be a class of its own instead of a `String`. + * Allowed `add` feature to create and assign `Tag` upon adding a new contact. [\#83](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/83) + * Removed the editing tag functionality in the `edit` feature, abstracting this feature into an `assign` and `unassign` feature for tags. [\#83](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/83) + * Modified the help window by improving the GUI for displaying useful links to different sections of the User Guide. [\#190](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/190) + +* **Documentation**: + * User Guide: + * Added documentation for the features `assign tag`, `create tag`, `delete tag`, `find tag` and `unassign tag` (Pull requests [\#26](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/26), [\#90](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/90), [\#95](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/95), [\#115](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/115), [\#189](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/189)) + * Edited existing documentations for `edit` feature [\#90](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/90) + * Updated the UI images for the help window. [\#190](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/190) + + * Developer Guide: + * Added implementation details of the following features: + * Find Tag + + * Added sequence diagram in the implementation details of Find Tag. + * Added instructions for manual testing for Create Tag feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#87](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/87), [\#89](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/89), [\#196](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/196) + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S2/forum/issues/84#issuecomment-1028047189), [2](https://github.com/nus-cs2103-AY2122S2/forum/issues/84#issuecomment-1028530974)) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/glennljw/ped/issues/4), [2](https://github.com/glennljw/ped/issues/3), [3](https://github.com/glennljw/ped/issues/2)) + + diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..057dad573be 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -3,9 +3,10 @@ layout: page title: John Doe's Project Portfolio Page --- -### Project: AddressBook Level 3 +### Project: d'Intérieur + +d'Intérieur is a desktop address book application, designed with interior designers in mind. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. Given below are my contributions to the project. diff --git a/docs/team/takufunkai.md b/docs/team/takufunkai.md new file mode 100644 index 00000000000..fff0aa11710 --- /dev/null +++ b/docs/team/takufunkai.md @@ -0,0 +1,67 @@ +--- +layout: page +title: Ezekiel Toh's Project Portfolio Page +--- + +### Project: d'Intérieur + +d'Intérieur is a desktop app for interior designers to manage their contacts and projects. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 26 kLoC. + +Interior designers can use d'Intèrieur to improve contact management and take on the needs of an ever-growing client base, so that they can focus on what matters most - delivering quality service for clients. + +Given below are my contributions to the project. + +* **New Feature**: Added images feature that allows user to add, delete and view images of a contact. + * What it does: Allows the user to add, delete, and view images associated with each of their contacts. + * Justification: Interior designers frequently work with images. Their clients' preferences, project designs and + other details are often best represented through an image and not merely through words. Having these images + can greatly improve their productivity, organization and help with attention to detail. + * Highlights: In addition to understanding the fundamentals of the application such as how a contact's information is + encapsulated and the interactions between the UI, Logic and Model, this feature requires understanding of how data + should be saved and retrieved locally, as well as handling situations of corrupted/missing images. Displaying images + in a responsive and user-friendly manner, as well as being able to click on the images, requires a decent level of + understanding of the UI. + * Credits: Final look and design of the UI was discussed and agreed on as a team. + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + * What it does: Allows the user to cycle through old and latest input histories using their up/down keys. + * Justification: It is common for certain commands to be repeatedly inputted with slight modifications to the command. + For example, a designer might want to favourite a bunch of users in quick succession. They can just press the up + key, and replace the index value with the correct value, saving them the time to retype `fav ` repeatedly. + * Highlights: Implementing the feature would require understanding of how the command input and processing flow works, + beginning from how commands are being received by the UI and how they are being sent to the logic for processing. + To save the history, it had to pass from the UI to the logic and then to the model, requiring understanding of the + UI, logic and model's implementations. Pros and cons had to be weighed as well as to how best to encapsulate the + history of a user. Understanding of how keystrokes can be recorded and kick-in a method through the UI was also required. + +* **New Feature**: Added favourites feature to set contacts as favourites and to filter them. + * What it does: Allows the user to set and unset their contacts as favourites, and to list these favourite contacts. + * Justification: Certain sets of contacts may be referenced repeatedly during different phases of a project. This + feature allows the designers to quickly pull up a truncated list of contacts-of-interest to reduce on search time and + the need to recall contact names for searching. + * Highlights: This feature involved changes to the model manager, and the person model. This requires understanding of + the underlying implementation of the person as the favourite status was added as a field of the person, as well as + the implementation of the model and its interactions with the logic (and indirectly, the UI) through the use of + predicates and filtered list. + * Credits: Position and look of the star representation of the Favourite status was discussed and finalized as a team. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=takufunkai&breakdown=true) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (5 releases) on GitHub + +* **Enhancements to existing features**: + * Improved the detailed view interface to be more responsive and user-friendly. + +* **Documentation**: + * User Guide: + * Added documentation for the features `favourite`, `images` and `commandhistory` [\#23](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/23), [\#103](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/103), [\#206](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/206) + * Improved the introduction paragraph of the User Guide and updated help command documentation [\#89](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/89). + * Added tutorial section to User Guide and updated sample images [\#206](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/206) + * Developer Guide: + * Added implementation details of the `favourite` and `images` feature [\#76](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/76) [\#210](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/210) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#68](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/68), [\#99](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/99), [\#93](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/93) + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S2/forum/issues/81#issuecomment-1027890350), [2](https://github.com/nus-cs2103-AY2122S2/forum/issues/193#issuecomment-1055513252), [3](https://github.com/nus-cs2103-AY2122S2/forum/issues/187#issuecomment-1059743036), [4](https://github.com/nus-cs2103-AY2122S2/forum/issues/29#issuecomment-1020795493), [5](https://github.com/nus-cs2103-AY2122S2/forum/issues/85#issuecomment-1028099491)) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/nus-cs2103-AY2122S2/forum/issues/192), [2](https://github.com/nus-cs2103-AY2122S2/forum/issues/100#issuecomment-1029240062), [3](https://github.com/nus-cs2103-AY2122S2/forum/issues/83#issuecomment-1028106761)) diff --git a/docs/team/tehkokhoe.md b/docs/team/tehkokhoe.md new file mode 100644 index 00000000000..43cc5474ffe --- /dev/null +++ b/docs/team/tehkokhoe.md @@ -0,0 +1,35 @@ +--- +layout: page +title: Teh Kok Hoe's Project Portfolio Page +--- + +### Project: d'Intérieur +d'Intérieur is a desktop app for interior designers to manage their contacts and projects. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 26 kLoC. + +Interior designers can use d'Intérieur to improve contact management and take on the needs of an ever-growing client base, so that they can focus on what matters most - delivering quality service for clients. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add deadlines to contact. + * Justification: This feature is essential because interior designer can keep track of project deadlines. + * Highlights: This enhancement involves the Logic, Model and UI objects, which require a good understanding in how these objects interact with each other. + * Credits: Adding deadlines and removing deadlines without refreshing + the list of deadlines was implemented by Wei Jue. + +* **New Feature**: Added a sort command. + * Justification: This feature is essential as contact list grows larger, it is essential for interior designers to find top contacts based on a certain criteria. + * Highlights: This enhancement involves implementing multiple + Comparators for the different models, it also involved some changes to + UI to display a sorted list. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=T12&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18&tabOpen=true&tabType=authorship&tabAuthor=tehkokhoe&tabRepo=AY2122S2-CS2103T-T12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + +* **Project management**: + Active participation in project with consistent feedback. + +* **Documentation**: + * User Guide: + * Added documentation for [`sort*`](https://ay2122s2-cs2103t-t12-2.github.io/tp/UserGuide.html#prioritising-relevant-contacts-to-you--sort) and [`deadline*`](https://ay2122s2-cs2103t-t12-2.github.io/tp/UserGuide.html#adding-deadlines-to-meet-in-relation-to-a-contact--deadline) [#92](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/92) [#24](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/24) + * Developer Guide: + * Added implementation details of the [`deadline*`](https://ay2122s2-cs2103t-t12-2.github.io/tp/DeveloperGuide.html#deadline-feature) feature. [#74](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/74) + diff --git a/docs/team/weijuey.md b/docs/team/weijuey.md new file mode 100644 index 00000000000..d4ca0c513d0 --- /dev/null +++ b/docs/team/weijuey.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Yoong Wei Jue's Project Portfolio Page +--- + +### Project: d'Intérieur + +d'Intérieur is a desktop app for interior designers to manage their contacts and projects. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 26 kLoC. + +Interior designers can use d'Intèrieur to improve contact management and take on the needs of an ever-growing client base, so that they can focus on what matters most - delivering quality service for clients. + +Given below are my contributions to the project. + +* **New Feature**: Added notes feature + * What it does: allows the user to add and delete notes to each contact, to be referenced in the future. + * Justification: This feature is flexible in utility, allowing the user to save any information they deem important, thus enhancing the product. + * Highlights: Implementing the feature required understanding how modification of internal data is done. Integrating a variably-sized UI element also required careful consideration of pros and cons of different solutions. + * Credits: UI design decisions were discussed and finalised as a team. + +* **New Feature**: Added detailed view feature + * What it does: allows the user to view a contact in detail, which the app will switch to a more focused view. + * Justification: This feature enables the user to do more focused work with one particular contact, especially because some information is only fully available to view by using this feature. It can allow adding as many new information to a contact as desired. + * Highlights: The feature involved multiple components of the application and adding new interfaces to each of them to interact, thus it required a good understanding of their current interactions. Adding new capabilities to current commands also required understanding their existing behaviour. + * Credits: Idea to communicate to MainWindow the panel view to show was first implemented by Ezekiel for his Images feature, and was adapted from his implementation. Final design of the detailed contact view was also refined by Ezekiel. Normal command execution path of new features were first implemented by team members. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=weijuey&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (5 releases) on GitHub + * Added target user profile, value proposition and glossary to Developer Guide + +* **Enhancements to existing features**: + * Update existing commands to work in the new detailed view feature mentioned above, with simpler command formats. + +* **Documentation**: + * User Guide: + * Added documentation for the new features `note` and `view` [\#28](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/28), [\#93](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/93) + * Reorganised commands to sections and added table of contents for more flexible linking to sections. [\#183](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/183) + * Added information to each existing commands on compatibility with new detailed view feature, and update command summary table to capture the information succinctly. [\#99](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/99) + * Developer Guide: + * Added implementation details and diagrams for the `note` and `view` features. [#72](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/72), [\#203](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/203) + * Update existing class diagrams to match new architecture. [\#203](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/203) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#44](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/44), [\#74](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/74), [\#85](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/85), [\#92](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/92) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/weijuey/ped/issues/1), [2](https://github.com/weijuey/ped/issues/3), [3](https://github.com/weijuey/ped/issues/5) + diff --git a/docs/team/xsaints19x.md b/docs/team/xsaints19x.md new file mode 100644 index 00000000000..9064ac04c23 --- /dev/null +++ b/docs/team/xsaints19x.md @@ -0,0 +1,47 @@ +--- +layout: page +title: David Limantara's Project Portfolio Page +--- + +### Project: d'Intérieur + +d'Intérieur is a desktop address book application, designed with interior designers in mind. The designer interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 26 kLoC. + +Interior designers can use d'Intèrieur to improve contact management and take on the needs of an ever-growing client base, so that they can focus on what matters most - delivering quality service for clients. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add address as an optional field. + * Justification: This feature was done because it is not critical for an interior designer to know a client's address. + +* **New Feature**: Added a high importance flag icon which appears beside the contact name + * What it does: Each contact will have an unlit flag beside the contact name by default. When activated, it turns red. + * Justification: This feature is used to indicate which contacts are of high importance, whereby there are some important concerns that the interior designers should take note of for each particular contact. + * Highlights: This feature should be used in tandem with the `note` feature for best results as the `note` feature can be used to write down key concerns that the interior designers should take note of for a particular contact. + +* **New Feature**: Added a list important contacts command. + * What it does: It acts like a filter to only display contacts who have the red flag lit up beside their name . + * Justification: This feature will help interior designers pay special attention to those contacts listed so that their needs or concerns will be met. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=xsaints19x&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (5 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the help window by adding a link to the command summary (Pull request [\#111](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/111)) + * Added an alert to inform users that either links has been copied successfully for the help window (Pull request [\#111](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/111)) + +* **Documentation**: + * User Guide: + * Updated description for the commands `add`, `impt`, `impts` and `help`: [\#29](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/29), [\#88](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/88), [\#110](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/110), [\#196](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/196) + * Updated command summary for the commands `add`, `impt` and `impts`: [\#29](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/29), [\#88](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/88) + * Updated UG throughout to standardise styling and emphasis on key points: [\#196](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/196) + * Developer Guide: + * Added description for the enhanced `add` command and added code snippet + * Added implementation details and UML diagram for the `high importance flag` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#20](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/20), [\#99](https://github.com/AY2122S2-CS2103T-T12-2/tp/pull/99) + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S2/forum/issues/244), [2](https://github.com/nus-cs2103-AY2122S2/forum/issues/111)) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/xSaints19x/ped/issues/1), [2](https://github.com/xSaints19x/ped/issues/2), [3](https://github.com/xSaints19x/ped/issues/3)) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..e19227eeb5f 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..e3a38082af8 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -9,5 +9,9 @@ public class Messages { public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_CRITERIA = "%1$s is an invalid criteria!"; + public static final String MESSAGE_SORTED = "List sorted by %1$s"; + public static final String MESSAGE_INCOMPATIBLE_VIEW_MODE = "This command cannot be run in this view mode!"; + public static final String MESSAGE_INDEX_OUT_OF_BOUND = "The given index is out of bound!"; } diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index b1e2767cdd9..af8d3febf66 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -5,6 +5,9 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.logging.Logger; + +import seedu.address.MainApp; /** * Writes and reads files @@ -12,6 +15,7 @@ public class FileUtil { private static final String CHARSET = "UTF-8"; + private static Logger logger = Logger.getLogger(String.valueOf(MainApp.class)); public static boolean isFileExists(Path file) { return Files.exists(file) && Files.isRegularFile(file); @@ -80,4 +84,13 @@ public static void writeToFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(CHARSET)); } + /** + * Deletes the file at the given path. + */ + public static void deleteFile(Path file) { + if (!file.toFile().delete()) { + logger.warning(String.format("File was not deleted: %s", file)); + } + } + } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..99581d06ead 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -3,12 +3,16 @@ import java.nio.file.Path; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * API of the Logic component @@ -16,6 +20,7 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. @@ -23,6 +28,19 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult executeInDetailedViewMode(String commandText) throws CommandException, ParseException; + + /** + * Caches raw user input. + */ + void cacheCommandText(String commandText); + + /** + * Retrieves the command text + * @param i the number of commands to backstep to + */ + CommandHistoryEntry getCommandText(int i); + /** * Returns the AddressBook. * @@ -30,9 +48,18 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ + /** + * Returns an unmodifiable view of the filtered list of persons + */ ObservableList getFilteredPersonList(); + ObservableList getActivatedTagList(); + + SortedList getSortedPersonList(); + + /** Returns the detailed view of a contact */ + ObservableList getDetailedContactView(); + /** * Returns the user prefs' address book file path. */ @@ -47,4 +74,10 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** + * Gets images to be displayed. + */ + ImageDetailsList getImagesToView(); + } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..78576a59002 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -5,16 +5,21 @@ import java.util.logging.Logger; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DetailedViewExecutable; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; import seedu.address.storage.Storage; /** @@ -54,6 +59,23 @@ public CommandResult execute(String commandText) throws CommandException, ParseE return commandResult; } + @Override + public CommandResult executeInDetailedViewMode(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + + CommandResult commandResult; + DetailedViewExecutable command = addressBookParser.parseDetailedViewCommand(commandText); + commandResult = command.executeInDetailedView(model); + + try { + storage.saveAddressBook(model.getAddressBook()); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + + return commandResult; + } + @Override public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); @@ -64,11 +86,31 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getActivatedTagList() { + return model.getActivatedTagList(); + } + + @Override + public SortedList getSortedPersonList() { + return model.getSortedPersonList(); + } + + @Override + public ObservableList getDetailedContactView() { + return model.getDetailedContactView(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); } + @Override + public ImageDetailsList getImagesToView() { + return model.getImagesToView(); + } + @Override public GuiSettings getGuiSettings() { return model.getGuiSettings(); @@ -78,4 +120,14 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public void cacheCommandText(String commandText) { + model.updateCommandHistory(commandText); + } + + @Override + public CommandHistoryEntry getCommandText(int i) { + return model.getCommandHistory(i); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..12e948129a7 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -7,14 +7,17 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Set; + 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 person to the address book. */ -public class AddCommand extends Command { +public class AddCommand extends Command implements DetailedViewExecutable { public static final String COMMAND_WORD = "add"; @@ -26,12 +29,11 @@ public class AddCommand extends Command { + PREFIX_ADDRESS + "ADDRESS " + "[" + PREFIX_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_NAME + "Mary Jane " + + PREFIX_PHONE + "12345678 " + + PREFIX_EMAIL + "maryJ@example.com " + + PREFIX_ADDRESS + "Bukit Timah " + + PREFIX_TAG + "completed "; 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"; @@ -49,15 +51,29 @@ public AddCommand(Person person) { @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); + Set tagList = toAdd.getTags(); if (model.hasPerson(toAdd)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } model.addPerson(toAdd); + + for (Tag tag : tagList) { + if (!model.hasTag(tag)) { + Command createTagCommand = new CreateTagCommand(tag.tagName); + createTagCommand.execute(model); + } + } + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); } + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + return execute(model); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/logic/commands/AddImageCommand.java b/src/main/java/seedu/address/logic/commands/AddImageCommand.java new file mode 100644 index 00000000000..8c8b229c960 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddImageCommand.java @@ -0,0 +1,178 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.VIEW_IMAGES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +import javafx.stage.FileChooser; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.image.util.ImageUtil; +import seedu.address.model.person.Person; + +public class AddImageCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "addimg"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds an image to the person identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String ADD_IMAGE_SUCCESS = "Added %d image(s) to person: %s"; + public static final String ADD_IMAGE_NONE_SELECTED = "No images were selected to be added"; + public static final String ADD_IMAGE_FAIL = "Failed to add image(s)"; + public static final String DUPLICATE_IMAGES = "An image with the name: \"%s\" already exists"; + + private static final Logger logger = Logger.getLogger(String.valueOf(AddImageCommand.class)); + + private final Index targetIndex; + + /** + * Constructs a new add image command object. + * + * @param targetIndex the index of the person to add images to. + */ + public AddImageCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + public AddImageCommand() { + this.targetIndex = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + requireNonNull(targetIndex); + + List lastShownList = model.getSortedPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + List images = openImageChooser(); + + if (images.isEmpty()) { + return new CommandResult(ADD_IMAGE_NONE_SELECTED); + } + + StringBuilder resultStringBuilder = new StringBuilder(); + List imagesToAdd = new ArrayList<>(); + for (File imgFile : images) { + Path destPath = model.getContactImagesFilePath().resolve(imgFile.getName()); + if (ImageUtil.fileExists(imgFile, model.getContactImagesFilePath())) { + resultStringBuilder + .append(String.format(DUPLICATE_IMAGES, imgFile.getName())) + .append("\n"); + continue; + } + ImageDetails copiedImage; + try { + copiedImage = ImageUtil.copyTo(imgFile, destPath); + } catch (IOException ioe) { + throw new CommandException(ADD_IMAGE_FAIL); + } + + imagesToAdd.add(copiedImage); + } + + Person personToEdit = lastShownList.get(targetIndex.getZeroBased()); + Person editedPerson = addImages(personToEdit, imagesToAdd); + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.setImagesToView(editedPerson.getImageDetailsList()); + resultStringBuilder.append(String.format(ADD_IMAGE_SUCCESS, imagesToAdd.size(), editedPerson)); + + return new CommandResult(resultStringBuilder.toString(), VIEW_IMAGES); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + + List images = openImageChooser(); + + if (images.isEmpty()) { + return new CommandResult(ADD_IMAGE_NONE_SELECTED, CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + StringBuilder resultStringBuilder = new StringBuilder(); + List imagesToAdd = new ArrayList<>(); + for (File imgFile : images) { + Path destPath = model.getContactImagesFilePath().resolve(imgFile.getName()); + if (ImageUtil.fileExists(imgFile, model.getContactImagesFilePath())) { + resultStringBuilder + .append(String.format(DUPLICATE_IMAGES, imgFile.getName())) + .append("\n"); + continue; + } + ImageDetails copiedImage; + try { + copiedImage = ImageUtil.copyTo(imgFile, destPath); + } catch (IOException ioe) { + throw new CommandException(ADD_IMAGE_FAIL); + } + + imagesToAdd.add(copiedImage); + } + + Person personToEdit = model.getDetailedContactViewPerson(); + Person editedPerson = addImages(personToEdit, imagesToAdd); + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + resultStringBuilder.append(String.format(ADD_IMAGE_SUCCESS, imagesToAdd.size(), editedPerson)); + + return new CommandResult(resultStringBuilder.toString(), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + private static Person addImages(Person personToEdit, List images) { + ImageDetailsList oldImages = personToEdit.getImageDetailsList(); + ImageDetailsList newImages = oldImages.appendImageDetails(images); + return new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getDeadlines(), personToEdit.getNotes(), + personToEdit.getTags(), personToEdit.getFavouriteStatus(), + personToEdit.getHighImportanceStatus(), newImages); + } + + private List openImageChooser() { + logger.info("Launching file chooser"); + FileChooser fileChooser = ImageUtil.openImageFileChooser(); + + List selectedFiles = fileChooser.showOpenMultipleDialog(null); + if (selectedFiles == null) { + return new ArrayList<>(); + } + return selectedFiles; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddImageCommand)) { + return false; + } + + AddImageCommand e = (AddImageCommand) other; + + return Objects.equals(this.targetIndex, e.targetIndex); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AssignTagCommand.java b/src/main/java/seedu/address/logic/commands/AssignTagCommand.java new file mode 100644 index 00000000000..a3c768ce5f3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AssignTagCommand.java @@ -0,0 +1,170 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Assigns a tag to a contact in the address book. + */ +public class AssignTagCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "assign"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Assigns a tag with a given tag name " + + "(case-insensitive) to a contact identified by a given index number used in the displayed " + + "contacts list.\n" + + "Parameters: INDEX (must be a positive integer), TAGNAME" + + "Example: " + COMMAND_WORD + " 1 friends"; + + public static final String MESSAGE_SUCCESS = "Tag successfully assigned: %1$s"; + public static final String MESSAGE_DUPLICATE_TAG = "This tag was already assigned to this person previously"; + public static final String MESSAGE_UNKNOWN_TAG = "Tag '%1$s' has not been created yet."; + + private final String tagName; + private final Index targetIndex; + + /** + * Creates an AssignTagCommand to assign a {@code Tag} to a {@code Person}. + * @param targetIndex the index of the contact specified. + * @param tagName the name of the Tag. + */ + public AssignTagCommand(Index targetIndex, String tagName) { + this.tagName = tagName; + this.targetIndex = targetIndex; + } + + /** + * Creates an AssignTagCommand to assign a {@code Tag} to the {@code Person} + * in detailed view. + * @param tagName the name of the Tag. + */ + public AssignTagCommand(String tagName) { + this.tagName = tagName; + this.targetIndex = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Tag newTag = new Tag(tagName); + boolean tagHasBeenCreated = model.hasTag(newTag); + + if (!tagHasBeenCreated) { + throw new CommandException(String.format(MESSAGE_UNKNOWN_TAG, tagName)); + } + + List lastShownList = model.getSortedPersonList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(targetIndex.getZeroBased()); + + if (!canAddTag(personToEdit, newTag)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + + Person editedPerson = addTagToNewPerson(personToEdit, newTag); + model.setPerson(personToEdit, editedPerson); + return new CommandResult(String.format(MESSAGE_SUCCESS, editedPerson)); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + Tag newTag = new Tag(tagName); + boolean tagHasBeenCreated = model.hasTag(newTag); + + if (!tagHasBeenCreated) { + throw new CommandException(String.format(MESSAGE_UNKNOWN_TAG, tagName)); + } + + Person personToEdit = model.getDetailedContactViewPerson(); + + if (!canAddTag(personToEdit, newTag)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + + Person editedPerson = addTagToNewPerson(personToEdit, newTag); + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + /** + * Adds a specific {@code Tag} to the {@code Set} of a {@code Person}. + * @param personToEdit the person to add the tag to. + * @param newTag the tag to add. + * @return the person with the added tag. + */ + private static Person addTagToNewPerson(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + Set tags = personToEdit.getTags(); + Favourite favourite = personToEdit.getFavouriteStatus(); + HighImportance highImportance = personToEdit.getHighImportanceStatus(); + ImageDetailsList images = personToEdit.getImageDetailsList(); + + Set newTags = new HashSet<>(tags); + newTags.add(newTag); + + return new Person(name, phone, email, address, deadlines, notes, newTags, favourite, highImportance, images); + } + + /** + * Checks if a specific {@code Tag} can be added to the {@code Set} of a {@code Person}. + * @param personToEdit the person to add the tag to. + * @param newTag the tag to add. + * @return boolean value of whether the tag can be added. + */ + private static boolean canAddTag(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + Set tags = personToEdit.getTags(); + Set newTags = new HashSet<>(tags); + return !newTags.contains(newTag); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AssignTagCommand)) { + return false; + } + + AssignTagCommand e = (AssignTagCommand) other; + return Objects.equals(targetIndex, e.targetIndex) + && this.tagName.equals(e.tagName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..b021139adb4 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -17,6 +17,7 @@ public class ClearCommand extends Command { @Override public CommandResult execute(Model model) { requireNonNull(model); + model.clearDetailedContactView(); model.setAddressBook(new AddressBook()); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..b001e747ce5 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -11,19 +11,23 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ - private final boolean showHelp; + public enum SpecialCommandResult { + SHOW_HELP, // Help information should be shown to the user + EXIT, // The application should exit + VIEW_IMAGES, // Should load up the contact's images + DETAILED_VIEW, // Should show the detailed contact view + LIST_VIEW, // Should show the entire list of contacts + NONE + } - /** The application should exit. */ - private final boolean exit; + private final SpecialCommandResult specialCommandResult; /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, SpecialCommandResult specialCommandResult) { this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; + this.specialCommandResult = requireNonNull(specialCommandResult); } /** @@ -31,19 +35,15 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, SpecialCommandResult.NONE); } public String getFeedbackToUser() { return feedbackToUser; } - public boolean isShowHelp() { - return showHelp; - } - - public boolean isExit() { - return exit; + public SpecialCommandResult getSpecialCommandResult() { + return specialCommandResult; } @Override @@ -57,15 +57,13 @@ public boolean equals(Object other) { return false; } - CommandResult otherCommandResult = (CommandResult) other; - return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + return feedbackToUser.equals(((CommandResult) other).feedbackToUser) + && specialCommandResult == ((CommandResult) other).specialCommandResult; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, specialCommandResult); } } diff --git a/src/main/java/seedu/address/logic/commands/CreateTagCommand.java b/src/main/java/seedu/address/logic/commands/CreateTagCommand.java new file mode 100644 index 00000000000..b38a7a3d050 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CreateTagCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; + +public class CreateTagCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Creates a tag with a given tag name.\n" + + "Parameters: TAG_NAME (case insensitive)\n" + + "Example: " + COMMAND_WORD + " Friends"; + + public static final String MESSAGE_CREATE_TAG_SUCCESS = "Created tag: %1$s"; + public static final String MESSAGE_DUPLICATE_TAG = "This tag already exists in the address book."; + + private final String tagName; + + /** + * Creates a CreateTagCommand to add to {@code UniqueTagList}. + * @param tagName the name of the Tag. + */ + public CreateTagCommand(String tagName) { + requireNonNull(tagName); + this.tagName = tagName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Tag createdTag = new Tag(this.tagName); + if (model.hasTag(createdTag)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + model.addTag(createdTag); + return new CommandResult(String.format(MESSAGE_CREATE_TAG_SUCCESS, this.tagName)); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + Tag createdTag = new Tag(this.tagName); + if (model.hasTag(createdTag)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + model.addTag(createdTag); + return new CommandResult(String.format(MESSAGE_CREATE_TAG_SUCCESS, this.tagName), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CreateTagCommand // instanceof handles nulls + && tagName.equals(((CreateTagCommand) other).tagName)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeadlineCommand.java b/src/main/java/seedu/address/logic/commands/DeadlineCommand.java new file mode 100644 index 00000000000..354228b0ace --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeadlineCommand.java @@ -0,0 +1,120 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEADLINE; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Person; + +public class DeadlineCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "deadline"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Gives a deadline with a description to person identified\n" + + "by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)" + + PREFIX_DEADLINE + "DESCRIPTION DATE (DATE should be in dd/mm/yyyy)\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_DEADLINE + "windows 01/01/2022"; + + public static final String MESSAGE_ADD_DEADLINE_SUCCESS = "Added deadline for: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_NO_DEADLINES_ADDED = "No deadlines given."; + + private final Index targetIndex; + private final DeadlineList deadlines; + + /** + * Creates a DeadlineCommand to add to specified {@code Person}. + * + * @param targetIndex the index of the person specified. + * @param deadlines the date of the deadline. + */ + public DeadlineCommand(Index targetIndex, DeadlineList deadlines) { + requireNonNull(targetIndex); + requireNonNull(deadlines); + this.targetIndex = targetIndex; + this.deadlines = deadlines; + } + + /** + * Creates a DeadlineCommand to add to {@code Person} in detailed view. + * + * @param deadlines the date of the deadline. + */ + public DeadlineCommand(DeadlineList deadlines) { + requireNonNull(deadlines); + this.targetIndex = null; + this.deadlines = deadlines; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToAddDeadline = lastShownList.get(targetIndex.getZeroBased()); + DeadlineList newDeadlineList = personToAddDeadline.getDeadlines().appendDeadlines(deadlines); + Person editedPerson = new Person( + personToAddDeadline.getName(), personToAddDeadline.getPhone(), personToAddDeadline.getEmail(), + personToAddDeadline.getAddress(), newDeadlineList, personToAddDeadline.getNotes(), + personToAddDeadline.getTags(), personToAddDeadline.getFavouriteStatus(), + personToAddDeadline.getHighImportanceStatus(), personToAddDeadline.getImageDetailsList()); + + if (!personToAddDeadline.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToAddDeadline, editedPerson); + return new CommandResult(String.format(MESSAGE_ADD_DEADLINE_SUCCESS, personToAddDeadline)); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + requireNonNull(model); + + Person personToAddDeadline = model.getDetailedContactViewPerson(); + DeadlineList newDeadlineList = personToAddDeadline.getDeadlines().appendDeadlines(deadlines); + Person editedPerson = new Person( + personToAddDeadline.getName(), personToAddDeadline.getPhone(), personToAddDeadline.getEmail(), + personToAddDeadline.getAddress(), newDeadlineList, personToAddDeadline.getNotes(), + personToAddDeadline.getTags(), personToAddDeadline.getFavouriteStatus(), + personToAddDeadline.getHighImportanceStatus(), personToAddDeadline.getImageDetailsList()); + + model.setPerson(personToAddDeadline, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_ADD_DEADLINE_SUCCESS, personToAddDeadline), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeadlineCommand)) { + return false; + } + + // state check + DeadlineCommand e = (DeadlineCommand) other; + return Objects.equals(this.targetIndex, e.targetIndex) + && deadlines.equals(e.deadlines); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..b8826a488d5 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -33,7 +33,7 @@ public DeleteCommand(Index targetIndex) { @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getSortedPersonList(); if (targetIndex.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); diff --git a/src/main/java/seedu/address/logic/commands/DeleteDeadlineCommand.java b/src/main/java/seedu/address/logic/commands/DeleteDeadlineCommand.java new file mode 100644 index 00000000000..64353746c84 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteDeadlineCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Person; + +public class DeleteDeadlineCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "deldl"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the deadline identified by its index number in the list of" + + " deadlines of this person.\n" + + "Parameters: INDEX (must be a positive integer) " + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_DEADLINE_SUCCESS = "Deleted deadline"; + + private final Index index; + + /** + * @param index index of the deadline in the list to delete. + */ + public DeleteDeadlineCommand(Index index) { + requireNonNull(index); + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + throw new CommandException(Messages.MESSAGE_INCOMPATIBLE_VIEW_MODE); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + Person personToEdit = model.getDetailedContactViewPerson(); + if (index.getZeroBased() >= personToEdit.getDeadlines().size()) { + throw new CommandException(Messages.MESSAGE_INDEX_OUT_OF_BOUND); + } + DeadlineList newDeadlineList = personToEdit.getDeadlines().delete(index.getZeroBased()); + Person editedPerson = new Person( + personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), newDeadlineList, personToEdit.getNotes(), + personToEdit.getTags(), personToEdit.getFavouriteStatus(), + personToEdit.getHighImportanceStatus(), personToEdit.getImageDetailsList()); + + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(MESSAGE_DELETE_DEADLINE_SUCCESS, + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteImageCommand.java b/src/main/java/seedu/address/logic/commands/DeleteImageCommand.java new file mode 100644 index 00000000000..0e24ed61433 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteImageCommand.java @@ -0,0 +1,153 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.VIEW_IMAGES; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.image.util.ImageUtil; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +public class DeleteImageCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "delimg"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the image identified by the index number used in the displayed images list of " + + "the person identified by the index number used in the displayed person list.\n" + + "Parameters: PERSON_INDEX (must be a positive integer) i/IMAGE_INDEX\n" + + "Example: " + COMMAND_WORD + " 1 i/2"; + + public static final String MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX = "The image index provided is invalid"; + public static final String MESSAGE_DELETE_IMAGE_SUCCESSFUL = "Image %d has been deleted for person %s"; + + private final Index personIndex; + private final Index imageIndex; + + /** + * Constructs a command to delete an image. + * + * @param personIndex of the person to delete. + * @param imageIndex of the image to delete, relative to the person to delete. + */ + public DeleteImageCommand(Index personIndex, Index imageIndex) { + requireNonNull(personIndex); + requireNonNull(imageIndex); + + this.personIndex = personIndex; + this.imageIndex = imageIndex; + } + + /** + * Constructs a command to delete the image of the {@code Person} in detailed view. + * @param imageIndex of the image to delete. + */ + public DeleteImageCommand(Index imageIndex) { + this.personIndex = null; + this.imageIndex = imageIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + requireNonNull(personIndex); + + List lastShownList = model.getSortedPersonList(); + + if (personIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(personIndex.getZeroBased()); + ImageDetailsList images = personToEdit.getImageDetailsList(); + + if (imageIndex.getZeroBased() >= images.size()) { + throw new CommandException(MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX); + } + + ImageDetails imageToDelete = images.get(imageIndex.getZeroBased()); + ImageUtil.removeFile(imageToDelete); + ImageDetailsList sanitizedList = ImageUtil.sanitizeList(images, model.getContactImagesFilePath()); + Person editedPerson = createImageDeletedPerson(personToEdit, sanitizedList); + + model.setPerson(personToEdit, editedPerson); + model.setImagesToView(editedPerson.getImageDetailsList()); + + return new CommandResult( + String.format(MESSAGE_DELETE_IMAGE_SUCCESSFUL, imageIndex.getOneBased(), editedPerson), VIEW_IMAGES); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + + Person personToEdit = model.getDetailedContactViewPerson(); + ImageDetailsList images = personToEdit.getImageDetailsList(); + + if (imageIndex.getZeroBased() >= images.size()) { + throw new CommandException(MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX); + } + + ImageDetails imageToDelete = images.get(imageIndex.getZeroBased()); + ImageUtil.removeFile(imageToDelete); + ImageDetailsList sanitizedList = ImageUtil.sanitizeList(images, model.getContactImagesFilePath()); + Person editedPerson = createImageDeletedPerson(personToEdit, sanitizedList); + + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + + return new CommandResult( + String.format(MESSAGE_DELETE_IMAGE_SUCCESSFUL, imageIndex.getOneBased(), editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + private static Person createImageDeletedPerson(Person personToEdit, ImageDetailsList sanitizedList) { + assert personToEdit != null; + assert sanitizedList != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + HighImportance highImportanceStatus = personToEdit.getHighImportanceStatus(); + Favourite favouriteStatus = personToEdit.getFavouriteStatus(); + Set tags = personToEdit.getTags(); + + return new Person(name, phone, email, address, deadlines, + notes, tags, favouriteStatus, highImportanceStatus, sanitizedList); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DeleteImageCommand)) { + return false; + } + + DeleteImageCommand e = (DeleteImageCommand) other; + return Objects.equals(this.personIndex, e.personIndex) + && Objects.equals(this.imageIndex, e.imageIndex); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java new file mode 100644 index 00000000000..75137275ac3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java @@ -0,0 +1,61 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; + +public class DeleteNoteCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "delnote"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the note identified by its index number in the list of" + + " notes of this person.\n" + + "Parameters: INDEX (must be a positive integer) " + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_NOTE_SUCCESS = "Deleted note"; + + private final Index index; + + /** + * @param index index of the note in the notes list to delete. + */ + public DeleteNoteCommand(Index index) { + requireNonNull(index); + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + throw new CommandException(Messages.MESSAGE_INCOMPATIBLE_VIEW_MODE); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + Person personToEdit = model.getDetailedContactViewPerson(); + if (index.getZeroBased() >= personToEdit.getNotes().value.size()) { + throw new CommandException(Messages.MESSAGE_INDEX_OUT_OF_BOUND); + } + Person editedPerson = updateNotes(personToEdit, index.getZeroBased()); + + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(MESSAGE_DELETE_NOTE_SUCCESS, + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + private static Person updateNotes(Person personToEdit, int index) { + Notes oldNotes = personToEdit.getNotes(); + Notes newNotes = oldNotes.delete(index); + return new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getDeadlines(), newNotes, + personToEdit.getTags(), personToEdit.getFavouriteStatus(), personToEdit.getHighImportanceStatus(), + personToEdit.getImageDetailsList()); + } +} 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..61af44c9b83 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteTagCommand.java @@ -0,0 +1,149 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javafx.collections.ObservableList; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Deletes tags that contain any of the argument keywords, and unassigns the tag from any contacts. + * Keyword matching is case insensitive. + */ +public class DeleteTagCommand extends Command { + + public static final String COMMAND_WORD = "deltag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes tags that contain any of " + + "the specified keywords (case-insensitive), automatically unassigns the tag from any contacts " + + "and displays a list of contacts who had the tags unassigned from them." + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " friends colleague"; + + public static final String MESSAGE_TAG_NOT_EXIST = "One or more of the specified tag(s) does not exist: %s"; + public static final String MESSAGE_DELETE_SUCCESS = "Deleted tags: %s"; + public static final String MESSAGE_DELETE_FAIL = "All specified tag(s) do not exist."; + + private final List keywords; + + public DeleteTagCommand(List keywords) { + this.keywords = keywords; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.clearActivatedTagList(); + model.clearDetailedContactView(); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + ObservableList allPersons = model.getFilteredPersonList(); + + Set tagsToDelete = new HashSet<>(); + Set tagsNotExist = new HashSet<>(); + + boolean hasUncreatedTags = false; + + for (String keyword : keywords) { + Tag toDelete = new Tag(keyword); + if (!model.hasTag(toDelete)) { + hasUncreatedTags = true; + tagsNotExist.add(keyword); + continue; + } + + for (Person person : allPersons) { + if (canRemoveTag(person, toDelete)) { + Person editedPerson = removeTagFromNewPerson(person, toDelete); + model.setPerson(person, editedPerson); + } + } + + tagsToDelete.add(toDelete); + } + + for (Tag tag : tagsToDelete) { + model.deleteTag(tag); + } + + + + Set tagNamesToDelete = tagsToDelete.stream().map(tag -> tag.tagName).collect(Collectors.toSet()); + + if (hasUncreatedTags) { + if (tagNamesToDelete.size() >= 1) { + String result = MESSAGE_TAG_NOT_EXIST + "\n" + MESSAGE_DELETE_SUCCESS; + return new CommandResult(String.format(result, tagsNotExist, tagNamesToDelete)); + } else { + throw new CommandException(MESSAGE_DELETE_FAIL); + } + } + + return new CommandResult(String.format(MESSAGE_DELETE_SUCCESS, tagNamesToDelete)); + } + + + /** + * Removes a specific {@code Tag} from the {@code Set} of a {@code Person}. + * @param personToEdit the person to remove the tag from. + * @param newTag the tag to remove. + * @return the person with the removed tag. + */ + private static Person removeTagFromNewPerson(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + Set tags = personToEdit.getTags(); + Favourite favourite = personToEdit.getFavouriteStatus(); + HighImportance highImportance = personToEdit.getHighImportanceStatus(); + ImageDetailsList images = personToEdit.getImageDetailsList(); + + Set newTags = new HashSet<>(tags); + newTags.remove(newTag); + + return new Person(name, phone, email, address, deadlines, notes, newTags, favourite, highImportance, images); + } + + /** + * Checks if a specific {@code Tag} can be removed from the {@code Set} of a {@code Person}. + * @param personToEdit the person to remove the tag from. + * @param newTag the tag to remove. + * @return boolean value of whether the tag can be removed. + */ + private static boolean canRemoveTag(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + Set tags = personToEdit.getTags(); + Set newTags = new HashSet<>(tags); + return newTags.contains(newTag); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteTagCommand // instanceof handles nulls + && keywords.equals(((DeleteTagCommand) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/DetailedViewExecutable.java b/src/main/java/seedu/address/logic/commands/DetailedViewExecutable.java new file mode 100644 index 00000000000..e6a96a3aca1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DetailedViewExecutable.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents the ability to execute on Model when a Person is in DetailedContactView + */ +public interface DetailedViewExecutable { + /** + * Executes the command in the detailed view context and returns the result. Commands that + * execute with this method should operate on the Person in detailed view and return + * a {@code CommandResult} that has the {@code SpecialCommandResult} of {@code DETAILED_VIEW}, + * unless it is a command that changes the view. + * + * @param model {@code} Model that the command should operate on. + * @return feedback message of the operation result to display. + * @throws CommandException If an error occurs during command execution. + */ + CommandResult executeInDetailedView(Model model) throws CommandException; +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..74adac553b0 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -2,15 +2,14 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEADLINE; 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.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import java.util.Collections; -import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -19,9 +18,14 @@ import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -29,7 +33,7 @@ /** * Edits the details of an existing person in the address book. */ -public class EditCommand extends Command { +public class EditCommand extends Command implements DetailedViewExecutable { public static final String COMMAND_WORD = "edit"; @@ -41,6 +45,7 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_DEADLINE + "DEADLINE] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -65,10 +70,20 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } + /** + * Constructs an EditCommand for {@code Person} in detailed view + * @param editPersonDescriptor details to edit the person with + */ + public EditCommand(EditPersonDescriptor editPersonDescriptor) { + requireNonNull(editPersonDescriptor); + this.index = null; + this.editPersonDescriptor = editPersonDescriptor; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getSortedPersonList(); if (index.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); @@ -82,10 +97,26 @@ public CommandResult execute(Model model) throws CommandException { } model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + + Person personToEdit = model.getDetailedContactViewPerson(); + Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + /** * Creates and returns a {@code Person} with the details of {@code personToEdit} * edited with {@code editPersonDescriptor}. @@ -97,9 +128,16 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + DeadlineList oldDeadlines = personToEdit.getDeadlines(); + Notes oldNotes = personToEdit.getNotes(); + Set oldTags = personToEdit.getTags(); + Favourite favouriteStatus = personToEdit.getFavouriteStatus(); + HighImportance highImportanceStatus = personToEdit.getHighImportanceStatus(); + ImageDetailsList imageDetailsList = personToEdit.getImageDetailsList(); + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, oldDeadlines, + oldNotes, oldTags, favouriteStatus, highImportanceStatus, imageDetailsList); } @Override @@ -116,7 +154,7 @@ public boolean equals(Object other) { // state check EditCommand e = (EditCommand) other; - return index.equals(e.index) + return Objects.equals(this.index, e.index) && editPersonDescriptor.equals(e.editPersonDescriptor); } @@ -129,7 +167,6 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; - private Set tags; public EditPersonDescriptor() {} @@ -142,14 +179,13 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); - setTags(toCopy.tags); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address); } public void setName(Name name) { @@ -184,23 +220,6 @@ public Optional
getAddress() { return Optional.ofNullable(address); } - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - @Override public boolean equals(Object other) { // short circuit if same object @@ -219,8 +238,7 @@ public boolean equals(Object other) { return getName().equals(e.getName()) && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); + && getAddress().equals(e.getAddress()); } } } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..dd2e910a5be 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -5,7 +5,7 @@ /** * Terminates the program. */ -public class ExitCommand extends Command { +public class ExitCommand extends Command implements DetailedViewExecutable { public static final String COMMAND_WORD = "exit"; @@ -13,7 +13,11 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, CommandResult.SpecialCommandResult.EXIT); } + @Override + public CommandResult executeInDetailedView(Model model) { + return execute(model); + } } diff --git a/src/main/java/seedu/address/logic/commands/FavouriteCommand.java b/src/main/java/seedu/address/logic/commands/FavouriteCommand.java new file mode 100644 index 00000000000..99bdd2a933d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FavouriteCommand.java @@ -0,0 +1,122 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +public class FavouriteCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "fav"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Favourites the person identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_FAVOURITE_PERSON_SUCCESS = "Changed Person's Favourite Status: %1$s"; + + private final Index targetIndex; + + /** + * @param targetIndex of the person in the filtered person list to favourite. + */ + public FavouriteCommand(Index targetIndex) { + requireNonNull(targetIndex); + + this.targetIndex = targetIndex; + } + + public FavouriteCommand() { + this.targetIndex = null; + } + + /** + * Executes the Favourite command based on the {@code targetIndex} given. + * + * @param model {@code Model} which the command should operate on. + * @return the result of the command. + * @throws CommandException if the index given is out of bounds. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToFavourite = lastShownList.get(targetIndex.getZeroBased()); + Favourite newFavouriteStatus = personToFavourite.isFavourite() + ? Favourite.NOT_FAVOURITE + : Favourite.IS_FAVOURITE; + Person editedPerson = createFavouritedPerson(personToFavourite, newFavouriteStatus); + + model.setPerson(personToFavourite, editedPerson); + return new CommandResult(String.format(MESSAGE_FAVOURITE_PERSON_SUCCESS, editedPerson)); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + requireNonNull(model); + + Person personToFavourite = model.getDetailedContactViewPerson(); + Favourite newFavouriteStatus = personToFavourite.isFavourite() + ? Favourite.NOT_FAVOURITE + : Favourite.IS_FAVOURITE; + Person editedPerson = createFavouritedPerson(personToFavourite, newFavouriteStatus); + + model.setPerson(personToFavourite, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_FAVOURITE_PERSON_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + private static Person createFavouritedPerson(Person personToEdit, Favourite newFavouriteStatus) { + assert personToEdit != null; + assert newFavouriteStatus != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + HighImportance highImportanceStatus = personToEdit.getHighImportanceStatus(); + Set tags = personToEdit.getTags(); + ImageDetailsList imageDetailsList = personToEdit.getImageDetailsList(); + + return new Person(name, phone, email, address, deadlines, + notes, tags, newFavouriteStatus, highImportanceStatus, imageDetailsList); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FavouriteCommand)) { + return false; + } + + FavouriteCommand e = (FavouriteCommand) other; + return Objects.equals(targetIndex, e.targetIndex); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index d6b19b0a0de..91c06c43884 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -28,6 +28,8 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { @Override public CommandResult execute(Model model) { requireNonNull(model); + model.clearDetailedContactView(); + model.clearActivatedTagList(); model.updateFilteredPersonList(predicate); return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); diff --git a/src/main/java/seedu/address/logic/commands/FindTagCommand.java b/src/main/java/seedu/address/logic/commands/FindTagCommand.java new file mode 100644 index 00000000000..fb9d3a4a5c6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindTagCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.TagContainsKeywordsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Finds and lists all contacts in address book whose tag contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindTagCommand extends Command { + + public static final String COMMAND_WORD = "findtag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts whose tags 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 + " friends colleague"; + + public static final String TAG_NOT_EXIST = "One or more tags do not exist"; + + public static final String TAG_ALREADY_ACTIVATED = "One or more tags have already been selected"; + + private final List keywords; + + public FindTagCommand(List keywords) { + this.keywords = keywords; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.clearDetailedContactView(); + + boolean hasAllTag = true; + List tagsToAdd = new ArrayList<>(); + for (String keyword : keywords) { + Tag tagToAdd = new Tag(keyword); + hasAllTag = hasAllTag && model.hasTag(new Tag(keyword)); + boolean tagActivated = model.getActivatedTagList().contains(tagToAdd); + + if (!hasAllTag) { + throw new CommandException(TAG_NOT_EXIST); + } + + if (tagActivated) { + throw new CommandException(TAG_ALREADY_ACTIVATED); + } + tagsToAdd.add(tagToAdd); + } + + for (Tag tag : tagsToAdd) { + model.addActivatedTag(tag); + } + + if (keywords.size() == 0) { + throw new CommandException(MESSAGE_USAGE); + } + ObservableList activatedTagList = model.getActivatedTagList(); + + TagContainsKeywordsPredicate predicate = + new TagContainsKeywordsPredicate(activatedTagList.stream().map(tag -> tag.tagName) + .collect(Collectors.toList())); + model.updateFilteredPersonList(predicate); + + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindTagCommand // instanceof handles nulls + // && predicate.equals(((FindTagCommand) other).predicate)); // state check + && keywords.equals(((FindTagCommand) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..4c36823b7d2 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -5,7 +5,7 @@ /** * Format full help instructions for every command for display. */ -public class HelpCommand extends Command { +public class HelpCommand extends Command implements DetailedViewExecutable { public static final String COMMAND_WORD = "help"; @@ -16,6 +16,11 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, CommandResult.SpecialCommandResult.SHOW_HELP); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + return execute(model); } } diff --git a/src/main/java/seedu/address/logic/commands/HighImportanceCommand.java b/src/main/java/seedu/address/logic/commands/HighImportanceCommand.java new file mode 100644 index 00000000000..5cbe862fb2f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/HighImportanceCommand.java @@ -0,0 +1,129 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + + +public class HighImportanceCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "impt"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a high importance tag to the person identified by the index number " + + "used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS = "Changed Person's Importance Status: %1$s"; + + private final Index index; + + /** + * @param index of the person in the filtered person list to favourite. + */ + public HighImportanceCommand(Index index) { + requireNonNull(index); + + this.index = index; + } + + public HighImportanceCommand() { + this.index = null; + } + + /** + * Executes the HighImportance command based on the {@code index} given. + * + * @param model {@code Model} which the command should operate on. + * @return the result of the command. + * @throws CommandException if the index given is out of bounds. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person highImportancePerson = lastShownList.get(index.getZeroBased()); + // Checks if person is already of high importance + HighImportance highImportanceStatus = + highImportancePerson.hasHighImportance() + ? HighImportance.NOT_HIGH_IMPORTANCE + : HighImportance.HIGH_IMPORTANCE; + Person editedPerson = createHighImportancePerson(highImportancePerson, highImportanceStatus); + + model.setPerson(highImportancePerson, editedPerson); + return new CommandResult(String.format(MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS, editedPerson)); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + requireNonNull(model); + + Person highImportancePerson = model.getDetailedContactViewPerson(); + // Checks if person is already of high importance + HighImportance highImportanceStatus = + highImportancePerson.hasHighImportance() + ? HighImportance.NOT_HIGH_IMPORTANCE + : HighImportance.HIGH_IMPORTANCE; + Person editedPerson = createHighImportancePerson(highImportancePerson, highImportanceStatus); + + model.setPerson(highImportancePerson, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + private static Person createHighImportancePerson(Person highImportancePerson, + HighImportance newHighImportanceStatus) { + assert highImportancePerson != null; + assert newHighImportanceStatus != null; + + Name name = highImportancePerson.getName(); + Phone phone = highImportancePerson.getPhone(); + Email email = highImportancePerson.getEmail(); + Address address = highImportancePerson.getAddress(); + DeadlineList deadlines = highImportancePerson.getDeadlines(); + Notes notes = highImportancePerson.getNotes(); + Favourite favouriteStatus = highImportancePerson.getFavouriteStatus(); + Set tags = highImportancePerson.getTags(); + ImageDetailsList images = highImportancePerson.getImageDetailsList(); + + return new Person(name, phone, email, address, deadlines, + notes, tags, favouriteStatus, newHighImportanceStatus, images); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof HighImportanceCommand)) { + return false; + } + + HighImportanceCommand e = (HighImportanceCommand) other; + return Objects.equals(index, e.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ImagesCommand.java b/src/main/java/seedu/address/logic/commands/ImagesCommand.java new file mode 100644 index 00000000000..d73a216c3b3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImagesCommand.java @@ -0,0 +1,152 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.address.MainApp; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.image.util.ImageUtil; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +public class ImagesCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "images"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows the images of the person identified " + + "by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1\n"; + + public static final String MESSAGE_IMAGES_SUCCESS = "%d Images for Person [%s]:\n %s"; + + private static final Logger logger = Logger.getLogger(String.valueOf(MainApp.class)); + + private final Index index; + + /** + * Creates the command to show all images of a person. + * + * @param index of the persons whose images are to be displayed. + */ + public ImagesCommand(Index index) { + requireNonNull(index); + this.index = index; + } + + public ImagesCommand() { + this.index = null; + } + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + requireNonNull(index); + + List lastShownList = model.getSortedPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person targetPerson = lastShownList.get(index.getZeroBased()); + logger.info(String.format("Sanitizing images of person at index %d, at file path: %s", index.getZeroBased(), + model.getContactImagesFilePath())); + + ImageDetailsList originalList = targetPerson.getImageDetailsList(); + ImageDetailsList sanitizedList = ImageUtil.sanitizeList(originalList, model.getContactImagesFilePath()); + Person sanitizedPerson = createImageDeletedPerson(targetPerson, sanitizedList); + logger.info(String.format("Result of sanitization: %d -> %d", originalList.size(), sanitizedList.size())); + + if (originalList.size() != sanitizedList.size()) { + model.setPerson(targetPerson, sanitizedPerson); + } + + model.setImagesToView(sanitizedList); + + String result = String.format(MESSAGE_IMAGES_SUCCESS, sanitizedList.size(), + index.getOneBased(), sanitizedList); + return new CommandResult(result, CommandResult.SpecialCommandResult.VIEW_IMAGES); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + requireNonNull(model); + + Person targetPerson = model.getDetailedContactViewPerson(); + logger.info("Sanitizing images of person in detailed view"); + + ImageDetailsList originalList = targetPerson.getImageDetailsList(); + ImageDetailsList sanitizedList = ImageUtil.sanitizeList(originalList, model.getContactImagesFilePath()); + Person sanitizedPerson = createImageDeletedPerson(targetPerson, sanitizedList); + logger.info(String.format("Result of sanitization: %d -> %d", originalList.size(), sanitizedList.size())); + + if (originalList.size() != sanitizedList.size()) { + model.setPerson(targetPerson, sanitizedPerson); + } + + model.setImagesToView(sanitizedList); + model.clearDetailedContactView(); + + String result = + String.format(MESSAGE_IMAGES_SUCCESS, originalList.size(), targetPerson.getName(), sanitizedList); + return new CommandResult(result, CommandResult.SpecialCommandResult.VIEW_IMAGES); + } + + private static Person createImageDeletedPerson(Person personToEdit, ImageDetailsList sanitizedList) { + assert personToEdit != null; + assert sanitizedList != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + HighImportance highImportanceStatus = personToEdit.getHighImportanceStatus(); + Favourite favouriteStatus = personToEdit.getFavouriteStatus(); + Set tags = personToEdit.getTags(); + + return new Person(name, phone, email, address, deadlines, + notes, tags, favouriteStatus, highImportanceStatus, sanitizedList); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ImagesCommand)) { + return false; + } + + ImagesCommand e = (ImagesCommand) other; + + return Objects.equals(this.index, e.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..4d6b184f10b 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -8,17 +8,28 @@ /** * Lists all persons in the address book to the user. */ -public class ListCommand extends Command { +public class ListCommand extends Command implements DetailedViewExecutable { public static final String COMMAND_WORD = "list"; public static final String MESSAGE_SUCCESS = "Listed all persons"; - @Override public CommandResult execute(Model model) { requireNonNull(model); + model.clearDetailedContactView(); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.clearActivatedTagList(); return new CommandResult(MESSAGE_SUCCESS); } + + @Override + public CommandResult executeInDetailedView(Model model) { + return execute(model); + } + + @Override + public boolean equals(Object other) { + return other instanceof ListCommand; + } } diff --git a/src/main/java/seedu/address/logic/commands/ListFavouritesCommand.java b/src/main/java/seedu/address/logic/commands/ListFavouritesCommand.java new file mode 100644 index 00000000000..4555a03dd44 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListFavouritesCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.PersonIsFavouriteContactPredicate; + +/** + * Finds and lists all persons in address book who are favourite contacts. + */ +public class ListFavouritesCommand extends Command { + + public static final String COMMAND_WORD = "favourites"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all person who have been set as a " + + "favourite contact and displays them as a list with index numbers.\n" + + "Example: " + COMMAND_WORD; + + private static final PersonIsFavouriteContactPredicate predicate = new PersonIsFavouriteContactPredicate(); + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.clearDetailedContactView(); + model.clearActivatedTagList(); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ListFavouritesCommand); // instanceof handles nulls + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListImportantCommand.java b/src/main/java/seedu/address/logic/commands/ListImportantCommand.java new file mode 100644 index 00000000000..b4cb3124eb2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListImportantCommand.java @@ -0,0 +1,35 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.PersonHasHighImportancePredicate; + +/** + * Finds and lists all persons in address book who are of high importance contacts. + */ +public class ListImportantCommand extends Command { + + public static final String COMMAND_WORD = "impts"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts who have been set as a " + + "contact with high importance and displays them as a list with index numbers.\n"; + + private static final PersonHasHighImportancePredicate predicate = new PersonHasHighImportancePredicate(); + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.clearActivatedTagList(); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ListImportantCommand); // instanceof handles nulls + } +} diff --git a/src/main/java/seedu/address/logic/commands/NoteCommand.java b/src/main/java/seedu/address/logic/commands/NoteCommand.java new file mode 100644 index 00000000000..e179ce32528 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/NoteCommand.java @@ -0,0 +1,136 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; + + +/** + * Updates the notes of a person + */ +public class NoteCommand extends Command implements DetailedViewExecutable { + public static final String COMMAND_WORD = "note"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds the given note to the person identified " + + "by the index number used in the last person listing.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_NOTE + "NOTES\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_NOTE + "Likes to swim."; + + public static final String MESSAGE_UPDATE_NOTE_SUCCESS = "Added note to Person: %1$s"; + + public static final String MESSAGE_NO_UPDATE_TO_NOTES = "No note added to Person: %1$s"; + + private final Index index; + private final String note; + + /** + * @param index index of the person in the filtered list to edit + * @param note new note to update with + */ + public NoteCommand(Index index, String note) { + requireAllNonNull(index, note); + + this.index = index; + this.note = note; + } + + /** + * Constructs NoteCommand without Index that operates on Person in detailed view. + * @param note new note to update with + */ + public NoteCommand(String note) { + requireNonNull(note); + + this.index = null; + this.note = note; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + if (!Notes.isValidNote(note)) { + return new CommandResult(generateNoChangeMessage(personToEdit)); + } + Person editedPerson = updateNotes(personToEdit, note); + model.setPerson(personToEdit, editedPerson); + return new CommandResult(generateSuccessMessage(editedPerson)); + } + + @Override + public CommandResult executeInDetailedView(Model model) { + requireNonNull(model); + Person personToEdit = model.getDetailedContactViewPerson(); + if (!Notes.isValidNote(note)) { + return new CommandResult(generateNoChangeMessage(personToEdit), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + Person editedPerson = updateNotes(personToEdit, note); + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(generateSuccessMessage(editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + /** + * Creates the message to be printed for the result of this NoteCommand + * @param personToEdit the person edited by this NoteCommand + * @return message to be printed + */ + public String generateSuccessMessage(Person personToEdit) { + return String.format(MESSAGE_UPDATE_NOTE_SUCCESS, personToEdit); + } + + public String generateNoChangeMessage(Person person) { + return String.format(MESSAGE_NO_UPDATE_TO_NOTES, person); + } + + /** + * Creates new Person object with the note added + * @param personToEdit person to add note to + * @param note note to add + * @return new Person with note added + */ + private static Person updateNotes(Person personToEdit, String note) { + Notes oldNotes = personToEdit.getNotes(); + Notes newNotes = oldNotes.updateNotes(note); + return new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getDeadlines(), newNotes, + personToEdit.getTags(), personToEdit.getFavouriteStatus(), personToEdit.getHighImportanceStatus(), + personToEdit.getImageDetailsList()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof NoteCommand)) { + return false; + } + + NoteCommand e = (NoteCommand) other; + return Objects.equals(this.index, e.index) + && note.equals(e.note); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortsCommand.java b/src/main/java/seedu/address/logic/commands/SortsCommand.java new file mode 100644 index 00000000000..664828f90af --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortsCommand.java @@ -0,0 +1,83 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; + +/** + * Sorts list based on criteria given. + * Criteria matching is case sensitive. + */ +public class SortsCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts the list of people by specified " + + "criteria and displays them as a list with index numbers.\n" + + "Parameters: CRITERIA\n" + + "Example: " + COMMAND_WORD + " name\n" + + "Valid criteria: %1$s"; + + private final String criteria; + + public SortsCommand(String criteria) { + this.criteria = criteria; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + switch (criteria) { + + case Name.MODEL_NAME: + model.sortFilteredPersonListByName(); + break; + + case Address.MODEL_NAME: + model.sortFilteredPersonListByAddress(); + break; + + case DeadlineList.MODEL_NAME: + model.sortFilteredPersonListByDeadlineList(); + break; + + case Email.MODEL_NAME: + model.sortFilteredPersonListByEmail(); + break; + + case Phone.MODEL_NAME: + model.sortFilteredPersonListByPhone(); + break; + + case Favourite.MODEL_NAME: + model.sortFilteredPersonListByFavourite(); + break; + + case HighImportance.MODEL_NAME: + model.sortFilteredPersonListByHighImportance(); + break; + + default: + throw new CommandException(String.format(Messages.MESSAGE_INVALID_CRITERIA, criteria)); + } + return new CommandResult( + String.format(Messages.MESSAGE_SORTED, criteria)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SortsCommand // instanceof handles nulls + && criteria.equals(((SortsCommand) other).criteria)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/UnassignTagCommand.java b/src/main/java/seedu/address/logic/commands/UnassignTagCommand.java new file mode 100644 index 00000000000..17033aa7384 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UnassignTagCommand.java @@ -0,0 +1,170 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Unassigns a tag to a contact in the address book. + */ +public class UnassignTagCommand extends Command implements DetailedViewExecutable { + + public static final String COMMAND_WORD = "unassign"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Unassigns a tag with a given tag name " + + "(case-insensitive) to a contact identified by a given index number used in the displayed " + + "contacts list.\n" + + "Parameters: INDEX (must be a positive integer), TAGNAME" + + "Example: " + COMMAND_WORD + " 1 friends"; + + public static final String MESSAGE_SUCCESS = "Tag successfully unassigned: %1$s"; + public static final String MESSAGE_NOT_TAGGED = "This tag was not assigned to this person previously"; + public static final String MESSAGE_UNKNOWN_TAG = "Tag '%1$s' has not been created yet."; + + private final String tagName; + private final Index targetIndex; + + /** + * Creates an UnassignTagCommand to un-assign a {@code Tag} from a {@code Person}. + * @param targetIndex the index of the contact specified. + * @param tagName the name of the Tag. + */ + public UnassignTagCommand(Index targetIndex, String tagName) { + this.tagName = tagName; + this.targetIndex = targetIndex; + } + + /** + * Creates an UnassignTagCommand to un-assign a {@code Tag} from the {@code Person} + * in detailed view. + * @param tagName the name of the Tag. + */ + public UnassignTagCommand(String tagName) { + this.tagName = tagName; + this.targetIndex = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Tag newTag = new Tag(tagName); + boolean tagHasBeenCreated = model.hasTag(newTag); + + if (!tagHasBeenCreated) { + throw new CommandException(String.format(MESSAGE_UNKNOWN_TAG, tagName)); + } + + List lastShownList = model.getSortedPersonList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(targetIndex.getZeroBased()); + + if (!canRemoveTag(personToEdit, newTag)) { + throw new CommandException(MESSAGE_NOT_TAGGED); + } + + Person editedPerson = removeTagFromNewPerson(personToEdit, newTag); + model.setPerson(personToEdit, editedPerson); + return new CommandResult(String.format(MESSAGE_SUCCESS, editedPerson)); + } + + @Override + public CommandResult executeInDetailedView(Model model) throws CommandException { + requireNonNull(model); + Tag newTag = new Tag(tagName); + boolean tagHasBeenCreated = model.hasTag(newTag); + + if (!tagHasBeenCreated) { + throw new CommandException(String.format(MESSAGE_UNKNOWN_TAG, tagName)); + } + + Person personToEdit = model.getDetailedContactViewPerson(); + + if (!canRemoveTag(personToEdit, newTag)) { + throw new CommandException(MESSAGE_NOT_TAGGED); + } + + Person editedPerson = removeTagFromNewPerson(personToEdit, newTag); + model.setPerson(personToEdit, editedPerson); + model.setDetailedContactView(editedPerson); + return new CommandResult(String.format(MESSAGE_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + /** + * Removes a specific {@code Tag} from the {@code Set} of a {@code Person}. + * @param personToEdit the person to remove the tag from. + * @param newTag the tag to remove. + * @return the person with the removed tag. + */ + private static Person removeTagFromNewPerson(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + DeadlineList deadlines = personToEdit.getDeadlines(); + Notes notes = personToEdit.getNotes(); + Set tags = personToEdit.getTags(); + Favourite favourite = personToEdit.getFavouriteStatus(); + HighImportance highImportance = personToEdit.getHighImportanceStatus(); + ImageDetailsList images = personToEdit.getImageDetailsList(); + + Set newTags = new HashSet<>(tags); + newTags.remove(newTag); + + return new Person(name, phone, email, address, deadlines, notes, newTags, favourite, highImportance, images); + } + + /** + * Checks if a specific {@code Tag} can be removed from the {@code Set} of a {@code Person}. + * @param personToEdit the person to remove the tag from. + * @param newTag the tag to remove. + * @return boolean value of whether the tag can be removed. + */ + private static boolean canRemoveTag(Person personToEdit, Tag newTag) { + requireNonNull(personToEdit); + requireNonNull(newTag); + Set tags = personToEdit.getTags(); + Set newTags = new HashSet<>(tags); + return newTags.contains(newTag); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof UnassignTagCommand)) { + return false; + } + + UnassignTagCommand e = (UnassignTagCommand) other; + return Objects.equals(targetIndex, e.targetIndex) + && this.tagName.equals(e.tagName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewCommand.java b/src/main/java/seedu/address/logic/commands/ViewCommand.java new file mode 100644 index 00000000000..c98df9c1d2b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +public class ViewCommand extends Command { + public static final String COMMAND_WORD = "view"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Opens a more detailed view of the contact identified " + + "by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String VIEW_SUCCESS = "Viewing %1$s"; + + private final Index index; + + public ViewCommand(Index index) { + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List lastShownList = model.getSortedPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToView = lastShownList.get(index.getZeroBased()); + model.setDetailedContactView(personToView); + return new CommandResult(String.format(VIEW_SUCCESS, personToView.getName().fullName), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ViewCommand)) { + return false; + } + + ViewCommand e = (ViewCommand) other; + return Objects.equals(this.index, e.index); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e8..a63cfc31961 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -7,14 +7,20 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -33,7 +39,7 @@ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } @@ -41,10 +47,22 @@ public AddCommand parse(String args) throws ParseException { 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 tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Optional optionalAddress = argMultimap.getValue(PREFIX_ADDRESS); + String addressString = ""; + Address address = Address.EMPTY_ADDRESS; + if (optionalAddress.isPresent()) { + addressString = optionalAddress.get(); + address = ParserUtil.parseAddress(addressString); + } + + DeadlineList deadlines = new DeadlineList(); + Notes notes = Notes.getNewNotes(); + ImageDetailsList emptyImageList = new ImageDetailsList(); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Person person = new Person(name, phone, email, address, deadlines, notes, + tagList, Favourite.NOT_FAVOURITE, HighImportance.NOT_HIGH_IMPORTANCE, + emptyImageList); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddImageCommandParser.java b/src/main/java/seedu/address/logic/parser/AddImageCommandParser.java new file mode 100644 index 00000000000..c8c5e6d4e06 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddImageCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddImageCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddImageCommand object. + */ +public class AddImageCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddImageCommand + * and returns a AddImageCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public AddImageCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new AddImageCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddImageCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..19fc666af5e 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -1,5 +1,6 @@ package seedu.address.logic.parser; +import static seedu.address.commons.core.Messages.MESSAGE_INCOMPATIBLE_VIEW_MODE; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; @@ -7,14 +8,33 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddImageCommand; +import seedu.address.logic.commands.AssignTagCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CreateTagCommand; +import seedu.address.logic.commands.DeadlineCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteDeadlineCommand; +import seedu.address.logic.commands.DeleteImageCommand; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.commands.DeleteTagCommand; +import seedu.address.logic.commands.DetailedViewExecutable; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FavouriteCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HighImportanceCommand; +import seedu.address.logic.commands.ImagesCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListFavouritesCommand; +import seedu.address.logic.commands.ListImportantCommand; +import seedu.address.logic.commands.NoteCommand; +import seedu.address.logic.commands.SortsCommand; +import seedu.address.logic.commands.UnassignTagCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -68,6 +88,151 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case NoteCommand.COMMAND_WORD: + return new NoteCommandParser().parse(arguments); + + case FavouriteCommand.COMMAND_WORD: + return new FavouriteCommandParser().parse(arguments); + + case ListFavouritesCommand.COMMAND_WORD: + return new ListFavouritesCommand(); + + case FindTagCommand.COMMAND_WORD: + return new FindTagCommandParser().parse(arguments); + + case DeadlineCommand.COMMAND_WORD: + return new DeadlineCommandParser().parse(arguments); + + case ViewCommand.COMMAND_WORD: + return new ViewCommandParser().parse(arguments); + + case HighImportanceCommand.COMMAND_WORD: + return new HighImportanceCommandParser().parse(arguments); + + case ListImportantCommand.COMMAND_WORD: + return new ListImportantCommand(); + + case CreateTagCommand.COMMAND_WORD: + return new CreateTagCommandParser().parse(arguments); + + case SortsCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case AssignTagCommand.COMMAND_WORD: + return new AssignTagCommandParser().parse(arguments); + + case UnassignTagCommand.COMMAND_WORD: + return new UnassignTagCommandParser().parse(arguments); + + case AddImageCommand.COMMAND_WORD: + return new AddImageCommandParser().parse(arguments); + + case ImagesCommand.COMMAND_WORD: + return new ImagesCommandParser().parse(arguments); + + case DeleteImageCommand.COMMAND_WORD: + return new DeleteImageCommandParser().parse(arguments); + + case DeleteTagCommand.COMMAND_WORD: + return new DeleteTagCommandParser().parse(arguments); + + case DeleteDeadlineCommand.COMMAND_WORD: + // Fallthrough + case DeleteNoteCommand.COMMAND_WORD: + throw new ParseException(MESSAGE_INCOMPATIBLE_VIEW_MODE); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * Parses user input for a command to execute in detailed view mode. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public DetailedViewExecutable parseDetailedViewCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + case AddCommand.COMMAND_WORD: + return new AddCommandParser().parse(arguments); + + case DeadlineCommand.COMMAND_WORD: + return new DeadlineCommandParser().parseInDetailedViewContext(arguments); + + case EditCommand.COMMAND_WORD: + return new EditCommandParser().parseInDetailedViewContext(arguments); + + case NoteCommand.COMMAND_WORD: + return new NoteCommandParser().parseInDetailedViewContext(arguments); + + case FavouriteCommand.COMMAND_WORD: + return new FavouriteCommand(); + + case HighImportanceCommand.COMMAND_WORD: + return new HighImportanceCommand(); + + case CreateTagCommand.COMMAND_WORD: + return new CreateTagCommandParser().parse(arguments); + + case AssignTagCommand.COMMAND_WORD: + return new AssignTagCommandParser().parseInDetailedViewContext(arguments); + + case UnassignTagCommand.COMMAND_WORD: + return new UnassignTagCommandParser().parseInDetailedViewContext(arguments); + + case DeleteNoteCommand.COMMAND_WORD: + return new DeleteNoteCommandParser().parseInDetailedViewContext(arguments); + + case DeleteDeadlineCommand.COMMAND_WORD: + return new DeleteDeadlineCommandParser().parseInDetailedViewContext(arguments); + + case AddImageCommand.COMMAND_WORD: + return new AddImageCommand(); + + case DeleteImageCommand.COMMAND_WORD: + return new DeleteImageCommandParser().parseInDetailedViewContext(arguments); + + case ImagesCommand.COMMAND_WORD: + return new ImagesCommand(); + + case ListCommand.COMMAND_WORD: + return new ListCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ClearCommand.COMMAND_WORD: + // Fallthrough + case DeleteCommand.COMMAND_WORD: + // Fallthrough + case DeleteTagCommand.COMMAND_WORD: + // Fallthrough + case FindCommand.COMMAND_WORD: + // Fallthrough + case FindTagCommand.COMMAND_WORD: + // Fallthrough + case ListFavouritesCommand.COMMAND_WORD: + // Fallthrough + case ListImportantCommand.COMMAND_WORD: + // Fallthrough + case SortsCommand.COMMAND_WORD: + // Fallthrough + case ViewCommand.COMMAND_WORD: + throw new ParseException(MESSAGE_INCOMPATIBLE_VIEW_MODE); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/AssignTagCommandParser.java b/src/main/java/seedu/address/logic/parser/AssignTagCommandParser.java new file mode 100644 index 00000000000..38d19615379 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AssignTagCommandParser.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AssignTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AssignTagCommand object + */ +public class AssignTagCommandParser implements Parser, + DetailedViewExecutableParser { + + /** + * Parses the given {@code String} of arguments in the context of the AssignTagCommand + * and returns a AssignTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AssignTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AssignTagCommand.MESSAGE_USAGE)); + } + + String[] inputs = trimmedArgs.split("\\s+"); + if (inputs.length <= 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AssignTagCommand.MESSAGE_USAGE)); + } + + try { + Index index = ParserUtil.parseIndex(inputs[0]); + String tagName = inputs[1]; + return new AssignTagCommand(index, tagName); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AssignTagCommand.MESSAGE_USAGE), pe); + } + } + + @Override + public AssignTagCommand parseInDetailedViewContext(String args) throws ParseException { + String trimmedArgs = args.trim(); + String[] inputs = trimmedArgs.split("\\s+"); + String tag = inputs[inputs.length - 1]; + + if (tag.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AssignTagCommand.MESSAGE_USAGE)); + } + + return new AssignTagCommand(tag); + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..0c7ee9a6240 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -10,6 +10,9 @@ public class CliSyntax { 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_DEADLINE = new Prefix("d/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_NOTE = new Prefix("r/"); + public static final Prefix PREFIX_IMAGE = new Prefix("i/"); } diff --git a/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java b/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java new file mode 100644 index 00000000000..a51d3909011 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.tag.Tag.VALIDATION_REGEX; + +import seedu.address.logic.commands.CreateTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new CreateTagCommand object + */ +public class CreateTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CreateTagCommand + * and returns a CreateTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CreateTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateTagCommand.MESSAGE_USAGE)); + } + + if (!trimmedArgs.matches(VALIDATION_REGEX)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateTagCommand.MESSAGE_USAGE)); + } + + return new CreateTagCommand(trimmedArgs); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeadlineCommandParser.java b/src/main/java/seedu/address/logic/parser/DeadlineCommandParser.java new file mode 100644 index 00000000000..62adcce153b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeadlineCommandParser.java @@ -0,0 +1,57 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEADLINE; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.DeadlineCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.DeadlineList; + +/** + * Parses input arguments and creates a new DeadlineCommand object + */ +public class DeadlineCommandParser implements Parser, DetailedViewExecutableParser { + + /** + * Parses the given {@code String} of arguments in the context of the DeadlineCommand + * and returns a DeadlineCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeadlineCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_DEADLINE); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeadlineCommand.MESSAGE_USAGE), ive); + } + DeadlineList deadlines = ParserUtil.parseDeadlines(argMultimap.getAllValues(PREFIX_DEADLINE)); + if (deadlines.size() == 0) { + throw new ParseException(DeadlineCommand.MESSAGE_NO_DEADLINES_ADDED); + } + + return new DeadlineCommand(index, deadlines); + } + + @Override + public DeadlineCommand parseInDetailedViewContext(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_DEADLINE); + + DeadlineList deadlines = ParserUtil.parseDeadlines(argMultimap.getAllValues(PREFIX_DEADLINE)); + if (deadlines.size() == 0) { + throw new ParseException(DeadlineCommand.MESSAGE_NO_DEADLINES_ADDED); + } + + return new DeadlineCommand(deadlines); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteDeadlineCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteDeadlineCommandParser.java new file mode 100644 index 00000000000..447f7455a5c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteDeadlineCommandParser.java @@ -0,0 +1,21 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteDeadlineCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class DeleteDeadlineCommandParser implements DetailedViewExecutableParser { + + @Override + public DeleteDeadlineCommand parseInDetailedViewContext(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteDeadlineCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteDeadlineCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteImageCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteImageCommandParser.java new file mode 100644 index 00000000000..0364dfe97a9 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteImageCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_IMAGE; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteImageCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class DeleteImageCommandParser implements Parser, + DetailedViewExecutableParser { + + /** + * Parses {@code args} into a command and returns it. + * + * @param args the user input to parse + * @throws ParseException if {@code userInput} does not conform the expected format + */ + @Override + public DeleteImageCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_IMAGE); + + if (!arePrefixesPresent(argMultimap, PREFIX_IMAGE) || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteImageCommand.MESSAGE_USAGE)); + } + + Index personIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + Index imageIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_IMAGE).get()); + + return new DeleteImageCommand(personIndex, imageIndex); + } + + @Override + public DeleteImageCommand parseInDetailedViewContext(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_IMAGE); + + if (!arePrefixesPresent(argMultimap, PREFIX_IMAGE)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteImageCommand.MESSAGE_USAGE)); + } + + Index imageIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_IMAGE).get()); + + return new DeleteImageCommand(imageIndex); + } + + /** + * 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()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java new file mode 100644 index 00000000000..02bc16c0eba --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java @@ -0,0 +1,21 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class DeleteNoteCommandParser implements DetailedViewExecutableParser { + + @Override + public DeleteNoteCommand parseInDetailedViewContext(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteNoteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteNoteCommand.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..228a5472f01 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteTagCommandParser.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.tag.Tag.VALIDATION_REGEX; + +import java.util.Arrays; + +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 { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns a DeleteTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTagCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + boolean isValidArg = true; + for (String keyword : nameKeywords) { + isValidArg = isValidArg && keyword.matches(VALIDATION_REGEX); + } + + if (!isValidArg) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTagCommand.MESSAGE_USAGE)); + } + + return new DeleteTagCommand(Arrays.asList(nameKeywords)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DetailedViewExecutableParser.java b/src/main/java/seedu/address/logic/parser/DetailedViewExecutableParser.java new file mode 100644 index 00000000000..0b4bef927bd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DetailedViewExecutableParser.java @@ -0,0 +1,18 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.commands.DetailedViewExecutable; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Represents a Parser that is able to parse user input to create a + * {@code DetailViewExecutable} of type {@code T}. + */ +public interface DetailedViewExecutableParser { + + /** + * Parses {@code userInput} into a command that can execute in detailed view mode + * and returns it. + * @throws ParseException if {@code userInput} does not conform the expected format + */ + T parseInDetailedViewContext(String userInput) throws ParseException; +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..f4941cc445b 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -6,23 +6,16 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object */ -public class EditCommandParser implements Parser { +public class EditCommandParser implements Parser, DetailedViewExecutableParser { /** * Parses the given {@code String} of arguments in the context of the EditCommand @@ -32,7 +25,7 @@ public class EditCommandParser implements Parser { 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_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); Index index; @@ -55,7 +48,6 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -64,19 +56,31 @@ public EditCommand parse(String args) throws ParseException { return new EditCommand(index, editPersonDescriptor); } - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; + @Override + public EditCommand parseInDetailedViewContext(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + } - if (tags.isEmpty()) { - return Optional.empty(); + if (!editPersonDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); + + return new EditCommand(editPersonDescriptor); } } diff --git a/src/main/java/seedu/address/logic/parser/FavouriteCommandParser.java b/src/main/java/seedu/address/logic/parser/FavouriteCommandParser.java new file mode 100644 index 00000000000..f3aa22b6bf6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FavouriteCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.FavouriteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class FavouriteCommandParser implements Parser { + + /** + * Parses {@code args} into a FavouriteCommand and returns it. + * + * @param args Index of the person to favourite. + * @throws ParseException if {@code userInput} does not conform the expected format + */ + @Override + public FavouriteCommand parse(String args) throws ParseException { + requireNonNull(args); + try { + Index index = ParserUtil.parseIndex(args); + return new FavouriteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FavouriteCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java new file mode 100644 index 00000000000..ea968fff9c8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new FindTagCommand object + */ +public class FindTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns a FindTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindTagCommand(Arrays.asList(nameKeywords)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/HighImportanceCommandParser.java b/src/main/java/seedu/address/logic/parser/HighImportanceCommandParser.java new file mode 100644 index 00000000000..38f12da583c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/HighImportanceCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.HighImportanceCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class HighImportanceCommandParser implements Parser { + /** + * Parses {@code args} into a FavouriteCommand and returns it. + * + * @param args Index of the person to favourite. + * @throws ParseException if {@code userInput} does not conform the expected format + */ + @Override + public HighImportanceCommand parse(String args) throws ParseException { + requireNonNull(args); + try { + Index index = ParserUtil.parseIndex(args); + return new HighImportanceCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, HighImportanceCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ImagesCommandParser.java b/src/main/java/seedu/address/logic/parser/ImagesCommandParser.java new file mode 100644 index 00000000000..471aef9cd07 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImagesCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ImagesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class ImagesCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ImagesCommand + * and returns a ImagesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImagesCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ImagesCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImagesCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/NoteCommandParser.java b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java new file mode 100644 index 00000000000..3b8555a90f5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.NoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class NoteCommandParser implements Parser, DetailedViewExecutableParser { + @Override + public NoteCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_NOTE); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + NoteCommand.MESSAGE_USAGE), ive); + } + + String note = argMultimap.getValue(PREFIX_NOTE).orElse(""); + + return new NoteCommand(index, note); + } + + @Override + public NoteCommand parseInDetailedViewContext(String args) { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_NOTE); + String note = argMultimap.getValue(PREFIX_NOTE).orElse(""); + + return new NoteCommand(note); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..92c4210247a 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -10,6 +11,8 @@ import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; @@ -25,6 +28,7 @@ public class ParserUtil { /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -95,6 +99,54 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String deadline} into a {@code Deadline}. + * Leading and trailing whitespaces will be trimmed. + * + * @param deadline the date of the deadline. + * @return the resulting {@code Deadline} object. + * @throws ParseException if the given {@code deadline} is invalid. + */ + public static Deadline parseDeadline(String deadline) throws ParseException { + requireNonNull(deadline); + String trimmedDeadline = deadline.trim(); + StringBuilder originalInput = new StringBuilder(trimmedDeadline); + originalInput.reverse(); + String[] reversedAndSplit = originalInput.toString().split("\\s+", 2); + if (reversedAndSplit.length < 2) { + throw new ParseException(Deadline.MESSAGE_CONSTRAINTS); + } + String description = new StringBuilder(reversedAndSplit[1]).reverse().toString(); + String date = new StringBuilder(reversedAndSplit[0]).reverse().toString(); + if (!Deadline.isValidDeadline(description, date)) { + throw new ParseException(Deadline.MESSAGE_CONSTRAINTS); + } + + return new Deadline(description, date); + } + + /** + * parses {@code String deadlines} into a {@code DeadlineList}. + * Leading and trailing whitespaces will be trimmed. + * + * @param deadlines the string representing deadline/s + * @return the resulting {@code DeadlineList} object + * @throws ParseException if one of the given {@code deadlines} is invalid. + */ + public static DeadlineList parseDeadlines(Collection deadlines) throws ParseException { + requireNonNull(deadlines); + ArrayList deadlineList = new ArrayList<>(); + + if (deadlines.size() < 1) { + deadlineList.add(new Deadline()); + } + + for (String deadline : deadlines) { + deadlineList.add(parseDeadline(deadline)); + } + return new DeadlineList(deadlineList); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..64a7b8d6425 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,72 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_CRITERIA; + +import java.util.Arrays; + +import seedu.address.logic.commands.SortsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; + +/** + * Parses input arguments and creates a new SortsCommand object + */ +public class SortCommandParser implements Parser { + public static final String[] MODEL_NAMES = { + Name.MODEL_NAME, + Phone.MODEL_NAME, + Email.MODEL_NAME, + Address.MODEL_NAME, + DeadlineList.MODEL_NAME, + Favourite.MODEL_NAME, + HighImportance.MODEL_NAME + }; + + /** + * Parses the given {@code String} of arguments in the context of the SortsCommand + * and returns a SortsCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public SortsCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(SortsCommand.MESSAGE_USAGE, criteriaListForUsageMessage()))); + } + + if (!Arrays.asList(MODEL_NAMES).contains(trimmedArgs)) { + throw new ParseException(String.format(MESSAGE_INVALID_CRITERIA, trimmedArgs)); + } + + return new SortsCommand(trimmedArgs); + } + + /** + * Creates a list of criteria to be printed + * @return the String representing a list of valid criteria + */ + public static String criteriaListForUsageMessage() { + String result = ""; + + for (int i = 0; i < MODEL_NAMES.length; i++) { + if (i == MODEL_NAMES.length - 1) { + result += MODEL_NAMES[i]; + break; + } + + result += MODEL_NAMES[i] + ", "; + } + + return result; + } + +} diff --git a/src/main/java/seedu/address/logic/parser/UnassignTagCommandParser.java b/src/main/java/seedu/address/logic/parser/UnassignTagCommandParser.java new file mode 100644 index 00000000000..87364a5811f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UnassignTagCommandParser.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.UnassignTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new UnassignTagCommand object + */ +public class UnassignTagCommandParser implements Parser, + DetailedViewExecutableParser { + + /** + * Parses the given {@code String} of arguments in the context of the UnassignTagCommand + * and returns a UnassignTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public UnassignTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnassignTagCommand.MESSAGE_USAGE)); + } + + String[] inputs = trimmedArgs.split("\\s+"); + if (inputs.length <= 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + UnassignTagCommand.MESSAGE_USAGE)); + } + + try { + Index index = ParserUtil.parseIndex(inputs[0]); + String tagName = inputs[1]; + return new UnassignTagCommand(index, tagName); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnassignTagCommand.MESSAGE_USAGE), pe); + } + } + + @Override + public UnassignTagCommand parseInDetailedViewContext(String args) throws ParseException { + String trimmedArgs = args.trim(); + String[] inputs = trimmedArgs.split("\\s+"); + String tag = inputs[inputs.length - 1]; + + if (tag.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnassignTagCommand.MESSAGE_USAGE)); + } + + return new UnassignTagCommand(tag); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ViewCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java new file mode 100644 index 00000000000..bde521e7d2f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java @@ -0,0 +1,25 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class ViewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewCommand + * and returns a ViewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ViewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..36354013bf9 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -3,10 +3,14 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Objects; import javafx.collections.ObservableList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.tag.ActivatedTagList; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; /** * Wraps all data at the address-book level @@ -15,6 +19,8 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueTagList tags; + private final ActivatedTagList activatedTags; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -25,6 +31,8 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + tags = new UniqueTagList(); + activatedTags = new ActivatedTagList(); } public AddressBook() {} @@ -47,6 +55,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the tag list with {@code tags}. + * {@code tags} must not contain duplicate tags. + */ + public void setTags(List tags) { + this.tags.setTags(tags); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -54,6 +70,7 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setTags(newData.getTagList()); } //// person-level operations @@ -93,6 +110,49 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Returns true if a tag with the same identity as {@code tag} exists in the address book. + */ + public boolean hasTag(Tag tag) { + requireNonNull(tag); + return tags.contains(tag); + } + + /** + * Adds a tag to the address book. + * The tag must not already exist in the address book. + */ + public void addTag(Tag p) { + tags.add(p); + } + + /** + * Replaces the given tag {@code target} in the list with {@code editedTag}. + * {@code target} must exist in the address book. + * The tag identity of {@code editedTag} must not be the same as another existing tag in the address book. + */ + public void setTag(Tag target, Tag editedTag) { + requireNonNull(editedTag); + + tags.setTag(target, editedTag); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeTag(Tag key) { + tags.remove(key); + } + + public void addActivatedTag(Tag tag) { + activatedTags.add(tag); + } + + public void clearActivatedTagList() { + activatedTags.clear(); + } + //// util methods @Override @@ -106,15 +166,27 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getTagList() { + return tags.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getActivatedTagList() { + return activatedTags.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); + && persons.equals(((AddressBook) other).persons) + && tags.equals(((AddressBook) other).tags)) + && activatedTags.equals(((AddressBook) other).activatedTags); } @Override public int hashCode() { - return persons.hashCode(); + return Objects.hash(persons, tags); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..7cae5f713b0 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -4,16 +4,25 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ + /** + * {@code Predicate} that always evaluate to true + */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluates to false */ + Predicate PREDICATE_HIDE_ALL_PERSONS = unused -> false; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -44,12 +53,24 @@ public interface Model { */ void setAddressBookFilePath(Path addressBookFilePath); + /** + * Returns the user prefs' contact images file path. + */ + Path getContactImagesFilePath(); + + /** + * Sets the user prefs' contact images file path. + */ + void setContactImagesFilePath(Path contactImagesFilePath); + /** * Replaces address book data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ + /** + * Returns the AddressBook + */ ReadOnlyAddressBook getAddressBook(); /** @@ -76,12 +97,111 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** + * Returns true if a person with the same identity as {@code tag} exists in the address book. + */ + boolean hasTag(Tag tag); + + /** + * Adds the given tag + * {@code tag} must not already exist in the address book. + */ + void addTag(Tag tag); + + /** + * Replaces the given tag {@code target} with {@code editedTag}. + * {@code target} must exist in the address book. + * The tag identity of {@code editedTag} must not be the same as another existing tag in the address book. + */ + void setTag(Tag target, Tag editedTag); + + /** + * Deletes the given tag. + * The tag must exist in the address book. + */ + void deleteTag(Tag target); + + /** + * Adds the given tag to the {@code ActivatedTagList} + * {@code tag} must already exist in the address book. + */ + void addActivatedTag(Tag tag); + + /** + * Returns an unmodifiable view of the filtered person list + */ ObservableList getFilteredPersonList(); + /** + * Returns an unmodifiable view of the activated tag list + */ + ObservableList getActivatedTagList(); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + void sortFilteredPersonListByName(); + + void sortFilteredPersonListByAddress(); + + void sortFilteredPersonListByDeadlineList(); + + void sortFilteredPersonListByEmail(); + + void sortFilteredPersonListByPhone(); + + void sortFilteredPersonListByFavourite(); + + void sortFilteredPersonListByHighImportance(); + + SortedList getSortedPersonList(); + + /** + * Clears the {@code tags} within the {@code ActivatedTagList} + */ + void clearActivatedTagList(); + + /** Returns the contact that is being viewed in detail */ + ObservableList getDetailedContactView(); + + /** + * Sets the given {@code Person} as the contact to be viewed in detail. + * @param person person to be viewed + */ + void setDetailedContactView(Person person); + + /** Removes the contact that is being viewed in detail */ + void clearDetailedContactView(); + + /** + * Gets the {@code Person} in detailed view. + */ + Person getDetailedContactViewPerson(); + + /** + * Updates the images to be displayed. + */ + void setImagesToView(ImageDetailsList images); + + /** + * Gets the images to be displayed. + */ + ImageDetailsList getImagesToView(); + + /** + * Updates the commandText history + */ + void updateCommandHistory(String commandText); + + /** + * Retrieves the i-th latest command history + * @param i the number of commands we should backstep to retrieve + * @return the command text + */ + CommandHistoryEntry getCommandHistory(int i); + } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 86c1df298d7..361a62cb648 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -7,11 +7,24 @@ import java.util.function.Predicate; import java.util.logging.Logger; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.commandhistory.CommandHistory; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.comparator.AddressComparator; +import seedu.address.model.comparator.DeadlineListComparator; +import seedu.address.model.comparator.EmailComparator; +import seedu.address.model.comparator.FavouriteComparator; +import seedu.address.model.comparator.HighImportanceComparator; +import seedu.address.model.comparator.NameComparator; +import seedu.address.model.comparator.PhoneComparator; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * Represents the in-memory model of the address book data. @@ -22,6 +35,13 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final SortedList sortedPersons; + private final ObservableList detailedContactView; + private final CommandHistory commandHistory; + private final ObservableList activatedTags; + + private ImageDetailsList imagesToView; + /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +54,11 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + sortedPersons = new SortedList<>(filteredPersons); + detailedContactView = FXCollections.observableArrayList(); + activatedTags = new FilteredList<>(this.addressBook.getActivatedTagList()); + this.imagesToView = new ImageDetailsList(); + this.commandHistory = new CommandHistory(); } public ModelManager() { @@ -75,6 +100,17 @@ public void setAddressBookFilePath(Path addressBookFilePath) { userPrefs.setAddressBookFilePath(addressBookFilePath); } + @Override + public Path getContactImagesFilePath() { + return userPrefs.getContactImagesFilePath(); + } + + @Override + public void setContactImagesFilePath(Path contactImagesFilePath) { + requireNonNull(contactImagesFilePath); + userPrefs.setContactImagesFilePath(contactImagesFilePath); + } + //=========== AddressBook ================================================================================ @Override @@ -111,6 +147,35 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + @Override + public boolean hasTag(Tag tag) { + requireNonNull(tag); + return addressBook.hasTag(tag); + } + + @Override + public void addTag(Tag tag) { + addressBook.addTag(tag); + } + + @Override + public void setTag(Tag target, Tag editedTag) { + requireAllNonNull(target, editedTag); + + addressBook.setTag(target, editedTag); + } + + @Override + public void deleteTag(Tag target) { + addressBook.removeTag(target); + } + + @Override + public void addActivatedTag(Tag tag) { + requireNonNull(tag); + addressBook.addActivatedTag(tag); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -122,12 +187,122 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + @Override + public SortedList getSortedPersonList() { + return sortedPersons; + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + @Override + public ObservableList getActivatedTagList() { + return this.activatedTags; + } + + @Override + public void clearActivatedTagList() { + logger.fine("Clearing activated tag lists"); + addressBook.clearActivatedTagList(); + } + + //=========== Detailed Contact View methods ============================================================= + + @Override + public ObservableList getDetailedContactView() { + return detailedContactView.filtered(PREDICATE_SHOW_ALL_PERSONS); + } + + @Override + public void setDetailedContactView(Person person) { + if (detailedContactView.size() > 0) { + clearDetailedContactView(); + } + logger.fine("Setting " + person.getName().fullName + " in detailed view"); + detailedContactView.add(person); + assert(detailedContactView.size() == 1); + } + + @Override + public void clearDetailedContactView() { + logger.fine("Clearing detailed view"); + detailedContactView.clear(); + } + + @Override + public Person getDetailedContactViewPerson() { + assert detailedContactView.size() == 1; + return detailedContactView.get(0); + } + + //=========== Person Images to View ============================================================================== + @Override + public void setImagesToView(ImageDetailsList images) { + this.imagesToView = images; + } + + @Override + public ImageDetailsList getImagesToView() { + return this.imagesToView; + } + + /** + * Updates the commandText history + * + * @param commandText the text to cache + */ + @Override + public void updateCommandHistory(String commandText) { + commandHistory.cacheCommand(commandText); + } + + /** + * Retrieves the i-th latest command text + * @return + */ + @Override + public CommandHistoryEntry getCommandHistory(int i) { + return commandHistory.retrieveCommand(i); + } + + @Override + public void sortFilteredPersonListByName() { + sortedPersons.setComparator(new NameComparator()); + } + + @Override + public void sortFilteredPersonListByAddress() { + sortedPersons.setComparator(new AddressComparator()); + } + + @Override + public void sortFilteredPersonListByDeadlineList() { + sortedPersons.setComparator(new DeadlineListComparator()); + } + + @Override + public void sortFilteredPersonListByEmail() { + sortedPersons.setComparator(new EmailComparator()); + } + + @Override + public void sortFilteredPersonListByPhone() { + sortedPersons.setComparator(new PhoneComparator()); + } + + @Override + public void sortFilteredPersonListByFavourite() { + sortedPersons.setComparator(new FavouriteComparator()); + } + + @Override + public void sortFilteredPersonListByHighImportance() { + sortedPersons.setComparator(new HighImportanceComparator()); + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -144,7 +319,10 @@ public boolean equals(Object obj) { ModelManager other = (ModelManager) obj; return addressBook.equals(other.addressBook) && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); + && filteredPersons.equals(other.filteredPersons) + && detailedContactView.equals(other.detailedContactView) + && imagesToView.equals(other.imagesToView) + && commandHistory.equals(other.commandHistory) + && activatedTags.equals(other.activatedTags); } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..08f7fcb548e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -2,6 +2,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * Unmodifiable view of an address book @@ -14,4 +15,15 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the tags list. + * This list will not contain any duplicate tags. + */ + ObservableList getTagList(); + + /** + * Returns an unmodifiable view of the currently activated tag list from the find tag command. + * This list will not contain any duplicate tags + */ + ObservableList getActivatedTagList(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c73..e8f682bff65 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -13,4 +13,5 @@ public interface ReadOnlyUserPrefs { Path getAddressBookFilePath(); + Path getContactImagesFilePath(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..75c22cf96d5 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -15,6 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path contactImagesFilePath = Paths.get("data", "images"); /** * Creates a {@code UserPrefs} with default values. @@ -36,6 +37,7 @@ public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setContactImagesFilePath(newUserPrefs.getContactImagesFilePath()); } public GuiSettings getGuiSettings() { @@ -56,6 +58,16 @@ public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } + @Override + public Path getContactImagesFilePath() { + return contactImagesFilePath; + } + + public void setContactImagesFilePath(Path contactImagesFilePath) { + requireNonNull(contactImagesFilePath); + this.contactImagesFilePath = contactImagesFilePath; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/commandhistory/CommandHistory.java b/src/main/java/seedu/address/model/commandhistory/CommandHistory.java new file mode 100644 index 00000000000..e66bc8bd6e5 --- /dev/null +++ b/src/main/java/seedu/address/model/commandhistory/CommandHistory.java @@ -0,0 +1,58 @@ +package seedu.address.model.commandhistory; + +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +public class CommandHistory implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Caches the command into the history for future retrieval. + * @param commandText the command text to be cached. + */ + public void cacheCommand(String commandText) { + requireNonNull(commandText); + + if (commandText.isBlank()) { + return; + } + + this.internalList.add(new CommandHistoryEntry(commandText)); + } + + /** + * Retrieves the command at (size - offset) position. + * If offset is out of bounds, returns an empty history entry. + * + * @param offset the number of commands to step back from + * @return the resulting entry + */ + public CommandHistoryEntry retrieveCommand(int offset) { + if (offset > internalList.size() || offset <= 0) { + return CommandHistoryEntry.getEmptyHistory(); + } + return internalList.get(internalList.size() - offset); + } + + /** + * Returns an iterator over elements of type {@code T}. + * + * @return an Iterator. + */ + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CommandHistory // instanceof handles nulls + && internalList.equals(((CommandHistory) other).internalList)); + } +} diff --git a/src/main/java/seedu/address/model/commandhistory/CommandHistoryEntry.java b/src/main/java/seedu/address/model/commandhistory/CommandHistoryEntry.java new file mode 100644 index 00000000000..48392f49dd6 --- /dev/null +++ b/src/main/java/seedu/address/model/commandhistory/CommandHistoryEntry.java @@ -0,0 +1,64 @@ +package seedu.address.model.commandhistory; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.commandhistory.exceptions.HistoryDoesNotExistException; + +public class CommandHistoryEntry { + + private static final CommandHistoryEntry EMPTY_COMMAND_HISTORY = new CommandHistoryEntrySentinel(); + + private final String commandText; + + // hidden default constructor + private CommandHistoryEntry() { + commandText = ""; + } + + /** + * Creates a command history entry. Encapsulates the information of a past command made by the user. + * + * @param commandText the raw input provided by the user. + */ + public CommandHistoryEntry(String commandText) { + requireNonNull(commandText); + this.commandText = commandText; + } + + public static CommandHistoryEntry getEmptyHistory() { + return EMPTY_COMMAND_HISTORY; + } + + public boolean exists() { + return true; + } + + public String getCommandText() { + return this.commandText; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CommandHistoryEntry // instanceof handles nulls + && commandText.equals(((CommandHistoryEntry) other).commandText)); + } + + private static class CommandHistoryEntrySentinel extends CommandHistoryEntry { + + @Override + public boolean exists() { + return false; + } + + @Override + public String getCommandText() { + throw new HistoryDoesNotExistException(); + } + + @Override + public boolean equals(Object other) { + return other instanceof CommandHistoryEntrySentinel; + } + } +} diff --git a/src/main/java/seedu/address/model/commandhistory/exceptions/HistoryDoesNotExistException.java b/src/main/java/seedu/address/model/commandhistory/exceptions/HistoryDoesNotExistException.java new file mode 100644 index 00000000000..c545be669f8 --- /dev/null +++ b/src/main/java/seedu/address/model/commandhistory/exceptions/HistoryDoesNotExistException.java @@ -0,0 +1,4 @@ +package seedu.address.model.commandhistory.exceptions; + +public class HistoryDoesNotExistException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/comparator/AddressComparator.java b/src/main/java/seedu/address/model/comparator/AddressComparator.java new file mode 100644 index 00000000000..f332e3b2c47 --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/AddressComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class AddressComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getAddress().compareTo(p2.getAddress()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/DeadlineComparator.java b/src/main/java/seedu/address/model/comparator/DeadlineComparator.java new file mode 100644 index 00000000000..4a2bafe989e --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/DeadlineComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Deadline; + +public class DeadlineComparator implements Comparator { + @Override + public int compare(Deadline d1, Deadline d2) { + return d1.compareTo(d2); + } +} diff --git a/src/main/java/seedu/address/model/comparator/DeadlineListComparator.java b/src/main/java/seedu/address/model/comparator/DeadlineListComparator.java new file mode 100644 index 00000000000..ed14aa67d2f --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/DeadlineListComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class DeadlineListComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getDeadlines().compareTo(p2.getDeadlines()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/EmailComparator.java b/src/main/java/seedu/address/model/comparator/EmailComparator.java new file mode 100644 index 00000000000..7f5cefe10dc --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/EmailComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class EmailComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getEmail().compareTo(p2.getEmail()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/FavouriteComparator.java b/src/main/java/seedu/address/model/comparator/FavouriteComparator.java new file mode 100644 index 00000000000..ea2dc06817d --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/FavouriteComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class FavouriteComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getFavouriteStatus().compareTo(p2.getFavouriteStatus()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/HighImportanceComparator.java b/src/main/java/seedu/address/model/comparator/HighImportanceComparator.java new file mode 100644 index 00000000000..53e465a79a3 --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/HighImportanceComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class HighImportanceComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getHighImportanceStatus().compareTo(p2.getHighImportanceStatus()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/NameComparator.java b/src/main/java/seedu/address/model/comparator/NameComparator.java new file mode 100644 index 00000000000..8bc2855cef0 --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/NameComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class NameComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getName().compareTo(p2.getName()); + } +} diff --git a/src/main/java/seedu/address/model/comparator/PhoneComparator.java b/src/main/java/seedu/address/model/comparator/PhoneComparator.java new file mode 100644 index 00000000000..6f5125f075e --- /dev/null +++ b/src/main/java/seedu/address/model/comparator/PhoneComparator.java @@ -0,0 +1,12 @@ +package seedu.address.model.comparator; + +import java.util.Comparator; + +import seedu.address.model.person.Person; + +public class PhoneComparator implements Comparator { + @Override + public int compare(Person p1, Person p2) { + return p1.getPhone().compareTo(p2.getPhone()); + } +} diff --git a/src/main/java/seedu/address/model/image/ImageDetails.java b/src/main/java/seedu/address/model/image/ImageDetails.java new file mode 100644 index 00000000000..8242cc9878e --- /dev/null +++ b/src/main/java/seedu/address/model/image/ImageDetails.java @@ -0,0 +1,59 @@ +package seedu.address.model.image; + +import static java.util.Objects.requireNonNull; + +import java.io.File; + +public class ImageDetails { + public final File imageFile; + + /** + * Creates an image details object that encapsulates the information of an image file. + * + * @param imageFile the file that is being encapsulated. + */ + public ImageDetails(File imageFile) { + requireNonNull(imageFile); + this.imageFile = imageFile; + } + + public String getName() { + return this.imageFile.getName(); + } + + public File getImageFile() { + return this.imageFile; + } + + /** + * The string representation of the path returned is relative to the project root. + * + * @return path of the image file, relative to the project root. + */ + public String getPath() { + return this.imageFile.getPath(); + } + + public String getJavaFxImageUrl() { + return String.format("file:%s", this.imageFile.getPath()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ImageDetails // instanceof handles nulls + && getName().equals(((ImageDetails) other).getName())); // state check + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + getName() + ']'; + } +} diff --git a/src/main/java/seedu/address/model/image/ImageDetailsList.java b/src/main/java/seedu/address/model/image/ImageDetailsList.java new file mode 100644 index 00000000000..73c63e5d2ce --- /dev/null +++ b/src/main/java/seedu/address/model/image/ImageDetailsList.java @@ -0,0 +1,85 @@ +package seedu.address.model.image; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class ImageDetailsList implements Iterable { + private final List images; + + public ImageDetailsList() { + this.images = new ArrayList<>(); + } + + public ImageDetailsList(List images) { + this.images = images; + } + + /** + * Creates an iterable list of image details, appended to this current list. + * The returned list is a new list. + * + * @param appendedImages the images to append to this list. + * @return the newly appended list. + */ + public ImageDetailsList appendImageDetails(List appendedImages) { + List newImages = new ArrayList<>(); + newImages.addAll(images); + newImages.addAll(appendedImages); + return new ImageDetailsList(newImages); + } + + public List getImages() { + return this.images; + } + + public ImageDetails get(int i) { + return this.images.get(i); + } + + public boolean isEmpty() { + return this.images.isEmpty(); + } + + /** + * Returns the size of the list, indicated by the number of ImageDetails within the list. + * + * @return size of the list. + */ + public int size() { + return this.images.size(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < images.size(); i++) { + sb.append(i + 1).append(": ").append(images.get(i)).append("\n"); + } + return sb.toString(); + } + + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ImageDetailsList // instanceof handles nulls + && images.equals(((ImageDetailsList) other).images)); + } + + @Override + public int hashCode() { + return images.hashCode(); + } + + + /** + * Returns an iterator over elements of type {@code Images}. + * + * @return an Iterator. + */ + @Override + public Iterator iterator() { + return this.images.iterator(); + } +} diff --git a/src/main/java/seedu/address/model/image/exceptions/ImageDetailsNotFoundException.java b/src/main/java/seedu/address/model/image/exceptions/ImageDetailsNotFoundException.java new file mode 100644 index 00000000000..83ef7f4e4bf --- /dev/null +++ b/src/main/java/seedu/address/model/image/exceptions/ImageDetailsNotFoundException.java @@ -0,0 +1,10 @@ +package seedu.address.model.image.exceptions; + +/** + * Signals that the operation is unable to find the specified image. + */ +public class ImageDetailsNotFoundException extends RuntimeException { + public ImageDetailsNotFoundException() { + super("Image not found."); + } +} diff --git a/src/main/java/seedu/address/model/image/util/ImageUtil.java b/src/main/java/seedu/address/model/image/util/ImageUtil.java new file mode 100644 index 00000000000..a62b9376997 --- /dev/null +++ b/src/main/java/seedu/address/model/image/util/ImageUtil.java @@ -0,0 +1,120 @@ +package seedu.address.model.image.util; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import javax.imageio.ImageIO; + +import javafx.stage.FileChooser; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; + +public class ImageUtil { + private static final String JPG_EXTENSION = "jpg"; + private static final String PNG_EXTENSION = "png"; + private static final FileChooser.ExtensionFilter IMAGE_FILE_FILTERS = new FileChooser.ExtensionFilter( + "Image Files", "*." + PNG_EXTENSION, "*." + JPG_EXTENSION); + + private static FileChooser fileChooser; + + /** + * Opens a FileChooser configured to view and select only image files. + * @return the FileChooser that can be opened + */ + public static FileChooser openImageFileChooser() { + if (fileChooser == null) { + fileChooser = new FileChooser(); + fileChooser.getExtensionFilters().add(IMAGE_FILE_FILTERS); + } + return fileChooser; + } + + /** + * Checks if the {@code file} exists in the {@code directoryDirectory}. + * @param file the file to check + * @param directoryPath path of the directory to check if file exists within + * @return true if the file exists + */ + public static boolean fileExists(File file, Path directoryPath) { + File directory = directoryPath.toFile(); + File[] directoryFiles = directory.listFiles(); + + if (directoryFiles == null) { + // Directory doesn't exist yet + return false; + } + + for (File f : directoryFiles) { + if (f.getName().equals(file.getName())) { + return true; + } + } + + return false; + } + + /** + * Copies the image specified at the source file, to the destination path specified. + * + * @param src the image to be copied + * @param destPath the location that the newly copied image should be saved to + * @return the image details of the copied image + * @throws IOException if reading or writing the image failed + */ + public static ImageDetails copyTo(File src, Path destPath) throws IOException { + BufferedImage srcImg = ImageIO.read(src); + + FileUtil.createIfMissing(destPath); + File destFile = destPath.toFile(); + + String imgExtension = getImageExtension(src.getName()); + + ImageIO.write(srcImg, imgExtension, destFile); + return new ImageDetails(destFile); + } + + private static String getImageExtension(String fileName) { + String[] splitFileName = fileName.split("\\."); + // FileChooser should have already ensured that the file must have _some_ valid extension + assert splitFileName.length > 1; + + String extension = splitFileName[splitFileName.length - 1]; + // FileChooser should have already ensured that the file must have some _valid_ extension + assert extension.equals(JPG_EXTENSION) || extension.equals(PNG_EXTENSION); + + return extension; + } + + /** + * Removes from this list images that no longer exist within the data/images directory. + * + * @return a sanitized {@code ImageDetailsList} object. + */ + public static ImageDetailsList sanitizeList(ImageDetailsList listToSanitize, Path pathToSanitize) { + List sanitizedList = new ArrayList<>(); + for (ImageDetails img : listToSanitize) { + if (fileExists(img.getImageFile(), pathToSanitize)) { + sanitizedList.add(img); + } + } + return new ImageDetailsList(sanitizedList); + } + + /** + * Deletes the file from the specified filepath. + * + * @param imageToDelete the details of the image to delete. + */ + public static void removeFile(ImageDetails imageToDelete) { + Path filePath = imageToDelete.getImageFile().toPath(); + if (!FileUtil.isFileExists(filePath)) { + return; + } + + FileUtil.deleteFile(filePath); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 60472ca22a0..555284d55f0 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -7,9 +7,10 @@ * Represents a Person's address in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ -public class Address { - +public class Address implements Comparable
{ + public static final String MODEL_NAME = "address"; public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + public static final Address EMPTY_ADDRESS = new Address("*No Address Specified*"); /* * The first character of the address must not be a whitespace, @@ -54,4 +55,9 @@ public int hashCode() { return value.hashCode(); } + @Override + public int compareTo(Address other) { + return this.toString().compareToIgnoreCase(other.toString()); + } + } diff --git a/src/main/java/seedu/address/model/person/Deadline.java b/src/main/java/seedu/address/model/person/Deadline.java new file mode 100644 index 00000000000..83de7cff773 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Deadline.java @@ -0,0 +1,123 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Represents a Person's deadline in the address book. + */ +public class Deadline implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "Deadlines can only take dd/mm/yyyy and must have a description"; + public static final String NO_DEADLINE_PLACEHOLDER = "*No deadline specified*"; + public static final String VALIDATION_REGEX = "[^\\s].*"; + public static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy"); + public static final String STORAGE_PARSE_ERROR = "Deadline in storage could not be parsed properly"; + + public final String description; + public final String value; + + /** + * Constructs a {@code Deadline}. + * + * @param dateAsString a valid date. + */ + public Deadline(String description, String dateAsString) { + requireNonNull(dateAsString); + checkArgument(isValidDeadline(description, dateAsString), MESSAGE_CONSTRAINTS); + if (dateAsString.equals(NO_DEADLINE_PLACEHOLDER)) { + new Deadline(); + } + this.description = description; + value = dateAsString; + } + + /** + * Constructs a {@code Deadline} object that is empty. + */ + public Deadline() { + description = ""; + value = NO_DEADLINE_PLACEHOLDER; + } + + private Date getDate() throws ParseException { + if (value.equals(NO_DEADLINE_PLACEHOLDER)) { + return new Date(Long.MAX_VALUE); + } + + FORMATTER.setLenient(false); + Date date; + + try { + date = FORMATTER.parse(value); + } catch (java.text.ParseException e) { + throw new ParseException(STORAGE_PARSE_ERROR); + } + + return date; + } + + @Override + public String toString() { + if (!description.equals("")) { + return description + ": " + value; + } + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Deadline // instanceof handles nulls + && value.equals(((Deadline) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(Deadline other) throws ClassCastException { + try { + return this.getDate().compareTo(other.getDate()); + } catch (ParseException e) { + throw new ClassCastException(e.getMessage()); + } + } + + /** + * Check if given string is a valid date. + * + * @param dateAsString the given string. + * @return true if given string is a valid date. + */ + public static boolean isValidDeadline(String description, String dateAsString) { + if (description.equals("") && dateAsString.equals(NO_DEADLINE_PLACEHOLDER)) { + return true; + } + + if (description.equals("")) { + return false; + } + + if (!description.matches(VALIDATION_REGEX)) { + return false; + } + + FORMATTER.setLenient(false); + Date date; + + try { + date = FORMATTER.parse(dateAsString); + + return date.before(FORMATTER.parse("01/01/10000")); + } catch (java.text.ParseException e) { + return false; + } + } +} diff --git a/src/main/java/seedu/address/model/person/DeadlineList.java b/src/main/java/seedu/address/model/person/DeadlineList.java new file mode 100644 index 00000000000..98f1e973df1 --- /dev/null +++ b/src/main/java/seedu/address/model/person/DeadlineList.java @@ -0,0 +1,113 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import seedu.address.model.comparator.DeadlineComparator; + +public class DeadlineList implements Comparable { + public static final String MODEL_NAME = "deadline"; + private static final Deadline EMPTY_DEADLINE = new Deadline(); + + private ArrayList deadlines = new ArrayList<>(); + + public DeadlineList() { + this.deadlines.add(new Deadline()); + } + + /** + * Constructs a {@code DeadlineList} object. + * + * @param deadlines the list of the deadlines to be put into {@code DeadlineList}. + */ + public DeadlineList(List deadlines) { + requireNonNull(deadlines); + this.deadlines.addAll(deadlines); + } + + public ArrayList getDeadlines() { + return deadlines; + } + + /** + * Adds the given deadlines to this list of deadlines and returns it in a new + * {@code DeadlineList}. + * @param deadlinesToAppend deadlines to add to this list. + * @return new DeadlineList with the result. + */ + public DeadlineList appendDeadlines(DeadlineList deadlinesToAppend) { + if (this.size() == 0) { + return deadlinesToAppend; + } + DeadlineList newDeadlines = new DeadlineList(this.deadlines); + newDeadlines.deadlines.addAll(deadlinesToAppend.deadlines); + return newDeadlines; + } + + /** + * Deletes the deadline at the given index of this list of deadlines and return it in + * a new {@code DeadlineList}. + * @param index index of deadline to delete. + * @return new DeadlineList with the result. + */ + public DeadlineList delete(int index) { + DeadlineList newDeadlineList = new DeadlineList(this.deadlines); + newDeadlineList.deadlines.remove(index); + if (newDeadlineList.deadlines.isEmpty()) { + return new DeadlineList(); + } + return newDeadlineList; + } + + /** + * Produces a string representation of this {@code Deadline} object for printing to GUI. + * + * @return list view of deadlines in this {@code Deadline} object. + */ + public String listFormat() { + if (deadlines.size() == 0) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (int i = 1; i <= deadlines.size(); i++) { + result.append(deadlines.get(i - 1)).append("\n"); + } + return result.toString(); + } + + /** + * Gives the size of this deadline list, considering only deadlines given by user. + */ + public int size() { + return this.deadlines.get(0).equals(EMPTY_DEADLINE) ? 0 + : this.deadlines.size(); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < deadlines.size(); i++) { + result.append(deadlines.get(i)); + if (i < deadlines.size() - 1) { + result.append(", "); + } + } + return result.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeadlineList // instanceof handles nulls + && deadlines.equals(((DeadlineList) other).deadlines)); // state check + } + + @Override + public int compareTo(DeadlineList other) { + return Collections.min(this.deadlines, new DeadlineComparator()) + .compareTo(Collections.min(other.getDeadlines(), new DeadlineComparator())); + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index f866e7133de..1164856f13a 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -7,8 +7,8 @@ * Represents a Person's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ -public class Email { - +public class Email implements Comparable { + public static final String MODEL_NAME = "email"; private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" @@ -68,4 +68,9 @@ public int hashCode() { return value.hashCode(); } + @Override + public int compareTo(Email other) { + return this.toString().compareToIgnoreCase(other.toString()); + } + } diff --git a/src/main/java/seedu/address/model/person/Favourite.java b/src/main/java/seedu/address/model/person/Favourite.java new file mode 100644 index 00000000000..bd0a7e1d692 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Favourite.java @@ -0,0 +1,89 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +public class Favourite implements Comparable { + public static final String MODEL_NAME = "fav"; + public static final Favourite IS_FAVOURITE = new Favourite(true); + public static final Favourite NOT_FAVOURITE = new Favourite(false); + public static final String MESSAGE_CONSTRAINTS = + "Favourite should be either true or false, and should not be blank."; + + /* + * The first character of the favourite status must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs a {@code Favourite} status object. + * + * @param isFavourite The favourite status. + */ + private Favourite(Boolean isFavourite) { + requireNonNull(isFavourite); + value = isFavourite.toString(); + } + + /** + * Takes the string representation of the boolean and returns the correct {@code Favourite} object. + * + * @param isFavourite is the String representation provided. + * @return the {@code Favourite} object that corresponds to the String provided. + */ + public static Favourite valueOf(String isFavourite) throws IllegalArgumentException { + if (!("true".equals(isFavourite) || "false".equals(isFavourite))) { + throw new IllegalArgumentException("Favourite can only be true or false."); + } + + return "true".equals(isFavourite) + ? IS_FAVOURITE + : NOT_FAVOURITE; + } + + /** + * Returns true if a given string is a valid favourite status. + */ + public static boolean isValidFavourite(String test) { + return test.matches(VALIDATION_REGEX) + && ("true".equals(test) || "false".equals(test)); + } + + + public boolean isFavourite() { + return this.equals(IS_FAVOURITE); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Favourite // instanceof handles nulls + && value.equals(((Favourite) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(Favourite other) { + if (this.isFavourite() && !other.isFavourite()) { + return -1; + } else if (this.isFavourite() == other.isFavourite()) { + return 0; + } else if (!this.isFavourite() && other.isFavourite()) { + return 1; + } else { + return 0; + } + } + +} diff --git a/src/main/java/seedu/address/model/person/HighImportance.java b/src/main/java/seedu/address/model/person/HighImportance.java new file mode 100644 index 00000000000..dc2c276c229 --- /dev/null +++ b/src/main/java/seedu/address/model/person/HighImportance.java @@ -0,0 +1,91 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +public class HighImportance implements Comparable { + public static final String MODEL_NAME = "impt"; + public static final HighImportance HIGH_IMPORTANCE = new HighImportance(true); + public static final HighImportance NOT_HIGH_IMPORTANCE = new HighImportance(false); + public static final String MESSAGE_CONSTRAINTS = + "HighImportance should be either true or false, and should not be blank."; + public static final String TRUE_STRING = String.valueOf(true); + public static final String FALSE_STRING = String.valueOf(false); + /* + * The first character of the HighImportance status must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs a {@code HighImportance} status object. + * + * @param hasHighImportance The HighImportance status. + */ + public HighImportance(Boolean hasHighImportance) { + requireNonNull(hasHighImportance); + value = hasHighImportance.toString(); + } + + /** + * Takes the string representation of the boolean and returns the correct {@code HighImportance} object. + * + * @param hasHighImportance is the String representation provided. + * @return the {@code HighImportance} object that corresponds to the String provided. + */ + public static HighImportance valueOf(String hasHighImportance) throws IllegalArgumentException { + if (!(TRUE_STRING.equals(hasHighImportance) || FALSE_STRING.equals(hasHighImportance))) { + throw new IllegalArgumentException("HighImportance can only be true or false."); + } + + return TRUE_STRING.equals(hasHighImportance) + ? HIGH_IMPORTANCE + : NOT_HIGH_IMPORTANCE; + } + + /** + * Returns true if a given string has a valid HighImportance status. + */ + public static boolean isValidHighImportance(String test) { + return test.matches(VALIDATION_REGEX) + && (TRUE_STRING.equals(test) || FALSE_STRING.equals(test)); + } + + /** + * Returns true if a person has high importance + */ + public boolean hasHighImportance() { + return this.equals(HIGH_IMPORTANCE); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof HighImportance // instanceof handles nulls + && value.equals(((HighImportance) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(HighImportance other) { + if (this.hasHighImportance() && !other.hasHighImportance()) { + return -1; + } else if (this.hasHighImportance() == other.hasHighImportance()) { + return 0; + } else if (!this.hasHighImportance() && other.hasHighImportance()) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 79244d71cf7..34c3048c580 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -7,8 +7,8 @@ * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ -public class Name { - +public class Name implements Comparable { + public static final String MODEL_NAME = "name"; public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces, and it should not be blank"; @@ -56,4 +56,9 @@ public int hashCode() { return fullName.hashCode(); } + @Override + public int compareTo(Name other) { + return this.toString().compareToIgnoreCase(other.toString()); + } + } diff --git a/src/main/java/seedu/address/model/person/Notes.java b/src/main/java/seedu/address/model/person/Notes.java new file mode 100644 index 00000000000..8508f80c4ed --- /dev/null +++ b/src/main/java/seedu/address/model/person/Notes.java @@ -0,0 +1,118 @@ +package seedu.address.model.person; + +import java.util.ArrayList; +import java.util.List; + +public class Notes { + + public static final String MESSAGE_CONSTRAINTS = "Notes can take any values, and it should not be blank"; + + private static final char WHITESPACE = ' '; + + public final List value; + + /** + * Constructs a new {@code Notes}. + */ + private Notes() { + value = new ArrayList<>(); + } + + /** + * Checks if the given note is a valid note that will be added. + * A note is valid as long as it contains at least 1 character that + * is not a whitespace. + * @param noteToAdd note to check validity + * @return true if string is valid, false otherwise + */ + public static boolean isValidNote(String noteToAdd) { + boolean isAllWhiteSpace = true; + int i = 0; + while (isAllWhiteSpace && i < noteToAdd.length()) { + isAllWhiteSpace = noteToAdd.charAt(i) == WHITESPACE; + i++; + } + return !isAllWhiteSpace; + } + + /** + * Create a fresh Notes for when a new contact is + * created, or when notes for a contact are cleared. + * @return Notes object with empty list + */ + public static Notes getNewNotes() { + return new Notes(); + } + + /** + * Initialises a Notes object containing notes that + * are in the List passed to it. + * @param notes list of notes to initialise the Notes object with + * @return Notes containing notes passed to it + */ + public static Notes loadNotesFromList(List notes) { + Notes newNotes = getNewNotes(); + newNotes.value.addAll(notes); + return newNotes; + } + + /** + * Creates a new Notes that has this Notes' contents and the given + * new note appended. + * @param newNote new note to be added + * @return new Notes + */ + public Notes updateNotes(String newNote) { + Notes newNotes = new Notes(); + newNotes.value.addAll(this.value); + newNotes.value.add(newNote); + return newNotes; + } + + /** + * Creates a new Notes that has this Notes' contents with the note at + * the given index deleted. + * @param index index of note to be deleted + * @return new Notes + */ + public Notes delete(int index) { + Notes newNotes = new Notes(); + newNotes.value.addAll(this.value); + newNotes.value.remove(index); + return newNotes; + } + + /** + * Produces a string representation of this Notes + * object for printing to GUI. + * @return list view of notes in this Notes object + */ + public String listFormat() { + if (value.size() == 0) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (int i = 1; i <= value.size(); i++) { + result.append(i).append(". ").append(value.get(i - 1)).append("\n"); + } + return result.toString(); + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Notes // instanceof handles nulls + && value.equals(((Notes) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8ff1d83fe89..ba1f70d3709 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -1,12 +1,11 @@ package seedu.address.model.person; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.tag.Tag; /** @@ -22,18 +21,29 @@ public class Person { // Data fields private final Address address; + private final DeadlineList deadlines; + private final Notes notes; private final Set tags = new HashSet<>(); + private final Favourite favouriteStatus; + private final ImageDetailsList imageDetailsList; + private final HighImportance highImportanceStatus; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, DeadlineList deadlines, Notes notes, + Set tags, Favourite favouriteStatus, HighImportance highImportanceStatus, + ImageDetailsList images) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.deadlines = deadlines; + this.notes = notes; + this.favouriteStatus = favouriteStatus; + this.highImportanceStatus = highImportanceStatus; this.tags.addAll(tags); + this.imageDetailsList = images; } public Name getName() { @@ -52,6 +62,34 @@ public Address getAddress() { return address; } + public DeadlineList getDeadlines() { + return deadlines; + } + + public Notes getNotes() { + return notes; + } + + public Favourite getFavouriteStatus() { + return favouriteStatus; + } + + public boolean isFavourite() { + return favouriteStatus.isFavourite(); + } + + public ImageDetailsList getImageDetailsList() { + return imageDetailsList; + } + + public HighImportance getHighImportanceStatus() { + return highImportanceStatus; + } + + public boolean hasHighImportance() { + return highImportanceStatus.hasHighImportance(); + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -92,13 +130,19 @@ public boolean equals(Object other) { && otherPerson.getPhone().equals(getPhone()) && otherPerson.getEmail().equals(getEmail()) && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); + && otherPerson.getDeadlines().equals(getDeadlines()) + && otherPerson.getNotes().equals(getNotes()) + && otherPerson.getTags().equals(getTags()) + && otherPerson.getFavouriteStatus().equals(getFavouriteStatus()) + && otherPerson.getImageDetailsList().equals(getImageDetailsList()) + && otherPerson.getHighImportanceStatus().equals(getHighImportanceStatus()); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, deadlines, notes, tags, favouriteStatus, imageDetailsList, + highImportanceStatus); } @Override @@ -110,14 +154,22 @@ public String toString() { .append("; Email: ") .append(getEmail()) .append("; Address: ") - .append(getAddress()); + .append(getAddress()) + .append("; Deadline(s): ") + .append(getDeadlines()) + .append("; Notes: ") + .append(getNotes()) + .append("; Favourite: ") + .append(getFavouriteStatus()) + .append("; Importance Status: ") + .append(getHighImportanceStatus()); Set tags = getTags(); + if (!tags.isEmpty()) { builder.append("; Tags: "); tags.forEach(builder::append); } return builder.toString(); } - } diff --git a/src/main/java/seedu/address/model/person/PersonHasHighImportancePredicate.java b/src/main/java/seedu/address/model/person/PersonHasHighImportancePredicate.java new file mode 100644 index 00000000000..5431c2fbe33 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonHasHighImportancePredicate.java @@ -0,0 +1,19 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Person} is a {@code HighImportance} contact. + */ +public class PersonHasHighImportancePredicate implements Predicate { + @Override + public boolean test(Person person) { + return person.getHighImportanceStatus().hasHighImportance(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonHasHighImportancePredicate); + } +} diff --git a/src/main/java/seedu/address/model/person/PersonIsFavouriteContactPredicate.java b/src/main/java/seedu/address/model/person/PersonIsFavouriteContactPredicate.java new file mode 100644 index 00000000000..66bae87b9c4 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonIsFavouriteContactPredicate.java @@ -0,0 +1,20 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + + +/** + * Tests that a {@code Person} is a {@code Favourite} contact. + */ +public class PersonIsFavouriteContactPredicate implements Predicate { + @Override + public boolean test(Person person) { + return person.getFavouriteStatus().isFavourite(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NameContainsKeywordsPredicate); + } +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index 872c76b382f..b01b29af975 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -7,9 +7,8 @@ * Represents a Person's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ -public class Phone { - - +public class Phone implements Comparable { + public static final String MODEL_NAME = "phone"; public static final String MESSAGE_CONSTRAINTS = "Phone numbers should only contain numbers, and it should be at least 3 digits long"; public static final String VALIDATION_REGEX = "\\d{3,}"; @@ -50,4 +49,9 @@ public int hashCode() { return value.hashCode(); } + @Override + public int compareTo(Phone other) { + return this.toString().compareToIgnoreCase(other.toString()); + } + } diff --git a/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..83b7b2342d2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java @@ -0,0 +1,51 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Constructs a TagContainKeywordsPredicate to test if a {@code Person} has all the tags listed in the {@code + * List} of keywords + * @param keywords the keywords to check + */ + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + boolean hasAllMatches = true; + + if (keywords.size() == 0) { + return false; + } + + for (String keyword : keywords) { + boolean doesKeywordMatchAnyTags = person.getTags().stream() + .anyMatch(tag -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword)); + hasAllMatches = hasAllMatches && doesKeywordMatchAnyTags; + } + + return hasAllMatches; + } + + public List getMatchedKeyowrds() { + return this.keywords; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/tag/ActivatedTagList.java b/src/main/java/seedu/address/model/tag/ActivatedTagList.java new file mode 100644 index 00000000000..ffe9b40dc24 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/ActivatedTagList.java @@ -0,0 +1,144 @@ +package seedu.address.model.tag; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +/** + * A list of activated tags that enforces uniqueness between its elements and does not allow nulls. + * A tag is considered unique by comparing using {@code Tag#isSameTag(Tag)}. As such, adding and updating of + * tags uses Tag#isSameTag(Tag) for equality so as to ensure that the tag being added or updated is + * unique in terms of identity in the ActivatedTagList. However, the removal of a tag uses Tag#equals(Object). + * + * Supports a minimal set of list operations. + * + * @see Tag#isSameTag(Tag) + */ +public class ActivatedTagList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent person as the given argument. + */ + public boolean contains(Tag toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameTag); + } + + /** + * Adds a person to the list. + * The person must not already exist in the list. + */ + public void add(Tag toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateTagException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the tag {@code target} in the list with {@code editedTag}. + * {@code target} must exist in the list. + * The tag identity of {@code editedTag} must not be the same as another existing tag in the list. + */ + public void setTag(Tag target, Tag editedTag) { + requireAllNonNull(target, editedTag); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new TagNotFoundException(); + } + + if (!target.isSameTag(editedTag) && contains(editedTag)) { + throw new DuplicateTagException(); + } + + internalList.set(index, editedTag); + } + + public void setTags(ActivatedTagList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code tags}. + * {@code tags} must not contain duplicate tags. + */ + public void setTags(List tags) { + requireAllNonNull(tags); + if (!tagsAreUnique(tags)) { + throw new DuplicateTagException(); + } + + internalList.setAll(tags); + } + + /** + * Removes the equivalent tag from the list. + * The tag must exist in the list. + */ + public void remove(Tag toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new TagNotFoundException(); + } + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ActivatedTagList // instanceof handles nulls + && internalList.equals(((ActivatedTagList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Clears the list of activated tags + */ + public void clear() { + internalList.clear(); + internalUnmodifiableList.clear(); + } + + /** + * Returns true if {@code tag} contains only unique tag. + */ + private boolean tagsAreUnique(List tags) { + for (int i = 0; i < tags.size() - 1; i++) { + for (int j = i + 1; j < tags.size(); j++) { + if (tags.get(i).isSameTag(tags.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..ef1ece7ef26 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -32,6 +32,18 @@ public static boolean isValidTagName(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns true if both tags have the same name. + * This defines a weaker notion of equality between two tags. + */ + public boolean isSameTag(Tag otherTag) { + if (otherTag == this) { + return true; + } + return otherTag != null + && otherTag.tagName.equalsIgnoreCase(tagName); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/model/tag/UniqueTagList.java b/src/main/java/seedu/address/model/tag/UniqueTagList.java new file mode 100644 index 00000000000..f54c34c0dd9 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/UniqueTagList.java @@ -0,0 +1,136 @@ +package seedu.address.model.tag; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +/** + * A list of tags that enforces uniqueness between its elements and does not allow nulls. + * A tag is considered unique by comparing using {@code Tag#isSameTag(Tag)}. As such, adding and updating of + * tags uses Tag#isSameTag(Tag) for equality so as to ensure that the tag being added or updated is + * unique in terms of identity in the UniqueTagList. However, the removal of a tag uses Tag#equals(Object). + * + * Supports a minimal set of list operations. + * + * @see Tag#isSameTag(Tag) + */ +public class UniqueTagList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent person as the given argument. + */ + public boolean contains(Tag toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameTag); + } + + /** + * Adds a person to the list. + * The person must not already exist in the list. + */ + public void add(Tag toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateTagException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the tag {@code target} in the list with {@code editedTag}. + * {@code target} must exist in the list. + * The tag identity of {@code editedTag} must not be the same as another existing tag in the list. + */ + public void setTag(Tag target, Tag editedTag) { + requireAllNonNull(target, editedTag); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new TagNotFoundException(); + } + + if (!target.isSameTag(editedTag) && contains(editedTag)) { + throw new DuplicateTagException(); + } + + internalList.set(index, editedTag); + } + + public void setTags(UniqueTagList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code tags}. + * {@code tags} must not contain duplicate tags. + */ + public void setTags(List tags) { + requireAllNonNull(tags); + if (!tagsAreUnique(tags)) { + throw new DuplicateTagException(); + } + + internalList.setAll(tags); + } + + /** + * Removes the equivalent tag from the list. + * The tag must exist in the list. + */ + public void remove(Tag toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new TagNotFoundException(); + } + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueTagList // instanceof handles nulls + && internalList.equals(((UniqueTagList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code tag} contains only unique tag. + */ + private boolean tagsAreUnique(List tags) { + for (int i = 0; i < tags.size() - 1; i++) { + for (int j = i + 1; j < tags.size(); j++) { + if (tags.get(i).isSameTag(tags.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java new file mode 100644 index 00000000000..87b555ed648 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java @@ -0,0 +1,11 @@ +package seedu.address.model.tag.exceptions; + +/** + * Signals that the operation will result in duplicate Tags (Tags are considered duplicates if they have the same + * name (case-insensitive)). + */ +public class DuplicateTagException extends RuntimeException { + public DuplicateTagException() { + super("Operation would result in duplicate tags"); + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java new file mode 100644 index 00000000000..febac1a415e --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java @@ -0,0 +1,10 @@ +package seedu.address.model.tag.exceptions; + +/** + * Signals that the operation is unable to find the specified tag. + */ +public class TagNotFoundException extends RuntimeException { + public TagNotFoundException() { + super("Tag not found."); + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..f167ac5e553 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,14 +1,23 @@ package seedu.address.model.util; +import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Address; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -18,25 +27,41 @@ */ public class SampleDataUtil { public static Person[] getSamplePersons() { - return new Person[] { + return new Person[]{ new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), new DeadlineList(), Notes.getNewNotes(), + getTagSet("friends"), Favourite.NOT_FAVOURITE, HighImportance.HIGH_IMPORTANCE, + new ImageDetailsList()), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new DeadlineList(), + Notes.getNewNotes(), getTagSet("colleagues", "friends"), Favourite.NOT_FAVOURITE, + HighImportance.NOT_HIGH_IMPORTANCE, new ImageDetailsList()), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), new DeadlineList(), Notes.getNewNotes(), + getTagSet("neighbours"), Favourite.NOT_FAVOURITE, HighImportance.NOT_HIGH_IMPORTANCE, + new ImageDetailsList()), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), new DeadlineList(), + Notes.getNewNotes(), getTagSet("family"), Favourite.NOT_FAVOURITE, + HighImportance.NOT_HIGH_IMPORTANCE, new ImageDetailsList()), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), + new Address("Blk 47 Tampines Street 20, #17-35"), new DeadlineList(), Notes.getNewNotes(), + getTagSet("classmates"), Favourite.NOT_FAVOURITE, HighImportance.NOT_HIGH_IMPORTANCE, + new ImageDetailsList()), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 45 Aljunied Street 85, #11-31"), new DeadlineList(), Notes.getNewNotes(), + getTagSet("colleagues"), Favourite.NOT_FAVOURITE, HighImportance.NOT_HIGH_IMPORTANCE, + new ImageDetailsList()) + }; + } + + public static Tag[] getSampleTags() { + return new Tag[]{ + new Tag("friends"), + new Tag("colleagues"), + new Tag("neighbours"), + new Tag("classmates"), + new Tag("family") }; } @@ -45,6 +70,11 @@ public static ReadOnlyAddressBook getSampleAddressBook() { for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + + for (Tag sampleTag : getSampleTags()) { + sampleAb.addTag(sampleTag); + } + return sampleAb; } @@ -57,4 +87,28 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + public static DeadlineList getDeadlineList(String[] strings) { + ArrayList deadlines = new ArrayList<>(); + for (String string : strings) { + String trimmedDeadline = string.trim(); + StringBuilder originalInput = new StringBuilder(trimmedDeadline); + originalInput.reverse(); + String[] reversedAndSplit = originalInput.toString().split("\\s+", 2); + String description = new StringBuilder(reversedAndSplit[1]).reverse().toString(); + String date = new StringBuilder(reversedAndSplit[0]).reverse().toString(); + deadlines.add(new Deadline(description, date)); + } + return new DeadlineList(deadlines); + } + + /** + * Returns an image details list containing the list of image details given + */ + public static ImageDetailsList getImageDetailsList(String... imageDetails) { + return new ImageDetailsList(Arrays.stream(imageDetails) + .map(File::new) + .map(ImageDetails::new) + .collect(Collectors.toList())); + } + } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedDeadline.java b/src/main/java/seedu/address/storage/JsonAdaptedDeadline.java new file mode 100644 index 00000000000..eada6b95275 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedDeadline.java @@ -0,0 +1,31 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Deadline; + +class JsonAdaptedDeadline { + private final String description; + private final String date; + + @JsonCreator + public JsonAdaptedDeadline(@JsonProperty("description") String description, + @JsonProperty("date") String date) { + this.description = description; + this.date = date; + } + + public JsonAdaptedDeadline(Deadline source) { + description = source.description; + date = source.value; + } + + public Deadline toModelType() throws IllegalValueException { + if (!Deadline.isValidDeadline(description, date)) { + throw new IllegalValueException(Deadline.MESSAGE_CONSTRAINTS); + } + return new Deadline(description, date); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedImageDetails.java b/src/main/java/seedu/address/storage/JsonAdaptedImageDetails.java new file mode 100644 index 00000000000..1a37a157663 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedImageDetails.java @@ -0,0 +1,43 @@ +package seedu.address.storage; + +import java.io.File; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.image.ImageDetails; + +public class JsonAdaptedImageDetails { + + private final String imageFilePath; + + /** + * Constructs a {@code JsonAdaptedImage} with the given {@code imageFilePath}. + */ + @JsonCreator + public JsonAdaptedImageDetails(String imageFilePath) { + this.imageFilePath = imageFilePath; + } + + /** + * Converts a given {@code source} into this class for Jackson use. + */ + public JsonAdaptedImageDetails(ImageDetails source) { + imageFilePath = source.getPath(); + } + + @JsonValue + public String getImageFilePath() { + return imageFilePath; + } + + /** + * Converts this Jackson-friendly adapted imageDetails object into the model's {@code ImageDetails} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted imageDetails. + */ + public ImageDetails toModelType() throws IllegalValueException { + return new ImageDetails(new File(imageFilePath)); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index a6321cec2ea..2c55f6d93c1 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,9 +10,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Address; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -28,22 +35,43 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final List deadlines = new ArrayList<>(); + private final List notes = new ArrayList<>(); private final List tagged = new ArrayList<>(); + private final String isFavourite; + private final List images = new ArrayList<>(); + private final String hasHighImportance; /** * 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("tagged") List tagged) { + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("deadlines") List deadlines, + @JsonProperty("notes") List notes, + @JsonProperty("tagged") List tagged, + @JsonProperty("isFavourite") String isFavourite, + @JsonProperty("images") List images, + @JsonProperty("hasHighImportance") String hasHighImportance) { this.name = name; this.phone = phone; this.email = email; this.address = address; + if (deadlines != null) { + this.deadlines.addAll(deadlines); + } + if (notes != null) { + this.notes.addAll(notes); + } if (tagged != null) { this.tagged.addAll(tagged); } + this.isFavourite = isFavourite; + if (images != null) { + this.images.addAll(images); + } + this.hasHighImportance = hasHighImportance; } /** @@ -54,9 +82,18 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + deadlines.addAll(source.getDeadlines().getDeadlines().stream() + .map(JsonAdaptedDeadline::new) + .collect(Collectors.toList())); + notes.addAll(source.getNotes().value); + isFavourite = source.getFavouriteStatus().value; + hasHighImportance = source.getHighImportanceStatus().value; tagged.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + images.addAll(source.getImageDetailsList().getImages().stream() + .map(JsonAdaptedImageDetails::new) + .collect(Collectors.toList())); } /** @@ -70,8 +107,14 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + final List personDeadlines = new ArrayList<>(); + for (JsonAdaptedDeadline deadline : deadlines) { + personDeadlines.add(deadline.toModelType()); + } + if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Name.class.getSimpleName())); } if (!Name.isValidName(name)) { throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); @@ -102,8 +145,37 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + final Notes modelNotes = Notes.loadNotesFromList(notes); + + if (isFavourite == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, Favourite.class.getSimpleName())); + } + if (!Favourite.isValidFavourite(isFavourite)) { + throw new IllegalValueException(Favourite.MESSAGE_CONSTRAINTS); + } + final Favourite modelFavourite = Favourite.valueOf(isFavourite); + + if (hasHighImportance == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, HighImportance.class.getSimpleName())); + } + if (!HighImportance.isValidHighImportance(hasHighImportance)) { + throw new IllegalValueException(HighImportance.MESSAGE_CONSTRAINTS); + } + final HighImportance modelHighImportance = HighImportance.valueOf(hasHighImportance); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + final DeadlineList modelDeadlines = new DeadlineList(personDeadlines); + + final List personImages = new ArrayList<>(); + for (JsonAdaptedImageDetails image : images) { + personImages.add(image.toModelType()); + } + final ImageDetailsList modelImages = new ImageDetailsList(personImages); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelDeadlines, + modelNotes, modelTags, modelFavourite, modelHighImportance, modelImages); } } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..947e75746b7 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -12,6 +12,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * An Immutable AddressBook that is serializable to JSON format. @@ -20,15 +21,19 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_TAG = "Tags list contain duplicate tag(s)."; private final List persons = new ArrayList<>(); + private final List tags = new ArrayList<>(); /** * Constructs a {@code JsonSerializableAddressBook} with the given persons. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("tags") List tags) { this.persons.addAll(persons); + this.tags.addAll(tags); } /** @@ -38,6 +43,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { private static final String FXML = "CommandBox.fxml"; private final CommandExecutor commandExecutor; + private final HistoryGetter historyGetter; + private int historyPosition = 0; @FXML private TextField commandTextField; @@ -24,9 +31,10 @@ public class CommandBox extends UiPart { /** * Creates a {@code CommandBox} with the given {@code CommandExecutor}. */ - public CommandBox(CommandExecutor commandExecutor) { + public CommandBox(CommandExecutor commandExecutor, HistoryGetter historyGetter) { super(FXML); this.commandExecutor = commandExecutor; + this.historyGetter = historyGetter; // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); } @@ -36,6 +44,8 @@ public CommandBox(CommandExecutor commandExecutor) { */ @FXML private void handleCommandEntered() { + historyPosition = 0; // Reset history pointer on new input + String commandText = commandTextField.getText(); if (commandText.equals("")) { return; @@ -49,6 +59,37 @@ private void handleCommandEntered() { } } + @FXML + private void handleKeyPressed(KeyEvent e) { + if (e.getCode().equals(KeyCode.UP)) { + getHistory(+1); + } + if (e.getCode().equals(KeyCode.DOWN)) { + getHistory(-1); + } + } + + private void getHistory(int step) { + int expectedHistoryPosition = historyPosition + step; + + if (expectedHistoryPosition < 0) { + return; + } + + if (expectedHistoryPosition == 0) { + commandTextField.setText(""); + historyPosition = expectedHistoryPosition; + return; + } + + CommandHistoryEntry commandText = requireNonNull(historyGetter.getCommandText(expectedHistoryPosition)); + if (commandText.exists()) { + historyPosition = expectedHistoryPosition; + commandTextField.setText(commandText.getCommandText()); + } + } + + /** * Sets the command box style to use the default style. */ @@ -82,4 +123,17 @@ public interface CommandExecutor { CommandResult execute(String commandText) throws CommandException, ParseException; } + /** + * Represents a function that can get history from the command history. + */ + @FunctionalInterface + public interface HistoryGetter { + /** + * Retrieves a command from the command history + * + * @see seedu.address.logic.Logic#getCommandText(int) + */ + CommandHistoryEntry getCommandText(int i); + } + } diff --git a/src/main/java/seedu/address/ui/CopyUrlWindow.java b/src/main/java/seedu/address/ui/CopyUrlWindow.java new file mode 100644 index 00000000000..4c42fa1e853 --- /dev/null +++ b/src/main/java/seedu/address/ui/CopyUrlWindow.java @@ -0,0 +1,79 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; + +public class CopyUrlWindow extends UiPart { + + public static final String SUCCESS_MESSAGE = "URL successfully copied!"; + + private static final Logger logger = LogsCenter.getLogger(CopyUrlWindow.class); + private static final String FXML = "CopyUrlWindow.fxml"; + + + @FXML + private Label successMessage; + + /** + * Creates a new CopyUrlWindow. + */ + public CopyUrlWindow(Stage root) { + super(FXML, root); + successMessage.setText(SUCCESS_MESSAGE); + } + + /** + * Creates a new CopyUrlWindow. + */ + public CopyUrlWindow() { + this(new Stage()); + } + + /** + * Shows the help window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing message that url has been copied successfully."); + getRoot().show(); + } + + /** + * Returns true if the help window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the help window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the help window. + */ + public void focus() { + getRoot().requestFocus(); + } +} diff --git a/src/main/java/seedu/address/ui/DetailedContactPanel.java b/src/main/java/seedu/address/ui/DetailedContactPanel.java new file mode 100644 index 00000000000..ce37553202e --- /dev/null +++ b/src/main/java/seedu/address/ui/DetailedContactPanel.java @@ -0,0 +1,42 @@ +package seedu.address.ui; + +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.model.person.Person; + +public class DetailedContactPanel extends UiPart { + private static final String FXML = "DetailedContactPanel.fxml"; + + @FXML + private ListView detailedContactViewPanel; + + /** + * Creates a {@code DetailedContactPanel} with the given {@code ObservableList}. + */ + public DetailedContactPanel(ObservableList detailedContactView) { + super(FXML); + detailedContactViewPanel.setItems(detailedContactView); + detailedContactViewPanel.setCellFactory(listView -> new DetailedPersonCardCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Person} using a + * {@code DetailedPersonCard}. + */ + class DetailedPersonCardCell extends ListCell { + @Override + protected void updateItem(Person person, boolean empty) { + super.updateItem(person, empty); + + if (empty || person == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new DetailedPersonCard(person).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/DetailedPersonCard.java b/src/main/java/seedu/address/ui/DetailedPersonCard.java new file mode 100644 index 00000000000..53a33a06219 --- /dev/null +++ b/src/main/java/seedu/address/ui/DetailedPersonCard.java @@ -0,0 +1,136 @@ +package seedu.address.ui; + +import java.util.Comparator; +import java.util.Objects; + +import javafx.fxml.FXML; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import seedu.address.commons.core.index.Index; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Person; + +/** + * A UI component that shows a full, detailed view of a Person + */ +public class DetailedPersonCard extends UiPart { + private static final String FXML = "DetailedPersonCard.fxml"; + private static final int MAXIMUM_IMAGES_TO_DISPLAY = 6; + + @FXML + private Label name; + @FXML + private Label phone; + @FXML + private Label address; + @FXML + private Label email; + @FXML + private VBox notesPane; + @FXML + private Label notesTitle; + @FXML + private Label notes; + @FXML + private VBox deadlinesPane; + @FXML + private Label deadlinesTitle; + @FXML + private Label deadlines; + @FXML + private FlowPane tags; + @FXML + private Canvas starCanvas; + @FXML + private ImageView flagImageView; + @FXML + private TilePane imageListView; + @FXML + private Label moreImages; + + private final Image highImportanceFlag = new Image( + Objects.requireNonNull(this.getClass().getResourceAsStream("/images/red_flag.png"))); + private final Image notHighImportanceFlag = new Image( + Objects.requireNonNull(this.getClass().getResourceAsStream("/images/white_flag.png"))); + + /** + * Creates a {@code DetailedPersonCard} with the given {@code Person} to display + * @param person the person to display + */ + public DetailedPersonCard(Person person) { + super(FXML); + + name.setText(person.getName().fullName); + phone.setText(person.getPhone().value); + address.setText(person.getAddress().value); + email.setText(person.getEmail().value); + notes.setText(person.getNotes().listFormat()); + deadlines.setText(person.getDeadlines().listFormat()); + deadlinesTitle.setText("Deadlines"); + notesTitle.setText("Notes"); + + person.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + if (person.isFavourite()) { + starCanvas.setVisible(true); + drawStarShape(starCanvas.getGraphicsContext2D()); + } + + // Red flag if importance, otherwise empty plain flag + if (person.hasHighImportance()) { + flagImageView.setImage(highImportanceFlag); + } else { + flagImageView.setImage(notHighImportanceFlag); + } + flagImageView.setFitHeight(20); + flagImageView.setFitWidth(20); + + setImageListView(person.getImageDetailsList()); + } + + private void setImageListView(ImageDetailsList images) { + double totalWidth = this.getRoot().getWidth(); + int individualWidth = (int) totalWidth / 3; + + for (int i = 0; i < Math.min(images.size(), MAXIMUM_IMAGES_TO_DISPLAY); i++) { + Index index = Index.fromZeroBased(i); + ImageDetails imageDetails = images.get(index.getZeroBased()); + ImageCard imageCard = new ImageCard(index.getOneBased(), imageDetails, 120, individualWidth); + imageListView.getChildren().add(imageCard.getRoot()); + } + + if (images.size() > MAXIMUM_IMAGES_TO_DISPLAY) { + moreImages.setText(String.format("... and %d more images. Use the images command to see everything.", + images.size() - MAXIMUM_IMAGES_TO_DISPLAY)); + moreImages.setVisible(true); + } + } + + private void drawStarShape(GraphicsContext gc) { + //@@author takufunkai-reused + //Reused from https://zetcode.com/gui/javafx/canvas/ + // with minor modifications to the points and fill, for suitable colour and size. + + double[] xpoints = {1, 7, 9, 11, 17, 13, 14, 9, 4, 5}; + double[] ypoints = {7, 6.5, 1, 6.5, 7, 10, 15, 12, 15, 10}; + + gc.setFill(Color.YELLOW); + gc.fillPolygon(xpoints, ypoints, xpoints.length); + + gc.setStroke(Color.BLACK); + gc.strokePolygon(xpoints, ypoints, xpoints.length); + + //@@author + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..95ae9a3bf88 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,17 +15,32 @@ */ public class HelpWindow extends UiPart { - 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 HELPWINDOW_MESSAGE = "Need help? Check out any of the links below!"; + public static final String USERGUIDE_URL = "https://ay2122s2-cs2103t-t12-2.github.io/tp/UserGuide.html#quick-start"; + public static final String USER_GUIDE_HELP_MESSAGE = "User Guide: " + USERGUIDE_URL; + public static final String COMMAND_SUMMARY_URL = + "https://ay2122s2-cs2103t-t12-2.github.io/tp/UserGuide.html#command-summary"; + public static final String COMMAND_SUMMARY_HELP_MESSAGE = "Command Summary: " + COMMAND_SUMMARY_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; + private CopyUrlWindow copyUrlWindow; + + @FXML + private Label helpWindowMessage; + @FXML - private Button copyButton; + private Button userGuideCopyButton; @FXML - private Label helpMessage; + private Label userGuideHelpMessage; + + @FXML + private Button commandSummaryCopyButton; + + @FXML + private Label commandSummaryHelpMessage; /** * Creates a new HelpWindow. @@ -34,7 +49,11 @@ public class HelpWindow extends UiPart { */ public HelpWindow(Stage root) { super(FXML, root); - helpMessage.setText(HELP_MESSAGE); + userGuideHelpMessage.setText(USER_GUIDE_HELP_MESSAGE); + commandSummaryHelpMessage.setText(COMMAND_SUMMARY_HELP_MESSAGE); + helpWindowMessage.setText(HELPWINDOW_MESSAGE); + + copyUrlWindow = new CopyUrlWindow(); } /** @@ -80,6 +99,7 @@ public boolean isShowing() { */ public void hide() { getRoot().hide(); + copyUrlWindow.hide(); } /** @@ -93,10 +113,35 @@ public void focus() { * Copies the URL to the user guide to the clipboard. */ @FXML - private void copyUrl() { + private void copyUserGuideUrl() { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent url = new ClipboardContent(); url.putString(USERGUIDE_URL); clipboard.setContent(url); + handleCopyUrlSuccess(); + } + + /** + * Copies the URL to the user guide to the clipboard. + */ + @FXML + private void copyCommandSummaryUrl() { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent url = new ClipboardContent(); + url.putString(COMMAND_SUMMARY_URL); + clipboard.setContent(url); + handleCopyUrlSuccess(); + } + + /** + * Opens the copy url success window or focuses on it if it's already opened. + */ + @FXML + public void handleCopyUrlSuccess() { + if (!copyUrlWindow.isShowing()) { + copyUrlWindow.show(); + } else { + copyUrlWindow.focus(); + } } } diff --git a/src/main/java/seedu/address/ui/ImageCard.java b/src/main/java/seedu/address/ui/ImageCard.java new file mode 100644 index 00000000000..122134cd2f9 --- /dev/null +++ b/src/main/java/seedu/address/ui/ImageCard.java @@ -0,0 +1,70 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.effect.ColorAdjust; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import seedu.address.model.image.ImageDetails; + +public class ImageCard extends UiPart { + + private static final String FXML = "ImageCard.fxml"; + + public final ImageDetails imageDetails; + public final Image image; + + @FXML + private Label indexLabel; + @FXML + private ImageView imageView; + + private ColorAdjust darkened; + private ImageWindow imageWindow; + + /** + * Creates an ImageCard component, used to display in the image list pane + * + * @param index the index of this imageCard relative to its parent image list pane + * @param imageDetails the details of the image to be displayed + * @param height height of the image + * @param width width of the image + */ + public ImageCard(int index, ImageDetails imageDetails, int height, int width) { + super(FXML); + this.imageDetails = imageDetails; + indexLabel.setText(String.valueOf(index)); + image = new Image(imageDetails.getJavaFxImageUrl(), width, height, true, true); + imageView.setImage(image); + + darkened = new ColorAdjust(); + darkened.setBrightness(-0.2); + } + + @FXML + private void handleClick() { + Image image = new Image(imageDetails.getJavaFxImageUrl()); + if (imageWindow == null) { + imageWindow = new ImageWindow(image); + imageWindow.show(); + } + + if (imageWindow.isShowing()) { + imageWindow.focus(); + } else { + imageWindow.show(); + } + } + + @FXML + public void handleEnter() { + this.imageView.setEffect(darkened); + } + + @FXML + public void handleExit() { + this.imageView.setEffect(null); + } + +} diff --git a/src/main/java/seedu/address/ui/ImageViewPanel.java b/src/main/java/seedu/address/ui/ImageViewPanel.java new file mode 100644 index 00000000000..a91936a8704 --- /dev/null +++ b/src/main/java/seedu/address/ui/ImageViewPanel.java @@ -0,0 +1,33 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.layout.Region; +import javafx.scene.layout.TilePane; +import seedu.address.commons.core.index.Index; +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; + +public class ImageViewPanel extends UiPart { + private static final String FXML = "ImageViewPanel.fxml"; + + @FXML + private TilePane imageListView; + + /** + * Creates a {@code ImageViewPanel} with the given {@code ImageDetailsList}. + */ + public ImageViewPanel(ImageDetailsList images) { + super(FXML); + + double totalWidth = this.getRoot().getWidth(); + int individualWidth = (int) totalWidth / 3; + + for (int i = 0; i < images.size(); i++) { + Index index = Index.fromZeroBased(i); + ImageDetails imageDetails = images.get(index.getZeroBased()); + ImageCard imageCard = new ImageCard(index.getOneBased(), imageDetails, 120, individualWidth); + imageListView.getChildren().add(imageCard.getRoot()); + } + } + +} diff --git a/src/main/java/seedu/address/ui/ImageWindow.java b/src/main/java/seedu/address/ui/ImageWindow.java new file mode 100644 index 00000000000..a4b938df579 --- /dev/null +++ b/src/main/java/seedu/address/ui/ImageWindow.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; + +public class ImageWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); + private static final String FXML = "ImageWindow.fxml"; + + @FXML + private ImageView imageView; + + /** + * Creates a new ImageWindow. + * + * @param root Stage to use as the root of the ImageWindow. + */ + public ImageWindow(Stage root, Image image) { + super(FXML, root); + this.imageView.setImage(image); + } + + /** + * Creates a new ImageWindow. + */ + public ImageWindow(Image image) { + this(new Stage(), image); + } + + /** + * Shows the image window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing full image."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the image window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the image window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the image window. + */ + public void focus() { + getRoot().requestFocus(); + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..b83a5d44357 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -16,6 +16,8 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.image.ImageDetailsList; /** * The Main Window. Provides the basic application layout containing @@ -32,9 +34,15 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private DetailedContactPanel detailedContactPanel; + private ImageViewPanel imageViewPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private enum Panel { PERSON_LIST, DETAILED_VIEW, IMAGE_VIEW } + + private Panel panelInDisplay; + @FXML private StackPane commandBoxPlaceholder; @@ -42,7 +50,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane informationDisplayPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -78,6 +86,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -110,8 +119,17 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + personListPanel = new PersonListPanel(logic.getSortedPersonList(), logic.getActivatedTagList()); + informationDisplayPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + + detailedContactPanel = new DetailedContactPanel(logic.getDetailedContactView()); + informationDisplayPanelPlaceholder.getChildren().add(detailedContactPanel.getRoot()); + detailedContactPanel.getRoot().setVisible(false); + + ImageDetailsList list = logic.getImagesToView(); + imageViewPanel = new ImageViewPanel(list); + informationDisplayPanelPlaceholder.getChildren().add(imageViewPanel.getRoot()); + imageViewPanel.getRoot().setVisible(false); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -119,10 +137,17 @@ void fillInnerParts() { StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); + CommandBox commandBox = new CommandBox(this::executeCommand, this::getCommandHistory); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } + private void setPanel(Panel panelToShow) { + panelInDisplay = panelToShow; + personListPanel.getRoot().setVisible(panelToShow.equals(Panel.PERSON_LIST)); + detailedContactPanel.getRoot().setVisible(panelToShow.equals(Panel.DETAILED_VIEW)); + imageViewPanel.getRoot().setVisible(panelToShow.equals(Panel.IMAGE_VIEW)); + } + /** * Sets the default size based on {@code guiSettings}. */ @@ -167,6 +192,30 @@ public PersonListPanel getPersonListPanel() { return personListPanel; } + @FXML + private void handleDetailedView() { + setPanel(Panel.DETAILED_VIEW); + } + + /** + * Loads the contact's images. + * Initializes a new ImageViewPanel for each new contact to clear the previous images. + */ + @FXML + private void handleViewImages() { + ImageDetailsList list = logic.getImagesToView(); + imageViewPanel = new ImageViewPanel(list); + informationDisplayPanelPlaceholder.getChildren().add(imageViewPanel.getRoot()); + setPanel(Panel.IMAGE_VIEW); + } + + /** + * Changes the panel view to the full contacts list view + */ + private void handleListView() { + setPanel(Panel.PERSON_LIST); + } + /** * Executes the command and returns the result. * @@ -174,16 +223,34 @@ public PersonListPanel getPersonListPanel() { */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { - CommandResult commandResult = logic.execute(commandText); + logic.cacheCommandText(commandText); + + CommandResult commandResult = panelInDisplay == Panel.DETAILED_VIEW + ? logic.executeInDetailedViewMode(commandText) + : logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - if (commandResult.isShowHelp()) { - handleHelp(); - } + handleListView(); - if (commandResult.isExit()) { + switch (commandResult.getSpecialCommandResult()) { + case NONE: + break; + case SHOW_HELP: + handleHelp(); + break; + case VIEW_IMAGES: + handleViewImages(); + break; + case DETAILED_VIEW: + handleDetailedView(); + break; + case EXIT: handleExit(); + break; + default: + logger.warning("Program execution should not reach here"); + assert false; } return commandResult; @@ -193,4 +260,13 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Retrieves the command history from i-commands ago + * @param i the amount of commands to back-step + * @return the retrieved command text + */ + private CommandHistoryEntry getCommandHistory(int i) { + return logic.getCommandText(i); + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 7fc927bc5d9..d922a28deda 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,12 +1,20 @@ package seedu.address.ui; +import static javafx.application.Application.launch; + import java.util.Comparator; +import java.util.Objects; import javafx.fxml.FXML; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.paint.Color; import seedu.address.model.person.Person; /** @@ -39,10 +47,22 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label notes; + @FXML + private Label deadlines; + @FXML private FlowPane tags; + @FXML + private Canvas starCanvas; + @FXML + private ImageView flagImageView; + private final Image highImportanceFlag = new Image( + Objects.requireNonNull(this.getClass().getResourceAsStream("/images/red_flag.png"))); + private final Image notHighImportanceFlag = new Image( + Objects.requireNonNull(this.getClass().getResourceAsStream("/images/white_flag.png"))); /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. + * Creates a {@code PersonCard} with the given {@code Person} and index to display. */ public PersonCard(Person person, int displayedIndex) { super(FXML); @@ -51,10 +71,47 @@ public PersonCard(Person person, int displayedIndex) { name.setText(person.getName().fullName); phone.setText(person.getPhone().value); address.setText(person.getAddress().value); + notes.setText(person.getNotes().listFormat()); email.setText(person.getEmail().value); + deadlines.setText(person.getDeadlines().listFormat()); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + if (person.isFavourite()) { + starCanvas.setVisible(true); + drawStarShape(starCanvas.getGraphicsContext2D()); + } + + // Red flag if importance, otherwise empty plain flag + if (person.hasHighImportance()) { + flagImageView.setImage(highImportanceFlag); + } else { + flagImageView.setImage(notHighImportanceFlag); + } + flagImageView.setFitHeight(20); + flagImageView.setFitWidth(20); + } + + private void drawStarShape(GraphicsContext gc) { + //@@author takufunkai-reused + //Reused from https://zetcode.com/gui/javafx/canvas/ + // with minor modifications to the points and fill, for suitable colour and size. + + double[] xpoints = {1, 7, 9, 11, 17, 13, 14, 9, 4, 5}; + double[] ypoints = {7, 6.5, 1, 6.5, 7, 10, 15, 12, 15, 10}; + + gc.setFill(Color.YELLOW); + gc.fillPolygon(xpoints, ypoints, xpoints.length); + + gc.setStroke(Color.BLACK); + gc.strokePolygon(xpoints, ypoints, xpoints.length); + + //@@author + } + + public static void main(String[] args) { + launch(args); } @Override diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..f8fefe3ad83 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -1,17 +1,22 @@ package seedu.address.ui; +import java.util.Comparator; import java.util.logging.Logger; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** - * Panel containing the list of persons. + * Panel containing the list of persons or a detailed view of a person. */ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; @@ -20,11 +25,26 @@ public class PersonListPanel extends UiPart { @FXML private ListView personListView; + @FXML + private FlowPane activatedTags; + /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + * Creates a {@code PersonListPanel} with the given {@code ObservableList} of persons and activated tags */ - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList personList, ObservableList activatedTagList) { super(FXML); + activatedTagList.addListener((ListChangeListener) change -> { + while (change.next()) { + activatedTags.getChildren().clear(); + activatedTagList.stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> activatedTags.getChildren().add(new Label(tag.tagName))); + + } + }); + activatedTagList.stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> activatedTags.getChildren().add(new Label(tag.tagName))); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); } @@ -45,5 +65,4 @@ protected void updateItem(Person person, boolean empty) { } } } - } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..6c1ce5123f0 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/d_interieur.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/checked.png b/src/main/resources/images/checked.png new file mode 100644 index 00000000000..d3998392dec Binary files /dev/null and b/src/main/resources/images/checked.png differ diff --git a/src/main/resources/images/d_interieur.png b/src/main/resources/images/d_interieur.png new file mode 100644 index 00000000000..ea8193de51b Binary files /dev/null and b/src/main/resources/images/d_interieur.png differ diff --git a/src/main/resources/images/red_flag.png b/src/main/resources/images/red_flag.png new file mode 100644 index 00000000000..ba5cbe5e0a9 Binary files /dev/null and b/src/main/resources/images/red_flag.png differ diff --git a/src/main/resources/images/white_flag.png b/src/main/resources/images/white_flag.png new file mode 100644 index 00000000000..a56d4ce12c4 Binary files /dev/null and b/src/main/resources/images/white_flag.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..d700c063d96 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -4,6 +4,6 @@ - + diff --git a/src/main/resources/view/CopyUrlWindow.css b/src/main/resources/view/CopyUrlWindow.css new file mode 100644 index 00000000000..c2ae5bafe47 --- /dev/null +++ b/src/main/resources/view/CopyUrlWindow.css @@ -0,0 +1,19 @@ +.Button, Label { + -fx-text-fill: white; +} + +.Button { + -fx-background-color: dimgray; +} + +.Button:hover { + -fx-background-color: gray; +} + +.Button:armed { + -fx-background-color: darkgray; +} + +#successMessageContainer { + -fx-background-color: derive(#1d1d1d, 20%); +} diff --git a/src/main/resources/view/CopyUrlWindow.fxml b/src/main/resources/view/CopyUrlWindow.fxml new file mode 100644 index 00000000000..5053390f232 --- /dev/null +++ b/src/main/resources/view/CopyUrlWindow.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..60a606b1724 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -3,6 +3,10 @@ background-color: #383838; /* Used in the default.html file */ } +.image_scroll_pane { + -fx-background-color: #383838; +} + .label { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; @@ -94,29 +98,33 @@ } .list-cell { - -fx-label-padding: 0 0 0 0; + -fx-label-padding: 0; -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; + -fx-padding: 10px 5px; } .list-cell:filled:even { -fx-background-color: #3c3e3f; + -fx-background-radius: 20px; } .list-cell:filled:odd { -fx-background-color: #515658; + -fx-background-radius: 20px; } .list-cell:filled:selected { -fx-background-color: #424d5f; + -fx-background-radius: 20px; } .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; + -fx-border-radius: 20px; + -fx-padding: 4px; } .list-cell .label { + -fx-padding: 1px 0; -fx-text-fill: white; } @@ -139,6 +147,8 @@ .pane-with-border { -fx-background-color: derive(#1d1d1d, 20%); -fx-border-color: derive(#1d1d1d, 10%); + -fx-border-radius: 10px; + -fx-border-insets: 4 2; -fx-border-top-width: 1px; } @@ -309,7 +319,7 @@ #cardPane { -fx-background-color: transparent; - -fx-border-width: 0; + -fx-border-width: 0px; } #commandTypeLabel { @@ -350,3 +360,57 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#activatedTags { + -fx-hgap: 7; + -fx-vgap: 3; + -fx-border-color: transparent; + -fx-padding: 0 0 10 0; +} + +#activatedTags .label { + -fx-text-fill: white; + -fx-background-color: #a9194a; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 12; +} + +#deadlinesPane { + -fx-border-color: #323232; + -fx-border-radius: 10; + -fx-border-insets: 10; + -fx-border-width: 3; + -fx-padding: 1 3 1 3; +} + +#notesPane { + -fx-border-color: #323232; + -fx-border-radius: 10; + -fx-border-insets: 10; + -fx-border-width: 3; + -fx-padding: 1 3 1 3; +} + +#deadlinesTitle { + -fx-font-size: 15pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-font-weight: 700; +} + +#notesTitle { + -fx-font-size: 15pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-font-weight: 700; +} + +#deadlines { + -fx-padding: 1 3 1 3; +} + +#notes { + -fx-padding: 1 3 1 3; +} diff --git a/src/main/resources/view/DetailedContactPanel.fxml b/src/main/resources/view/DetailedContactPanel.fxml new file mode 100644 index 00000000000..2a388572180 --- /dev/null +++ b/src/main/resources/view/DetailedContactPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/DetailedPersonCard.fxml b/src/main/resources/view/DetailedPersonCard.fxml new file mode 100644 index 00000000000..b46d6f9df11 --- /dev/null +++ b/src/main/resources/view/DetailedPersonCard.fxml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..d29d3daed51 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,19 +1,28 @@ -#copyButton, #helpMessage { +.Button, Label { -fx-text-fill: white; } -#copyButton { +.Button { -fx-background-color: dimgray; } -#copyButton:hover { +.Button:hover { -fx-background-color: gray; } -#copyButton:armed { +.Button:armed { -fx-background-color: darkgray; } #helpMessageContainer { -fx-background-color: derive(#1d1d1d, 20%); } + +#commandSummaryHelpContainer { + -fx-background-color: derive(#1d1d1d, 20%); +} + + +#helpWindowContainer { + -fx-background-color: derive(#1d1d1d, 20%); +} diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index 5dea0adef70..9e71d4d3459 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -7,9 +7,11 @@ + + - + @@ -18,27 +20,66 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ImageCard.fxml b/src/main/resources/view/ImageCard.fxml new file mode 100644 index 00000000000..051b057e8d0 --- /dev/null +++ b/src/main/resources/view/ImageCard.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ImageViewPanel.fxml b/src/main/resources/view/ImageViewPanel.fxml new file mode 100644 index 00000000000..06fa5d554ff --- /dev/null +++ b/src/main/resources/view/ImageViewPanel.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/view/ImageWindow.fxml b/src/main/resources/view/ImageWindow.fxml new file mode 100644 index 00000000000..cc6428806c3 --- /dev/null +++ b/src/main/resources/view/ImageWindow.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..c6179c277d2 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,15 +6,14 @@ - + title="d'Intérieur" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> - + @@ -46,11 +45,11 @@ - + - + diff --git a/src/main/resources/view/PCardTest.fxml b/src/main/resources/view/PCardTest.fxml new file mode 100644 index 00000000000..00664933d9d --- /dev/null +++ b/src/main/resources/view/PCardTest.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad55..3bc84a92b97 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -8,7 +8,10 @@ + + + @@ -26,11 +29,16 @@ diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 8836d323cc5..92e5133d5c8 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,8 +1,10 @@ + + diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..6dd34880f3b 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,30 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "deadlines" : [ { + "description" : "happy", + "date" : "1/1/2349" + } ], + "notes" : [ ], + "tagged" : [ "colleagues", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", - "address": "4th street" - } ] + "address": "4th street", + "deadlines" : [ { + "description" : "happy", + "date" : "1/1/2349" + } ], + "notes" : [ ], + "tagged" : [ "colleagues", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + } ], + "tags" : [ "friends", "colleagues", "neighbours", "classmates", "family" ] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json index ccd21f7d1a9..e638f2d6140 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -3,6 +3,16 @@ "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" - } ] + "address": "4th street", + "deadlines" : [ { + "description" : "happy", + "date" : "1/1/2349" + } ], + "notes" : [ ], + "tagged" : [ "colleagues", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + } ], + "tags" : [ "friends", "colleagues", "neighbours", "classmates", "family" ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index 48831cc7674..241a1e16782 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,23 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", - "tagged": [ "friends" ] + "deadlines" : [ { + "description" : "happy", + "date" : "1/1/2349" + } ], + "notes" : [ ], + "tagged" : [ "colleagues", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" }, { "name": "Alice Pauline", "phone": "94351253", "email": "pauline@example.com", - "address": "4th street" - } ] + "address": "4th street", + "deadlines" : [ ], + "isFavourite" : "true", + "hasHighImportance" : "false" + } ], + "tags" : [ "friends", "colleagues", "neighbours", "classmates", "family" ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicateTagAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicateTagAddressBook.json new file mode 100644 index 00000000000..53a9d307991 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/duplicateTagAddressBook.json @@ -0,0 +1,97 @@ +{ + "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", + "persons" : [ { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "hasHighImportance" : "false" + }, { + "name" : "Benson Meier", + "phone" : "98765432", + "email" : "johnd@example.com", + "address" : "311, Clementi Ave 2, #02-25", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "owesMoney", "friends" ], + "isFavourite" : "false", + "hasHighImportance" : "false" + }, { + "name" : "Carl Kurz", + "phone" : "95352563", + "email" : "heinz@example.com", + "address" : "wall street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "hasHighImportance" : "false" + }, { + "name" : "Daniel Meier", + "phone" : "87652533", + "email" : "cornelia@example.com", + "address" : "10th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "true", + "hasHighImportance" : "false" + }, { + "name" : "Elle Meyer", + "phone" : "9482224", + "email" : "werner@example.com", + "address" : "michegan ave", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "hasHighImportance" : "false" + }, { + "name" : "Fiona Kunz", + "phone" : "9482427", + "email" : "lydia@example.com", + "address" : "little tokyo", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "true", + "hasHighImportance" : "false" + }, { + "name" : "George Best", + "phone" : "9482442", + "email" : "anna@example.com", + "address" : "4th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "hasHighImportance" : "false" + } ], + "_comment2": "AddressBook save file with duplicate tags", + "tags": [ "friends", "owesMoney", "friends" ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidDeadlinesAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidDeadlinesAddressBook.json new file mode 100644 index 00000000000..0f5c49cc25f --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/invalidDeadlinesAddressBook.json @@ -0,0 +1,112 @@ +{ + "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", + "persons" : [ { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "deadlines" : [ { + "description" : "a", + "date" : "16/13/2028" + }, { + "description" : "d", + "date" : "18/10/2025" + }, { + "description" : "f", + "date" : "27/10/2024" + }], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" + }, { + "name" : "Benson Meier", + "phone" : "98765432", + "email" : "johnd@example.com", + "address" : "311, Clementi Ave 2, #02-25", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "owesMoney", "friends" ], + "isFavourite" : "true", + "images" : [ "src/test/data/images/test_image_1.png" ], + "hasHighImportance" : "true" + }, { + "name" : "Carl Kurz", + "phone" : "95352563", + "email" : "heinz@example.com", + "address" : "wall street", + "deadlines" : [ { + "description" : "c", + "date" : "16/07/2026" + }, { + "description" : "e", + "date" : "11/04/2022" + }], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" + }, { + "name" : "Daniel Meier", + "phone" : "87652533", + "email" : "cornelia@example.com", + "address" : "10th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" + }, { + "name" : "Elle Meyer", + "phone" : "9482224", + "email" : "werner@example.com", + "address" : "michegan ave", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Fiona Kunz", + "phone" : "9482427", + "email" : "lydia@example.com", + "address" : "little tokyo", + "deadlines" : [ { + "description" : "a", + "date" : "04/12/2027" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "true", + "images" : [ ], + "hasHighImportance" : "false" + }, { + "name" : "George Best", + "phone" : "9482442", + "email" : "anna@example.com", + "address" : "4th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" + } ], + "tags": [ "friends", "owesMoney" ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..b6f31197525 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,16 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" - } ] + "address": "4th street", + "deadlines" : [ { + "description" : "happy", + "date" : "1/1/2349" + } ], + "notes" : [ ], + "tagged" : [ "colleagues", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + } ], + "tags" : [ "friends", "colleagues", "neighbours", "classmates", "family" ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidTagAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidTagAddressBook.json new file mode 100644 index 00000000000..068b4a35589 --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/invalidTagAddressBook.json @@ -0,0 +1,104 @@ +{ + "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", + "persons" : [ { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Benson Meier", + "phone" : "98765432", + "email" : "johnd@example.com", + "address" : "311, Clementi Ave 2, #02-25", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "owesMoney", "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Carl Kurz", + "phone" : "95352563", + "email" : "heinz@example.com", + "address" : "wall street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Daniel Meier", + "phone" : "87652533", + "email" : "cornelia@example.com", + "address" : "10th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "true", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Elle Meyer", + "phone" : "9482224", + "email" : "werner@example.com", + "address" : "michegan ave", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "Fiona Kunz", + "phone" : "9482427", + "email" : "lydia@example.com", + "address" : "little tokyo", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "true", + "images" : [ ], + "hasHighImportance" : "true" + }, { + "name" : "George Best", + "phone" : "9482442", + "email" : "anna@example.com", + "address" : "4th street", + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" + } ], + "_comment2": "AddressBook save file with invalid tag", + "tags": [ "friends", "owesMoney", "+" ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index f10eddee12e..ee3ed96bbfd 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -5,42 +5,108 @@ "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", - "tagged" : [ "friends" ] + "deadlines" : [ { + "description" : "a", + "date" : "16/07/2028" + }, { + "description" : "d", + "date" : "18/10/2025" + }, { + "description" : "f", + "date" : "27/10/2024" + }], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", - "tagged" : [ "owesMoney", "friends" ] + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "owesMoney", "friends" ], + "isFavourite" : "true", + "images" : [ "src/test/data/images/test_image_1.png" ], + "hasHighImportance" : "true" }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", - "tagged" : [ ] + "deadlines" : [ { + "description" : "c", + "date" : "16/07/2026" + }, { + "description" : "e", + "date" : "11/04/2022" + }], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", - "tagged" : [ "friends" ] + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ "friends" ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", - "tagged" : [ ] + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "true" }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", - "tagged" : [ ] + "deadlines" : [ { + "description" : "a", + "date" : "04/12/2027" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "true", + "images" : [ ], + "hasHighImportance" : "false" }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", - "tagged" : [ ] - } ] + "deadlines" : [ { + "description" : "", + "date" : "*No deadline specified*" + } ], + "notes" : [ ], + "tagged" : [ ], + "isFavourite" : "false", + "images" : [ ], + "hasHighImportance" : "false" + } ], + "tags": [ "friends", "owesMoney" ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalTagsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalTagsAddressBook.json new file mode 100644 index 00000000000..6d3b5f5205c --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/typicalTagsAddressBook.json @@ -0,0 +1,4 @@ +{ + "persons": [], + "tags": [ "friends", "owesMoney" ] +} diff --git a/src/test/data/testImages/reference_image_for_test.png b/src/test/data/testImages/reference_image_for_test.png new file mode 100644 index 00000000000..d3998392dec Binary files /dev/null and b/src/test/data/testImages/reference_image_for_test.png differ diff --git a/src/test/data/testImages/test_image_1.png b/src/test/data/testImages/test_image_1.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_1.png differ diff --git a/src/test/data/testImages/test_image_2.png b/src/test/data/testImages/test_image_2.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_2.png differ diff --git a/src/test/data/testImages/test_image_3.png b/src/test/data/testImages/test_image_3.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_3.png differ diff --git a/src/test/data/testImages/test_image_4.png b/src/test/data/testImages/test_image_4.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_4.png differ diff --git a/src/test/data/testImages/test_image_5.png b/src/test/data/testImages/test_image_5.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_5.png differ diff --git a/src/test/data/testImages/test_image_6.png b/src/test/data/testImages/test_image_6.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_6.png differ diff --git a/src/test/data/testImages/test_image_7.png b/src/test/data/testImages/test_image_7.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_7.png differ diff --git a/src/test/data/testImages/test_image_8.png b/src/test/data/testImages/test_image_8.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_8.png differ diff --git a/src/test/data/testImages/test_image_9.png b/src/test/data/testImages/test_image_9.png new file mode 100644 index 00000000000..5712635045d Binary files /dev/null and b/src/test/data/testImages/test_image_9.png differ diff --git a/src/test/java/seedu/address/commons/util/AppUtilTest.java b/src/test/java/seedu/address/commons/util/AppUtilTest.java index 594de1e6365..08c36e5b4fe 100644 --- a/src/test/java/seedu/address/commons/util/AppUtilTest.java +++ b/src/test/java/seedu/address/commons/util/AppUtilTest.java @@ -9,7 +9,7 @@ public class AppUtilTest { @Test public void getImage_exitingImage() { - assertNotNull(AppUtil.getImage("/images/address_book_32.png")); + assertNotNull(AppUtil.getImage("/images/d_interieur.png")); } @Test diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 5865713d5dd..6be9ff002ba 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -5,6 +5,8 @@ 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.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.nio.file.Path; import java.util.ArrayList; @@ -14,16 +16,23 @@ import org.junit.jupiter.api.Test; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.UserPrefs; +import seedu.address.model.commandhistory.CommandHistoryEntry; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; public class AddCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); @Test public void constructor_nullPerson_throwsNullPointerException() { @@ -38,6 +47,7 @@ public void execute_personAcceptedByModel_addSuccessful() throws Exception { CommandResult commandResult = new AddCommand(validPerson).execute(modelStub); assertEquals(String.format(AddCommand.MESSAGE_SUCCESS, validPerson), commandResult.getFeedbackToUser()); + assertEquals(CommandResult.SpecialCommandResult.NONE, commandResult.getSpecialCommandResult()); assertEquals(Arrays.asList(validPerson), modelStub.personsAdded); } @@ -50,6 +60,35 @@ public void execute_duplicatePerson_throwsCommandException() { assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); } + @Test + public void execute_newTag_createNewTagSuccessful() throws Exception { + Person validPerson = new PersonBuilder().withTags("newTag").build(); + + CommandResult commandResult = new AddCommand(validPerson).execute(model); + + assertEquals(String.format(AddCommand.MESSAGE_SUCCESS, validPerson), commandResult.getFeedbackToUser()); + assertEquals(CommandResult.SpecialCommandResult.NONE, commandResult.getSpecialCommandResult()); + assertTrue(model.hasTag(new Tag("newTag"))); + assertTrue(model.hasPerson(validPerson)); + } + + @Test + public void executeInDetailedView_personAcceptedByModel_addSuccessful() throws Exception { + Person firstPerson = model.getSortedPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(firstPerson); //Enter detailed view mode + Person validPerson = new PersonBuilder().build(); + + CommandResult commandResult = new AddCommand(validPerson).executeInDetailedView(model); + + assertEquals(String.format(AddCommand.MESSAGE_SUCCESS, validPerson), commandResult.getFeedbackToUser()); + assertEquals(CommandResult.SpecialCommandResult.NONE, commandResult.getSpecialCommandResult()); + + //View mode doesn't change and stays on the current person + assertEquals(model.getDetailedContactViewPerson(), firstPerson); + + assertTrue(model.hasPerson(validPerson)); + } + @Test public void equals() { Person alice = new PersonBuilder().withName("Alice").build(); @@ -78,74 +117,201 @@ public void equals() { * A default model stub that have all of the methods failing. */ private class ModelStub implements Model { + private final AssertionError uncalledAE = new AssertionError("This method should not be called."); + @Override public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public ReadOnlyUserPrefs getUserPrefs() { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public GuiSettings getGuiSettings() { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public void setGuiSettings(GuiSettings guiSettings) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public Path getAddressBookFilePath() { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public void setAddressBookFilePath(Path addressBookFilePath) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; + } + + @Override + public Path getContactImagesFilePath() { + throw uncalledAE; + } + + @Override + public void setContactImagesFilePath(Path addressBookFilePath) { + throw uncalledAE; } @Override public void addPerson(Person person) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public void setAddressBook(ReadOnlyAddressBook newData) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public ReadOnlyAddressBook getAddressBook() { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public boolean hasPerson(Person person) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; } @Override public void deletePerson(Person target) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; + } + + @Override + public void addActivatedTag(Tag tag) { + throw uncalledAE; } @Override public void setPerson(Person target, Person editedPerson) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; + } + + @Override + public void addTag(Tag tag) { + throw uncalledAE; + } + + @Override + public boolean hasTag(Tag tag) { + throw uncalledAE; + } + + @Override + public void deleteTag(Tag target) { + throw uncalledAE; + } + + @Override + public void setTag(Tag target, Tag editedTag) { + throw uncalledAE; } @Override public ObservableList getFilteredPersonList() { - throw new AssertionError("This method should not be called."); + throw uncalledAE; + } + + @Override + public ObservableList getActivatedTagList() { + throw uncalledAE; } @Override public void updateFilteredPersonList(Predicate predicate) { - throw new AssertionError("This method should not be called."); + throw uncalledAE; + } + + @Override + public SortedList getSortedPersonList() { + throw uncalledAE; + } + + @Override + public void clearActivatedTagList() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByName() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByAddress() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByDeadlineList() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByEmail() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByPhone() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByFavourite() { + throw uncalledAE; + } + + @Override + public void sortFilteredPersonListByHighImportance() { + throw uncalledAE; + } + + @Override + public void setImagesToView(ImageDetailsList images) { + throw uncalledAE; + } + + @Override + public ImageDetailsList getImagesToView() { + throw uncalledAE; + } + + @Override + public void updateCommandHistory(String commandText) { + throw uncalledAE; + } + + @Override + public CommandHistoryEntry getCommandHistory(int i) { + throw uncalledAE; + } + + @Override + public ObservableList getDetailedContactView() { + throw uncalledAE; + } + + @Override + public void setDetailedContactView(Person person) { + throw uncalledAE; + } + + @Override + public void clearDetailedContactView() { + throw uncalledAE; + } + + @Override + public Person getDetailedContactViewPerson() { + throw uncalledAE; } } diff --git a/src/test/java/seedu/address/logic/commands/AddImageCommandTest.java b/src/test/java/seedu/address/logic/commands/AddImageCommandTest.java new file mode 100644 index 00000000000..693c59bd263 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddImageCommandTest.java @@ -0,0 +1,38 @@ +package seedu.address.logic.commands; + +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +class AddImageCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddImageCommand(null)); + } + + @Test + public void constructor_negativeIndex_throwsIndexOutOfBoundsException() { + assertThrows(IndexOutOfBoundsException.class, () -> new AddImageCommand(Index.fromZeroBased(-1))); + } + + @Test + public void execute_outOfBoundsIndex_throwsCommandException() { + assertThrows(CommandException.class, () -> + new AddImageCommand(Index.fromZeroBased(model.getSortedPersonList().size())).execute(model)); + } + + @Test + public void execute_detailedViewConstructor_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddImageCommand().execute(model)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/AssignTagCommandTest.java b/src/test/java/seedu/address/logic/commands/AssignTagCommandTest.java new file mode 100644 index 00000000000..c5eb6f8ee84 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AssignTagCommandTest.java @@ -0,0 +1,147 @@ +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.assertDetailedViewCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +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.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_OWESMONEY; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_TEST; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_OWESMONEY; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +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.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.PersonBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code AssignTagCommand}. + */ +public class AssignTagCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexTagExistsAndPersonNotTagged_success() { + Person personToAddTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + assertFalse(personToAddTag.getTags().contains(VALID_TAG_OWESMONEY)); + AssignTagCommand assignTagCommand = new AssignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_OWESMONEY); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Tag newTag = VALID_TAG_OWESMONEY; + Person editedPerson = new PersonBuilder(personToAddTag).withNewTag(newTag).build(); + String expectedMessage = String.format(AssignTagCommand.MESSAGE_SUCCESS, editedPerson); + expectedModel.setPerson(personToAddTag, editedPerson); + + assertCommandSuccess(assignTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_validIndexTagExistsAndPersonTagged_throwsCommandException() { + Person personToAddTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + assertTrue(personToAddTag.getTags().contains(VALID_TAG_FRIENDS)); + AssignTagCommand assignTagCommand = new AssignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_FRIENDS); + + assertCommandFailure(assignTagCommand, model, AssignTagCommand.MESSAGE_DUPLICATE_TAG); + } + + @Test + public void execute_validIndexTagDoesNotExist_throwsCommandException() { + AssignTagCommand assignTagCommand = new AssignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_TEST); + String expectedMessage = String.format(AssignTagCommand.MESSAGE_UNKNOWN_TAG, VALID_TAGNAME_TEST); + + assertCommandFailure(assignTagCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndexTagExists_throwsCommandException() { + 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()); + + AssignTagCommand assignTagCommand = new AssignTagCommand(outOfBoundIndex, VALID_TAGNAME_FRIENDS); + String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; + assertCommandFailure(assignTagCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndexTagDoesNotExist_throwsCommandException() { + 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()); + + AssignTagCommand assignTagCommand = new AssignTagCommand(outOfBoundIndex, VALID_TAGNAME_TEST); + String expectedMessage = String.format(AssignTagCommand.MESSAGE_UNKNOWN_TAG, VALID_TAGNAME_TEST); + assertCommandFailure(assignTagCommand, model, expectedMessage); + } + + @Test + public void executeInDetailedView_personNotTagged_success() { + Person personToAddTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToAddTag); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Tag newTag = VALID_TAG_OWESMONEY; + Person editedPerson = new PersonBuilder(personToAddTag).withNewTag(newTag).build(); + expectedModel.setPerson(personToAddTag, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + AssignTagCommand assignTagCommand = new AssignTagCommand(VALID_TAGNAME_OWESMONEY); + CommandResult expectedResult = new CommandResult(String.format(AssignTagCommand.MESSAGE_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(assignTagCommand, model, expectedResult, expectedModel); + } + + @Test + public void executeInDetailedView_personTagged_failure() { + Person personToAddTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToAddTag); + AssignTagCommand assignTagCommand = new AssignTagCommand(VALID_TAGNAME_FRIENDS); + + assertDetailedViewCommandFailure(assignTagCommand, model, AssignTagCommand.MESSAGE_DUPLICATE_TAG); + } + + @Test + public void equals() { + AssignTagCommand assignTagFirstCommand = new AssignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_FRIENDS); + AssignTagCommand assignTagSecondCommand = new AssignTagCommand(INDEX_SECOND_PERSON, VALID_TAGNAME_COLLEAGUES); + + // same object -> returns true + assertTrue(assignTagFirstCommand.equals(assignTagFirstCommand)); + + // same values -> returns true + AssignTagCommand assignTagFirstCommandCopy = new AssignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_FRIENDS); + assertTrue(assignTagFirstCommand.equals(assignTagFirstCommandCopy)); + + // different types -> returns false + assertFalse(assignTagFirstCommand.equals(1)); + + // null -> returns false + assertFalse(assignTagFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(assignTagFirstCommand.equals(assignTagSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java index 4f3eb46e9ef..b04bb69619d 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java @@ -4,6 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.DETAILED_VIEW; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.EXIT; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.NONE; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.SHOW_HELP; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.VIEW_IMAGES; import org.junit.jupiter.api.Test; @@ -14,7 +19,7 @@ public void equals() { // same values -> returns true assertTrue(commandResult.equals(new CommandResult("feedback"))); - assertTrue(commandResult.equals(new CommandResult("feedback", false, false))); + assertTrue(commandResult.equals(new CommandResult("feedback", NONE))); // same object -> returns true assertTrue(commandResult.equals(commandResult)); @@ -29,10 +34,17 @@ public void equals() { assertFalse(commandResult.equals(new CommandResult("different"))); // different showHelp value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", true, false))); + assertFalse(commandResult.equals(new CommandResult("feedback", SHOW_HELP))); // different exit value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + assertFalse(commandResult.equals(new CommandResult("feedback", EXIT))); + + // different view images value -> returns false + assertFalse(commandResult.equals(new CommandResult("feedback", VIEW_IMAGES))); + + // different detailed view value -> returns false + assertFalse(commandResult.equals(new CommandResult("feedback", DETAILED_VIEW))); + } @Test @@ -46,9 +58,16 @@ public void hashcode() { assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode()); // different showHelp value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", SHOW_HELP).hashCode()); // different exit value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", EXIT).hashCode()); + + // different view images value -> returns different hashcode + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", VIEW_IMAGES).hashCode()); + + // different detailed view value -> returns different hashcode + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", DETAILED_VIEW).hashCode()); + } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..11800752e43 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -3,6 +3,7 @@ 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_DEADLINE; 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; @@ -34,9 +35,14 @@ public class CommandTestUtil { public static final String VALID_EMAIL_BOB = "bob@example.com"; public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_DEADLINE_AMY = "return book 1/1/2024"; + public static final String VALID_DEADLINE_BOB = "take book 31/1/2024"; + public static final String VALID_NOTE_BOB = "Loves Japanese food"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; + public static final String EMPTY_ADDRESS = "*No Address Specified*"; + 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; @@ -45,6 +51,9 @@ public class CommandTestUtil { 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 EMPTY_ADDRESS_DESC = " " + PREFIX_ADDRESS + EMPTY_ADDRESS; + public static final String DEADLINE_DESC_AMY = " " + PREFIX_DEADLINE + VALID_DEADLINE_AMY; + public static final String DEADLINE_DESC_BOB = " " + PREFIX_DEADLINE + VALID_DEADLINE_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; @@ -62,11 +71,10 @@ public class CommandTestUtil { static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_FRIEND).build(); + .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).build(); + DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).build(); } /** @@ -75,10 +83,12 @@ public class CommandTestUtil { * - the {@code actualModel} matches {@code expectedModel} */ public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, - Model expectedModel) { + Model expectedModel) { try { CommandResult result = command.execute(actualModel); + expectedCommandResult.equals(result); assertEquals(expectedCommandResult, result); + System.out.println(expectedModel.equals(actualModel)); assertEquals(expectedModel, actualModel); } catch (CommandException ce) { throw new AssertionError("Execution of command should not fail.", ce); @@ -90,11 +100,54 @@ public static void assertCommandSuccess(Command command, Model actualModel, Comm * that takes a string {@code expectedMessage}. */ public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, - Model expectedModel) { + Model expectedModel) { CommandResult expectedCommandResult = new CommandResult(expectedMessage); assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); } + /** + * Executes the given {@code command}, in detailed view mode, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel} + * + * @param command the command in detailed view mode. + * @param actualModel the actual model produced by test. + * @param expectedCommandResult expected feedback to user. + * @param expectedModel the expected model when executing command. + */ + public static void assertCommandSuccessInDetailedViewMode(DetailedViewExecutable command, Model actualModel, + CommandResult expectedCommandResult, + Model expectedModel) { + try { + CommandResult result = command.executeInDetailedView(actualModel); + assertEquals(expectedCommandResult, result); + assertEquals(expectedModel, actualModel); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to {@link #assertCommandSuccessInDetailedViewMode(DetailedViewExecutable, Model, String, + * Model)} that takes a string {@code expectedMessage}. + * + * @param command the command in detailed view mode. + * @param actualModel the actual model produced by test. + * @param expectedMessage expected message to user. + * @param expectedModel the expected model when executing command. + */ + public static void assertCommandSuccessInDetailedViewMode(DetailedViewExecutable command, Model actualModel, + String expectedMessage, Model expectedModel) { + CommandResult.SpecialCommandResult specialCommandResult = CommandResult.SpecialCommandResult.DETAILED_VIEW; + + if (command.equals(new ListCommand())) { + specialCommandResult = CommandResult.SpecialCommandResult.NONE; + } + + CommandResult expectedCommandResult = new CommandResult(expectedMessage, specialCommandResult); + assertCommandSuccessInDetailedViewMode(command, actualModel, expectedCommandResult, expectedModel); + } + /** * Executes the given {@code command}, confirms that
* - a {@code CommandException} is thrown
@@ -111,6 +164,51 @@ public static void assertCommandFailure(Command command, Model actualModel, Stri assertEquals(expectedAddressBook, actualModel.getAddressBook()); assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); } + + /** + * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel} + */ + public static void assertDetailedViewCommandSuccess(DetailedViewExecutable command, Model actualModel, + CommandResult expectedCommandResult, Model expectedModel) { + try { + CommandResult result = command.executeInDetailedView(actualModel); + assertEquals(expectedCommandResult, result); + assertEquals(expectedModel, actualModel); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to {@link #assertDetailedViewCommandSuccess(DetailedViewExecutable, Model, CommandResult, + * Model)} that takes a string {@code expectedMessage}. + */ + public static void assertDetailedViewCommandSuccess(DetailedViewExecutable command, Model actualModel, + String expectedMessage, Model expectedModel) { + CommandResult expectedCommandResult = new CommandResult(expectedMessage); + assertDetailedViewCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); + } + + /** + * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the address book, filtered person list and selected person in {@code actualModel} remain unchanged + */ + public static void assertDetailedViewCommandFailure(DetailedViewExecutable command, Model actualModel, + String expectedMessage) { + // 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 expectedFilteredList = new ArrayList<>(actualModel.getFilteredPersonList()); + + assertThrows(CommandException.class, expectedMessage, () -> command.executeInDetailedView(actualModel)); + assertEquals(expectedAddressBook, actualModel.getAddressBook()); + assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); + } + /** * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the * {@code model}'s address book. diff --git a/src/test/java/seedu/address/logic/commands/CreateTagCommandTest.java b/src/test/java/seedu/address/logic/commands/CreateTagCommandTest.java new file mode 100644 index 00000000000..39e968763b9 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CreateTagCommandTest.java @@ -0,0 +1,74 @@ +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.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_NEIGHBOURS; + +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.tag.Tag; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code CreateTagCommand}. + */ +public class CreateTagCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validTagNameAndTagDoesNotExistInModel_success() { + CreateTagCommand createTagCommand = new CreateTagCommand(VALID_TAGNAME_NEIGHBOURS); + String expectedMessage = String.format(CreateTagCommand.MESSAGE_CREATE_TAG_SUCCESS, VALID_TAGNAME_NEIGHBOURS); + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.addTag(new Tag(VALID_TAGNAME_NEIGHBOURS)); + assertCommandSuccess(createTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_sameTagNameDifferentCase_throwsCommandException() { + Tag newTag = new Tag(VALID_TAGNAME_NEIGHBOURS); + model.addTag(newTag); + CreateTagCommand createTagCommand = new CreateTagCommand("frIenDS"); + String expectedMessage = CreateTagCommand.MESSAGE_DUPLICATE_TAG; + assertCommandFailure(createTagCommand, model, expectedMessage); + } + + @Test + public void execute_sameCaseSameTagExistsInModel_throwsCommandException() { + Tag newTag = new Tag(VALID_TAGNAME_NEIGHBOURS); + model.addTag(newTag); + CreateTagCommand createTagCommand = new CreateTagCommand(VALID_TAGNAME_NEIGHBOURS); + String expectedMessage = CreateTagCommand.MESSAGE_DUPLICATE_TAG; + assertCommandFailure(createTagCommand, model, expectedMessage); + } + + @Test + public void equals() { + CreateTagCommand createTagFirstCommand = new CreateTagCommand(VALID_TAGNAME_NEIGHBOURS); + CreateTagCommand createTagSecondCommand = new CreateTagCommand(VALID_TAGNAME_COLLEAGUES); + // same object -> returns true + assertTrue(createTagFirstCommand.equals(createTagFirstCommand)); + + // same values -> returns true + CreateTagCommand createTagFirstCommandCopy = new CreateTagCommand(VALID_TAGNAME_NEIGHBOURS); + assertTrue(createTagFirstCommand.equals(createTagFirstCommandCopy)); + + // different types -> returns false + assertFalse(createTagFirstCommand.equals(1)); + + // null -> returns false + assertFalse(createTagFirstCommand.equals(null)); + + // different tag -> returns false + assertFalse(createTagFirstCommand.equals(createTagSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/DeadlineCommandTest.java b/src/test/java/seedu/address/logic/commands/DeadlineCommandTest.java new file mode 100644 index 00000000000..eb83a133305 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeadlineCommandTest.java @@ -0,0 +1,169 @@ +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.assertNotNull; +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.assertCommandSuccessInDetailedViewMode; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +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.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.Person; +import seedu.address.model.util.SampleDataUtil; +import seedu.address.testutil.PersonBuilder; + +public class DeadlineCommandTest { + private static final String[] VALID_DEADLINE = new String[]{"a 1/1/2023"}; + private static final String[] INVALID_DEADLINE = new String[]{"a 1/13/2023"}; + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_addValidDeadlineInViewMode_success() { + Person firstPerson = model.getSortedPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(firstPerson); + Person personToAddDeadline = model.getDetailedContactViewPerson(); + Person editedPerson = new PersonBuilder(personToAddDeadline).withDeadlines(VALID_DEADLINE).build(); + + DeadlineCommand deadlineCommand = new DeadlineCommand(SampleDataUtil.getDeadlineList(VALID_DEADLINE)); + + String expectedMessage = String.format(DeadlineCommand.MESSAGE_ADD_DEADLINE_SUCCESS, firstPerson); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + assertCommandSuccessInDetailedViewMode(deadlineCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_addValidDeadlineUnfilteredList_success() { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(firstPerson).withDeadlines(VALID_DEADLINE).build(); + + DeadlineCommand deadlineCommand = new DeadlineCommand(INDEX_FIRST_PERSON, + SampleDataUtil.getDeadlineList(VALID_DEADLINE)); + + String expectedMessage = String.format(DeadlineCommand.MESSAGE_ADD_DEADLINE_SUCCESS, firstPerson); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + + assertCommandSuccess(deadlineCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_addValidDeadlineFilteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person firstPerson = model.getSortedPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(firstPerson).withDeadlines(VALID_DEADLINE).build(); + + DeadlineCommand deadlineCommand = new DeadlineCommand(INDEX_FIRST_PERSON, + SampleDataUtil.getDeadlineList(VALID_DEADLINE)); + + String expectedMessage = String.format(DeadlineCommand.MESSAGE_ADD_DEADLINE_SUCCESS, firstPerson); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + showPersonAtIndex(expectedModel, INDEX_FIRST_PERSON); + + assertCommandSuccess(deadlineCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() throws ParseException { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + DeadlineCommand deadlineCommand = new DeadlineCommand(outOfBoundIndex, + ParserUtil.parseDeadlines(Arrays.asList(VALID_DEADLINE))); + + assertCommandFailure(deadlineCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() throws ParseException { + 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()); + DeadlineCommand deadlineCommand = new DeadlineCommand(outOfBoundIndex, + ParserUtil.parseDeadlines(Arrays.asList(VALID_DEADLINE))); + + assertCommandFailure(deadlineCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_addInvalidDeadline_throwsParseException() { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Exception exception = assertThrows(IllegalArgumentException.class, () -> + new PersonBuilder(firstPerson).withDeadlines(INVALID_DEADLINE).build()); + String expectedMessage = Deadline.MESSAGE_CONSTRAINTS; + assertTrue(exception.getMessage().contains(expectedMessage)); + } + + @Test + public void executeInDetailedView_validDeadline_success() throws ParseException { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + + Person editedPerson = new PersonBuilder(personToEdit).withDeadlines(VALID_DEADLINE).build(); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + DeadlineCommand deadlineCommand = new DeadlineCommand(ParserUtil.parseDeadlines( + Arrays.asList(VALID_DEADLINE))); + CommandResult expectedResult = new CommandResult(String.format(DeadlineCommand.MESSAGE_ADD_DEADLINE_SUCCESS, + personToEdit), CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(deadlineCommand, model, expectedResult, expectedModel); + } + + @Test + public void equals() throws ParseException { + DeadlineCommand deadlineFirstCommand = new DeadlineCommand(INDEX_FIRST_PERSON, + ParserUtil.parseDeadlines(Arrays.asList(VALID_DEADLINE))); + DeadlineCommand deadlineSecondCommand = new DeadlineCommand(INDEX_SECOND_PERSON, + ParserUtil.parseDeadlines(Arrays.asList(VALID_DEADLINE))); + + // same object -> returns true + assertEquals(deadlineFirstCommand, deadlineFirstCommand); + + // same values -> returns true + DeadlineCommand deadlineFirstCommandCopy = new DeadlineCommand(INDEX_FIRST_PERSON, + ParserUtil.parseDeadlines(Arrays.asList(VALID_DEADLINE))); + assertEquals(deadlineFirstCommand, deadlineFirstCommandCopy); + + // different types -> returns false + assertNotEquals(1, deadlineFirstCommand); + + // null -> returns false + assertNotNull(deadlineFirstCommand); + + // different person -> returns false + assertNotEquals(deadlineFirstCommand, deadlineSecondCommand); + + // other is null -> returns false + assertNotEquals(deadlineFirstCommand, null); + + } +} diff --git a/src/test/java/seedu/address/logic/commands/DeleteImageCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteImageCommandTest.java new file mode 100644 index 00000000000..d025e98ab54 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteImageCommandTest.java @@ -0,0 +1,108 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.DETAILED_VIEW; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.VIEW_IMAGES; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +import static seedu.address.logic.commands.DeleteImageCommand.MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalSavedImages.populateTestImages; + +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +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.person.Person; +import seedu.address.testutil.PersonBuilder; + +class DeleteImageCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @BeforeEach + void setUp() throws IOException { + populateTestImages(); + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + } + + @Test + public void constructor_negativeIndex_throwsIndexOutOfBoundsException() { + assertThrows(IndexOutOfBoundsException.class, () -> new DeleteImageCommand(Index.fromZeroBased(-1))); + } + + @Test + public void execute_success() { + Person personToRemoveImage = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + Index imageToDeleteIndex = Index.fromOneBased(1); + DeleteImageCommand deleteImageCommand = new DeleteImageCommand(INDEX_SECOND_PERSON, imageToDeleteIndex); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Person editedPerson = new PersonBuilder(personToRemoveImage).withImageDetails().build(); + + CommandResult expectedResult = new CommandResult(String.format( + DeleteImageCommand.MESSAGE_DELETE_IMAGE_SUCCESSFUL, imageToDeleteIndex.getOneBased(), editedPerson), + VIEW_IMAGES); + expectedModel.setPerson(personToRemoveImage, editedPerson); + + assertCommandSuccess(deleteImageCommand, model, expectedResult, expectedModel); + } + + @Test + public void execute_nullPersonIndex_throwsNullPointerException() { + Command deleteImageCommand = new DeleteImageCommand(Index.fromZeroBased(0)); + assertThrows(NullPointerException.class, () -> deleteImageCommand.execute(model)); + } + + @Test + public void execute_outOfBoundsPersonIndex_throwsCommandException() { + Index personIndex = Index.fromZeroBased(model.getSortedPersonList().size()); + Command deleteImageCommand = new DeleteImageCommand(personIndex, Index.fromZeroBased(0)); + assertCommandFailure(deleteImageCommand, model, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_personWithNoImages_throwsCommandException() { + Index imageIndex = Index.fromZeroBased(0); + Command deleteImageCommand = new DeleteImageCommand(INDEX_FIRST_PERSON, imageIndex); + assertCommandFailure(deleteImageCommand, model, MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX); + } + + @Test + public void execute_outOfBoundsImageIndex_throwsCommandException() { + int size = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()).getImageDetailsList().size(); + Index imageIndex = Index.fromZeroBased(size); + Command deleteImageCommand = new DeleteImageCommand(INDEX_SECOND_PERSON, imageIndex); + assertCommandFailure(deleteImageCommand, model, MESSAGE_INVALID_IMAGE_DISPLAYED_INDEX); + } + + @Test + void executeInDetailedView_success() { + Person personToRemoveImage = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + Index imageToDeleteIndex = Index.fromOneBased(1); + + model.setDetailedContactView(personToRemoveImage); + + DeleteImageCommand deleteImageDetailedViewCommand = new DeleteImageCommand(imageToDeleteIndex); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Person editedPerson = new PersonBuilder(personToRemoveImage).withImageDetails().build(); + expectedModel.setDetailedContactView(editedPerson); + + CommandResult expectedResult = new CommandResult(String.format( + DeleteImageCommand.MESSAGE_DELETE_IMAGE_SUCCESSFUL, imageToDeleteIndex.getOneBased(), editedPerson), + DETAILED_VIEW); + expectedModel.setPerson(personToRemoveImage, editedPerson); + + assertDetailedViewCommandSuccess(deleteImageDetailedViewCommand, model, expectedResult, expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 214c6c2507b..c68b20c9e4c 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -6,19 +6,25 @@ import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIFTH_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SEVENTH_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import java.util.List; + import org.junit.jupiter.api.Test; import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; @@ -36,14 +42,18 @@ public class EditCommandTest { @Test public void execute_allFieldsSpecifiedUnfilteredList_success() { - Person editedPerson = new PersonBuilder().build(); + List lastShownList = model.getSortedPersonList(); + Person personToEdit = lastShownList.get(0); + Person editedPerson = new PersonBuilder() + .withDeadlines(new String[]{"a 16/07/2028", "d 18/10/2025", "f 27/10/2024"}) + .withTags("friends").build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(editedPerson).build(); EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, descriptor); String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); + expectedModel.setPerson(personToEdit, editedPerson); assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -54,18 +64,19 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() { Person lastPerson = model.getFilteredPersonList().get(indexLastPerson.getZeroBased()); PersonBuilder personInList = new PersonBuilder(lastPerson); - Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withTags(VALID_TAG_HUSBAND).build(); - + Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB).build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); + .withPhone(VALID_PHONE_BOB).build(); + EditCommand editCommand = new EditCommand(indexLastPerson, descriptor); String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); + System.out.println("expectedMessage: " + expectedMessage); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); expectedModel.setPerson(lastPerson, editedPerson); + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -93,7 +104,9 @@ public void execute_filteredList_success() { String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); + showPersonAtIndex(expectedModel, INDEX_FIRST_PERSON); + expectedModel.setPerson(model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()), editedPerson); + assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); } @@ -145,6 +158,39 @@ public void execute_invalidPersonIndexFilteredList_failure() { assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } + @Test + public void executeInDetailedView_success() { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + + Person editedPerson = new PersonBuilder(personToEdit).withName("Alex Yeoh").build(); + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptorBuilder(editedPerson).build(); + EditCommand editCommand = new EditCommand(editPersonDescriptor); + + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); + CommandResult expectedResult = new CommandResult(expectedMessage, + CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(editCommand, model, expectedResult, expectedModel); + } + + @Test + public void executeInDetailedView_duplicatePerson_errorThrown() { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIFTH_PERSON.getZeroBased()); + Person duplicatePerson = model.getFilteredPersonList().get(INDEX_SEVENTH_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + new HighImportanceCommand().executeInDetailedView(model); + + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptorBuilder(duplicatePerson).build(); + EditCommand editCommand = new EditCommand(editPersonDescriptor); + + assertThrows(CommandException.class, EditCommand.MESSAGE_DUPLICATE_PERSON, () -> + editCommand.executeInDetailedView(model)); + } + @Test public void equals() { final EditCommand standardCommand = new EditCommand(INDEX_FIRST_PERSON, DESC_AMY); diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java index e0288792e72..693db5a084c 100644 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java @@ -8,7 +8,6 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import org.junit.jupiter.api.Test; @@ -51,8 +50,5 @@ public void equals() { editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build(); assertFalse(DESC_AMY.equals(editedAmy)); - // different tags -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build(); - assertFalse(DESC_AMY.equals(editedAmy)); } } diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java index 9533c473875..e3070c5667c 100644 --- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java @@ -1,6 +1,8 @@ package seedu.address.logic.commands; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.EXIT; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccessInDetailedViewMode; import static seedu.address.logic.commands.ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT; import org.junit.jupiter.api.Test; @@ -14,7 +16,13 @@ public class ExitCommandTest { @Test public void execute_exit_success() { - CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, EXIT); assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel); } + + @Test + public void executeInDetailedView_success() { + CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, EXIT); + assertCommandSuccessInDetailedViewMode(new ExitCommand(), model, expectedCommandResult, expectedModel); + } } diff --git a/src/test/java/seedu/address/logic/commands/FavouriteCommandTest.java b/src/test/java/seedu/address/logic/commands/FavouriteCommandTest.java new file mode 100644 index 00000000000..f8df2c39b8e --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FavouriteCommandTest.java @@ -0,0 +1,117 @@ +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.assertDetailedViewCommandSuccess; +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.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +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.person.Person; +import seedu.address.testutil.PersonBuilder; + +class FavouriteCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Person personToFavourite = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person favouritedPerson = new PersonBuilder(personToFavourite).withFavourite("true").build(); + FavouriteCommand favouriteCommand = new FavouriteCommand(INDEX_FIRST_PERSON); + + String expectedMessage = String.format(FavouriteCommand.MESSAGE_FAVOURITE_PERSON_SUCCESS, favouritedPerson); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToFavourite, favouritedPerson); + + assertCommandSuccess(favouriteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + FavouriteCommand favouriteCommand = new FavouriteCommand(outOfBoundIndex); + + assertCommandFailure(favouriteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person personToFavourite = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person favouritedPerson = new PersonBuilder(personToFavourite).withFavourite("true").build(); + FavouriteCommand favouriteCommand = new FavouriteCommand(INDEX_FIRST_PERSON); + + String expectedMessage = String.format(FavouriteCommand.MESSAGE_FAVOURITE_PERSON_SUCCESS, favouritedPerson); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToFavourite, favouritedPerson); + showPersonAtIndex(expectedModel, INDEX_FIRST_PERSON); + + assertCommandSuccess(favouriteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + 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()); + + FavouriteCommand favouriteCommand = new FavouriteCommand(outOfBoundIndex); + + assertCommandFailure(favouriteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeInDetailedView_success() { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + + Person editedPerson = new PersonBuilder(personToEdit).withFavourite("true").build(); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + FavouriteCommand favouriteCommand = new FavouriteCommand(); + CommandResult expectedResult = new CommandResult(String.format( + FavouriteCommand.MESSAGE_FAVOURITE_PERSON_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(favouriteCommand, model, expectedResult, expectedModel); + } + + @Test + public void equals() { + FavouriteCommand favouriteFirstCommand = new FavouriteCommand(INDEX_FIRST_PERSON); + FavouriteCommand favouriteSecondCommand = new FavouriteCommand(INDEX_SECOND_PERSON); + + // same object -> returns true + assertTrue(favouriteFirstCommand.equals(favouriteFirstCommand)); + + // same values -> returns true + FavouriteCommand favouriteFirstCommandCopy = new FavouriteCommand(INDEX_FIRST_PERSON); + assertTrue(favouriteFirstCommand.equals(favouriteFirstCommandCopy)); + + // different types -> returns false + assertFalse(favouriteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(favouriteFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(favouriteFirstCommand.equals(favouriteSecondCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java new file mode 100644 index 00000000000..8345c78ac2e --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java @@ -0,0 +1,124 @@ +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.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_OWESMONEY; +import static seedu.address.testutil.TypicalTags.VALID_TAG_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_OWESMONEY; + +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.TagContainsKeywordsPredicate; +import seedu.address.model.tag.Tag; + +class FindTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + void equals() { + List firstKeywords = new ArrayList<>(); + firstKeywords.add("first"); + List secondKeywords = new ArrayList<>(); + secondKeywords.add("second"); + + FindTagCommand findFirstCommand = new FindTagCommand(firstKeywords); + FindTagCommand findSecondCommand = new FindTagCommand(secondKeywords); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindTagCommand findFirstCommandCopy = new FindTagCommand(firstKeywords); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_singleKeywordNoActivatedTags_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + model.addTag(VALID_TAG_COLLEAGUES); + List singleKeyword = new ArrayList<>(); + singleKeyword.add(VALID_TAGNAME_COLLEAGUES); + FindTagCommand command = new FindTagCommand(singleKeyword); + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(singleKeyword); + expectedModel.addTag(VALID_TAG_COLLEAGUES); + expectedModel.addActivatedTag(VALID_TAG_COLLEAGUES); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void execute_zeroKeywordsNoActivatedTags_throwsCommandException() { + String expectedMessage = FindTagCommand.MESSAGE_USAGE; + List emptyKeywords = new ArrayList<>(); + FindTagCommand command = new FindTagCommand(emptyKeywords); + assertCommandFailure(command, model, expectedMessage); + + } + + @Test + public void execute_singleKeywordNoActivatedTags_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + List singleKeyword = new ArrayList<>(); + singleKeyword.add(VALID_TAGNAME_FRIENDS); + FindTagCommand command = new FindTagCommand(singleKeyword); + TagContainsKeywordsPredicate predicate = preparePredicate("friends"); + expectedModel.addActivatedTag(VALID_TAG_FRIENDS); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList()); + } + + @Test + public void execute_singleKeywordHasActivatedTags_singlePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1); + List singleKeyword = new ArrayList<>(); + singleKeyword.add(VALID_TAGNAME_OWESMONEY); + Tag activatedTag = VALID_TAG_FRIENDS; + model.addActivatedTag(activatedTag); + FindTagCommand command = new FindTagCommand(singleKeyword); + TagContainsKeywordsPredicate predicate = preparePredicate("friends owesMoney"); + expectedModel.addActivatedTag(activatedTag); + expectedModel.addActivatedTag(VALID_TAG_OWESMONEY); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON), model.getFilteredPersonList()); + + } + + /** + * Parses {@code userInput} into a {@code TagContainsKeywordsPredicate}. + */ + private TagContainsKeywordsPredicate preparePredicate(String userInput) { + return new TagContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java index 4904fc4352e..33f35ebcb87 100644 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java @@ -1,6 +1,8 @@ package seedu.address.logic.commands; +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.SHOW_HELP; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccessInDetailedViewMode; import static seedu.address.logic.commands.HelpCommand.SHOWING_HELP_MESSAGE; import org.junit.jupiter.api.Test; @@ -14,7 +16,13 @@ public class HelpCommandTest { @Test public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, SHOW_HELP); assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); } + + @Test + public void executeInDetailedView_success() { + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, SHOW_HELP); + assertCommandSuccessInDetailedViewMode(new HelpCommand(), model, expectedCommandResult, expectedModel); + } } diff --git a/src/test/java/seedu/address/logic/commands/HighImportanceCommandTest.java b/src/test/java/seedu/address/logic/commands/HighImportanceCommandTest.java new file mode 100644 index 00000000000..ad8682fb001 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/HighImportanceCommandTest.java @@ -0,0 +1,117 @@ +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.assertDetailedViewCommandSuccess; +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.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +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.person.Person; +import seedu.address.testutil.PersonBuilder; + +class HighImportanceCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexListView_success() { + Person personToEditImportanceStatus = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person highImportancePerson = + new PersonBuilder(personToEditImportanceStatus).withHighImportance("true").build(); + HighImportanceCommand highImportanceCommand = new HighImportanceCommand(INDEX_FIRST_PERSON); + + String expectedMessage = + String.format(HighImportanceCommand.MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS, highImportancePerson); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEditImportanceStatus, highImportancePerson); + + assertCommandSuccess(highImportanceCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexListView_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + HighImportanceCommand highImportanceCommand = new HighImportanceCommand(outOfBoundIndex); + + assertCommandFailure(highImportanceCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person personToEditImportanceStatus = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person highImportancePerson = + new PersonBuilder(personToEditImportanceStatus).withHighImportance("true").build(); + HighImportanceCommand highImportanceCommand = new HighImportanceCommand(INDEX_FIRST_PERSON); + + String expectedMessage = + String.format(HighImportanceCommand.MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS, highImportancePerson); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEditImportanceStatus, highImportancePerson); + showPersonAtIndex(expectedModel, INDEX_FIRST_PERSON); + + assertCommandSuccess(highImportanceCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + 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()); + + HighImportanceCommand highImportanceCommand = new HighImportanceCommand(outOfBoundIndex); + + assertCommandFailure(highImportanceCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeInDetailedView_success() { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + + Person editedPerson = new PersonBuilder(personToEdit).withHighImportance("true").build(); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + HighImportanceCommand highImportanceCommand = new HighImportanceCommand(); + CommandResult expectedResult = new CommandResult(String.format( + HighImportanceCommand.MESSAGE_CHANGE_HIGH_IMPORTANCE_SUCCESS, editedPerson), + CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(highImportanceCommand, model, expectedResult, expectedModel); + } + + @Test + public void equals() { + HighImportanceCommand importantFirstContactCommand = new HighImportanceCommand(INDEX_FIRST_PERSON); + HighImportanceCommand importantSecondContactCommand = new HighImportanceCommand(INDEX_SECOND_PERSON); + + // same object -> returns true + assertTrue(importantFirstContactCommand.equals(importantFirstContactCommand)); + + // different types -> returns false + assertFalse(importantFirstContactCommand.equals(1)); + + // null -> returns false + assertFalse(importantFirstContactCommand.equals(null)); + + // different person -> returns false + assertFalse(importantFirstContactCommand.equals(importantSecondContactCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ImagesCommandTest.java b/src/test/java/seedu/address/logic/commands/ImagesCommandTest.java new file mode 100644 index 00000000000..f9fef844554 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ImagesCommandTest.java @@ -0,0 +1,87 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandResult.SpecialCommandResult.VIEW_IMAGES; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGES_DIRECTORY; +import static seedu.address.testutil.TypicalSavedImages.populateTestImages; + +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.person.Person; + +class ImagesCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() throws IOException { + populateTestImages(); + UserPrefs userPrefs = new UserPrefs(); + userPrefs.setContactImagesFilePath(TEST_IMAGES_DIRECTORY); + + model = new ModelManager(getTypicalAddressBook(), userPrefs); + expectedModel = new ModelManager(model.getAddressBook(), userPrefs); + } + + @Test + public void execute_nullPersonIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new ImagesCommand().execute(model)); + } + + @Test + public void execute_personWithNoImages_success() { + Command imagesCommand = new ImagesCommand(INDEX_FIRST_PERSON); + ImageDetailsList emptyList = new ImageDetailsList(); + CommandResult result = new CommandResult( + String.format(ImagesCommand.MESSAGE_IMAGES_SUCCESS, 0, INDEX_FIRST_PERSON.getOneBased(), emptyList), + VIEW_IMAGES); + assertCommandSuccess(imagesCommand, model, result, expectedModel); + } + + @Test + public void execute_personWithImages_success() { + Person personImagesToShow = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + + ImageDetailsList personImagesList = personImagesToShow.getImageDetailsList(); + + String expectedMessage = String.format(ImagesCommand.MESSAGE_IMAGES_SUCCESS, personImagesList.size(), + INDEX_SECOND_PERSON.getOneBased(), personImagesList); + CommandResult expectedResult = new CommandResult(expectedMessage, VIEW_IMAGES); + + expectedModel.setImagesToView(personImagesList); + Command imagesCommand = new ImagesCommand(INDEX_SECOND_PERSON); + + assertCommandSuccess(imagesCommand, model, expectedResult, expectedModel); + } + + @Test + public void executeInDetailedView_success() { + Person personImagesToShow = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + model.setDetailedContactView(personImagesToShow); + + ImageDetailsList personImagesList = personImagesToShow.getImageDetailsList(); + + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setImagesToView(personImagesList); + ImagesCommand imagesCommand = new ImagesCommand(); + + CommandResult expectedResult = new CommandResult(String.format(ImagesCommand.MESSAGE_IMAGES_SUCCESS, + personImagesList.size(), personImagesToShow.getName().fullName, personImagesList), + VIEW_IMAGES); + assertDetailedViewCommandSuccess(imagesCommand, model, expectedResult, expectedModel); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index 435ff1f7275..c5454ca489a 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -1,6 +1,7 @@ package seedu.address.logic.commands; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccessInDetailedViewMode; import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; @@ -36,4 +37,9 @@ public void execute_listIsFiltered_showsEverything() { showPersonAtIndex(model, INDEX_FIRST_PERSON); assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); } + + @Test + public void executeDetailedViewMode_showsEverything() { + assertCommandSuccessInDetailedViewMode(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); + } } diff --git a/src/test/java/seedu/address/logic/commands/ListFavouritesCommandTest.java b/src/test/java/seedu/address/logic/commands/ListFavouritesCommandTest.java new file mode 100644 index 00000000000..aef37d7cf3b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ListFavouritesCommandTest.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonIsFavouriteContactPredicate; +import seedu.address.testutil.PersonBuilder; + +class ListFavouritesCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + } + + @Test + public void execute_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 2); + PersonIsFavouriteContactPredicate predicate = new PersonIsFavouriteContactPredicate(); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(new ListFavouritesCommand(), model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON, FIONA), model.getFilteredPersonList()); + } + + @Test + public void execute_noPersonFound() { + // unfavourite all favourite persons + List typicalPersons = model.getFilteredPersonList(); + for (Person originalPerson : typicalPersons) { + if (originalPerson.getFavouriteStatus().isFavourite()) { + Person unfavouritedPerson = new PersonBuilder(originalPerson).withFavourite("false").build(); + model.setPerson(originalPerson, unfavouritedPerson); + } + } + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonIsFavouriteContactPredicate predicate = new PersonIsFavouriteContactPredicate(); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(new ListFavouritesCommand(), model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ListImportantCommandTest.java b/src/test/java/seedu/address/logic/commands/ListImportantCommandTest.java new file mode 100644 index 00000000000..ee664943610 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ListImportantCommandTest.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonHasHighImportancePredicate; +import seedu.address.testutil.PersonBuilder; + +class ListImportantCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + } + + @Test + void execute_multiplePersonsListed() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 2); + PersonHasHighImportancePredicate predicate = new PersonHasHighImportancePredicate(); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(new ListImportantCommand(), model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON, ELLE), model.getFilteredPersonList()); + } + + @Test + void execute_noPersonListed() { + // Remove high importance status for all contacts + List typicalPersons = model.getFilteredPersonList(); + for (Person originalPerson : typicalPersons) { + if (originalPerson.getHighImportanceStatus().hasHighImportance()) { + Person highImportancePerson = new PersonBuilder(originalPerson).withHighImportance("false").build(); + model.setPerson(originalPerson, highImportancePerson); + } + } + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonHasHighImportancePredicate predicate = new PersonHasHighImportancePredicate(); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(new ListImportantCommand(), model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/NoteCommandTest.java b/src/test/java/seedu/address/logic/commands/NoteCommandTest.java new file mode 100644 index 00000000000..024aa339405 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/NoteCommandTest.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +class NoteCommandTest { + private static final String VALID_NOTE = "Bestie"; + private static final String INVALID_NOTE = " "; + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_addValidNote_success() { + NoteCommand noteCommand = new NoteCommand(INDEX_FIRST_PERSON, VALID_NOTE); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withNotes(List.of(VALID_NOTE)).build(); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + String expectedMessage = String.format(NoteCommand.MESSAGE_UPDATE_NOTE_SUCCESS, editedPerson); + + assertCommandSuccess(noteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_addInvalidNote_noChange() { + NoteCommand noteCommand = new NoteCommand(INDEX_FIRST_PERSON, INVALID_NOTE); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + String expectedMessage = String.format(NoteCommand.MESSAGE_NO_UPDATE_TO_NOTES, personToEdit); + + assertCommandSuccess(noteCommand, model, expectedMessage, model); + } + + @Test + public void executeInDetailedView_addValidNote_success() { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + + Person editedPerson = new PersonBuilder(personToEdit).withNotes(List.of(VALID_NOTE)).build(); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setPerson(personToEdit, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + NoteCommand noteCommand = new NoteCommand(VALID_NOTE); + CommandResult expectedResult = new CommandResult(String.format(NoteCommand.MESSAGE_UPDATE_NOTE_SUCCESS, + editedPerson), CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(noteCommand, model, expectedResult, expectedModel); + } + + @Test + public void executeInDetailedView_addInvalidNote_noChange() { + NoteCommand noteCommand = new NoteCommand(INVALID_NOTE); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToEdit); + CommandResult expectedResult = new CommandResult(String.format(NoteCommand.MESSAGE_NO_UPDATE_TO_NOTES, + personToEdit), CommandResult.SpecialCommandResult.DETAILED_VIEW); + + assertDetailedViewCommandSuccess(noteCommand, model, expectedResult, model); + } + + @Test + public void equals() { + NoteCommand listViewNoteCommand = new NoteCommand(INDEX_FIRST_PERSON, VALID_NOTE); + NoteCommand detailedViewNoteCommand = new NoteCommand(VALID_NOTE); + assertFalse(listViewNoteCommand.equals(detailedViewNoteCommand)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java new file mode 100644 index 00000000000..fa691f9863d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java @@ -0,0 +1,123 @@ +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.assertCommandSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.GEORGE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class SortCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private String invalidCriteria = "invalidCriteria"; + + @Test + public void equals() { + SortsCommand sortFirstCommand = new SortsCommand("name"); + SortsCommand sortSecondCommand = new SortsCommand("phone"); + + // same object -> returns true + assertTrue(sortFirstCommand.equals(sortFirstCommand)); + + // same values -> returns true + SortsCommand sortFirstCommandCopy = new SortsCommand("name"); + assertTrue(sortFirstCommand.equals(sortFirstCommandCopy)); + + // different types -> returns false + assertFalse(sortFirstCommand.equals(1)); + + // null -> returns false + assertFalse(sortFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(sortFirstCommand.equals(sortSecondCommand)); + } + + @Test + public void execute_sortName_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "name"); + SortsCommand command = new SortsCommand("name"); + expectedModel.sortFilteredPersonListByName(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE), model.getSortedPersonList()); + } + + @Test + public void execute_sortDeadline_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "deadline"); + SortsCommand command = new SortsCommand("deadline"); + expectedModel.sortFilteredPersonListByDeadlineList(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CARL, ALICE, FIONA, BENSON, DANIEL, ELLE, GEORGE), model.getSortedPersonList()); + } + + @Test + public void execute_sortAddress_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "address"); + SortsCommand command = new SortsCommand("address"); + expectedModel.sortFilteredPersonListByAddress(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(DANIEL, ALICE, BENSON, GEORGE, FIONA, ELLE, CARL), model.getSortedPersonList()); + } + + @Test + public void execute_sortEmail_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "email"); + SortsCommand command = new SortsCommand("email"); + expectedModel.sortFilteredPersonListByEmail(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, GEORGE, DANIEL, CARL, BENSON, FIONA, ELLE), model.getSortedPersonList()); + } + + @Test + public void execute_sortFavourite_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "fav"); + SortsCommand command = new SortsCommand("fav"); + expectedModel.sortFilteredPersonListByFavourite(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON, FIONA, ALICE, CARL, DANIEL, ELLE, GEORGE), model.getSortedPersonList()); + } + + @Test + public void execute_sortHighImportance_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "impt"); + SortsCommand command = new SortsCommand("impt"); + expectedModel.sortFilteredPersonListByHighImportance(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON, ELLE, ALICE, CARL, DANIEL, FIONA, GEORGE), model.getSortedPersonList()); + } + + @Test + public void execute_sortPhone_sortedListUpdated() { + String expectedMessage = String.format(Messages.MESSAGE_SORTED, "phone"); + SortsCommand command = new SortsCommand("phone"); + expectedModel.sortFilteredPersonListByPhone(); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(DANIEL, ALICE, ELLE, FIONA, GEORGE, CARL, BENSON), model.getSortedPersonList()); + } + + @Test + public void execute_sortInvalidCriteria_errorThrown() { + String expectedMessage = String.format(Messages.MESSAGE_INVALID_CRITERIA, invalidCriteria); + SortsCommand command = new SortsCommand(invalidCriteria); + assertThrows(CommandException.class, expectedMessage, () -> command.execute(model)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/UnassignTagCommandTest.java b/src/test/java/seedu/address/logic/commands/UnassignTagCommandTest.java new file mode 100644 index 00000000000..c802ff93bac --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/UnassignTagCommandTest.java @@ -0,0 +1,149 @@ +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.assertDetailedViewCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertDetailedViewCommandSuccess; +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.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_OWESMONEY; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_TEST; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_OWESMONEY; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +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.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.PersonBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * {@code UnassignTagCommand}. + */ +public class UnassignTagCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexTagExistsAndPersonTagged_success() { + Person personToRemoveTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + assertTrue(personToRemoveTag.getTags().contains(VALID_TAG_FRIENDS)); + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_FRIENDS); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Tag removingTag = VALID_TAG_FRIENDS; + Person editedPerson = new PersonBuilder(personToRemoveTag).withoutTag(removingTag).build(); + String expectedMessage = String.format(UnassignTagCommand.MESSAGE_SUCCESS, editedPerson); + expectedModel.setPerson(personToRemoveTag, editedPerson); + + assertCommandSuccess(unassignTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_validIndexTagExistsAndPersonNotTagged_throwsCommandException() { + Person personToRemoveTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + assertFalse(personToRemoveTag.getTags().contains(VALID_TAG_OWESMONEY)); + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_OWESMONEY); + + assertCommandFailure(unassignTagCommand, model, UnassignTagCommand.MESSAGE_NOT_TAGGED); + } + + @Test + public void execute_validIndexTagDoesNotExist_throwsCommandException() { + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_TEST); + String expectedMessage = String.format(UnassignTagCommand.MESSAGE_UNKNOWN_TAG, VALID_TAGNAME_TEST); + + assertCommandFailure(unassignTagCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndexTagExists_throwsCommandException() { + 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()); + + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(outOfBoundIndex, VALID_TAGNAME_FRIENDS); + String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; + assertCommandFailure(unassignTagCommand, model, expectedMessage); + } + + @Test + public void execute_invalidIndexTagDoesNotExist_throwsCommandException() { + 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()); + + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(outOfBoundIndex, VALID_TAGNAME_TEST); + String expectedMessage = String.format(UnassignTagCommand.MESSAGE_UNKNOWN_TAG, VALID_TAGNAME_TEST); + assertCommandFailure(unassignTagCommand, model, expectedMessage); + } + + @Test + public void executeInDetailedView_personTagged_success() { + Person personToRemoveTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToRemoveTag); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Tag removingTag = VALID_TAG_FRIENDS; + Person editedPerson = new PersonBuilder(personToRemoveTag).withoutTag(removingTag).build(); + expectedModel.setPerson(personToRemoveTag, editedPerson); + expectedModel.setDetailedContactView(editedPerson); + + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(VALID_TAGNAME_FRIENDS); + CommandResult expectedResult = new CommandResult(String.format(UnassignTagCommand.MESSAGE_SUCCESS, + editedPerson), CommandResult.SpecialCommandResult.DETAILED_VIEW); + assertDetailedViewCommandSuccess(unassignTagCommand, model, expectedResult, expectedModel); + } + + @Test + public void executeInDetailedView_personNotTagged_failure() { + Person personToRemoveTag = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + model.setDetailedContactView(personToRemoveTag); + UnassignTagCommand unassignTagCommand = new UnassignTagCommand(VALID_TAGNAME_OWESMONEY); + + assertDetailedViewCommandFailure(unassignTagCommand, model, UnassignTagCommand.MESSAGE_NOT_TAGGED); + } + + @Test + public void equals() { + UnassignTagCommand unassignTagFirstCommand = new UnassignTagCommand(INDEX_FIRST_PERSON, VALID_TAGNAME_FRIENDS); + UnassignTagCommand unassignTagSecondCommand = new UnassignTagCommand(INDEX_SECOND_PERSON, + VALID_TAGNAME_COLLEAGUES); + + // same object -> returns true + assertTrue(unassignTagFirstCommand.equals(unassignTagFirstCommand)); + + // same values -> returns true + UnassignTagCommand unassignTagFirstCommandCopy = new UnassignTagCommand(INDEX_FIRST_PERSON, + VALID_TAGNAME_FRIENDS); + assertTrue(unassignTagFirstCommand.equals(unassignTagFirstCommandCopy)); + + // different types -> returns false + assertFalse(unassignTagFirstCommand.equals(1)); + + // null -> returns false + assertFalse(unassignTagFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(unassignTagFirstCommand.equals(unassignTagSecondCommand)); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5cf487d7ebb..0367a42294e 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -5,6 +5,8 @@ import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.EMPTY_ADDRESS; +import static seedu.address.logic.commands.CommandTestUtil.EMPTY_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; @@ -76,10 +78,22 @@ public void parse_allFieldsPresent_success() { @Test public void parse_optionalFieldsMissing_success() { + // zero tags - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); + Person expectedPersonNoTags = new PersonBuilder(AMY).withTags().build(); assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, - new AddCommand(expectedPerson)); + new AddCommand(expectedPersonNoTags)); + + // zero address + Person expectedPersonNoAddress = new PersonBuilder(AMY).withAddress(EMPTY_ADDRESS).build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + EMPTY_ADDRESS_DESC + + TAG_DESC_FRIEND, new AddCommand(expectedPersonNoAddress)); + + // zero address and tags + Person expectedPersonNoAddressNoTags = + new PersonBuilder(AMY).withAddress(EMPTY_ADDRESS).withTags().build(); + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + EMPTY_ADDRESS_DESC, + new AddCommand(expectedPersonNoAddressNoTags)); } @Test @@ -98,10 +112,6 @@ public void parse_compulsoryFieldMissing_failure() { assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, expectedMessage); - // missing address prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, - expectedMessage); - // all prefixes missing assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, expectedMessage); diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index d9659205b57..78ffbc8484a 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -2,27 +2,52 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.commons.core.Messages.MESSAGE_INCOMPATIBLE_VIEW_MODE; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalTags.VALID_TAGNAME_FRIENDS; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddImageCommand; +import seedu.address.logic.commands.AssignTagCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.CreateTagCommand; +import seedu.address.logic.commands.DeadlineCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteDeadlineCommand; +import seedu.address.logic.commands.DeleteImageCommand; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.commands.DeleteTagCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FavouriteCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HighImportanceCommand; +import seedu.address.logic.commands.ImagesCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListFavouritesCommand; +import seedu.address.logic.commands.ListImportantCommand; +import seedu.address.logic.commands.NoteCommand; +import seedu.address.logic.commands.SortsCommand; +import seedu.address.logic.commands.UnassignTagCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.testutil.EditPersonDescriptorBuilder; @@ -32,10 +57,10 @@ public class AddressBookParserTest { private final AddressBookParser parser = new AddressBookParser(); + private final Person person = new PersonBuilder().build(); @Test public void parseCommand_add() throws Exception { - Person person = new PersonBuilder().build(); AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); assertEquals(new AddCommand(person), command); } @@ -55,7 +80,6 @@ public void parseCommand_delete() throws Exception { @Test public void parseCommand_edit() throws Exception { - Person person = new PersonBuilder().build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); @@ -88,6 +112,21 @@ public void parseCommand_list() throws Exception { assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + @Test + public void parseCommand_view() throws Exception { + ViewCommand command = (ViewCommand) parser.parseCommand( + ViewCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new ViewCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_detailedViewCommands_throwsParseException() { + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseCommand(DeleteDeadlineCommand.COMMAND_WORD + " 3")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseCommand(DeleteNoteCommand.COMMAND_WORD + " 2")); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () @@ -98,4 +137,268 @@ public void parseCommand_unrecognisedInput_throwsParseException() { public void parseCommand_unknownCommand_throwsParseException() { assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); } + + @Test + public void parseCommand_findTag() throws Exception { + List keywords = Arrays.asList("foo", "bar", "baz"); + FindTagCommand command = (FindTagCommand) parser.parseCommand( + FindTagCommand.COMMAND_WORD + + " " + + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new FindTagCommand(keywords), command); + } + + @Test + public void parseCommand_tag() throws Exception { + String tagName = VALID_TAGNAME_FRIENDS; + CreateTagCommand command = (CreateTagCommand) parser.parseCommand( + CreateTagCommand.COMMAND_WORD + " " + tagName); + assertEquals(new CreateTagCommand(tagName), command); + } + + @Test + public void parseCommand_assign() throws Exception { + String tagName = VALID_TAGNAME_FRIENDS; + AssignTagCommand command = (AssignTagCommand) parser.parseCommand( + AssignTagCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " " + tagName); + assertEquals(new AssignTagCommand(INDEX_FIRST_PERSON, tagName), command); + } + + @Test + public void parseCommand_unassign() throws Exception { + String tagName = VALID_TAGNAME_FRIENDS; + UnassignTagCommand command = (UnassignTagCommand) parser.parseCommand( + UnassignTagCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " " + tagName); + assertEquals(new UnassignTagCommand(INDEX_FIRST_PERSON, tagName), command); + } + + @Test + public void parseCommand_deltag() throws Exception { + List keywords = Arrays.asList(VALID_TAGNAME_FRIENDS); + DeleteTagCommand command = (DeleteTagCommand) parser.parseCommand( + DeleteTagCommand.COMMAND_WORD + + " " + + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new DeleteTagCommand(keywords), command); + } + + @Test + public void parseCommand_fav() throws Exception { + FavouriteCommand command = (FavouriteCommand) parser.parseCommand( + FavouriteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new FavouriteCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_favourites() throws Exception { + ListFavouritesCommand command = (ListFavouritesCommand) parser.parseCommand( + ListFavouritesCommand.COMMAND_WORD); + assertEquals(new ListFavouritesCommand(), command); + assertTrue(parser.parseCommand( + ListFavouritesCommand.COMMAND_WORD + " 3") instanceof ListFavouritesCommand); + } + + @Test + public void parseCommand_impts() throws Exception { + ListImportantCommand command = (ListImportantCommand) parser.parseCommand( + ListImportantCommand.COMMAND_WORD); + assertEquals(new ListImportantCommand(), command); + assertTrue(parser.parseCommand( + ListImportantCommand.COMMAND_WORD + " 3") instanceof ListImportantCommand); + } + + @Test + public void parseCommand_deadline() throws Exception { + List deadlines = Arrays.asList(new Deadline("description", "11/11/2022")); + DeadlineList deadlineList = new DeadlineList(deadlines); + DeadlineCommand command = (DeadlineCommand) parser.parseCommand( + DeadlineCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + + " d/description 11/11/2022"); + assertEquals(new DeadlineCommand(INDEX_FIRST_PERSON, deadlineList), command); + } + + @Test + public void parseDetailedViewCommand_add() throws Exception { + AddCommand command = (AddCommand) parser.parseDetailedViewCommand(PersonUtil.getAddCommand(person)); + assertEquals(new AddCommand(person), command); + } + + @Test + public void parseDetailedViewCommand_exit() throws Exception { + assertTrue(parser.parseDetailedViewCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); + assertTrue(parser.parseDetailedViewCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + } + + @Test + public void parseDetailedViewCommand_help() throws Exception { + assertTrue(parser.parseDetailedViewCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); + assertTrue(parser.parseDetailedViewCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); + } + + @Test + public void parseDetailedViewCommand_edit() throws Exception { + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); + EditCommand command = (EditCommand) parser.parseDetailedViewCommand(EditCommand.COMMAND_WORD + " " + + PersonUtil.getEditPersonDescriptorDetails(descriptor)); + assertEquals(new EditCommand(descriptor), command); + + // same command with index given -> index ignored + EditCommand commandWithIndex = (EditCommand) parser.parseDetailedViewCommand(EditCommand.COMMAND_WORD + + " 1 " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); + assertEquals(command, commandWithIndex); + } + + @Test + public void parseDetailedViewCommand_deadline() throws Exception { + String deadlineString = "d/test 21/02/2022"; + DeadlineList deadlineList = ParserUtil.parseDeadlines(Collections.singletonList(deadlineString)); + DeadlineCommand command = (DeadlineCommand) parser.parseDetailedViewCommand(DeadlineCommand.COMMAND_WORD + + " " + deadlineString); + assertEquals(command, new DeadlineCommand(deadlineList)); + + // same command with index given -> index ignored + DeadlineCommand commandWithIndex = (DeadlineCommand) parser.parseDetailedViewCommand( + DeadlineCommand.COMMAND_WORD + " 2 " + deadlineString); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_note() throws Exception { + String note = "likes green"; + NoteCommand command = (NoteCommand) parser.parseDetailedViewCommand(NoteCommand.COMMAND_WORD + + " " + PREFIX_NOTE + note); + assertEquals(command, new NoteCommand(note)); + + // same command with index given -> index ignored + NoteCommand commandWithIndex = (NoteCommand) parser.parseDetailedViewCommand(NoteCommand.COMMAND_WORD + + " 2 " + PREFIX_NOTE + note); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_fav() throws Exception { + FavouriteCommand command = (FavouriteCommand) parser.parseDetailedViewCommand(FavouriteCommand.COMMAND_WORD); + assertEquals(command, new FavouriteCommand()); + + // same command with index given -> index ignored + FavouriteCommand commandWithIndex = (FavouriteCommand) parser.parseDetailedViewCommand( + FavouriteCommand.COMMAND_WORD + " 2"); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_impt() throws Exception { + HighImportanceCommand command = (HighImportanceCommand) parser.parseDetailedViewCommand( + HighImportanceCommand.COMMAND_WORD); + assertEquals(command, new HighImportanceCommand()); + + // same command with index given -> index ignored + HighImportanceCommand commandWithIndex = (HighImportanceCommand) parser.parseDetailedViewCommand( + HighImportanceCommand.COMMAND_WORD + " 2"); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_assign() throws Exception { + String tag = "tag"; + AssignTagCommand command = (AssignTagCommand) parser.parseDetailedViewCommand( + AssignTagCommand.COMMAND_WORD + " " + tag); + assertEquals(command, new AssignTagCommand(tag)); + + // same command with index given -> index ignored + AssignTagCommand commandWithIndex = (AssignTagCommand) parser.parseDetailedViewCommand( + AssignTagCommand.COMMAND_WORD + " 2 " + tag); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_unassign() throws Exception { + String tag = "tag"; + UnassignTagCommand command = (UnassignTagCommand) parser.parseDetailedViewCommand( + UnassignTagCommand.COMMAND_WORD + " " + tag); + assertEquals(command, new UnassignTagCommand(tag)); + + // same command with index given -> index ignored + UnassignTagCommand commandWithIndex = (UnassignTagCommand) parser.parseDetailedViewCommand( + UnassignTagCommand.COMMAND_WORD + " 2 " + tag); + assertEquals(commandWithIndex, command); + } + + @Test + public void parseDetailedViewCommand_tag() throws Exception { + String tag = "tag"; + // same command parsed regardless of panel view + CreateTagCommand parsedCommand = (CreateTagCommand) parser.parseCommand( + CreateTagCommand.COMMAND_WORD + " " + tag); + CreateTagCommand detailedViewParsedCommand = (CreateTagCommand) parser.parseDetailedViewCommand( + CreateTagCommand.COMMAND_WORD + " " + tag); + assertEquals(parsedCommand, detailedViewParsedCommand); + } + + @Test + public void parseDetailedViewCommand_listCommands_throwException() { + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(ClearCommand.COMMAND_WORD)); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(DeleteCommand.COMMAND_WORD + " 1")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(DeleteTagCommand.COMMAND_WORD + " tag")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(FindCommand.COMMAND_WORD + " alex")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(FindTagCommand.COMMAND_WORD + " tag")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(ListFavouritesCommand.COMMAND_WORD)); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(ListImportantCommand.COMMAND_WORD)); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(SortsCommand.COMMAND_WORD + " address")); + assertThrows(ParseException.class, MESSAGE_INCOMPATIBLE_VIEW_MODE, () + -> parser.parseDetailedViewCommand(ViewCommand.COMMAND_WORD + " 1")); + } + + @Test + public void parseCommand_impt() throws Exception { + HighImportanceCommand command = (HighImportanceCommand) parser.parseCommand( + HighImportanceCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new HighImportanceCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_addimg() throws Exception { + AddImageCommand command = (AddImageCommand) parser.parseCommand( + AddImageCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new AddImageCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_images() throws Exception { + ImagesCommand command = (ImagesCommand) parser.parseCommand( + ImagesCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + assertEquals(new ImagesCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_note() throws Exception { + NoteCommand command = (NoteCommand) parser.parseCommand( + NoteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " r/note"); + assertEquals(new NoteCommand(INDEX_FIRST_PERSON, "note"), command); + } + + @Test + public void parseCommand_delimg() throws Exception { + Index indexFirstImage = Index.fromOneBased(1); + DeleteImageCommand command = (DeleteImageCommand) parser.parseCommand( + DeleteImageCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " i/" + + indexFirstImage.getOneBased()); + assertEquals(new DeleteImageCommand(INDEX_FIRST_PERSON, indexFirstImage), command); + } + + @Test + public void parseCommand_sort() throws Exception { + SortsCommand command = (SortsCommand) parser.parseCommand( + SortsCommand.COMMAND_WORD + " " + "name"); + assertEquals(new SortsCommand("name"), command); + } + } diff --git a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java index e4c33515768..daec182274c 100644 --- a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java +++ b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DetailedViewExecutable; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -23,6 +24,23 @@ public static void assertParseSuccess(Parser parser, String userInput, Command e } } + /** + * Asserts that the parsing of {@code userInput} by {@code parser} is successful and + * the command created equals to {@code expectedCommand} + * @param parser the {@code DetailedViewExecutableParser} to parse {@code DetailedViewExecutable}. + * @param userInput the user input. + * @param expectedCommand the expected {@code DetailedViewExecutable}. + */ + public static void assertParseDetailedViewExecutableSuccess(DetailedViewExecutableParser parser, String userInput, + DetailedViewExecutable expectedCommand) { + try { + DetailedViewExecutable command = parser.parseInDetailedViewContext(userInput); + assertEquals(expectedCommand, command); + } catch (ParseException pe) { + throw new IllegalArgumentException("Invalid userInput.", pe); + } + } + /** * Asserts that the parsing of {@code userInput} by {@code parser} is unsuccessful and the error message * equals to {@code expectedMessage}. diff --git a/src/test/java/seedu/address/logic/parser/DeadlineCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeadlineCommandParserTest.java new file mode 100644 index 00000000000..b37f128ffc2 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeadlineCommandParserTest.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.DEADLINE_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_DEADLINE_AMY; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseDetailedViewExecutableSuccess; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.DeadlineCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class DeadlineCommandParserTest { + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeadlineCommand.MESSAGE_USAGE); + + private DeadlineCommandParser parser = new DeadlineCommandParser(); + + @Test + public void parse_validFieldSpecified_success() throws ParseException { + Index targetIndex = INDEX_THIRD_PERSON; + String userInput = targetIndex.getOneBased() + DEADLINE_DESC_AMY; + List deadlines = new ArrayList<>(); + deadlines.add(VALID_DEADLINE_AMY); + DeadlineCommand expectedCommand = new DeadlineCommand(targetIndex, ParserUtil.parseDeadlines(deadlines)); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parseInDetailedViewContext_validFieldSpecified_success() throws ParseException { + String userInput = DEADLINE_DESC_AMY; + List deadlines = new ArrayList<>(); + deadlines.add(VALID_DEADLINE_AMY); + DeadlineCommand expectedCommand = new DeadlineCommand(ParserUtil.parseDeadlines(deadlines)); + assertParseDetailedViewExecutableSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_invalidIndex_errorThrown() { + String userInput = "0" + DEADLINE_DESC_AMY; + assertThrows(IllegalValueException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeadlineCommand.MESSAGE_USAGE), () -> new DeadlineCommandParser().parse(userInput)); + } + + @Test + public void parse_noDeadlineAndPrefix_errorThrown() { + String userInput = String.valueOf(INDEX_THIRD_PERSON.getOneBased()); + assertThrows(ParseException.class, DeadlineCommand.MESSAGE_NO_DEADLINES_ADDED, () -> + new DeadlineCommandParser().parse(userInput)); + } + + @Test + public void parseInDetailedViewContext_noDeadline_success() { + assertThrows(ParseException.class, DeadlineCommand.MESSAGE_NO_DEADLINES_ADDED, () -> + new DeadlineCommandParser().parseInDetailedViewContext("")); + } +} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index 2ff31522486..a2a9a5a7101 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -9,12 +9,9 @@ import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; @@ -22,9 +19,6 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; 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; @@ -40,13 +34,10 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; import seedu.address.testutil.EditPersonDescriptorBuilder; 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); @@ -85,7 +76,6 @@ public void parse_invalidValue_failure() { assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address - assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag // invalid phone followed by valid email assertParseFailure(parser, "1" + INVALID_PHONE_DESC + EMAIL_DESC_AMY, Phone.MESSAGE_CONSTRAINTS); @@ -94,12 +84,6 @@ public void parse_invalidValue_failure() { // is tested at {@code parse_invalidValueFollowedByValidValue_success()} assertParseFailure(parser, "1" + PHONE_DESC_BOB + INVALID_PHONE_DESC, Phone.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); - // 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, Name.MESSAGE_CONSTRAINTS); @@ -108,12 +92,12 @@ public void parse_invalidValue_failure() { @Test public void parse_allFieldsSpecified_success() { Index targetIndex = INDEX_SECOND_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; + String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY; EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).build(); + EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); assertParseSuccess(parser, userInput, expectedCommand); @@ -157,24 +141,17 @@ public void parse_oneFieldSpecified_success() { descriptor = new EditPersonDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build(); expectedCommand = new EditCommand(targetIndex, descriptor); assertParseSuccess(parser, userInput, expectedCommand); - - // tags - userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; - descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); } @Test public void parse_multipleRepeatedFields_acceptsLast() { Index targetIndex = INDEX_FIRST_PERSON; String 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; + + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB; EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).build(); EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); assertParseSuccess(parser, userInput, expectedCommand); @@ -198,14 +175,4 @@ public void parse_invalidValueFollowedByValidValue_success() { assertParseSuccess(parser, userInput, expectedCommand); } - @Test - public void parse_resetTags_success() { - Index targetIndex = INDEX_THIRD_PERSON; - String userInput = targetIndex.getOneBased() + TAG_EMPTY; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withTags().build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } } diff --git a/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java new file mode 100644 index 00000000000..00896a76929 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FindTagCommand; + +public class FindTagCommandParserTest { + + private final FindTagCommandParser parser = new FindTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindTagCommand() { + // no leading and trailing whitespaces + FindTagCommand expectedFindTagCommand = + new FindTagCommand(Arrays.asList("friends", "colleagues")); + assertParseSuccess(parser, "friends colleagues", expectedFindTagCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n friends \n \t colleagues \t", expectedFindTagCommand); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java new file mode 100644 index 00000000000..06ae64146cb --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_CRITERIA; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.SortsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DeadlineList; +import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; + +public class SortCommandParserTest { + private SortCommandParser parser = new SortCommandParser(); + private String invalidCriteria = "invalidCriteria"; + + @Test + public void parse_validArgs_returnsSortCommand() { + // no leading and trailing whitespaces + SortsCommand expectedSortCommand = + new SortsCommand("name"); + assertParseSuccess(parser, "name", expectedSortCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n name \n \t \t", expectedSortCommand); + } + + @Test + public void criteriaListForUsageMessage_returnsString() { + String expected = Name.MODEL_NAME + ", " + Phone.MODEL_NAME + ", " + Email.MODEL_NAME + ", " + + Address.MODEL_NAME + ", " + DeadlineList.MODEL_NAME + ", " + Favourite.MODEL_NAME + ", " + + HighImportance.MODEL_NAME; + assertEquals(SortCommandParser.criteriaListForUsageMessage(), expected); + } + + @Test + public void parse_noCriteriaSpecified_errorThrown() { + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(SortsCommand.MESSAGE_USAGE, SortCommandParser.criteriaListForUsageMessage())), () -> + new SortCommandParser().parse("")); + } + + @Test + public void parse_invalidCriteriaSpecified_errorThrown() { + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_CRITERIA, invalidCriteria), () -> + new SortCommandParser().parse(invalidCriteria)); + } +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 87782528ecd..e937ae406d7 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -8,6 +8,8 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_NEIGHBOURS; import java.util.Arrays; import java.util.Collection; @@ -20,6 +22,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; public class AddressBookTest { @@ -49,7 +52,8 @@ public void resetData_withDuplicatePersons_throwsDuplicatePersonException() { Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) .build(); List newPersons = Arrays.asList(ALICE, editedAlice); - AddressBookStub newData = new AddressBookStub(newPersons); + List newTags = Arrays.asList(VALID_TAG_FRIENDS, VALID_TAG_NEIGHBOURS); + AddressBookStub newData = new AddressBookStub(newPersons, newTags); assertThrows(DuplicatePersonException.class, () -> addressBook.resetData(newData)); } @@ -88,15 +92,27 @@ public void getPersonList_modifyList_throwsUnsupportedOperationException() { */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList persons = FXCollections.observableArrayList(); + private final ObservableList tags = FXCollections.observableArrayList(); + private final ObservableList activatedTags = FXCollections.observableArrayList(); - AddressBookStub(Collection persons) { + AddressBookStub(Collection persons, Collection tags) { this.persons.setAll(persons); + this.tags.setAll(tags); } @Override public ObservableList getPersonList() { return persons; } + + @Override + public ObservableList getTagList() { + return tags; + } + + public ObservableList getActivatedTagList() { + return activatedTags; + } } } diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..712f4d8ba0d 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -7,14 +7,17 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_1; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.testutil.AddressBookBuilder; @@ -38,6 +41,7 @@ public void setUserPrefs_nullUserPrefs_throwsNullPointerException() { public void setUserPrefs_validUserPrefs_copiesUserPrefs() { UserPrefs userPrefs = new UserPrefs(); userPrefs.setAddressBookFilePath(Paths.get("address/book/file/path")); + userPrefs.setContactImagesFilePath(Paths.get("contact/images/file/path")); userPrefs.setGuiSettings(new GuiSettings(1, 2, 3, 4)); modelManager.setUserPrefs(userPrefs); assertEquals(userPrefs, modelManager.getUserPrefs()); @@ -45,6 +49,7 @@ public void setUserPrefs_validUserPrefs_copiesUserPrefs() { // Modifying userPrefs should not modify modelManager's userPrefs UserPrefs oldUserPrefs = new UserPrefs(userPrefs); userPrefs.setAddressBookFilePath(Paths.get("new/address/book/file/path")); + userPrefs.setContactImagesFilePath(Paths.get("new/contact/images/file/path")); assertEquals(oldUserPrefs, modelManager.getUserPrefs()); } @@ -72,6 +77,18 @@ public void setAddressBookFilePath_validPath_setsAddressBookFilePath() { assertEquals(path, modelManager.getAddressBookFilePath()); } + @Test + public void setContactImagesFilePath_nullPath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> modelManager.setContactImagesFilePath(null)); + } + + @Test + public void setContactImagesFilePath_validPath_setsAddressBookFilePath() { + Path path = Paths.get("contact/images/file/path"); + modelManager.setContactImagesFilePath(path); + assertEquals(path, modelManager.getContactImagesFilePath()); + } + @Test public void hasPerson_nullPerson_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> modelManager.hasPerson(null)); @@ -93,6 +110,29 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredPersonList().remove(0)); } + @Test + public void setDetailedContactView_personInContactView_newPersonInContactViewOnly() { + modelManager.setDetailedContactView(ALICE); + modelManager.setDetailedContactView(BENSON); + assertTrue(modelManager.getDetailedContactView().size() == 1); + assertEquals(modelManager.getDetailedContactView().get(0), BENSON); + modelManager.clearDetailedContactView(); + } + + @Test + public void setImagesToView_personWithImages_success() { + modelManager.setImagesToView(BENSON.getImageDetailsList()); + ImageDetailsList expectedList = new ImageDetailsList(List.of(TEST_IMAGE_1)); + assertEquals(modelManager.getImagesToView(), expectedList); + } + + @Test + public void setImagesToView_personWithoutImages_success() { + modelManager.setImagesToView(ALICE.getImageDetailsList()); + ImageDetailsList expectedList = new ImageDetailsList(); + assertEquals(modelManager.getImagesToView(), expectedList); + } + @Test public void equals() { AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); @@ -124,6 +164,11 @@ public void equals() { // resets modelManager to initial state for upcoming tests modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + // different contactFullView -> returns false + modelManager.setDetailedContactView(ALICE); + assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); + modelManager.clearDetailedContactView(); + // different userPrefs -> returns false UserPrefs differentUserPrefs = new UserPrefs(); differentUserPrefs.setAddressBookFilePath(Paths.get("differentFilePath")); diff --git a/src/test/java/seedu/address/model/UserPrefsTest.java b/src/test/java/seedu/address/model/UserPrefsTest.java index b1307a70d52..5f716ef796b 100644 --- a/src/test/java/seedu/address/model/UserPrefsTest.java +++ b/src/test/java/seedu/address/model/UserPrefsTest.java @@ -18,4 +18,9 @@ public void setAddressBookFilePath_nullPath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> userPrefs.setAddressBookFilePath(null)); } + @Test + public void setContactImagesFilePath_nullPath_throwsNullPointerException() { + UserPrefs userPrefs = new UserPrefs(); + assertThrows(NullPointerException.class, () -> userPrefs.setContactImagesFilePath(null)); + } } diff --git a/src/test/java/seedu/address/model/commandhistory/CommandHistoryEntryTest.java b/src/test/java/seedu/address/model/commandhistory/CommandHistoryEntryTest.java new file mode 100644 index 00000000000..be106ecdd82 --- /dev/null +++ b/src/test/java/seedu/address/model/commandhistory/CommandHistoryEntryTest.java @@ -0,0 +1,66 @@ +package seedu.address.model.commandhistory; + +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.commandhistory.CommandHistoryEntry.getEmptyHistory; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.commandhistory.exceptions.HistoryDoesNotExistException; + +class CommandHistoryEntryTest { + + private static final CommandHistoryEntry sentinel = getEmptyHistory(); + + @Test + void constructor_nullText_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CommandHistoryEntry(null)); + } + + @Test + void exists_commandHistoryEntry_returnsTrue() { + CommandHistoryEntry commandHistoryEntry = new CommandHistoryEntry("random command"); + assertTrue(commandHistoryEntry.exists()); + } + + @Test + void exists_commandHistorySentinel_returnsFalse() { + assertFalse(sentinel.exists()); + } + + @Test + void getCommandText_commandHistoryEntry_success() { + String commandText = "list"; + CommandHistoryEntry commandHistoryEntry = new CommandHistoryEntry(commandText); + assertEquals(commandText, commandHistoryEntry.getCommandText()); + } + + @Test + void getCommandText_commandHistorySentinel_throwsHistoryDoesNotExistException() { + assertThrows(HistoryDoesNotExistException.class, sentinel::getCommandText); + } + + @Test + void equals_commandHistoryEntry() { + String commandText = "list"; + CommandHistoryEntry commandHistoryEntry = new CommandHistoryEntry(commandText); + + // same object -> return true + assertTrue(commandHistoryEntry.equals(commandHistoryEntry)); + + // different object, same value -> return true + CommandHistoryEntry historyCopy = new CommandHistoryEntry(commandText); + assertTrue(commandHistoryEntry.equals(historyCopy)); + + // different values -> return false + assertFalse(commandHistoryEntry.equals(new CommandHistoryEntry("addimg 1"))); + + // sentinel and entry -> return false + assertFalse(commandHistoryEntry.equals(sentinel)); + + // sentinel and sentinel -> return true + assertTrue(sentinel.equals(getEmptyHistory())); + } +} diff --git a/src/test/java/seedu/address/model/commandhistory/CommandHistoryTest.java b/src/test/java/seedu/address/model/commandhistory/CommandHistoryTest.java new file mode 100644 index 00000000000..64feaea2510 --- /dev/null +++ b/src/test/java/seedu/address/model/commandhistory/CommandHistoryTest.java @@ -0,0 +1,107 @@ +package seedu.address.model.commandhistory; + +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.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CommandHistoryTest { + + private CommandHistory commandHistory = new CommandHistory(); + private final CommandHistoryEntry sentinel = CommandHistoryEntry.getEmptyHistory(); + + @BeforeEach + void setUp() { + commandHistory = new CommandHistory(); + } + + @Test + void cacheCommand_success() { + CommandHistoryEntry expectedEntry = new CommandHistoryEntry("entry"); + + commandHistory.cacheCommand("entry"); + CommandHistoryEntry actualEntry = commandHistory.retrieveCommand(1); + assertEquals(expectedEntry, actualEntry); + } + + @Test + void cacheCommand_nullCommandText_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> commandHistory.cacheCommand(null)); + } + + @Test + void cacheCommand_emptyCommandText_historyUnchanged() { + CommandHistory emptyHistory = new CommandHistory(); + + // commandHistory should be empty before and after the cacheCommand method + commandHistory.cacheCommand(""); + assertEquals(emptyHistory, commandHistory); + } + + @Test + void retrieveCommand_success() { + String[] commandTexts = { "command 1", "command 2", "command 3" }; + List entries = new ArrayList<>(); + for (String text : commandTexts) { + entries.add(new CommandHistoryEntry(text)); + commandHistory.cacheCommand(text); + } + + // 1 step back in history, last to be added + assertEquals(entries.get(2), commandHistory.retrieveCommand(1)); + + // 2 steps back in history, second last to be added + assertEquals(entries.get(1), commandHistory.retrieveCommand(2)); + + // 3 steps back in history, third last to be added + assertEquals(entries.get(0), commandHistory.retrieveCommand(3)); + } + + @Test + void retrieveCommand_indexZero_returnsSentinel() { + assertEquals(sentinel, commandHistory.retrieveCommand(0)); + } + + @Test + void retrieveCommand_negativeIndex_returnsSentinel() { + assertEquals(sentinel, commandHistory.retrieveCommand(-1)); + } + + @Test + void retrieveCommand_olderThanHistory_returnsSentinel() { + String[] commandTexts = { "command 1", "command 2", "command 3" }; + for (String text : commandTexts) { + commandHistory.cacheCommand(text); + } + + assertEquals(sentinel, commandHistory.retrieveCommand(4)); + } + + @Test + void equals() { + String[] commandTexts = { "command 1", "command 2", "command 3" }; + for (String text : commandTexts) { + commandHistory.cacheCommand(text); + } + + // same object -> returns true + assertTrue(commandHistory.equals(commandHistory)); + + // different object, same values -> returns true + CommandHistory copyCommandHistory = new CommandHistory(); + for (String text : commandTexts) { + copyCommandHistory.cacheCommand(text); + } + assertTrue(commandHistory.equals(copyCommandHistory)); + + // different values -> returns false + assertFalse(commandHistory.equals(new CommandHistory())); + + } +} diff --git a/src/test/java/seedu/address/model/image/ImageDetailsListTest.java b/src/test/java/seedu/address/model/image/ImageDetailsListTest.java new file mode 100644 index 00000000000..618c7867b4b --- /dev/null +++ b/src/test/java/seedu/address/model/image/ImageDetailsListTest.java @@ -0,0 +1,108 @@ +package seedu.address.model.image; + +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.TypicalSavedImages.TEST_IMAGE_1; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_2; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_3; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_4; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_5; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_6; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_7; +import static seedu.address.testutil.TypicalSavedImages.getTypicalImageDetailsList; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; + +class ImageDetailsListTest { + private final ImageDetailsList emptyList = new ImageDetailsList(); + + @Test + void appendImageDetails_success() { + ImageDetailsList expectedList = new ImageDetailsList(List.of( + TEST_IMAGE_1, TEST_IMAGE_2, TEST_IMAGE_3, TEST_IMAGE_4, TEST_IMAGE_5, TEST_IMAGE_6, TEST_IMAGE_7 + )); + + final ImageDetailsList originalTestList = getTypicalImageDetailsList(); + List listToAppend = List.of(TEST_IMAGE_6, TEST_IMAGE_7); + ImageDetailsList actualList = originalTestList.appendImageDetails(listToAppend); + + assertEquals(expectedList, actualList); + } + + @Test + void getImages_typicalImageDetailsList_success() { + List expectedList = List.of(TEST_IMAGE_1, TEST_IMAGE_2, TEST_IMAGE_3, TEST_IMAGE_4, TEST_IMAGE_5); + + ImageDetailsList originalTestList = getTypicalImageDetailsList(); + List actualList = originalTestList.getImages(); + + assertEquals(expectedList, actualList); + } + + @Test + void size() { + assertEquals(0, emptyList.size()); + + assertEquals(5, getTypicalImageDetailsList().size()); + } + + @Test + void getImages_emptyImagesDetailsList_success() { + assertEquals(List.of(), emptyList.getImages()); + } + + @Test + void isEmpty_emptyList_returnsTrue() { + // empty list + assertTrue(emptyList.isEmpty()); + } + + @Test + void isEmpty_nonEmptyList_returnsFalse() { + // non-empty list + assertFalse(getTypicalImageDetailsList().isEmpty()); + } + + @Test + void get_success() { + ImageDetailsList list = getTypicalImageDetailsList(); + // first item in list + assertEquals(TEST_IMAGE_1, list.get(Index.fromOneBased(1).getZeroBased())); + + // last item in list + assertEquals(TEST_IMAGE_5, list.get(Index.fromOneBased(list.size()).getZeroBased())); + } + + @Test + void get_outOfBounds_throwsIndexOutOfBoundsException() { + ImageDetailsList list = getTypicalImageDetailsList(); + // index beyond size + Index outOfBoundsIndex = Index.fromZeroBased(getTypicalImageDetailsList().size()); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(outOfBoundsIndex.getZeroBased())); + + // index negative; + assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + } + + @Test + void equals() { + ImageDetailsList imageDetailsList = getTypicalImageDetailsList(); + ImageDetailsList otherImageDetailsList = new ImageDetailsList(List.of( + TEST_IMAGE_1, TEST_IMAGE_2, TEST_IMAGE_3, TEST_IMAGE_4, TEST_IMAGE_5)); + + // same object -> return true + assertTrue(imageDetailsList.equals(imageDetailsList)); + + // different object, same values -> return true + assertTrue(imageDetailsList.equals(otherImageDetailsList)); + + // different object, different values -> return false + assertFalse(imageDetailsList.equals(emptyList)); + } +} diff --git a/src/test/java/seedu/address/model/image/ImageDetailsTest.java b/src/test/java/seedu/address/model/image/ImageDetailsTest.java new file mode 100644 index 00000000000..e9032417c32 --- /dev/null +++ b/src/test/java/seedu/address/model/image/ImageDetailsTest.java @@ -0,0 +1,54 @@ +package seedu.address.model.image; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGES_DIRECTORY; + +import java.io.File; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class ImageDetailsTest { + private String testFileName = "test_image_1.png"; + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new ImageDetails(null)); + } + + @Test + void getName() { + File file = TEST_IMAGES_DIRECTORY.resolve(testFileName).toFile(); + ImageDetails imageDetails = new ImageDetails(file); + assertEquals(testFileName, imageDetails.getName()); + } + + @Test + void getImageFile() { + File file = TEST_IMAGES_DIRECTORY.resolve(testFileName).toFile(); + ImageDetails imageDetails = new ImageDetails(file); + assertEquals(file, imageDetails.getImageFile()); + } + + @Test + void getPath() { + Path path = TEST_IMAGES_DIRECTORY.resolve(testFileName); + File file = path.toFile(); + ImageDetails imageDetails = new ImageDetails(file); + + assertEquals(path.toString(), imageDetails.getPath()); + } + + @Test + void getJavaFxImageUrl() { + Path path = TEST_IMAGES_DIRECTORY.resolve(testFileName); + + String expected = String.format("file:%s", path); + + File file = path.toFile(); + ImageDetails imageDetails = new ImageDetails(file); + + assertEquals(expected, imageDetails.getJavaFxImageUrl()); + } +} diff --git a/src/test/java/seedu/address/model/image/util/ImageUtilTest.java b/src/test/java/seedu/address/model/image/util/ImageUtilTest.java new file mode 100644 index 00000000000..2b78555751b --- /dev/null +++ b/src/test/java/seedu/address/model/image/util/ImageUtilTest.java @@ -0,0 +1,123 @@ +package seedu.address.model.image.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGES_DIRECTORY; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_1_FILE; +import static seedu.address.testutil.TypicalSavedImages.TEST_IMAGE_2_FILE; +import static seedu.address.testutil.TypicalSavedImages.getTypicalImageDetailsList; +import static seedu.address.testutil.TypicalSavedImages.populateTestImages; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; + +class ImageUtilTest { + + private static final Path tempDirectory = TEST_IMAGES_DIRECTORY.resolve("temp"); + + /** + * Sets up the temp directory for files to be copied into. + * + * @throws IOException if something goes wrong with the creation of the directory. + */ + @BeforeAll + static void setUp() throws IOException { + populateTestImages(); + Files.createDirectory(tempDirectory); + } + + @Test + void fileExists_validFile_returnsTrue() { + assertTrue(ImageUtil.fileExists(TEST_IMAGE_1_FILE, TEST_IMAGES_DIRECTORY)); + } + + @Test + void fileExists_invalidDirectory_returnsFalse() { + assertFalse(ImageUtil.fileExists(TEST_IMAGE_1_FILE, Paths.get("does_not_exist"))); + } + + @Test + void fileExists_invalidFile_returnsFalse() { + assertFalse( + ImageUtil.fileExists(TEST_IMAGES_DIRECTORY.resolve("does_not_exist").toFile(), TEST_IMAGES_DIRECTORY)); + } + + @Test + void fileExists_identicalFilePath_returnsFalse() { + assertFalse(ImageUtil.fileExists(TEST_IMAGE_1_FILE, TEST_IMAGE_1_FILE.toPath())); + } + + @Test + void copyTo_success() throws IOException { + File fileToCopy = TEST_IMAGE_1_FILE; + ImageDetails copiedImage = ImageUtil.copyTo(fileToCopy, tempDirectory.resolve(fileToCopy.getName())); + + assertTrue(ImageUtil.fileExists(copiedImage.getImageFile(), tempDirectory)); + } + + @Test + void copyTo_invalidFile_throwsIoException() { + File nonExistentFile = new File("does_not_exist"); + assertThrows(IOException.class, () -> ImageUtil.copyTo( + nonExistentFile, tempDirectory.resolve(nonExistentFile.getName()))); + } + + @Test + void sanitizeList_success() { + File nonExistentFile = new File("non-existent"); + ImageDetailsList unsanitizedList = getTypicalImageDetailsList() + .appendImageDetails(List.of(new ImageDetails(nonExistentFile))); + + // before sanitize -> size 6 + assertEquals(6, unsanitizedList.size()); + + ImageDetailsList sanitizedList = ImageUtil.sanitizeList(unsanitizedList, TEST_IMAGES_DIRECTORY); + + // after sanitize -> size 5 + assertEquals(5, sanitizedList.size()); + assertEquals(getTypicalImageDetailsList(), sanitizedList); + } + + @Test + void removeFile_success() throws IOException { + File fileToCopy = TEST_IMAGE_2_FILE; + ImageDetails copiedImage = ImageUtil.copyTo(fileToCopy, tempDirectory.resolve(fileToCopy.getName())); + + // newly created file exists + assertTrue(ImageUtil.fileExists(copiedImage.getImageFile(), tempDirectory)); + + ImageUtil.removeFile(copiedImage); + + // newly creted file does not exist after deletion + assertFalse(ImageUtil.fileExists(copiedImage.getImageFile(), tempDirectory)); + } + + /** + * Removes the temp folder after the tests. + * + * @throws IOException if the delete operation fails. + */ + @AfterAll + static void tearDown() throws IOException { + File[] contents = tempDirectory.toFile().listFiles(); + if (contents != null) { + for (File f : contents) { + Files.delete(f.toPath()); + } + } + Files.delete(tempDirectory); + } +} diff --git a/src/test/java/seedu/address/model/person/DeadlineListTest.java b/src/test/java/seedu/address/model/person/DeadlineListTest.java new file mode 100644 index 00000000000..b466af777d8 --- /dev/null +++ b/src/test/java/seedu/address/model/person/DeadlineListTest.java @@ -0,0 +1,14 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.TypicalPersons.ALICE; + +import org.junit.jupiter.api.Test; + +public class DeadlineListTest { + @Test + public void listFormat_returnsStringArray() { + String expected = "a: 16/07/2028\nd: 18/10/2025\nf: 27/10/2024\n"; + assertEquals(ALICE.getDeadlines().listFormat(), expected); + } +} diff --git a/src/test/java/seedu/address/model/person/DeadlineTest.java b/src/test/java/seedu/address/model/person/DeadlineTest.java new file mode 100644 index 00000000000..ec9537f80d5 --- /dev/null +++ b/src/test/java/seedu/address/model/person/DeadlineTest.java @@ -0,0 +1,24 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +public class DeadlineTest { + private String validDate = "1/1/2024"; + @Test + public void hashcode_duplicateDeadline_equal() { + assertEquals(new Deadline().hashCode(), new Deadline().hashCode()); + } + + @Test + public void isValidDeadline_emptyDescription_false() { + assertFalse(Deadline.isValidDeadline("", validDate)); + } + + @Test + public void isValidDeadline_invalidDescription_false() { + assertFalse(Deadline.isValidDeadline(" ", validDate)); + } +} diff --git a/src/test/java/seedu/address/model/person/FavouriteTest.java b/src/test/java/seedu/address/model/person/FavouriteTest.java new file mode 100644 index 00000000000..f92c9d78646 --- /dev/null +++ b/src/test/java/seedu/address/model/person/FavouriteTest.java @@ -0,0 +1,43 @@ +package seedu.address.model.person; + +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; + +class FavouriteTest { + + @Test + void valueOf_invalidBooleanString_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> Favourite.valueOf("notBoolean")); + } + + @Test + void isValidFavourite() { + // null favourite + assertThrows(NullPointerException.class, () -> Favourite.isValidFavourite(null)); + + // invalid favourite values + assertFalse(Favourite.isValidFavourite("")); // empty string + assertFalse(Favourite.isValidFavourite(" ")); // spaces only + assertFalse(Favourite.isValidFavourite("no")); // non-boolean + assertFalse(Favourite.isValidFavourite("yes")); // non-boolean + assertFalse(Favourite.isValidFavourite("1")); // binary digit + assertFalse(Favourite.isValidFavourite("0")); // binary digit + assertFalse(Favourite.isValidFavourite("True")); // capitalized boolean string + + // valid favourite values + assertTrue(Favourite.isValidFavourite("true")); + assertTrue(Favourite.isValidFavourite("false")); + } + + @Test + void isFavourite() { + // not favourite + assertFalse(Favourite.valueOf("false").isFavourite()); + + // is favourite + assertTrue(Favourite.valueOf("true").isFavourite()); + } +} diff --git a/src/test/java/seedu/address/model/person/HighImportanceTest.java b/src/test/java/seedu/address/model/person/HighImportanceTest.java new file mode 100644 index 00000000000..c6904f17418 --- /dev/null +++ b/src/test/java/seedu/address/model/person/HighImportanceTest.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +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; + +class HighImportanceTest { + + @Test + void valueOf_invalidBooleanString_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> HighImportance.valueOf("NOT_BOOLEAN")); + } + + @Test + void isValidHighImportance() { + // null high importance value + assertThrows(NullPointerException.class, () -> HighImportance.isValidHighImportance(null)); + + // invalid high importance values + assertFalse(HighImportance.isValidHighImportance("")); // empty string + assertFalse(HighImportance.isValidHighImportance(" ")); // whitespaces only + assertFalse(HighImportance.isValidHighImportance("no")); // non-boolean + assertFalse(HighImportance.isValidHighImportance("yes")); // non-boolean + assertFalse(HighImportance.isValidHighImportance("0")); // binary digit + assertFalse(HighImportance.isValidHighImportance("1")); // binary digit + assertFalse(HighImportance.isValidHighImportance("False")); // capitalized boolean string + assertFalse(HighImportance.isValidHighImportance("True")); // capitalized boolean string + assertFalse(HighImportance.isValidHighImportance("FALSE")); // capitalized boolean string + assertFalse(HighImportance.isValidHighImportance("TRUE")); // capitalized boolean string + + // valid high importance values + assertTrue(HighImportance.isValidHighImportance("false")); + assertTrue(HighImportance.isValidHighImportance("true")); + } + + @Test + void hasHighImportance() { + // not high importance + assertFalse(HighImportance.valueOf("false").hasHighImportance()); + + // not high importance + assertTrue(HighImportance.valueOf("true").hasHighImportance()); + } +} diff --git a/src/test/java/seedu/address/model/person/NotesTest.java b/src/test/java/seedu/address/model/person/NotesTest.java new file mode 100644 index 00000000000..feb0866afb2 --- /dev/null +++ b/src/test/java/seedu/address/model/person/NotesTest.java @@ -0,0 +1,18 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NotesTest { + + @Test + void isValidNote() { + assertFalse(Notes.isValidNote("")); + assertFalse(Notes.isValidNote(" ")); + + assertTrue(Notes.isValidNote("likes bread")); + assertTrue(Notes.isValidNote(" likes bread ")); + } +} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index b29c097cfd4..05c9e6e671a 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -5,12 +5,15 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NOTE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import java.util.List; + import org.junit.jupiter.api.Test; import seedu.address.testutil.PersonBuilder; @@ -84,8 +87,20 @@ public void equals() { editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build(); assertFalse(ALICE.equals(editedAlice)); + // different notes -> return false + editedAlice = new PersonBuilder(ALICE).withNotes(List.of(VALID_NOTE_BOB)).build(); + assertFalse(ALICE.equals(editedAlice)); + // different tags -> returns false editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); assertFalse(ALICE.equals(editedAlice)); + + // different favourites -> returns false + editedAlice = new PersonBuilder(ALICE).withFavourite(String.valueOf(!ALICE.isFavourite())).build(); + assertFalse(ALICE.equals(editedAlice)); + + // different high importance status -> returns false + editedAlice = new PersonBuilder(ALICE).withHighImportance(String.valueOf(!ALICE.hasHighImportance())).build(); + assertFalse(ALICE.equals(editedAlice)); } } diff --git a/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..a03b68c7ad7 --- /dev/null +++ b/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java @@ -0,0 +1,76 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PersonBuilder; + +public class TagContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + TagContainsKeywordsPredicate firstPredicate = new TagContainsKeywordsPredicate(firstPredicateKeywordList); + TagContainsKeywordsPredicate secondPredicate = new TagContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + TagContainsKeywordsPredicate firstPredicateCopy = new TagContainsKeywordsPredicate(firstPredicateKeywordList); + 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_tagContainsKeywords_returnsTrue() { + // One keyword + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.singletonList("friend")); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friend").build())); + + // Multiple keywords + predicate = new TagContainsKeywordsPredicate(Arrays.asList("friend", "colleague")); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Tan").withTags("friend", "colleague").build())); + + // Only one matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("friend")); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Tan").withTags("friend", "colleague").build())); + + // Mixed-case keywords + predicate = new TagContainsKeywordsPredicate(Arrays.asList("fRiEnd", "coLLeaGUe")); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Tan").withTags("friend", "colleague").build())); + } + + @Test + public void test_tagDoesNotContainKeywords_returnsFalse() { + // Zero keywords + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").build())); + + // Non-matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("colleague")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friend").build())); + + // Keywords match name, phone, email and address, but does not match tag + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Alice", "12345", "alice@email.com", "Main", + "Street")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").build())); + } +} diff --git a/src/test/java/seedu/address/model/tag/ActivatedTagListTest.java b/src/test/java/seedu/address/model/tag/ActivatedTagListTest.java new file mode 100644 index 00000000000..b633896ffaa --- /dev/null +++ b/src/test/java/seedu/address/model/tag/ActivatedTagListTest.java @@ -0,0 +1,153 @@ +package seedu.address.model.tag; + +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.TypicalTags.VALID_TAG_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_NEIGHBOURS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_TEST; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +class ActivatedTagListTest { + private final ActivatedTagList activatedTagList = new ActivatedTagList(); + + @Test + public void contains_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> activatedTagList.contains(null)); + } + + @Test + public void contains_tagNotInList_returnsFalse() { + assertFalse(activatedTagList.contains(VALID_TAG_FRIENDS)); + } + + @Test + public void contains_tagInList_returnsTrue() { + activatedTagList.add(VALID_TAG_FRIENDS); + assertTrue(activatedTagList.contains(VALID_TAG_FRIENDS)); + } + + @Test + public void contains_tagWithSameNameDifferentCase_returnsTrue() { + activatedTagList.add(VALID_TAG_NEIGHBOURS); + Tag sameNameDifferentCaseTag = new Tag("neighBouRs"); + assertTrue(activatedTagList.contains(sameNameDifferentCaseTag)); + } + + @Test + public void add_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> activatedTagList.add(null)); + } + + @Test + public void add_duplicateTag_throwsDuplicateTagException() { + activatedTagList.add(VALID_TAG_FRIENDS); + assertThrows(DuplicateTagException.class, () -> activatedTagList.add(VALID_TAG_FRIENDS)); + } + + @Test + public void setTag_nullTargetTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> activatedTagList.setTag(null, VALID_TAG_FRIENDS)); + } + + @Test + public void setTag_nullEditedTag_throwsNullPointerException() { + activatedTagList.add(VALID_TAG_FRIENDS); + assertThrows(NullPointerException.class, () -> activatedTagList.setTag(VALID_TAG_FRIENDS, null)); + } + + @Test + public void setTag_targetTagNotInLIst_throwsTagNotFoundException() { + assertThrows(TagNotFoundException.class, () -> activatedTagList.setTag(VALID_TAG_FRIENDS, VALID_TAG_FRIENDS)); + } + + @Test + public void setTag_editedTagIsSameTag_success() { + activatedTagList.add(VALID_TAG_NEIGHBOURS); + activatedTagList.setTag(VALID_TAG_NEIGHBOURS, VALID_TAG_NEIGHBOURS); + ActivatedTagList expectedActivatedTagList = new ActivatedTagList(); + expectedActivatedTagList.add(VALID_TAG_NEIGHBOURS); + assertEquals(expectedActivatedTagList, activatedTagList); + } + + @Test + public void setTag_editedTagHasSameNameDifferentCase_success() { + activatedTagList.add(VALID_TAG_FRIENDS); + Tag sameNameDifferentCaseTag = new Tag("frIeNDs"); + activatedTagList.setTag(VALID_TAG_FRIENDS, sameNameDifferentCaseTag); + ActivatedTagList expectedActivatedTagList = new ActivatedTagList(); + expectedActivatedTagList.add(sameNameDifferentCaseTag); + assertEquals(expectedActivatedTagList, activatedTagList); + } + + @Test + public void setTag_editedTagHasNonUniqueIdentity_throwsDuplicateTagException() { + activatedTagList.add(VALID_TAG_TEST); + activatedTagList.add(VALID_TAG_FRIENDS); + assertThrows(DuplicateTagException.class, () -> activatedTagList.setTag(VALID_TAG_TEST, VALID_TAG_FRIENDS)); + } + + @Test + public void remove_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> activatedTagList.remove(null)); + } + + @Test + public void remove_tagDoesNotExist_throwsTagNotFoundException() { + assertThrows(TagNotFoundException.class, () -> activatedTagList.remove(VALID_TAG_FRIENDS)); + } + + @Test + public void remove_existingTag_removesTag() { + activatedTagList.add(VALID_TAG_FRIENDS); + activatedTagList.remove(VALID_TAG_FRIENDS); + ActivatedTagList expectedActivatedTagList = new ActivatedTagList(); + assertEquals(expectedActivatedTagList, activatedTagList); + } + + @Test + public void setTags_activatedTagList_replacesOwnListWithProvidedActivatedTagList() { + activatedTagList.add(VALID_TAG_FRIENDS); + ActivatedTagList expectedActivatedTagList = new ActivatedTagList(); + expectedActivatedTagList.add(VALID_TAG_TEST); + activatedTagList.setTags(expectedActivatedTagList); + assertEquals(expectedActivatedTagList, activatedTagList); + } + + @Test + public void setTags_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> activatedTagList.setTags((List) null)); + } + + @Test + public void setTags_list_replacesOwnListWithProvidedList() { + activatedTagList.add(VALID_TAG_NEIGHBOURS); + List tagList = Collections.singletonList(VALID_TAG_FRIENDS); + activatedTagList.setTags(tagList); + ActivatedTagList expectedActivatedTagList = new ActivatedTagList(); + expectedActivatedTagList.add(VALID_TAG_FRIENDS); + assertEquals(expectedActivatedTagList, activatedTagList); + } + + @Test + public void setTags_listWithDuplicateTags_throwsDuplicatePersonException() { + List listWithDuplicatePersons = Arrays.asList(VALID_TAG_COLLEAGUES, VALID_TAG_COLLEAGUES); + assertThrows(DuplicateTagException.class, () -> activatedTagList.setTags(listWithDuplicatePersons)); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> activatedTagList.asUnmodifiableObservableList().remove(0)); + } +} diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java index 64d07d79ee2..45a38ef0673 100644 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ b/src/test/java/seedu/address/model/tag/TagTest.java @@ -1,6 +1,9 @@ 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 static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; import org.junit.jupiter.api.Test; @@ -23,4 +26,17 @@ public void isValidTagName() { assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null)); } + @Test + public void isSameTag() { + // same object -> returns true + assertTrue(VALID_TAG_FRIENDS.isSameTag(VALID_TAG_FRIENDS)); + + // null -> returns false + assertFalse(VALID_TAG_FRIENDS.isSameTag(null)); + + // name differs in case -> returns true + Tag editedTag = new Tag("frIeNds"); + assertTrue(VALID_TAG_FRIENDS.isSameTag(editedTag)); + } + } diff --git a/src/test/java/seedu/address/model/tag/UniqueTagListTest.java b/src/test/java/seedu/address/model/tag/UniqueTagListTest.java new file mode 100644 index 00000000000..4a0427c8e33 --- /dev/null +++ b/src/test/java/seedu/address/model/tag/UniqueTagListTest.java @@ -0,0 +1,167 @@ +package seedu.address.model.tag; + +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.TypicalTags.VALID_TAG_COLLEAGUES; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_NEIGHBOURS; +import static seedu.address.testutil.TypicalTags.VALID_TAG_TEST; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +public class UniqueTagListTest { + + private final UniqueTagList uniqueTagList = new UniqueTagList(); + + @Test + public void contains_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.contains(null)); + } + + @Test + public void contains_tagNotInList_returnsFalse() { + assertFalse(uniqueTagList.contains(VALID_TAG_FRIENDS)); + } + + @Test + public void contains_tagInList_returnsTrue() { + uniqueTagList.add(VALID_TAG_NEIGHBOURS); + assertTrue(uniqueTagList.contains(VALID_TAG_NEIGHBOURS)); + } + + @Test + public void contains_tagWithSameNameDifferentCase_returnsTrue() { + uniqueTagList.add(VALID_TAG_FRIENDS); + Tag sameNameDifferentCaseTag = new Tag("frIenDs"); + assertTrue(uniqueTagList.contains(sameNameDifferentCaseTag)); + } + + @Test + public void add_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.add(null)); + } + + @Test + public void add_duplicateTag_throwsDuplicateTagException() { + uniqueTagList.add(VALID_TAG_NEIGHBOURS); + assertThrows(DuplicateTagException.class, () -> uniqueTagList.add(VALID_TAG_NEIGHBOURS)); + } + + @Test + public void setTag_nullTargetTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.setTag(null, VALID_TAG_FRIENDS)); + } + + @Test + public void setTag_nullEditedTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.setTag(VALID_TAG_COLLEAGUES, null)); + } + + @Test + public void setTag_targetTagNotInList_throwsTagNotFoundException() { + assertThrows(TagNotFoundException.class, () -> uniqueTagList.setTag(VALID_TAG_TEST, VALID_TAG_TEST)); + } + + @Test + public void setTag_editedTagIsSameTag_success() { + uniqueTagList.add(VALID_TAG_NEIGHBOURS); + uniqueTagList.setTag(VALID_TAG_NEIGHBOURS, VALID_TAG_NEIGHBOURS); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + expectedUniqueTagList.add(VALID_TAG_NEIGHBOURS); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTag_editedTagHasSameNameDifferentCase_success() { + uniqueTagList.add(VALID_TAG_FRIENDS); + Tag sameNameDifferentCaseTag = new Tag("frIeNdS"); + uniqueTagList.setTag(VALID_TAG_FRIENDS, sameNameDifferentCaseTag); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + expectedUniqueTagList.add(sameNameDifferentCaseTag); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTag_editedTagHasDifferentIdentity_success() { + uniqueTagList.add(VALID_TAG_FRIENDS); + uniqueTagList.setTag(VALID_TAG_FRIENDS, VALID_TAG_NEIGHBOURS); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + expectedUniqueTagList.add(VALID_TAG_NEIGHBOURS); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTag_editedTagHasNonUniqueIdentity_throwsDuplicateTagException() { + uniqueTagList.add(VALID_TAG_TEST); + uniqueTagList.add(VALID_TAG_FRIENDS); + assertThrows(DuplicateTagException.class, () -> uniqueTagList.setTag(VALID_TAG_TEST, VALID_TAG_FRIENDS)); + } + + @Test + public void remove_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.remove(null)); + } + + @Test + public void remove_tagDoesNotExist_throwsTagNotFoundException() { + assertThrows(TagNotFoundException.class, () -> uniqueTagList.remove(VALID_TAG_FRIENDS)); + } + + @Test + public void remove_existingTag_removesTag() { + uniqueTagList.add(VALID_TAG_FRIENDS); + uniqueTagList.remove(VALID_TAG_FRIENDS); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTags_nullUniqueTagList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.setTags((UniqueTagList) null)); + } + + @Test + public void setTags_uniqueTagList_replacesOwnListWithProvidedUniqueTagList() { + uniqueTagList.add(VALID_TAG_FRIENDS); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + expectedUniqueTagList.add(VALID_TAG_TEST); + uniqueTagList.setTags(expectedUniqueTagList); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTags_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagList.setTags((List) null)); + } + + @Test + public void setTags_list_replacesOwnListWithProvidedList() { + uniqueTagList.add(VALID_TAG_NEIGHBOURS); + List tagList = Collections.singletonList(VALID_TAG_FRIENDS); + uniqueTagList.setTags(tagList); + UniqueTagList expectedUniqueTagList = new UniqueTagList(); + expectedUniqueTagList.add(VALID_TAG_FRIENDS); + assertEquals(expectedUniqueTagList, uniqueTagList); + } + + @Test + public void setTags_listWithDuplicateTags_throwsDuplicatePersonException() { + List listWithDuplicatePersons = Arrays.asList(VALID_TAG_COLLEAGUES, VALID_TAG_COLLEAGUES); + assertThrows(DuplicateTagException.class, () -> uniqueTagList.setTags(listWithDuplicatePersons)); + } + + @Test + public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () + -> uniqueTagList.asUnmodifiableObservableList().remove(0)); + } +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..0ee0589b19b 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -14,6 +14,8 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; @@ -23,14 +25,26 @@ public class JsonAdaptedPersonTest { private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_FAVOURITE = "yes"; + private static final String INVALID_IMPORTANCE_STATUS = "no"; 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 VALID_DEADLINES = BENSON.getDeadlines().getDeadlines().stream() + .map(JsonAdaptedDeadline::new) + .collect(Collectors.toList()); + private static final List VALID_NOTES = BENSON.getNotes().value; + private static final String VALID_FAVOURITE = BENSON.getFavouriteStatus().toString(); + private static final String VALID_IMPORTANCE_STATUS = BENSON.getHighImportanceStatus().toString(); private static final List VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); + private static final List VALID_IMAGES = BENSON.getImageDetailsList().getImages() + .stream() + .map(JsonAdaptedImageDetails::new) + .collect(Collectors.toList()); @Test public void toModelType_validPersonDetails_returnsPerson() throws Exception { @@ -41,14 +55,17 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +73,17 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,14 +91,17 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -86,14 +109,17 @@ public void toModelType_nullEmail_throwsIllegalValueException() { @Test public void toModelType_invalidAddress_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -103,8 +129,28 @@ public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, invalidTags, VALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); assertThrows(IllegalValueException.class, person::toModelType); } + @Test + public void toModelType_invalidFavourite_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson( + VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, INVALID_FAVOURITE, VALID_IMAGES, VALID_IMPORTANCE_STATUS); + String expectedMessage = Favourite.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + + } + + @Test + public void toModelType_invalidImportanceStatus_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson( + VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_DEADLINES, + VALID_NOTES, VALID_TAGS, VALID_FAVOURITE, VALID_IMAGES, INVALID_IMPORTANCE_STATUS); + String expectedMessage = HighImportance.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + + } } diff --git a/src/test/java/seedu/address/storage/JsonAdaptedTagTest.java b/src/test/java/seedu/address/storage/JsonAdaptedTagTest.java new file mode 100644 index 00000000000..be83e95f745 --- /dev/null +++ b/src/test/java/seedu/address/storage/JsonAdaptedTagTest.java @@ -0,0 +1,28 @@ +package seedu.address.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalTags.VALID_TAG_FRIENDS; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.tag.Tag; + +public class JsonAdaptedTagTest { + + @Test + public void toModelType_validTag_returnsTag() throws Exception { + JsonAdaptedTag tag = new JsonAdaptedTag(VALID_TAG_FRIENDS); + assertEquals(VALID_TAG_FRIENDS, tag.toModelType()); + } + + @Test + public void toModelType_invalidTagName_throwsIllegalValueException() { + JsonAdaptedTag tag = + new JsonAdaptedTag("+"); + String expectedMessage = Tag.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, tag::toModelType); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java index 188c9058d20..323d3dcd22b 100644 --- a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java +++ b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java @@ -12,6 +12,7 @@ import seedu.address.commons.util.JsonUtil; import seedu.address.model.AddressBook; import seedu.address.testutil.TypicalPersons; +import seedu.address.testutil.TypicalTags; public class JsonSerializableAddressBookTest { @@ -19,6 +20,10 @@ public class 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"); + private static final Path DUPLICATE_TAG_FILE = TEST_DATA_FOLDER.resolve("duplicateTagAddressBook.json"); + private static final Path INVALID_TAG_FILE = TEST_DATA_FOLDER.resolve("invalidTagAddressBook.json"); + private static final Path TYPICAL_TAGS_FILE = TEST_DATA_FOLDER.resolve("typicalTagsAddressBook.json"); + private static final Path INVALID_DEADLINES_FILE = TEST_DATA_FOLDER.resolve("invalidDeadlinesAddressBook.json"); @Test public void toModelType_typicalPersonsFile_success() throws Exception { @@ -44,4 +49,34 @@ public void toModelType_duplicatePersons_throwsIllegalValueException() throws Ex dataFromFile::toModelType); } + @Test + public void toModelType_duplicateTags_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_TAG_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_TAG, + dataFromFile::toModelType); + } + + @Test + public void toModelType_invalidTagFile_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_TAG_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, dataFromFile::toModelType); + } + + @Test + public void toModelType_typicalTagsFile_success() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_TAGS_FILE, + JsonSerializableAddressBook.class).get(); + AddressBook addressBookFromFile = dataFromFile.toModelType(); + AddressBook typicalTagsAddressBook = TypicalTags.getTypicalAddressBook(); + assertEquals(addressBookFromFile, typicalTagsAddressBook); + } + + @Test + public void readAddressBook_invalidDeadlinesAddressBook_throwDataConversionException() throws Exception { + JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_DEADLINES_FILE, + JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, dataFromFile::toModelType); + } } diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java index 4584bd5044e..f9539401912 100644 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java @@ -1,16 +1,11 @@ package seedu.address.testutil; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; 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; /** * A utility class to help with building EditPersonDescriptor objects. @@ -36,7 +31,6 @@ public EditPersonDescriptorBuilder(Person person) { descriptor.setPhone(person.getPhone()); descriptor.setEmail(person.getEmail()); descriptor.setAddress(person.getAddress()); - descriptor.setTags(person.getTags()); } /** @@ -71,16 +65,6 @@ public EditPersonDescriptorBuilder withAddress(String address) { return this; } - /** - * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} - * that we are building. - */ - public EditPersonDescriptorBuilder withTags(String... tags) { - Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); - descriptor.setTags(tagSet); - return this; - } - public EditPersonDescriptor build() { return descriptor; } diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..a8765810843 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -1,11 +1,18 @@ package seedu.address.testutil; import java.util.HashSet; +import java.util.List; import java.util.Set; +import seedu.address.model.image.ImageDetailsList; import seedu.address.model.person.Address; +import seedu.address.model.person.Deadline; +import seedu.address.model.person.DeadlineList; import seedu.address.model.person.Email; +import seedu.address.model.person.Favourite; +import seedu.address.model.person.HighImportance; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -20,12 +27,19 @@ public class PersonBuilder { public static final String DEFAULT_PHONE = "85355255"; public static final String DEFAULT_EMAIL = "amy@gmail.com"; public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; + public static final String DEFAULT_FAVOURITE = "false"; + public static final String DEFAULT_HIGH_IMPORTANCE = "false"; private Name name; private Phone phone; private Email email; private Address address; + private DeadlineList deadlines; + private Notes notes; + private Favourite favouriteStatus; + private HighImportance highImportanceStatus; private Set tags; + private ImageDetailsList images; /** * Creates a {@code PersonBuilder} with the default details. @@ -35,7 +49,12 @@ public PersonBuilder() { phone = new Phone(DEFAULT_PHONE); email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); + deadlines = new DeadlineList(); + notes = Notes.getNewNotes(); + favouriteStatus = Favourite.valueOf(DEFAULT_FAVOURITE); + highImportanceStatus = HighImportance.valueOf(DEFAULT_HIGH_IMPORTANCE); tags = new HashSet<>(); + images = new ImageDetailsList(); } /** @@ -46,7 +65,12 @@ public PersonBuilder(Person personToCopy) { phone = personToCopy.getPhone(); email = personToCopy.getEmail(); address = personToCopy.getAddress(); + deadlines = personToCopy.getDeadlines(); + notes = Notes.loadNotesFromList(personToCopy.getNotes().value); + favouriteStatus = personToCopy.getFavouriteStatus(); + highImportanceStatus = personToCopy.getHighImportanceStatus(); tags = new HashSet<>(personToCopy.getTags()); + images = personToCopy.getImageDetailsList(); } /** @@ -73,6 +97,18 @@ public PersonBuilder withAddress(String address) { return this; } + /** + * Sets the {@code Deadline} of the {@code Person} that we are building. + */ + public PersonBuilder withDeadlines(String[] deadlines) { + if (deadlines.length == 1 && deadlines[0].equals(Deadline.NO_DEADLINE_PLACEHOLDER)) { + this.deadlines = new DeadlineList(); + return this; + } + this.deadlines = this.deadlines.appendDeadlines(SampleDataUtil.getDeadlineList(deadlines)); + return this; + } + /** * Sets the {@code Phone} of the {@code Person} that we are building. */ @@ -89,8 +125,70 @@ public PersonBuilder withEmail(String email) { return this; } + /** + * Sets the {@code Notes} of the {@code Person} that we are building. + */ + public PersonBuilder withNotes(List notes) { + this.notes = Notes.loadNotesFromList(notes); + return this; + } + + /** + * Sets the {@code FavouriteStatus} of the {@code Person} that we are building. + */ + public PersonBuilder withFavourite(String favourite) { + this.favouriteStatus = Favourite.valueOf(favourite); + return this; + } + + /** + * Adds a {@code Tag} to the current {@code Set} of the {@code Person} that we are building. + */ + public PersonBuilder withNewTag(Tag tag) { + Set newTags = new HashSet<>(this.tags); + newTags.add(tag); + this.tags = newTags; + return this; + } + + /** + * Removes a {@code Tag} from the current {@code Set} of the {@code Person} that we are building. + */ + public PersonBuilder withoutTag(Tag tag) { + Set withoutTag = new HashSet<>(this.tags); + withoutTag.remove(tag); + this.tags = withoutTag; + return this; + } + + /** + * Sets the {@code ImageDetailsList} of the {@code Person} that we are building. + */ + public PersonBuilder withImageDetails(String... imagePaths) { + if (imagePaths.length == 0) { + this.images = new ImageDetailsList(); + } else { + this.images = SampleDataUtil.getImageDetailsList(imagePaths); + } + return this; + } + + /** + * Sets the {@code FavouriteStatus} of the {@code Person} that we are building. + */ + public PersonBuilder withHighImportance(String highImportance) { + this.highImportanceStatus = HighImportance.valueOf(highImportance); + return this; + } + + /** + * Builds the person based on the values supplied to the builder. + * + * @return the built person + */ public Person build() { - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, deadlines, notes, tags, favouriteStatus, highImportanceStatus, + images); } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..a4d78ae3566 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -6,12 +6,9 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Set; - import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.model.person.Person; -import seedu.address.model.tag.Tag; /** * A utility class for Person. @@ -49,14 +46,6 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip 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(" ")); - if (descriptor.getTags().isPresent()) { - Set tags = descriptor.getTags().get(); - if (tags.isEmpty()) { - sb.append(PREFIX_TAG); - } else { - tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); - } - } return sb.toString(); } } diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..369271aa430 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_FOURTH_PERSON = Index.fromOneBased(4); + public static final Index INDEX_FIFTH_PERSON = Index.fromOneBased(5); + public static final Index INDEX_SIXTH_PERSON = Index.fromOneBased(6); + public static final Index INDEX_SEVENTH_PERSON = Index.fromOneBased(7); } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..2cd8d96edab 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -11,36 +11,59 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import seedu.address.model.AddressBook; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * A utility class containing a list of {@code Person} objects to be used in tests. */ public class TypicalPersons { - public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") - .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") + .withAddress("123, Jurong West Ave 6, #08-111") + .withEmail("alice@example.com") .withPhone("94351253") - .withTags("friends").build(); + .withDeadlines(new String[]{"a 16/07/2028", "d 18/10/2025", "f 27/10/2024"}) + .withTags("friends") + .withHighImportance("false").build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") .withAddress("311, Clementi Ave 2, #02-25") .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); - public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); - public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); - public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); - public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); - public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withTags("owesMoney", "friends") + .withFavourite("true") + .withImageDetails(Path.of("src", "test", "data", "testImages", "test_image_1.png").toString()) + .withHighImportance("true").build(); + public static final Person CARL = new PersonBuilder().withName("Carl Kurz") + .withPhone("95352563") + .withEmail("heinz@example.com") + .withAddress("wall street") + .withDeadlines(new String[]{"c 16/07/2026", "e 11/04/2022"}) + .build(); + public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier") + .withPhone("87652533") + .withEmail("cornelia@example.com") + .withAddress("10th street") + .withTags("friends").build(); + public static final Person ELLE = new PersonBuilder().withName("Elle Meyer") + .withPhone("9482224") + .withEmail("werner@example.com") + .withAddress("michegan ave") + .withHighImportance("true").build(); + public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz") + .withPhone("9482427") + .withEmail("lydia@example.com") + .withAddress("little tokyo") + .withDeadlines(new String[]{"a 04/12/2027"}) + .withFavourite("true").build(); + public static final Person GEORGE = new PersonBuilder().withName("George Best") + .withPhone("9482442") + .withEmail("anna@example.com") + .withAddress("4th street").build(); // Manually added public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") @@ -52,8 +75,8 @@ public class TypicalPersons { 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(); public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER @@ -67,10 +90,15 @@ public static AddressBook getTypicalAddressBook() { for (Person person : getTypicalPersons()) { ab.addPerson(person); } + + for (Tag tag : TypicalTags.getTypicalTags()) { + ab.addTag(tag); + } return ab; } public static List getTypicalPersons() { return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); } + } diff --git a/src/test/java/seedu/address/testutil/TypicalSavedImages.java b/src/test/java/seedu/address/testutil/TypicalSavedImages.java new file mode 100644 index 00000000000..5ebd3b3ef50 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalSavedImages.java @@ -0,0 +1,78 @@ +package seedu.address.testutil; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.image.ImageDetails; +import seedu.address.model.image.ImageDetailsList; +import seedu.address.model.image.util.ImageUtil; + +/** + * A utility class containing a list of {@code ImageDetails} objects to be used in tests. + */ +public class TypicalSavedImages { + public static final Path TEST_IMAGES_DIRECTORY = Paths.get("src", "test", "data", "testImages"); + + // Typical files + public static final File TEST_IMAGE_1_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_1.png").toFile(); + public static final File TEST_IMAGE_2_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_2.png").toFile(); + public static final File TEST_IMAGE_3_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_3.png").toFile(); + public static final File TEST_IMAGE_4_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_4.png").toFile(); + public static final File TEST_IMAGE_5_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_5.png").toFile(); + + // Manually added files + public static final File TEST_IMAGE_6_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_6.png").toFile(); + public static final File TEST_IMAGE_7_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_7.png").toFile(); + public static final File TEST_IMAGE_8_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_8.png").toFile(); + public static final File TEST_IMAGE_9_FILE = TEST_IMAGES_DIRECTORY.resolve("test_image_9.png").toFile(); + + // Typical images + public static final ImageDetails TEST_IMAGE_1 = new ImageDetails(TEST_IMAGE_1_FILE); + public static final ImageDetails TEST_IMAGE_2 = new ImageDetails(TEST_IMAGE_2_FILE); + public static final ImageDetails TEST_IMAGE_3 = new ImageDetails(TEST_IMAGE_3_FILE); + public static final ImageDetails TEST_IMAGE_4 = new ImageDetails(TEST_IMAGE_4_FILE); + public static final ImageDetails TEST_IMAGE_5 = new ImageDetails(TEST_IMAGE_5_FILE); + + // Manually added images + public static final ImageDetails TEST_IMAGE_6 = new ImageDetails(TEST_IMAGE_6_FILE); + public static final ImageDetails TEST_IMAGE_7 = new ImageDetails(TEST_IMAGE_7_FILE); + public static final ImageDetails TEST_IMAGE_8 = new ImageDetails(TEST_IMAGE_8_FILE); + public static final ImageDetails TEST_IMAGE_9 = new ImageDetails(TEST_IMAGE_9_FILE); + + // prevents instantiation + private TypicalSavedImages() {} + + /** + * Returns an {@code ImageDetailsList} with all the typical images. + */ + public static ImageDetailsList getTypicalImageDetailsList() { + return new ImageDetailsList(getTypicalImageDetails()); + } + + public static List getTypicalImageDetails() { + return new ArrayList<>(Arrays.asList(TEST_IMAGE_1, TEST_IMAGE_2, TEST_IMAGE_3, TEST_IMAGE_4, TEST_IMAGE_5)); + } + + public static List getAllImageDetails() { + return new ArrayList<>(Arrays.asList(TEST_IMAGE_1, TEST_IMAGE_2, TEST_IMAGE_3, TEST_IMAGE_4, TEST_IMAGE_5, + TEST_IMAGE_6, TEST_IMAGE_7, TEST_IMAGE_8, TEST_IMAGE_9)); + } + + /** + * Used to populate the test images directory with the test images. Recommended to be called + * in the set up method of JUnit tests. {@see ImagesCommandTest.java} + * @throws IOException + */ + public static void populateTestImages() throws IOException { + final File referenceImageForTests = TEST_IMAGES_DIRECTORY.resolve("reference_image_for_test.png").toFile(); + for (ImageDetails image : getAllImageDetails()) { + ImageUtil.copyTo(referenceImageForTests, TEST_IMAGES_DIRECTORY.resolve(image.getName())); + } + } + +} diff --git a/src/test/java/seedu/address/testutil/TypicalTags.java b/src/test/java/seedu/address/testutil/TypicalTags.java new file mode 100644 index 00000000000..4c8a0930bbe --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalTags.java @@ -0,0 +1,39 @@ +package seedu.address.testutil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.AddressBook; +import seedu.address.model.tag.Tag; + +/** + * A utility class containing a list of {@code Test} objects to be used in tests. + */ +public class TypicalTags { + public static final String VALID_TAGNAME_FRIENDS = "friends"; + public static final String VALID_TAGNAME_COLLEAGUES = "colleagues"; + public static final String VALID_TAGNAME_NEIGHBOURS = "neighbours"; + public static final String VALID_TAGNAME_TEST = "test"; + public static final String VALID_TAGNAME_OWESMONEY = "owesMoney"; + public static final Tag VALID_TAG_FRIENDS = new Tag(VALID_TAGNAME_FRIENDS); + public static final Tag VALID_TAG_COLLEAGUES = new Tag(VALID_TAGNAME_COLLEAGUES); + public static final Tag VALID_TAG_NEIGHBOURS = new Tag(VALID_TAGNAME_NEIGHBOURS); + public static final Tag VALID_TAG_TEST = new Tag(VALID_TAGNAME_TEST); + public static final Tag VALID_TAG_OWESMONEY = new Tag(VALID_TAGNAME_OWESMONEY); + + /** + * Returns an {@code AddressBook} with all the typical tags. + */ + public static AddressBook getTypicalAddressBook() { + AddressBook ab = new AddressBook(); + for (Tag tag : getTypicalTags()) { + ab.addTag(tag); + } + return ab; + } + + public static List getTypicalTags() { + return new ArrayList<>(Arrays.asList(VALID_TAG_FRIENDS, VALID_TAG_OWESMONEY)); + } +}