diff --git a/.gitignore b/.gitignore index 71c9194e8bd..ea7f77f4909 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /build/ src/main/resources/docs/ +# Settings files +/.settings/ + # IDEA files /.idea/ /out/ @@ -20,3 +23,5 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +.project +./docs/*.md.orig diff --git a/README.md b/README.md index 13f5c77403f..efda5998978 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,49 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +# UNIon + +UNIon is a desktop application **tailored for Computing Students**, +helping to **organise** and **navigate** the sea of +communication-based **data** in school so that users +can better achieve higher levels of **productivity**. + +### Motivation + +UNIon was created as a productivity application, aiming to organise school contacts for students.
+The name `UNIon` came from the function of the application to +be a collection of a groups of people designed for university students. + +### Build Status + +[![CI Status](https://github.com/AY2122S1-CS2103-T16-1/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S1-CS2103-T16-1/tp/actions) + +### Screenshots ![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. +### Features + +* Add contacts to UNIon +* Search for a mobile number of a contact +* Update contact details +* Delete contacts +* Create folders for your contacts +* Add contacts to folders +* List all folders +* Delete folders +* Use Unix commands to navigate UNIon + +**Coming Soon...** +* GUI window update +* Folders saved locally +* Find specific folders +* Add multiple contacts at once +* Rename folder + +### Built With + +* Java +* JavaFX + +### Acknowledgement + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + diff --git a/build.gradle b/build.gradle index be2d2905dde..1761321f2f6 100644 --- a/build.gradle +++ b/build.gradle @@ -65,8 +65,13 @@ dependencies { testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } + shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'UNIon.jar' } defaultTasks 'clean', 'test' + +run { + enableAssertions = true +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..6c8579efff5 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -3,57 +3,66 @@ layout: page title: About Us --- -We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). +We believe in providing **simplicity** in everything we do here. If you're the kind of person who likes to organize your +communication-based data easily with just a keyboard, you will enjoy UNIon. By re-purposing Unix commands, we ensure +that you can get up to speed quickly and be productive with your data. -You can reach us at the email `seer[at]comp.nus.edu.sg` +# Our Team -## Project team +## Lee Hur Sebastian -### John Doe +**Role: Team Lead** - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[![Sebastian PPP](images/portfolio.png)](https://ay2122s1-cs2103-t16-1.github.io/tp/team/sebbycake.html)   +[![Sebastian LinkedIn](images/linkedin.png)](https://www.linkedin.com/in/sebastian-lee-329b45198/)   +[![Sebastian GitHub](images/github.png)](https://github.com/sebbycake) -* Role: Project Advisor +
-### Jane Doe +## Lye Wen Jun - +**Role: Code Quality** -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Team Lead -* Responsibilities: UI +[![Wen Jun PPP](images/portfolio.png)](https://ay2122s1-cs2103-t16-1.github.io/tp/team/xlzior.html)   +[![Wen Jun LinkedIn](images/linkedin.png)](https://www.linkedin.com/in/wen-jun-lye/)   +[![Wen Jun GitHub](images/github.png)](https://github.com/xlzior/) -### Johnny Doe +
- +## Lee Wei, David -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +**Role: Documentation** -* Role: Developer -* Responsibilities: Data + -### Jean Doe +[![David PPP](images/portfolio.png)](https://ay2122s1-cs2103-t16-1.github.io/tp/team/itsyme.html)   +[![David LinkedIn](images/linkedin.png)](https://www.linkedin.com/in/david-lee-4a147a1b1/)   +[![David GitHub](images/github.png)](https://github.com/itsyme) - +
-[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +## Rehman Sajid -* Role: Developer -* Responsibilities: Dev Ops + Threading +**Role: Integration** -### James Doe + - +[![Rehman PPP](images/portfolio.png)](https://ay2122s1-cs2103-t16-1.github.io/tp/team/rehmmann.html)   +[![Rehman LinkedIn](images/linkedin.png)](https://www.linkedin.com/in/rehman-sajid/)   +[![Rehman GitHub](images/github.png)](https://github.com/rehmmann/) -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +
-* Role: Developer -* Responsibilities: UI +## Ching Jia Rong + +**Role: Testing** + + + +[![Jia Rong PPP](images/portfolio.png)](https://ay2122s1-cs2103-t16-1.github.io/tp/team/jiarong15.html)   +[![Jia Rong LinkedIn](images/linkedin.png)](https://www.linkedin.com/in/jiarong15//)   +[![Jia Rong GitHub](images/github.png)](https://github.com/jiarong15) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..94d28eded2e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,7 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Apart from AB3, we did not reference any other sources -------------------------------------------------------------------------------------------------------------------- @@ -19,6 +19,8 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- +
+ ## **Design**
@@ -42,6 +44,8 @@ Given below is a quick overview of main components and how they interact with ea [**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. +
+ The rest of the App consists of four components. * [**`UI`**](#ui-component): The UI of the App. @@ -61,12 +65,16 @@ Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. * implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +
+ For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. The sections below give more details of each component. +
+ ### 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) @@ -84,6 +92,8 @@ The `UI` component, * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. * depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +
+ ### Logic component **API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) @@ -95,9 +105,11 @@ Here's a (partial) class diagram of the `Logic` component: How the `Logic` component works: 1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command. 1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to add a person). +1. The command can communicate with the `Model` when it is executed (e.g. to add a contact). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +
+ The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) @@ -105,6 +117,8 @@ The Sequence Diagram below illustrates the interactions within the `Logic` compo
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
+
+ Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: @@ -113,25 +127,30 @@ How the parsing works: * When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +
+ ### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object) and `Folder` objects (which are contained in a `UniqueFolderList` object). * stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the currently 'selected' `Folder` objects (e.g., results of a search query)) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores 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 @@ -150,96 +169,138 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa -------------------------------------------------------------------------------------------------------------------- +
+ ## **Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Create a folder: `mkdir` -#### Proposed Implementation +#### Implementation -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +Folders are saved in a `UniqueFolderList` in `AddressBook`. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +The following diagram shows how `mkdir` works: -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +![CreateFolderSequenceDiagram](images/CreateFolderSequenceDiagram.png) -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +#### Design considerations -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +* **Alternative 1 (current choice)**: Folders hold references to contacts + * Pros: Easier management of folders + * Cons: More difficult to implement -![UndoRedoState0](images/UndoRedoState0.png) +Diagram: -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +![CreateFolderAlternative1](images/CreateFolderAlternative1.png) -![UndoRedoState1](images/UndoRedoState1.png) +* **Alternative 2**: Contacts hold references to folders + * Pros: Easy to implement + * Cons: More complex management of folders, Similar to tags which is already implemented -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Diagram: -![UndoRedoState2](images/UndoRedoState2.png) +![CreateFolderAlternative2](images/CreateFolderAlternative2.png) -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +-------------------------------------------------------------------------------------------------------------------- -
+### View list of folders: `ls -folders` -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +#### Implementation -![UndoRedoState3](images/UndoRedoState3.png) +In our GUI, we would like to display the list of folders that users can create to organize +their contacts into different classes. The implementation is very similar to `PersonListCard` and +`PersonListPanel` for viewing list of contacts. -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +#### Design considerations -
+* Alternative 1: 2-row layout + * Pros: Ability to see additional details of folders and contacts with a small-sized GUI. + * Cons: Extra effort to scroll down the GUI to look into the details of contacts. +* Alternative 2: 2-column layout + * Pros: Ability to see both folders and contacts data at a glance without initial scrolling needed + * Cons: Truncated details of folders and contacts will be displayed due to the small-sized GUI. -The following sequence diagram shows how the undo operation works: +Alternative 1 is selected, implemented using additional`StackPane` on top of the existing `StackPane` +for the list of contacts placed vertically. This additional `StackPane` is placed under a `VBox` +component in `MainWindow`. -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +-------------------------------------------------------------------------------------------------------------------- -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
-
+### Add contacts to folder: `echo INDEX_1 ... INDEX_N >> Folder` -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +#### Implementation +Contacts are added by updating the `ObservableList` in `UniqueFolderList`. +A new `Folder` object is created containing the new `Person` and replaces the old folder in the `UniqueFolderList`. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +#### Design considerations -
+* Alternative 1: If there is an invalid index, allow adding of contact for the remaining valid index + * Pros: Easier to implement. + * Cons: Difficult for user to know which contacts have been added into the folder +* Alternative 2: Only allow adding of contacts when all indexes are valid + * Pros: Easier for user to know which contacts have been added into the folder + * Cons: Requires more code and effort to ensure all indexes are valid before adding them to folder -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Alternative 2 is selected, as it is more user-friendly and intuitive. -![UndoRedoState4](images/UndoRedoState4.png) +-------------------------------------------------------------------------------------------------------------------- -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +
-![UndoRedoState5](images/UndoRedoState5.png) +### Locate folders by name: `find -folders` -The following activity diagram summarizes what happens when a user executes a new command: +#### Implementation - +The Sequence Diagram below illustrates the interactions within the Logic component for the `execute("find -folders CS")` API call. -#### Design considerations: +![FindFoldersSequenceDiagram](images/FindFoldersSequenceDiagram.png) -**Aspect: How undo & redo executes:** +#### Design considerations -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +When deciding on the design for `find -folders`, we considered the existing `find` command to search for contacts by name. This `find` command uses `StringUtil.containsWordIgnoreCase` which only matches full words. -* **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. +However, one use case we wanted to cater to was students who group their contacts by shared modules. For example, students take CS2103T and CS2101 concurrently, and may wish to search for these two folders (and other CS modules) by using the keyword `CS`. If we only matched full words, `find -folders CS` would not contain *either* of the modules `CS2103T` or `CS2101`, which is undesirable. -_{more aspects and alternatives to be added}_ +* **Alternative 1 (current choice)**: Match partial words + * Pros: More intuitive behaviour for the above use case + * Cons: Requires a new method to be implemented +* **Alternative 2**: Match full words only + * Pros: Easy to implement as this uses the same logic as finding contacts by name + * Cons: May lead to unexpected behaviour for the above use case -### \[Proposed\] Data archiving +Alternative 1 was chosen, and the new method is under `StringUtil.containsTextIgnoreCase`. -_{Explain here how the data archiving feature will be implemented}_ +-------------------------------------------------------------------------------------------------------------------- +
--------------------------------------------------------------------------------------------------------------------- +### Edit folder name: `mv OLD_FOLDER_NAME | NEW_FOLDER_NAME` + +#### Implementation + +Folders are removed by first specifying the old folder's name and followed +by the new folder's name. + +The underlying implementation essentially creates a new folder with the +newly specified name and copies all the contacts from the old to the new +folder. The new folder is saved in the `UniqueFolderList` in `AddressBook` +just like the old folder. + +The following diagram shows how `mv` works: + +![EditFolderNameSequenceDiagram](images/EditFolderNameSequenceDiagram.png) + +#### Design considerations + +* **Alternative 1 (current choice)**: Old folder and new folder name are separated by the pipe operator `|`. + * Pros: Easy to distinguish between the folder to be replaced and the new incoming folder name considering how folder name can have blank spaces in between + * Cons: More difficult to implement + +
## **Documentation, logging, testing, configuration, dev-ops** @@ -251,6 +312,8 @@ _{Explain here how the data archiving feature will be implemented}_ -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Requirements** ### Product scope @@ -262,69 +325,213 @@ _{Explain here how the data archiving feature will be implemented}_ * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps +* computing student with prior experience in using Unix-commands -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +### Value Proposition +- manage contacts faster and more conveniently than a typical mouse/GUI driven app +- use a set of Unix-like commands that CS students are already familiar with so that they don't have to re-learn, thus helping ease of adoption +- skills learned while using this app are transferable to other tools ### 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…​ | +| -------- | ------------------------------------------ | ----------------------------------------------------- | ---------------------------------------------------------- | +| `* * *` | student | create a folder | classify related contacts together | +| `* * *` | student | view all the folders that I created | | +| `* * *` | student | delete a folder | remove folders when they become irrelevant | +| `* * *` | student | add a user to a folder | classify related contacts together and find them efficiently | +| `* * *` | experienced computing student | use my knowledge of Unix commands to navigate the app | use the app without learning a new set of commands | +| `* * *` | student | save to a JSON file automatically | achieve data persistence between the application and local storage | +| `* *` | student | remove a contact from a folder | update contacts in folders easily | +| `* *` | student | clear all folders | reset my folders easily | +| `* *` | student | add several contacts into a folder at one go | reduce time wastage adding them 1 by 1 | +| `* *` | student | update my folder name | efficiently manage outdated folders | +| `* *` | student | find a specific folder | minimize time spent on looking for my folder | ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `UNIon` and the **Actor** is the `student`, unless specified otherwise) + +**Use case: UC01 - Add a new folder** + +**Guarantees:** + * A new folder with the specified name is created only if the input format is valid. + * The newly-created folder is empty. + +**MSS:** + 1. User requests to add a new folder. + 2. UNIon accepts input, and a new folder is created. + + Use case ends. + +**Extensions:** +* 1a. The folder name specified contains non-alphanumeric characters or wrong command format is used. + * 1a1. No new folder is created. + * 1a2. User requests to add a new folder. + * Steps 1a1-1a2 are repeated until the data entered are correct. + +**Use case: UC02 - Add a new contact to UNIon** + +**Guarantees:** + * A new contact with the specified details is added to the contact list only if the input format is valid. + +**MSS:** + 1. User requests to add new contact. + 2. UNIon accepts input and adds contact to the pool of contacts. + + Use case ends. -**Use case: Delete a person** +**Extensions:** +* 1a. The given input has missing fields. + * 1a1. No new contact is created by UNIon. + * 1a2. User requests to add new contact. + * Steps 1a1-1a2 are repeated until the data format entered are correct. -**MSS** +* 1b. Name supplied exceeds character limit. + * 1b1. No new contact is created by UNIon + * 1b2. User requests to add new contact. + * Steps 1b1-1b2 are repeated until the data format entered are correct. -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 +* 1c. Name supplied already exists in UNIon. + * 1c1. No new contact is created by UNIon + * 1c2. User requests to add new contact. + * Steps 1c1-1c2 are repeated until the data format entered are correct. + +**Use case: UC03 - Add existing contact to an existing folder** - Use case ends. +**Preconditions:** Folder and contact already exists in UNIon + +**MSS:** + + 1. User adds a new folder named A (UC01) + 2. User adds a new contact named Clarence (UC02) + 3. User requests to add contact(s) to a named folder. + 4. UNIon adds the contact specified into the respective folder. + + Use case ends. + +**Extensions:** +* 3a. User inputs an invalid index. + * 3a1. Contact not added to folder. + * 3a2. User request contacts to be added to the named folder. + * Steps 3a1-3a2 are repeated until the data format entered are correct. + + +* 3b. User specifies a folder name that does not exist. + * 3b1. Contact not added to any folder. + * 3b2. User request contacts to be added to the named folder. + * Steps 3b1-3b2 are repeated until the data format entered are correct. + +**Use case: UC04 - Delete specified folder** + +**Guarantees:** + * Folder specified is deleted from UNIon only if the input format is valid. + * Contacts in the specified folder is removed as well. + +**Preconditions:** Folder must already exist in UNIon + +**MSS:** + 1. User requests to delete folder. + 2. UNIon removes the specified folder from its system. + +Use case ends. **Extensions** +* 1a. Folder specified does not exist in UNIon. + * 1a1. UNIon rejects command and nothing is executed. + * 1a2. User requests to delete folder. + * Steps 1a1 - 1a2 are repeated until data format is correct. -* 2a. The list is empty. +* 1b. Command format is erroneous. + * 1b1. UNIon rejects command and nothing is executed. + * 1b2. User requests to delete folder. + * Steps 1b1 - 1b2 are repeated until data format is correct. + +**Use case: UC05 - Remove all contacts** - Use case ends. +**Guarantees:** All contacts are removed from UNIon only if the input format is valid. -* 3a. The given index is invalid. +**MSS:** + 1. User requests to remove all contacts. + 2. UNIon clears all the contacts it has stored thus far. - * 3a1. AddressBook shows an error message. + Use case ends. - Use case resumes at step 2. +**Extensions** +* 1a. UNIon detects error in input of command supplied by user. + * 1a1. All contacts remain in UNIon. + * 1a2. User re-inputs command. + * Steps 1a1 - 1a2 are repeated until data format is correct. -*{More to be added}* + +**Use case: UC06 - Remove specified contact from folder** + +**Guarantees:** Contact specified is removed from folder + +**Preconditions:** + * Contact must already exist in contact list and within the specified folder. + * Folder must already exist in folder list. + +**MSS:** +1. User requests the index of contact from contact list to be removed. +2. UNIon removes the specified contact from the folder. + +Use case ends. + +**Extensions** +* 1a. Contact does not exist in the folder. + * 1a1. Folder remains unchanged. + * 1a2. User requests the index of contact from contact list to be removed. + * Steps 1a1 - 1a2 are repeated until data format is correct. + +* 1b. Folder does not exist in UNIon. + * 1b1. Contact not removed from folder. + * 1b2. User requests the index of contact from contact list to be removed. + * Steps 1b1 - 1b2 are repeated until data format is correct. + +**Use case: UC07 - Remove all folders** + +**Guarantees:** All folders are deleted from UNIon + +**MSS:** +1. User requests to remove all folders. +2. UNIon clears all the folders it has stored thus far. + +Use case ends. + +**Extensions** +* 1a. UNIon detects error of input command supplied by user. + * 1a1. All folders remain in UNIon. + * 1a2. User requests to remove all folders. + * Steps 1a1 - 1a2 are repeated until data format is correct. + +
### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +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 well for standard screen resolutions 1920x1080 and higher, and, for screen scales 100% and 125%. +5. Should handle invalid user input gracefully and not crash. +6. On invalid user input, should prompt the user with information on the correct input format expected so that the user can learn how to use the commands. -*{More to be added}* +Project scope +- The app is not required to handle communication with people, only keeping track of metadata associated with contacts +- The app is not required to handle integration with messaging apps like Telegram and WhatsApp ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Experienced Computing Student**: A student that has already completed introductory modules in their respective universities. -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. @@ -337,41 +544,204 @@ testers are expected to do more *exploratory* testing. ### Launch and shutdown 1. Initial launch - 1. Download the jar file and copy into an empty folder - - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Run the jar file using the command `java -jar UNIon.jar`
Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. 1. Saving window preferences - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. 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 - - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. - - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. - -1. _{ more test cases …​ }_ - -### Saving data - -1. Dealing with missing/corrupted data files - - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ - -1. _{ more test cases …​ }_ + +### Managing contacts + +#### Adding a contact: `touch` + +1. Creating a contact + 1. Test case: `touch -n John Doe -p 98765432 -e johnd@example.com -a John street, block 123, #01-01`
+ Expected: John Doe is added + 1. Test case: `touch -n John Doe -p 98765432 -e johnd@example.com -a John street, block 123, #01-01`
+ Expected: John Doe is not added as he is already in the list. Error message is shown. + 1. Test case: `touch -nBetsy Doe -p 98765432 -e johnd@example.com -a John street, block 123, #01-01`
+ Expected: Betsy Doe is not added as the command is invalid (due to the lack of a space after the `-n` flag). Error message is shown. + 1. Test case: `touch -n Betsy Doe -p 98765432 -e johnd@example.com`
+ Expected: Betsy Doe is not added as the command is invalid (due to missing compulsory address parameter). Error message is shown. + +#### Listing all contacts: `ls -contacts` + +1. Listing all contacts + 1. Prerequisite: List is populated with contacts (see above) + 1. Test case: `ls -contacts`
+ Expected: Full list of contacts is shown +2. Listing all contacts after filtering with `find -contacts` + 1. Prerequisite: List has been filtered by running a `find -contacts` command + 1. Test case: `ls -contacts`
+ Expected: Full list of contacts is shown + +#### Editing a contact: `vim` + +1. Editing a contact while all contacts are being shown + 1. Prerequisite: There is an existing contact in the contact list + 1. Test case: `vim 1 -n Jane Doe`
+ Expected: Contact at first position is renamed to Jane Doe + 1. Incorrect edit commands to try: `vim`, `vim 0 -n Jane Doe`, `vim x -n Jane Doe` (where x is larger than the list size) +2. Editing a contact while a filter is in place + 1. Prerequisite: Run a search using the `find -contacts` command. Multiple contacts in the filtered list. + 1. Similar to previous, but index number is with reference to the filtered list. + +#### Locating contacts by name: `find -contacts` + +1. Finding contacts + 1. Prerequisite: There are multiple existing contacts, including someone named Janet Doe + 1. Test case: `find -contacts Janet`
+ Expected: List of contacts is filtered to show Janet Doe + 1. Test case: `find -contacts Jane`
+ Expected: No contacts found as only full words are matched. + +#### Deleting a contact: `rm` + +1. Deleting a contact while all contacts are being shown + + 1. Prerequisites: List all contacts using the `ls -contacts` command. Multiple contacts in the list. + 1. Test case: `rm 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 1. Test case: `rm 0`
+ Expected: No contact is deleted. Error details shown in the status message. Status bar remains the same. + 1. Other incorrect delete commands to try: `rm`, `rm x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting a contact while a filter is in place + 1. Prerequisite: Run a search using the `find -contacts` command. Multiple contacts in the filtered list. + 1. Similar to previous, but index number is with reference to the filtered list. + +#### Clearing all contacts: `rm -contacts` + +1. Clearing all contacts + 1. Prerequisites: Multiple contacts in the list. + 1. Test case: `rm -contacts`
+ Expected: all contacts are removed. + +### Managing folders + +#### Adding a folder: `mkdir` + +1. Creating a folder + 1. Test case: `mkdir CS2103`
+ Expected: A new folder called `CS2103` is created + 1. Test case: `mkdir CS2103`
+ Expected: A new folder is not created as folders must have unique names + 1. Test case: `mkdir ---`
+ Expected: a new folder is not created as folder names cannot contain special characters + +#### Adding contacts to a folder: `echo` + +1. Adding contacts to a folder while all contacts and all folders are being shown + 1. Prerequisites: There are existing contacts and existing folders + 1. Test case: `echo 1 >> CS2103`
+ Expected: Adds the first contact in the list to the folder `CS2103` + 1. Test case: `echo 1 2 >> CS2103`
+ Expected: Adds the first two contacts in the list to the folder `CS2103` + +1. Adding contacts to a folder while there is a filter in place for contacts + 1. Prerequisites: Run a search using the `find -contacts` command. Multiple contacts in the filtered list. + 1. Similar to point 1 but index is with reference to the filtered list. + +1. Adding contacts to a folder while there is a filter in place for folders + 1. Prerequisites: Run a search using the `find -folders` command. Multiple folders in the filtered list. + 1. Similar to point 1 but folder that is being added to must be present in the filtered list + +
+ +#### Deleting a contact from folder: `rm` + +1. Removing contacts from a folder while all contacts and all folders are being shown + 1. Prerequisites: There are existing contacts and existing folders + 1. Test case: `rm 1 >> CS2103`
+ Expected: Removes the first contact in the list from the folder `CS2103` + +1. Removing contacts from a folder while there is a filter in place for contacts + 1. Prerequisites: Run a search using the `find -contacts` command. Multiple contacts in the filtered list. + 1. Similar to point 1 but index is with reference to the filtered list. + +1. Removing contacts from a folder while there is a filter in place for folders + 1. Prerequisites: Run a search using the `find -folders` command. Multiple folders in the filtered list. + 1. Similar to point 1 but folder that is being removed from must be present in the filtered list + +#### Listing all folders: `ls -folders` + +1. Listing all folders + 1. Prerequisite: List is populated with folders + 1. Test case: `ls -folders`
+ Expected: Full list of folders is shown +2. Listing all folders after filtering with `find -folders` + 1. Prerequisite: List has been filtered by running a `find -folders` command + 1. Test case: `ls -folders`
+ Expected: Full list of folders is shown + +#### Editing a folder name: `mv` + +1. Editing a folder while all folders are being shown + 1. Prerequisite: There is an existing folder in the contact list + 1. Test case: `mv CS2103 | CS2103T`
+ Expected: The folder `CS2103` is renamed to `CS2103T` + 1. Incorrect edit commands to try: `mv`, `mv CS2103 CS2103T` +2. Editing a folder while a filter is in place + 1. Prerequisite: Run a search using the `find -folders` command. Multiple folders in the filtered list. + 1. Similar to previous, but folder to be edited must be in the filtered list. + +
+ +#### Locating folders by name: `find -folders` + +1. Finding folders + 1. Prerequisite: There are multiple existing folders, including someone named Janet Doe + 1. Test case: `find -folders CS`
+ Expected: List of folders is filtered to show folders whose names contain `CS`, including `CS2103` and `CS2101` + +#### Deleting a folder: `rmdir` + +1. Deleting a folder + 1. Test case: `rmdir CS2103`
+ Expected: The folder CS2103 is removed + 1. Incorrect delete commands to try: `rmdir`, `rmdir X` where the folder `X` does not exist
+ Expected: Similar to previous. + +1. Deleting a folder while there is a filter in place for folders + 1. Prerequisites: Run a search using the `find -folders` command. Multiple folders in the filtered list. + 1. Similar to point 1 but folder that is being deleted must be present in the filtered list + +#### Clearing all folders: `rm -folders` + +1. Clearing all folders + 1. Prerequisites: Multiple folders in the list. + 1. Test case: `rm -folders`
+ Expected: all folders are removed. + +
+ +## **Appendix: Effort** + +The effort we put into UNIon was more than that of the individual project. +While we did not have to start from scratch, since it is a brown-field project, there were more things that +we had to do. One of the additional challenges were creating our scope, generating user +stories and coming up with what our product would look like. Creating readable, succinct +documentation was also challenging. Weekly meetings also took up more of our time, compared +to the individual project. Moreover, we had internal deadlines to meet, putting +our time management to the test. Our team did a good job of delegating work and +each person had their role to play. Each member took responsibility for one part of UNIon +and had secondary responsibilities as well. + +UNIon does reuse some code from AB3 when implementing folders. However, it was not +just a simple copy and paste. Because of the differing behaviours of folders and +contacts, the code had to be adapted to quite a large extent to suit our purposes. +For folders, only about 5% of the effort was saved by reusing the code from AB3. +This mostly came in the form of saving time and effort from typing out the code and +understanding how the different components interact with models like `Person`. Since +`Folder` is a model as well, the `Person` served as an example to help us understand +how the models interacted with other components. However, the code from `Person` merely +served as the base. The difference in behaviour of folders and contacts meant that we had +to come up with the rest of the code involving the behaviour of folders ourselves. + +UNIon deals with more entities than AB3, with both contacts and folders. Not only does +this make coding more difficult, testing is more difficult as well. The GUI of UNIon +includes the display of folders as well. It was more difficult to ensure that UNIon +runs smoothly with more entities present, creating more chances for bugs to sneak in. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..d88c9d35efd 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,190 +3,316 @@ 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. +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. +If you are already familiar with Unix commands, then UNIon will be easy for you to use. * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- -## Quick start - -1. Ensure you have Java `11` or above installed in your Computer. - -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +## Quick start -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) +1. Ensure you have Java `11` or above installed in your Computer +1. Download the latest JAR file from [here](https://github.com/AY2122S1-CS2103-T16-1/tp/releases) +1. Copy the file to the folder you want to use as the _home folder_ for your UNIon +1. Double-click the file to start the app. The GUI should appear in a few seconds, and it should look similar to this image below. Note how the app contains some sample contacts
+ +startup ui -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press Enter to execute it. e.g. typing `help` and pressing Enter will open the help window.
Some example commands you can try: - - * **`list`** : Lists all contacts. - - * **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. - - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. - - * **`clear`** : Deletes all contacts. - - * **`exit`** : Exits the app. - -1. Refer to the [Features](#features) below for details of each command. + * `ls -contacts`: Lists all contacts + * `touch -n John Doe -p 98765432 -e johnd@example.com -a John street, block 123, #01-01`: Adds a contact named `John Doe` to UNIon + * `rm 3`: Deletes the 3rd contact shown in the current list + * `rm -contacts`: Deletes all contacts + * `exit`: Exits the app +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +
+ ## Features
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPER_CASE` are the parameters to be supplied by the user
+ e.g. in `touch -n NAME`, `NAME` is a parameter which can be used as `touch -n John Doe` + +* Items in square brackets are optional
+ e.g. `-n NAME [-t TAG]` can be used as `-n John Doe -t friend` or as `-n John Doe` -* 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`. +* 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 -* 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
+ e.g. if the command specifies `-n NAME -p PHONE`, `-p PHONE -n NAME` is also acceptable -* Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +* 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
+ e.g. if you specify `-p 12341234 -p 56785678`, only `-p 56785678` will be taken -* 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.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. +* For commands that do not take in parameters (such as `help` and `exit`) , extraneous parameters will be ignored
+ e.g. if the command specifies `help 123`, it will be interpreted as `help` -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Flags such as `-contacts` and `-folders` are part of the command word and must be placed immediately after the main command word
+ e.g. (`ls -contacts` not `ls -invalid_flag -contacts`)
-### Viewing help : `help` +
-Shows a message explaning how to access the help page. +### Viewing help: `help` + +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) Format: `help` +### Managing contacts + +#### Adding a contact: `touch` -### Adding a person: `add` +Adds a contact to UNIon. -Adds a person to the address book. +Format: `touch -n NAME -p PHONE -e EMAIL -a ADDRESS [-t TAG]…​` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* `NAME` has a character limit of 70 characters +* `NAME` is case-sensitive as John Doe and john doe will be treated as different contacts +* `PHONE` accepts any length to allow for phone formats from different countries +* `ADDRESS` is truncated by an ellipsis if the contents cannot fit in one line +* Duplicate `TAG`s are ignored +* We recommend that `TAG`s are kept to a maximum of 50 characters. If a tag has more than 50 characters, you may not be able to view the entire tag (depending on the size of the window)
:bulb: **Tip:** -A person can have any number of tags (including 0) +A contact can have any number of tags (including 0)
Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `touch -n John Doe -p 98765432 -e johnd@example.com -a John street, block 123, #01-01` +* `touch -n Betsy Crowe -t friend -e betsycrowe@example.com -a Newgate Prison -p 1234567 -t criminal` -### Listing all persons : `list` +#### Listing all contacts: `ls -contacts` -Shows a list of all persons in the address book. +Shows a list of all contacts in UNIon. -Format: `list` +Format: `ls -contacts` -### Editing a person : `edit` +
-Edits an existing person in the address book. +#### Editing a contact: `vim` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Edits an existing contact in the UNIon. -* 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: `vim INDEX [-n NAME] [-p PHONE] [-e EMAIL] [-a ADDRESS] [-t TAG]…​` + +* 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 +* `NAME` has a character limit of 70 characters +* `PHONE` accepts any length to allow for phone formats from different countries +* `ADDRESS` is truncated by an ellipsis if the contents cannot fit in one line +* Duplicate `TAG`s are ignored +* We recommend that `TAG`s are kept to a maximum of 50 characters. If a tag has more than 50 characters, you may not be able to view the entire tag (depending on the size of the window) +* Existing values will be updated to the input values +* When editing tags, the existing tags of the contact will be removed i.e. adding of tags is not cumulative +* You can remove all the contact’s tags by typing `-t` without specifying any tags after it Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `vim 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 +* `vim 2 -n Betsy Crower -t` Edits the name of the 2nd contact to be `Betsy Crower` and clears all existing tags -### Locating persons by name: `find` +#### Locating contacts by name: `find -contacts` -Finds persons whose names contain any of the given keywords. +Finds contacts whose names contain any of the given keywords. -Format: `find KEYWORD [MORE_KEYWORDS]` +Format: `find -contacts KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` +* The search is case-insensitive. e.g. `hans` will match `Hans` * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. +* Only the name is searched * Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). +* Contacts matching at least one keyword will be returned (i.e. `OR` search) e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +* All flags after `-contacts` will be ignored e.g. `find -contacts -invalid_flag Hans` is equivalent to `find -contacts Hans` + +
Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `find -contacts John` returns `john` and `John Doe` +* `find -contacts alex david` returns `Alex Yeoh`, `David Li`
-### Deleting a person : `delete` +result for 'find -contacts alex david' -Deletes the specified person from the address book. +#### Deleting a contact: `rm` -Format: `delete INDEX` +Deletes the specified contact from UNIon. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +Format: `rm INDEX` + +* 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. +* `ls -contacts` followed by `rm 2` deletes the 2nd contact in UNIon +* `find -contacts Betsy` followed by `rm 1` deletes the 1st contact in the results of the `find -contacts` command + +#### Clearing all contacts: `rm -contacts` + +Clears all contacts from UNIon. + +Format: `rm -contacts` + +
+ +### Managing folders + +#### Adding a folder: `mkdir` + +Creates a folder for contacts to be added into. + +Format: `mkdir FOLDER_NAME` + +* Creates a folder with the name `FOLDER_NAME` +* `FOLDER_NAME` has a maximum character limit of 30 +* Duplicate folder names are not allowed + +Examples: + +* `mkdir CS2103` creates a folder with the name `CS2103` + +#### Adding contacts to a folder: `echo` + +Adds existing contacts into a folder. + +Format: `echo INDEX [INDEX]... >> FOLDER_NAME` + +* Adds the contacts at the specified indices into the given folder. The index refers to the index number shown in the displayed contact list. The index **must be a positive integer** 1, 2, 3, ... +* `INDEX` must be a valid index referring to an existing contact in the current list of contacts shown +* `INDEX` must not already be in the folder +* If multiple `INDEX` are passed, they must be unique and cannot contain duplicates +* `FOLDER_NAME` must be an existing folder + +Examples: +* `echo 3 >> CS2103` Adds contact 3 to CS2103 folder +* `echo 3 4 1 9 10 >> CS2103` Add contacts 3, 4, 1, 9, 10 to CS2103 folder + +
-### Clearing all entries : `clear` +#### Deleting a contact from folder: `rm` -Clears all entries from the address book. +Deletes the specified contact from the list of contacts from the folder. -Format: `clear` +Format: `rm INDEX >> FOLDER_NAME` -### Exiting the program : `exit` +* Deletes contact at `INDEX` as seen on the contact list from the folder named `FOLDER_NAME` + +Examples: + +* `rm 1 >> CS1010` deletes a contact that corresponds to index 1 in the contact list from the folder `CS1010` + +#### Listing all folders: `ls -folders` + +Retrieves list of all folders created. + +Format: `ls -folders` + +#### Editing a folder name: `mv` + +Replaces the old folder name with the new folder name. + +Format: `mv OLD_FOLDER_NAME | NEW_FOLDER_NAME` + +* `NEW_FOLDER_NAME` has a maximum character limit of 30 + +#### Locating folders by name: `find -folders` + +Finds folders whose name contains any of the given keywords. + +Format: `find -folders KEYWORD [MORE_KEYWORDS]` + +* The search is case-insensitive. e.g. `cs2103` will match `CS2103` +* The order of the keywords does not matter. e.g. `Team Project CS2103` will match `CS2103 Team Project` +* Partial words will be matched e.g. `CS` will match `CS2103` and `CS2101` +* Folders matching at least one keyword will be returned (i.e. `OR` search). + e.g. `CS2103 Team Project` will return `CS2103`, `Team Project` +* All flags after `-folders` will be ignored e.g. `find -folders -invalid_flag CS2103` is equivalent to `find -contacts CS2103` + +Examples: +* `find -folders CS` returns `CS2103` and `CS2101` +* `find -folders CS2103 Team Project` returns `CS2103`, `Team Project` + +
+ +#### Deleting a folder: `rmdir` + +Deletes a specified folder + +Format: `rmdir FOLDER_NAME` + +* Deletes folder with the name `FOLDER_NAME` + +Examples: + +* `rmdir CS1010` deletes a folder with the name `CS1010` + +#### Clearing all folders: `rm -folders` + +Clears all folders from UNIon. + +Format: `rm -folders` + +### Exiting the program: `exit` Exits the program. Format: `exit` +-------------------------------------------------------------------------------------------------------------------- + ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +UNIon 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. +UNIon 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, UNIon will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` - -_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 on the other computer and overwrite the empty data file it creates with the file that contains the data of your previous UNIon 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` +**Add new contact** | `touch -n NAME -p PHONE -e EMAIL -a ADDRESS [-t TAG]`
e.g., `touch -n James Ho -p 22224444 -e jamesho@example.com -a 123, Clementi Rd, 1234665 -t friend -t colleague` +**List contacts** | `ls -contacts` +**Edit contact** | `vim INDEX [-n NAME] [-p PHONE] [-e EMAIL] [-a ADDRESS] [-t TAG]`
e.g., `vim 2 -n James Lee -e jameslee@example.com` +**Find contact** | `find -contacts KEYWORD [MORE_KEYWORDS]`
e.g., `find -contacts James Jake` +**Delete contact** | `rm INDEX`
e.g., `rm 3` +**Clear contacts** | `rm -contacts` +**Add new folder** | `mkdir FOLDER_NAME`
e.g. `mkdir CS2103` +**Add contact to folder** | `echo INDEX >> FOLDER_NAME`
e.g., `echo 3 >> CS2103` +**Add multiple contacts to folder** | `echo INDEX [INDEX]... >> FOLDER_NAME`
e.g. `echo 3 4 1 9 10 >> CS2103` +**Delete contact from folder** | `rm INDEX >> FOLDER_NAME`
e.g., `rm 1 >> CS2102` +**List folders** | `ls -folders` +**Edit folder name** | `mv OLD_FOLDER_NAME | NEW_FOLDER_NAME`
e.g., `mv CS2103 | CS2102` +**Find folders** | `find -folders KEYWORD [MORE_KEYWORDS]`
e.g., `find -folders CS2103` +**Delete folder** | `rmdir FOLDER_NAME`
e.g., `rmdir CS1010` +**Clear folders** | `rm -folders` diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..ebe3cf713fd 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "UNIon" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S1-CS2103-T16-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..0c92f667458 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: "UNIon"; font-size: 32px; } } diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5ec6976efa..990fc7007de 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -10,3 +10,6 @@ height: 21px; width: 21px } + +.post-content h1, .post-content h2, .post-content h3 { margin-top: 10px; } +.post-content h4, .post-content h5, .post-content h6 { margin-top: 10px; } diff --git a/docs/diagrams/AddToFolderSequenceDiagram.puml b/docs/diagrams/AddToFolderSequenceDiagram.puml new file mode 100644 index 00000000000..ddd1f517bce --- /dev/null +++ b/docs/diagrams/AddToFolderSequenceDiagram.puml @@ -0,0 +1,50 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml + +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "f:AddToFolderCommand" as AddToFolderCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box +[-> LogicManager : execute(echo) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(echo) +activate AddressBookParser + +create AddToFolderCommand +AddressBookParser -> AddToFolderCommand +activate AddToFolderCommand + +AddToFolderCommand --> AddressBookParser +deactivate AddToFolderCommand + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> AddToFolderCommand : execute(m) +activate AddToFolderCommand + +AddToFolderCommand -> Model : addContactToFolder(f.person,f.folderName) +activate Model + +Model --> AddToFolderCommand +deactivate Model + +AddToFolderCommand --> LogicManager : result +deactivate AddToFolderCommand + +AddToFolderCommand -[hidden]-> LogicManager : result +destroy AddToFolderCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..ee654c3b652 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -6,12 +6,15 @@ skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList AddressBook *-right-> "1" UniqueTagList +AddressBook *--> "1" UniqueFolderList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList *-right-> "*" Tag UniquePersonList -right-> Person - +UniqueFolderList --> "*" Folder +Folder --> "*" Person +Folder --> FolderName Person -up-> "*" Tag Person *--> Name diff --git a/docs/diagrams/CreateFolderAlternative1.puml b/docs/diagrams/CreateFolderAlternative1.puml new file mode 100644 index 00000000000..f617c1edb55 --- /dev/null +++ b/docs/diagrams/CreateFolderAlternative1.puml @@ -0,0 +1,26 @@ +@startuml +!include style.puml +'https://plantuml.com/class-diagram + +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +class Folder +class FolderName +class Person +class Name +class Phone +class Tag +class Address +class Email + + +Folder --> "*" Person +Folder --> FolderName +Person --> Name +Person --> Phone +Person --> Address +Person --> Email +Person --> "*" Tag +@enduml diff --git a/docs/diagrams/CreateFolderAlternative2.puml b/docs/diagrams/CreateFolderAlternative2.puml new file mode 100644 index 00000000000..f5120b80bce --- /dev/null +++ b/docs/diagrams/CreateFolderAlternative2.puml @@ -0,0 +1,25 @@ +@startuml +!include style.puml +'https://plantuml.com/class-diagram + +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +class Folder +class Person +class FolderName +class Name +class Phone +class Tag +class Address +class Email + +Person --> Name +Person --> Phone +Person --> Address +Person --> Email +Person --> "*" Tag +Person --> "*" Folder +Folder --> FolderName +@enduml diff --git a/docs/diagrams/CreateFolderSequenceDiagram.puml b/docs/diagrams/CreateFolderSequenceDiagram.puml new file mode 100644 index 00000000000..51cc8cc8f2d --- /dev/null +++ b/docs/diagrams/CreateFolderSequenceDiagram.puml @@ -0,0 +1,50 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include style.puml + +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "f:CreateFolderCommand" as CreateFolderCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box +[-> LogicManager : execute(mkdir) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(mkdir) +activate AddressBookParser + +create CreateFolderCommand +AddressBookParser -> CreateFolderCommand +activate CreateFolderCommand + +CreateFolderCommand --> AddressBookParser +deactivate CreateFolderCommand + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> CreateFolderCommand : execute(m) +activate CreateFolderCommand + +CreateFolderCommand -> Model : addFolder(f.folderToAdd) +activate Model + +Model --> CreateFolderCommand +deactivate Model + +CreateFolderCommand --> LogicManager : result +deactivate CreateFolderCommand + +CreateFolderCommand -[hidden]-> LogicManager : result +destroy CreateFolderCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/DeleteFolderSequenceDiagram.puml b/docs/diagrams/DeleteFolderSequenceDiagram.puml new file mode 100644 index 00000000000..d275a91c6b6 --- /dev/null +++ b/docs/diagrams/DeleteFolderSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteFolderCommandParser" as DeleteFolderCommandParser LOGIC_COLOR +participant "d:DeleteFolderCommand" as DeleteFolderCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("rmdir CS2103") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("rmdir CS2103") +activate AddressBookParser + +create DeleteFolderCommandParser +AddressBookParser -> DeleteFolderCommandParser +activate DeleteFolderCommandParser + +DeleteFolderCommandParser --> AddressBookParser +deactivate DeleteFolderCommandParser + +AddressBookParser -> DeleteFolderCommandParser : parse("CS2103") +activate DeleteFolderCommandParser + +create DeleteFolderCommand +DeleteFolderCommandParser -> DeleteFolderCommand +activate DeleteFolderCommand + +DeleteFolderCommand --> DeleteFolderCommandParser : d +deactivate DeleteFolderCommand + +DeleteFolderCommandParser --> AddressBookParser : d +deactivate DeleteFolderCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteFolderCommandParser -[hidden]-> AddressBookParser +destroy DeleteFolderCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> DeleteFolderCommand : execute() +activate DeleteFolderCommand + +DeleteFolderCommand -> Model : deleteFolder(folderToRemove) +activate Model + +Model --> DeleteFolderCommand +deactivate Model + +create CommandResult +DeleteFolderCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteFolderCommand +deactivate CommandResult + +DeleteFolderCommand --> LogicManager : result +deactivate DeleteFolderCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditFolderNameSequenceDiagram.puml b/docs/diagrams/EditFolderNameSequenceDiagram.puml new file mode 100644 index 00000000000..2577a2867cd --- /dev/null +++ b/docs/diagrams/EditFolderNameSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditFolderNameCommandParser" as EditFolderNameCommandParser LOGIC_COLOR +participant "d:EditFolderNameCommand" as EditFolderNameCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("mv CS2103 | CS2100") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("mv CS2103 | CS2100") +activate AddressBookParser + +create EditFolderNameCommandParser +AddressBookParser -> EditFolderNameCommandParser +activate EditFolderNameCommandParser + +EditFolderNameCommandParser --> AddressBookParser +deactivate EditFolderNameCommandParser + +AddressBookParser -> EditFolderNameCommandParser : parse("CS2103 | CS2100") +activate EditFolderNameCommandParser + +create EditFolderNameCommand +EditFolderNameCommandParser -> EditFolderNameCommand +activate EditFolderNameCommand + +EditFolderNameCommand --> EditFolderNameCommandParser : d +deactivate EditFolderNameCommand + +EditFolderNameCommandParser --> AddressBookParser : d +deactivate EditFolderNameCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditFolderNameCommandParser -[hidden]-> AddressBookParser +destroy EditFolderNameCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> EditFolderNameCommand : execute() +activate EditFolderNameCommand + +EditFolderNameCommand -> Model : setNewFolder(oldFolder, newFolder) +activate Model + +Model --> EditFolderNameCommand +deactivate Model + +create CommandResult +EditFolderNameCommand -> CommandResult +activate CommandResult + +CommandResult --> EditFolderNameCommand +deactivate CommandResult + +EditFolderNameCommand --> LogicManager : result +deactivate EditFolderNameCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindFoldersSequenceDiagram.puml b/docs/diagrams/FindFoldersSequenceDiagram.puml new file mode 100644 index 00000000000..4a8926e4f73 --- /dev/null +++ b/docs/diagrams/FindFoldersSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindFoldersCommandParser" as FindFoldersCommandParser LOGIC_COLOR +participant "f:FindFoldersCommand" as FindFoldersCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("find -folders CS") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find -folders CS") +activate AddressBookParser + +create FindFoldersCommandParser +AddressBookParser -> FindFoldersCommandParser +activate FindFoldersCommandParser + +FindFoldersCommandParser --> AddressBookParser +deactivate FindFoldersCommandParser + +AddressBookParser -> FindFoldersCommandParser : parse("CS") +activate FindFoldersCommandParser + +create FindFoldersCommand +FindFoldersCommandParser -> FindFoldersCommand +activate FindFoldersCommand + +FindFoldersCommand --> FindFoldersCommandParser : f +deactivate FindFoldersCommand + +FindFoldersCommandParser --> AddressBookParser : f +deactivate FindFoldersCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindFoldersCommandParser -[hidden]-> AddressBookParser +destroy FindFoldersCommandParser + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> FindFoldersCommand : execute() +activate FindFoldersCommand + +FindFoldersCommand -> Model : updateFilteredFolderList(predicate) +activate Model + +Model --> FindFoldersCommand +deactivate Model + +create CommandResult +FindFoldersCommand -> CommandResult +activate CommandResult + +CommandResult --> FindFoldersCommand +deactivate CommandResult + +FindFoldersCommand --> LogicManager : result +deactivate FindFoldersCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 1122257bd9a..7a9b4a816ed 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -15,8 +15,10 @@ Class ModelManager Class UserPrefs Class ReadOnlyUserPrefs - +Class UniqueFolderList Class UniquePersonList +Class Folder +Class FolderName Class Person Class Address Class Email @@ -38,8 +40,12 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs +AddressBook *--> "1" UniqueFolderList AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person +UniqueFolderList --> "~* all" Folder +Folder --> FolderName +Folder -right-> "*" Person Person *--> Name Person *--> Phone Person *--> Email @@ -51,4 +57,5 @@ Phone -[hidden]right-> Address Address -[hidden]right-> Email ModelManager -->"~* filtered" Person +ModelManager -->"~* filtered" Folder @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 85ac3ea2dee..4a99f9f8073 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,7 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedFolder } } @@ -38,6 +39,8 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson +JsonSerializableAddressBook --> "*" JsonAdaptedFolder +JsonAdaptedFolder -right-> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index ecae4876432..91a86c163d6 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,8 +11,10 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay +Class FolderListPanel Class PersonListPanel Class PersonCard +Class FolderCard Class StatusBarFooter Class CommandBox } @@ -33,21 +35,26 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" FolderListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +FolderListPanel -down-> "*" FolderCard MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart +FolderListPanel --|> UiPart PersonCard --|> UiPart +FolderCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +FolderCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml deleted file mode 100644 index 96e30744d24..00000000000 --- a/docs/diagrams/UndoRedoState0.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title Initial state - -package States { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 -hide State2 -hide State3 - -class Pointer as "Current State" #FFFFF -Pointer -up-> State1 -@end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml deleted file mode 100644 index 01fcb9b2b96..00000000000 --- a/docs/diagrams/UndoRedoState1.puml +++ /dev/null @@ -1,22 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "delete 5" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -hide State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml deleted file mode 100644 index bccc230a5d1..00000000000 --- a/docs/diagrams/UndoRedoState2.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "add n/David" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -@end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml deleted file mode 100644 index ea29c9483e4..00000000000 --- a/docs/diagrams/UndoRedoState3.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "undo" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml deleted file mode 100644 index 1b784cece80..00000000000 --- a/docs/diagrams/UndoRedoState4.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "list" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml deleted file mode 100644 index 88927be32bc..00000000000 --- a/docs/diagrams/UndoRedoState5.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "clear" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab3:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -note right on link: State ab2 deleted. -@end diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml deleted file mode 100644 index 410aab4e412..00000000000 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ /dev/null @@ -1,53 +0,0 @@ -@startuml -!include style.puml - -box Logic LOGIC_COLOR_T1 -participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant "u:UndoCommand" as UndoCommand LOGIC_COLOR -end box - -box Model MODEL_COLOR_T1 -participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR -end box -[-> LogicManager : execute(undo) -activate LogicManager - -LogicManager -> AddressBookParser : parseCommand(undo) -activate AddressBookParser - -create UndoCommand -AddressBookParser -> UndoCommand -activate UndoCommand - -UndoCommand --> AddressBookParser -deactivate UndoCommand - -AddressBookParser --> LogicManager : u -deactivate AddressBookParser - -LogicManager -> UndoCommand : execute() -activate UndoCommand - -UndoCommand -> Model : undoAddressBook() -activate Model - -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook - -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook - -Model --> UndoCommand -deactivate Model - -UndoCommand --> LogicManager : result -deactivate UndoCommand -UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand - -[<--LogicManager -deactivate LogicManager -@enduml diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 1ec62caa2a5..f7fab08a320 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CreateFolderAlternative1.png b/docs/images/CreateFolderAlternative1.png new file mode 100644 index 00000000000..12bc7a7275b Binary files /dev/null and b/docs/images/CreateFolderAlternative1.png differ diff --git a/docs/images/CreateFolderAlternative2.png b/docs/images/CreateFolderAlternative2.png new file mode 100644 index 00000000000..ad65833adf2 Binary files /dev/null and b/docs/images/CreateFolderAlternative2.png differ diff --git a/docs/images/CreateFolderSequenceDiagram.png b/docs/images/CreateFolderSequenceDiagram.png new file mode 100644 index 00000000000..a8a1fe38ede Binary files /dev/null and b/docs/images/CreateFolderSequenceDiagram.png differ diff --git a/docs/images/EditFolderNameSequenceDiagram.png b/docs/images/EditFolderNameSequenceDiagram.png new file mode 100644 index 00000000000..0409e59cb7b Binary files /dev/null and b/docs/images/EditFolderNameSequenceDiagram.png differ diff --git a/docs/images/FindFoldersSequenceDiagram.png b/docs/images/FindFoldersSequenceDiagram.png new file mode 100644 index 00000000000..1c98902fb62 Binary files /dev/null and b/docs/images/FindFoldersSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 39d7aec4b33..cbd73b7937a 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 82c66f8f16e..9eb9d3f57e8 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 91488fd1a0f..cacdc9ad51a 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 4bb8b2ce591..41b1e682a9e 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index 8f7538cd884..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index df9908d0948..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 36519c1015b..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 19959d01712..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 4c623e4f2c5..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index 84ad2afa6bd..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index 6addcd3a8d9..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..89cd5267272 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/github.png b/docs/images/github.png new file mode 100644 index 00000000000..fcd55a90de2 Binary files /dev/null and b/docs/images/github.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..7fe5619e270 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/itsyme.png b/docs/images/itsyme.png new file mode 100644 index 00000000000..ffe707cb6c4 Binary files /dev/null and b/docs/images/itsyme.png differ diff --git a/docs/images/jiarong15.png b/docs/images/jiarong15.png new file mode 100644 index 00000000000..6d93b18e6e5 Binary files /dev/null and b/docs/images/jiarong15.png differ diff --git a/docs/images/linkedin.png b/docs/images/linkedin.png new file mode 100644 index 00000000000..ceb5c8d01fa Binary files /dev/null and b/docs/images/linkedin.png differ diff --git a/docs/images/portfolio.png b/docs/images/portfolio.png new file mode 100644 index 00000000000..f262860fd05 Binary files /dev/null and b/docs/images/portfolio.png differ diff --git a/docs/images/rehmmann.png b/docs/images/rehmmann.png new file mode 100644 index 00000000000..af025afde2f Binary files /dev/null and b/docs/images/rehmmann.png differ diff --git a/docs/images/sebbycake.png b/docs/images/sebbycake.png new file mode 100644 index 00000000000..6df9d988fd1 Binary files /dev/null and b/docs/images/sebbycake.png differ diff --git a/docs/images/startupUi.png b/docs/images/startupUi.png new file mode 100644 index 00000000000..2dd07c00d86 Binary files /dev/null and b/docs/images/startupUi.png differ diff --git a/docs/images/xlzior.png b/docs/images/xlzior.png new file mode 100644 index 00000000000..693db1a6060 Binary files /dev/null and b/docs/images/xlzior.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..e5d99cbdbb0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,22 @@ --- layout: page -title: AddressBook Level-3 +title: UNIon - Manage your wide range of college contacts easily --- -[![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/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2122S1-CS2103-T16-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2122S1-CS2103-T16-1/tp/branch/master/graph/badge.svg?token=JPVK5O2KBO)](https://codecov.io/gh/AY2122S1-CS2103-T16-1/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). - -* 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. +**UNIon is a desktop app for organizing various types of contacts in college**, optimized for use for the vast majority of computing students. +While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface) based on **Unix**. +* If you are interested in using UNIon, head over to the [_Quick Start_ section of the **User + Guide**](UserGuide.html#quick-start). +* If you are interested about developing UNIon, 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) +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson) + , [JUnit5](https://github.com/junit-team/junit5) diff --git a/docs/team/itsyme.md b/docs/team/itsyme.md new file mode 100644 index 00000000000..e4acb26d6b3 --- /dev/null +++ b/docs/team/itsyme.md @@ -0,0 +1,38 @@ +--- +layout: page +title: David's Project Portfolio Page +--- + +### Project: UNIon + +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. If you are already familiar with Unix commands, then UNIon will be easy for you to use. + +Given below are my contributions to the project. + +* **New Feature**: Implement folders and command to create folders to UNIon [\#46](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/46) + * What it does: Allows the user to organise contacts by adding them to folders. + * Justification: This feature allows users to see a summary of all their social groups and which of their contacts are in which folders. + * Highlights: Besides the command to add folders, all the other classes required for folders to work were implemented here. This enhancement affects commands to be added in the future. It required an analysis of current models in order to implement a folder class that fits with the other components. The implementation was challenging due to the sheer number of classes that had to be implemented. + +* **New Feature**: Saving of folders to JSON file [\#82](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/82) + * What it does: Allows UNIon to save folders to a local JSON file. + * Justification: As UNIon allows users to create folders, folders must be saved to allow users to close UNIon and come back to the same state as they left it in. + * Highlights: This enhancement required an analysis of the `Storage` component. Implementing this feature was challenging as it required learning about JSON files and how to read and write them. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=itsyme) + +* **Project management**: + * Created the name for UNIon + * In charge of documentation for UNIon + +* **Documentation**: + * User Guide: + * Added documentation for the features `create folder` [\#15](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/15). + * Tweaked documentation to fix documentation bugs from PE-D [\#188](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/188). + * Developer Guide: + * Added implementation details of the `create folder` feature [\#83](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/83). + * Added Appendix: Effort and two use cases [\#202](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/202) + +* **Community**: + * Contributed to module forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S1/forum/issues/118#issuecomment-907715971)) + * Reported bugs and suggestions for other teams in the class ([examples](https://github.com/itsyme/ped/issues)) diff --git a/docs/team/jiarong15.md b/docs/team/jiarong15.md new file mode 100644 index 00000000000..92deda792ca --- /dev/null +++ b/docs/team/jiarong15.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Jia Rong's Project Portfolio Page +--- + +### Project: UNIon + +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. If you are already familiar with Unix commands, then UNIon will be easy for you to use. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to remove unwanted folders command. [\#51](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/51) + * What it does: Allows users to remove folders that they no longer need one at a time. + * Justification: This feature improves the product significantly as we do not want unwanted folders to remain in the app, causing wastage of storage space and searching of folders. + * Highlights: This enhancement affects commands to be added in the future. We required it to reflect the unix command of removing directory, in this case, folders. + +* **New Feature**: Added the ability to change folder names to a new one. [\#67](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/67) + * What it does: Allows users to rename outdated folder names. + * Justification: This feature will prevent users having to delete the folder and add the entire list of contacts again when they wish to change folder's name. In the process, they may forget the contacts in the folder when there are large amount of contacts in the folder. + +* **New Feature**: Added the availability to remove specified contacts from specified folder. [\#98](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/98) + * What it does: Allows users to remove contacts no longer belonging to the folder. + * Justification: This feature improves the product significantly as the inability to do so compels user to go through the whole process of deleting folder and adding everyone except that one guy that should be left out. + * Highlights: This enhancement is affected by filtering of folders and contacts as well. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=jiarong15) + +* **Enhancements to existing features**: + * Ensured consistency of only able to make changes to filtered list and contacts [\#186](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/186) + +* **Documentation**: + * User Guide: + * Added documentation for the feature `rmdir` [\#42](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/42) + * Added documentation for the feature `mv` [\#68](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/68) + * Added documentation for the feature of removing contact from folder `rm` [\#68](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/110) + * Developer Guide: + * Added use case details of all the commands we are implementing [\#32](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/32). + * Added UML diagram and implementation details of `mv` feature [\#96](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/96). + +* **Community**: + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S1/forum/issues/8), [2](https://github.com/nus-cs2103-AY2122S1/forum/issues/47), [3](https://github.com/nus-cs2103-AY2122S1/forum/issues/122), [4](https://github.com/nus-cs2103-AY2122S1/forum/issues/173)) + * Reported bugs and suggestions for other teams in the class ([examples](https://github.com/jiarong15/ped/issues)) + diff --git a/docs/team/rehmmann.md b/docs/team/rehmmann.md new file mode 100644 index 00000000000..ce59473a6e8 --- /dev/null +++ b/docs/team/rehmmann.md @@ -0,0 +1,36 @@ +--- +layout: page +title: Rehman Sajid's Project Portfolio Page +--- + +### Project: UNIon + +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. If you are already familiar with Unix commands, then UNIon will be easy for you to use. + +Given below are my contributions to the project. + +* **New Feature**: Adding contact to a folder [\#49](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/49) + * What it does: Allows the user to add contacts to folder. + * Justification: To be able to organize all your contacts, it will be essential to be able to group them together. (E.g. User might want to group his classmates from tutorial classes or maybe his CCA friends) + * Highlights: This enhancement affects existing commands and commands to be added in the future. It required an in-depth analysis of design alternatives.This non-trivial feature required creation and modification of several files across the code base which gave me a good understanding of how the different components function. + +* **New Feature**: Adding multiple contacts to a folder [\#95](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/95) + * What it does: Allows the user to add multiple contacts into a folder in one command + * Justification: UNIon allows users to add contacts to a folder one at a time however if a particular folder has many contacts it will be tiring for a user to add all of them in one by one. + * Highlights: This command required changing some classes written in my first feature and required some careful planning on how it will handle certain user inputs. It was also a feature that required rigorous testing and debugging as there were plenty of edge cases. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=rehmmann) + +* **Project management**: + * Creating an initial mock up for our product + * In charge of ensuring smooth integration of all developers code + +* **Documentation**: + * User Guide: + * Added documentation for adding contact to folder feature [\#16](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/16). + * Developer Guide: + * Added implementation details of the `echo` command, and a sequence diagram [\#92](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/92). + +* **Community**: + * Contributed to module forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S1/forum/issues/55)) + * Reported bugs and suggestions for other teams in the class ([examples](https://github.com/rehmmann/ped/issues)) diff --git a/docs/team/sebbycake.md b/docs/team/sebbycake.md new file mode 100644 index 00000000000..c58c36590f0 --- /dev/null +++ b/docs/team/sebbycake.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Sebastian's Project Portfolio Page +--- + +### Project: UNIon + +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. +If you are already familiar with Unix commands, then UNIon will be easy for you to use. + +Given below are my contributions to the project. + +* **New Feature**: Display folder list [#48](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/48) + * What it does: Allows users to view list of folders create. Data shown: Folder name and list of contact names + * Justification: By having a component to view the folders that contain the name and the list of contacts, it allows the user to visualize, organize and manage efficiently and effectively with the massive number of contacts they collected while in college. + * Highlights: This has data dependency with the persons' data. If any of the person data is modified, we need to be able to update in the folder list as well. In particular, we need to update the person in the folder that contains the updated person data. + +* **New Feature**: Implement command to clear all folders [#108](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/108) + * What it does: Allows users to clear all folders + * Justification: With the marching of time, some folders, if not all, become obsolete. This feature comes in handy when they want to remove all folders at one go. This helps to improve user experience as they do not have to delete one by one, especially if he/she has created a significant number of it. + * Highlights: This command is similar to that of clearing all persons. However, there is one key difference. When clearing the folders, we need to ensure that the list of person/contacts still remain. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=sebbycake) + +* **Project management**: + * Conducted team meetings and drove discussions on Zoom + * Prepared weekly meeting notes and wrote minutes + * Reviewed other team member's PRs and gave constructive feedback where necessary + * Managed day-to-day activities and addressed key issues or challenges brought up by the members + +* **Critical bugs fixes**: + * Contacts' data inconsistency between folder list and contact list due to change(s) to contacts [#107](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/107) + * Inability to remove folders with contacts populated [#62](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/62) + +* **Contributions to team-based tasks**: + * Setting up [GitHub team org/repo](https://github.com/AY2122S1-CS2103-T16-1/tp) and [Codecov](https://app.codecov.io/gh/AY2122S1-CS2103-T16-1/tp) + * Changed product icon + * Documenting project details like description, target audience and value proposition + +* **Documentation**: + * User Guide: + * Added documentation for the feature to view all folders [#29](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/29) and `rm -folders` [#108](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/108) + * Developer Guide: + * Added implementation details of the `ls -folders` feature [#85](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/85). + +* **Community**: + * Reported bugs and suggestions for other teams in the class ([examples](https://github.com/sebbycake/ped/issues)) diff --git a/docs/team/xlzior.md b/docs/team/xlzior.md new file mode 100644 index 00000000000..475905ba57a --- /dev/null +++ b/docs/team/xlzior.md @@ -0,0 +1,37 @@ +--- +layout: page +title: Wen Jun's Project Portfolio Page +--- + +### Project: UNIon + +UNIon is a desktop app for organizing various types of contacts, optimized for use for the vast majority of computing students. If you are already familiar with Unix commands, then UNIon will be easy for you to use. + +Given below are my contributions to the project. + +* **New Feature**: Convert old command format to Unix commands [\#44](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/44) + * What it does: Allows the user to use a familiar set of Unix commands in UNIon. This includes changing the command words (e.g. from `list` to `ls -contacts`, as well as replacing the old prefixes `n/` with flags like `-n`. + * Justification: This feature allows UNIon to be relatively easy to pick up and remember for computing students who already have some knowledge of Unix commands. + * Highlights: This enhancement affects existing commands and commands to be added in the future. Since this affected a core part of the original AB3, many files needed to be changed while monitoring for possible regressions. One change required was that command words could now comprise multiple words, which the original AB3 did not allow. The other change was that the AB3 prefixes did not need to end with a space e.g. `n/John Doe`, but our flags would need an extra space at the end such as `-n John Doe`. Hence, the implementation was non-trivial and was not simply a matter of replacing command words. + +* **New Feature**: Implement command to search for folders [\#77](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/77) + * What it does: Allows the user to search for folders by keyword + * Justification: As UNIon allows users to create folders, users may want to search for folders to narrow down the list of folders they can see. + * Highlights: This enhancement is similar to the original `find` command for persons. However, the command to find persons only matches full words while the command to find folders allows partial words to match e.g. the keyword `CS` matches `CS2103`. Furthermore, by adding the `-folders` tag to the command, some validation checks needed to be updated in order to ignore the flags from the search terms. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=xlzior) + +* **Project management**: + * Managed project deliverables to ensure module deadlines are met + * In charge of reviewing other team member's PRs (together with one other team member) + +* **Documentation**: + * User Guide: + * Added documentation for the features `Unix commands` [\#41](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/41) and `find -folders` [\#55](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/55) + * Developer Guide: + * Added implementation details of the `find -folders` feature [\#84](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/84). + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#46](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/46), [\#57](https://github.com/AY2122S1-CS2103-T16-1/tp/pull/57) + * Contributed to module forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S1/forum/issues/4#issuecomment-898862824), [2](https://github.com/nus-cs2103-AY2122S1/forum/issues/102#issuecomment-905653729), [3](https://github.com/nus-cs2103-AY2122S1/forum/issues/142#issuecomment-908973467), [4](https://github.com/nus-cs2103-AY2122S1/forum/issues/241#issuecomment-920977286), [5](https://github.com/nus-cs2103-AY2122S1/forum/issues/277#issuecomment-928639397), [6](https://github.com/nus-cs2103-AY2122S1/forum/issues/286#issuecomment-934306682)) + * Reported bugs and suggestions for other teams in the class ([examples](https://github.com/xlzior/ped/issues)) diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 8919d8eaa17..48e9915cade 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -340,7 +340,7 @@ save it with `Model#setPerson()`. List lastShownList = model.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MESSAGE_INVALID_INDEX_EXCEEDS_LIST_SIZE); } Person personToEdit = lastShownList.get(index.getZeroBased()); diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..0346495cc2c 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing UNIon ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -151,7 +151,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty UNIon"); initializedPrefs = new UserPrefs(); } @@ -167,13 +167,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting UNIon " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping UNIon ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..be7bc2b2490 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -7,7 +7,21 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_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_PERSON_IN_UNION = "This person does not exist in UNIon"; + public static final String MESSAGE_NONEXISTENT_FOLDER_IN_CURRENT_LIST = "Folder name supplied " + + "is not found in the current folder's list below."; + private static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d person%2$s listed!"; + private static final String MESSAGE_FOLDERS_LISTED_OVERVIEW = "%1$d folder%2$s listed!"; + + private static String getPluralModifier(int numberOfObject) { + return numberOfObject != 1 ? "s" : ""; + } + + public static String getMessagePersonsListedOverview(int numberOfPersons) { + return String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, numberOfPersons, getPluralModifier((numberOfPersons))); + } + public static String getMessageFoldersListedOverview(int numberOfFolders) { + return String.format(MESSAGE_FOLDERS_LISTED_OVERVIEW, numberOfFolders, getPluralModifier((numberOfFolders))); + } } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..9255de7b044 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,29 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code text}. + * Ignores case and matches partial words + *
examples:
+     *       containsTextIgnoreCase("ABc def", "abc") == true
+     *       containsTextIgnoreCase("ABc def", "DEF") == true
+     *       containsTextIgnoreCase("ABc def", "AB") == true // partial words match, unlike containsWordIgnoreCase
+     *       
+ * @param sentence cannot be null + * @param text cannot be null, cannot be empty, must be a single word + */ + + public static boolean containsTextIgnoreCase(String sentence, String text) { + requireNonNull(sentence); + requireNonNull(text); + + String preppedWord = text.trim(); + checkArgument(!preppedWord.isEmpty(), "Text parameter cannot be empty"); + checkArgument(preppedWord.split("\\s+").length == 1, "Text parameter should be a single word"); + + return sentence.toLowerCase().contains(preppedWord.toLowerCase()); + } + /** * Returns a detailed message of the t, including the stack trace. */ @@ -65,4 +88,43 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Removes flags (words starting with "-" character) from a string + * @param s input string + * @return string with flags removed + */ + public static String stripFlags(String s) { + requireNonNull(s); + String[] result = Arrays.stream(s.split("\\s+")) + .filter(word -> !word.startsWith("-")) + .toArray(String[]::new); + return String.join(" ", result); + } + + /** + * Removes extra whitespace characters found within the command + * @param s input string + * @return string with extra whitespace characters replaced with a single space + */ + public static String removeExtraWhitespace(String s) { + requireNonNull(s); + return String.join(" ", s.split("\\s+")).trim(); + } + + /** + * Checks if a sentence starts with a given command + * + * The command must be followed by a whitespace character or the end of the string + * + * @param sentence Sentence to check if it starts with a given command, which may have leading/trailing spaces + * @param command Given command, which may have spaces in the middle but not leading/trailing + * @return Whether the given sentence starts with the given command + */ + public static boolean startsWithCommand(String sentence, String command) { + assert command.trim().equals(command); // command should not have leading or trailing spaces + requireNonNull(command); + String cleanSentence = removeExtraWhitespace(sentence); + return cleanSentence.startsWith(command + " ") || cleanSentence.equals(command); + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..069479579f5 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,6 +8,7 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.folder.Folder; import seedu.address.model.person.Person; /** @@ -16,10 +17,11 @@ 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. - * @throws ParseException If an error occurs during parsing. + * @throws ParseException If an error occurs during parsing. */ CommandResult execute(String commandText) throws CommandException, ParseException; @@ -30,9 +32,17 @@ 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(); + /** + * Returns an unmodifiable view of the filtered list of folders + */ + ObservableList getFilteredFolderList(); + + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..78a6b00340e 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -14,6 +14,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.folder.Folder; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -64,6 +65,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredFolderList() { + return model.getFilteredFolderList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..9a50b2a5654 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -12,13 +12,13 @@ import seedu.address.model.person.Person; /** - * Adds a person to the address book. + * Adds a person to UNIon. */ public class AddCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_WORD = "touch"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to UNIon. " + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " @@ -34,7 +34,8 @@ public class AddCommand extends Command { + PREFIX_TAG + "owesMoney"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in UNIon"; + public static final String MESSAGE_NAME_TOO_LONG = "Your friend's name is too long, unfriend them"; private final Person toAdd; @@ -53,6 +54,9 @@ public CommandResult execute(Model model) throws CommandException { if (model.hasPerson(toAdd)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + if (toAdd.getName().toString().length() > 70) { + throw new CommandException(MESSAGE_NAME_TOO_LONG); + } model.addPerson(toAdd); return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); diff --git a/src/main/java/seedu/address/logic/commands/AddToFolderCommand.java b/src/main/java/seedu/address/logic/commands/AddToFolderCommand.java new file mode 100644 index 00000000000..7f65ba83549 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddToFolderCommand.java @@ -0,0 +1,108 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +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.folder.Folder; +import seedu.address.model.folder.FolderName; +import seedu.address.model.person.Person; + +public class AddToFolderCommand extends Command { + + public static final String COMMAND_WORD = "echo"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to folder in UNIon. " + + "Parameters: " + + "CONTACT_INDEX >> FOLDERNAME \n" + + "Example: " + + COMMAND_WORD + " " + + "3 >> CS2103"; + + public static final String MESSAGE_DUPLICATE_CONTACT = "This person already exists in this folder"; + public static final String MESSAGE_DUPLICATE_INDEX_PASSED = "Duplicate person indices passed"; + public static final String MESSAGE_SUCCESS = "Person added to folder: %1$s"; + + private final List indexList; + private final FolderName folderName; + + /** + * Creates a AddToFolderCommand to add the specified {@code FolderName} + * @param indexList + * @param folderName + */ + public AddToFolderCommand(List indexList, FolderName folderName) { + requireNonNull(indexList); + this.indexList = indexList; + this.folderName = folderName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Folder targetFolder = new Folder(folderName); + + List lastShownList = model.getFilteredPersonList(); + List lastShownFolderList = model.getFilteredFolderList(); + int indexOfFolder = lastShownFolderList.indexOf(targetFolder); + + if (duplicateIndexPassed()) { + throw new CommandException(MESSAGE_DUPLICATE_INDEX_PASSED); + } + + if (indexOfFolder == -1) { + throw new CommandException(Messages.MESSAGE_NONEXISTENT_FOLDER_IN_CURRENT_LIST); + } + + for (Index index : this.indexList) { + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_IN_UNION); + } + + Person personToAdd = lastShownList.get(index.getZeroBased()); + + if (model.folderContainsPerson(personToAdd, folderName)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); + } + + } + for (Index index : this.indexList) { + Person personToAdd = lastShownList.get(index.getZeroBased()); + model.addContactToFolder(personToAdd, folderName); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, folderName)); + } + + /** + * Checks if user input contains repeated indexes + * @return true if there are duplicate indexes + */ + public boolean duplicateIndexPassed() { + List uniqueIndexes = new ArrayList<>(); + for (Index index:this.indexList) { + if (uniqueIndexes.contains(index)) { + return true; + } + uniqueIndexes.add(index); + } + return false; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AddToFolderCommand)) { + return false; + } + AddToFolderCommand that = (AddToFolderCommand) other; + return this.indexList.equals(that.indexList) + && this.folderName.equals(that.folderName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..7d15bacae44 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -6,18 +6,19 @@ import seedu.address.model.Model; /** - * Clears the address book. + * Clears UNIon. */ public class ClearCommand extends Command { - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String COMMAND_WORD = "rm -contacts"; + public static final String MESSAGE_SUCCESS = "UNIon's contacts have been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); - model.setAddressBook(new AddressBook()); + AddressBook newAddressBook = AddressBook.withFolders(model); + model.setAddressBook(newAddressBook); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/ClearFoldersCommand.java b/src/main/java/seedu/address/logic/commands/ClearFoldersCommand.java new file mode 100644 index 00000000000..0fa86e23b4f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClearFoldersCommand.java @@ -0,0 +1,24 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; + +/** + * Clears UNIon. + */ +public class ClearFoldersCommand extends Command { + + public static final String COMMAND_WORD = "rm -folders"; + public static final String MESSAGE_SUCCESS = "UNIon's folders have been cleared!"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + AddressBook newAddressBook = AddressBook.withContacts(model); + model.setAddressBook(newAddressBook); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CreateFolderCommand.java b/src/main/java/seedu/address/logic/commands/CreateFolderCommand.java new file mode 100644 index 00000000000..a8c97d0e042 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CreateFolderCommand.java @@ -0,0 +1,64 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.folder.Folder; + +public class CreateFolderCommand extends Command { + + public static final String COMMAND_WORD = "mkdir"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Creates a folder in UNIon. " + + "Parameters: " + + "FOLDER_NAME \n" + + "Example: " + + COMMAND_WORD + " " + + "School"; + + public static final String MESSAGE_SUCCESS = "New folder added: %1$s"; + public static final String MESSAGE_DUPLICATE_FOLDER = "This folder already exists in UNIon"; + public static final String MESSAGE_FOLDER_NAME_TOO_LONG = "This folder name is too long! " + + "Please keep it to a maximum of 30 chars."; + + private final Folder folderToAdd; + + /** + * Creates a CreateFolderCommand to add the specified {@code Folder} + * @param folder + */ + public CreateFolderCommand(Folder folder) { + requireNonNull(folder); + + this.folderToAdd = folder; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + if (model.hasFolder(folderToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_FOLDER); + } + + if (folderToAdd.getFolderName().toString().length() > 30) { + throw new CommandException(MESSAGE_FOLDER_NAME_TOO_LONG); + } + + model.addFolder(folderToAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, folderToAdd)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CreateFolderCommand)) { + return false; + } + CreateFolderCommand that = (CreateFolderCommand) other; + return Objects.equals(folderToAdd, that.folderToAdd); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..90644f06cbe 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -11,11 +11,11 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person identified using it's displayed index from UNIon. */ public class DeleteCommand extends Command { - public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_WORD = "rm"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" @@ -36,7 +36,7 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_IN_UNION); } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); diff --git a/src/main/java/seedu/address/logic/commands/DeleteFolderCommand.java b/src/main/java/seedu/address/logic/commands/DeleteFolderCommand.java new file mode 100644 index 00000000000..f2f7a404f92 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteFolderCommand.java @@ -0,0 +1,67 @@ +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.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.folder.Folder; + +/** + * Deletes folder name specified by user. + */ +public class DeleteFolderCommand extends Command { + + public static final String COMMAND_WORD = "rmdir"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes a folder in UNIon. " + + "Parameters: " + + "FOLDER_NAME \n" + + "Example: " + + COMMAND_WORD + " " + + "CS2103"; + + public static final String MESSAGE_SUCCESS = "Deleted Folder: %1$s"; + + private final Folder folderToRemove; + + /** + * Creates a DeleteFolderCommand to remove the specified {@code Folder} + * @param folder + */ + public DeleteFolderCommand(Folder folder) { + requireNonNull(folder); + + this.folderToRemove = folder; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownFolderList = model.getFilteredFolderList(); + int indexOfFolder = lastShownFolderList.indexOf(folderToRemove); + + if (indexOfFolder == -1) { + throw new CommandException(Messages.MESSAGE_NONEXISTENT_FOLDER_IN_CURRENT_LIST); + } + + model.deleteFolder(folderToRemove); + return new CommandResult(String.format(MESSAGE_SUCCESS, folderToRemove)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof DeleteFolderCommand)) { + return false; + } + DeleteFolderCommand that = (DeleteFolderCommand) other; + return Objects.equals(folderToRemove, that.folderToRemove); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/DeletePersonFromFolderCommand.java b/src/main/java/seedu/address/logic/commands/DeletePersonFromFolderCommand.java new file mode 100644 index 00000000000..1afba76dffd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeletePersonFromFolderCommand.java @@ -0,0 +1,101 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +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.folder.Folder; +import seedu.address.model.person.Person; + + +/** + * Deletes the contact that belongs to an existing + * folder in UNIon. + */ +public class DeletePersonFromFolderCommand extends Command { + + public static final String COMMAND_WORD = "rm"; + public static final String COMMAND_IDENTIFIER = ">>"; + + public static final String MESSAGE_NO_SUCH_PERSON_IN_FOLDER = "This person does not exist in this folder"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the person identified by the index number from the folder specified.\n" + + "Parameters: INDEX (must be a positive integer) + " + + "FOLDER_NAME (must exist in UNIon)\n" + + "Example: " + + COMMAND_WORD + + " 1 " + COMMAND_IDENTIFIER + " CS2103"; + + public static final String MESSAGE_DELETE_PERSON_FROM_FOLDER_SUCCESS = "Deleted person from folder: %1$s"; + + private final Index targetIndex; + private final Folder targetFolder; + + /** + * Creates a DeletePersonFromFolderCommand to remove + * the specified {@code targetIndex} of contact from the + * {@code targetFolder}. + * @param targetIndex Index from which contact is removed from folder. + * @param targetFolder Folder in which contact is to be removed from. + */ + public DeletePersonFromFolderCommand( + Index targetIndex, + Folder targetFolder) { + requireAllNonNull(targetIndex, targetFolder); + this.targetIndex = targetIndex; + this.targetFolder = targetFolder; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List lastShownList = model.getFilteredPersonList(); + List lastShownFolderList = model.getFilteredFolderList(); + + int indexOfFolder = lastShownFolderList.indexOf(targetFolder); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_IN_UNION); + } + + if (indexOfFolder == -1) { + throw new CommandException(Messages.MESSAGE_NONEXISTENT_FOLDER_IN_CURRENT_LIST); + } + + Person personToRemove = lastShownList.get(targetIndex.getZeroBased()); + + if (!model.folderContainsPerson(personToRemove, + this.targetFolder.getFolderName())) { + throw new CommandException(MESSAGE_NO_SUCH_PERSON_IN_FOLDER); + } + + model.deletePersonFromFolder(personToRemove, targetFolder); + return new CommandResult( + String.format(MESSAGE_DELETE_PERSON_FROM_FOLDER_SUCCESS, + targetFolder)); + } + + @Override + public boolean equals(Object other) { + + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeletePersonFromFolderCommand)) { + return false; + } + + DeletePersonFromFolderCommand otherCommand = (DeletePersonFromFolderCommand) other; + + return targetIndex.equals(otherCommand.targetIndex) + && targetFolder.equals(otherCommand.targetFolder); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..58caf3b1f97 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -27,11 +27,11 @@ import seedu.address.model.tag.Tag; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing person in UNIon. */ public class EditCommand extends Command { - public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_WORD = "vim"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + "by the index number used in the displayed person list. " @@ -48,7 +48,8 @@ public class EditCommand extends Command { public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in UNIon."; + public static final String MESSAGE_NAME_TOO_LONG = "Your friend's name is too long, unfriend them"; private final Index index; private final EditPersonDescriptor editPersonDescriptor; @@ -71,12 +72,16 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_IN_UNION); } Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + if (editedPerson.getName().toString().length() > 70) { + throw new CommandException(MESSAGE_NAME_TOO_LONG); + } + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } diff --git a/src/main/java/seedu/address/logic/commands/EditFolderNameCommand.java b/src/main/java/seedu/address/logic/commands/EditFolderNameCommand.java new file mode 100644 index 00000000000..24ca26b6155 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditFolderNameCommand.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.folder.Folder; + +/** + * Edits the name of an existing folder in UNIon. + */ +public class EditFolderNameCommand extends Command { + + public static final String COMMAND_WORD = "mv"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Renames the folder identified by the old " + + "folder name used in the displayed folders list.\n" + + "Parameters: OLD_FOLDER_NAME (Name must be an existing folder)" + + " + NEW_FOLDER_NAME (Name must not already exist in folder list)\n" + + "Example: " + COMMAND_WORD + " OLD_FOLDER_NAME | " + "NEW_FOLDER_NAME"; + + public static final String MESSAGE_SUCCESS_EDIT_FOLDER_NAME = "Folder updated to: %1$s"; + public static final String MESSAGE_DUPLICATE_FOLDER = "This folder already exists in UNIon"; + public static final String MESSAGE_SAME_FOLDER_NAME_ENTERED = "This folder name is the same as the current one"; + public static final String MESSAGE_FOLDER_NAME_TOO_LONG = "This folder name is too long! " + + "Please keep it to a maximum of 30 chars."; + + private final Folder oldFolder; + private final Folder newFolder; + + /** + * Creates a EditFolderNameCommand to update + * the specified {@code oldFolder} to the + * {@code newFolder} name. + * @param oldFolder folder name to be changed. + * @param newFolder new folder to replace old folder. + */ + public EditFolderNameCommand(Folder oldFolder, Folder newFolder) { + requireAllNonNull(oldFolder, newFolder); + this.oldFolder = oldFolder; + this.newFolder = newFolder; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + + if (newFolder.getFolderName().equals(oldFolder.getFolderName())) { + throw new CommandException(MESSAGE_SAME_FOLDER_NAME_ENTERED); + } + + List lastShownFolderList = model.getFilteredFolderList(); + int indexOfFolder = lastShownFolderList.indexOf(oldFolder); + if (indexOfFolder == -1) { + throw new CommandException(Messages.MESSAGE_NONEXISTENT_FOLDER_IN_CURRENT_LIST); + } + + if (model.hasFolder(newFolder)) { + throw new CommandException(MESSAGE_DUPLICATE_FOLDER); + } + if (newFolder.getFolderName().toString().length() > 30) { + throw new CommandException(MESSAGE_FOLDER_NAME_TOO_LONG); + } + + model.setNewFolder(oldFolder, newFolder); + return new CommandResult(String.format(MESSAGE_SUCCESS_EDIT_FOLDER_NAME, newFolder)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditFolderNameCommand)) { + return false; + } + + // state check + EditFolderNameCommand e = (EditFolderNameCommand) other; + return this.oldFolder.equals(e.oldFolder) + && this.newFolder.equals(e.newFolder); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..4e6e4c9c18f 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,7 +9,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting UNIon as requested ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index d6b19b0a0de..5eaf905fa68 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -7,12 +7,12 @@ import seedu.address.model.person.NameContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all persons in UNIon whose name contains any of the argument keywords. * Keyword matching is case insensitive. */ public class FindCommand extends Command { - public static final String COMMAND_WORD = "find"; + public static final String COMMAND_WORD = "find -contacts"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" @@ -29,8 +29,7 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + return new CommandResult(Messages.getMessagePersonsListedOverview(model.getFilteredPersonList().size())); } @Override diff --git a/src/main/java/seedu/address/logic/commands/FindFoldersCommand.java b/src/main/java/seedu/address/logic/commands/FindFoldersCommand.java new file mode 100644 index 00000000000..8db3cfba064 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindFoldersCommand.java @@ -0,0 +1,45 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderNameContainsKeywordsPredicate; + +/** + * Finds and lists all folders in UNIon whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindFoldersCommand extends Command { + + public static final String COMMAND_WORD = "find -folders"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all folders whose names contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " CS2103 group project"; + + private final FolderNameContainsKeywordsPredicate predicate; + + public FindFoldersCommand(FolderNameContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredFolderList(predicate); + ObservableList filteredFolders = model.getFilteredFolderList(); + assert filteredFolders != null : "filteredFolders should not be null"; + return new CommandResult(Messages.getMessageFoldersListedOverview(filteredFolders.size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindFoldersCommand // instanceof handles nulls + && predicate.equals(((FindFoldersCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..5065e9ee408 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -6,11 +6,11 @@ import seedu.address.model.Model; /** - * Lists all persons in the address book to the user. + * Lists all persons in UNIon to the user. */ public class ListCommand extends Command { - public static final String COMMAND_WORD = "list"; + public static final String COMMAND_WORD = "ls -contacts"; public static final String MESSAGE_SUCCESS = "Listed all persons"; diff --git a/src/main/java/seedu/address/logic/commands/ListFoldersCommand.java b/src/main/java/seedu/address/logic/commands/ListFoldersCommand.java new file mode 100644 index 00000000000..28d9749d6b3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListFoldersCommand.java @@ -0,0 +1,24 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_FOLDERS; + +import seedu.address.model.Model; + +/** + * Lists all folders in UNIon to the user. + */ +public class ListFoldersCommand extends Command { + + public static final String COMMAND_WORD = "ls -folders"; + + public static final String MESSAGE_SUCCESS = "Listed all folders"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredFolderList(PREDICATE_SHOW_ALL_FOLDERS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddToFolderParser.java b/src/main/java/seedu/address/logic/parser/AddToFolderParser.java new file mode 100644 index 00000000000..0b64a7ac5e0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddToFolderParser.java @@ -0,0 +1,75 @@ +package seedu.address.logic.parser; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddToFolderCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.FolderName; + +public class AddToFolderParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddToFolderCommand + * and returns an AddToFolderCommand object for execution. + * @throws ParseException if the given {@code folderName} is invalid. + */ + public AddToFolderCommand parse(String args) throws ParseException { + List allValues = new ArrayList<>(Arrays.asList(args.split("\\s+"))); + if (allValues.size() <= 3) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddToFolderCommand.MESSAGE_USAGE)); + } + List index = extractContactIndex(allValues); + FolderName folderName = extractFolderName(allValues); + return new AddToFolderCommand(index, folderName); + } + + /** + * Extracts the elements relevant to the folder name from {@code List} of inputs + * and returns a folder name {@code FolderName} + * @param allValues {@code List} of inputs + * @return folder name {@code FolderName} + * @throws ParseException if the given {@code List} is invalid. + */ + private FolderName extractFolderName(List allValues) throws ParseException { + StringBuilder stringBuilder = new StringBuilder(); + if (!allValues.contains(">>")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddToFolderCommand.MESSAGE_USAGE)); + } + for (int i = 1; i < allValues.size(); i++) { + String curr = allValues.get(i).trim(); + if (!curr.equals(">>")) { + stringBuilder.append(" ").append(curr); + } + } + + return ParserUtil.parseFolderName(stringBuilder.toString()); + } + + /** + * Extracts the elements relevant to the index from {@code List} of inputs + * and returns an Index {@code Index} + * @param allValues {@code List} of inputs + * @return Index index {@code Index} + * @throws ParseException if the given {@code List} is invalid. + */ + private List extractContactIndex(List allValues) throws ParseException { + List contactsToAdd = new ArrayList<>(); + for (int i = 1; i < allValues.size(); i++) { + String currString = allValues.get(i); + if (currString.equals(">>")) { + break; + } + Index contactIndex = ParserUtil.parseIndex(currString, + new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddToFolderCommand.MESSAGE_USAGE))); + contactsToAdd.add(contactIndex); + i--; + allValues.remove(currString); + } + return contactsToAdd; + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..a5a46fbfa7d 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -6,26 +6,35 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import seedu.address.commons.util.StringUtil; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddToFolderCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.ClearFoldersCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CreateFolderCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteFolderCommand; +import seedu.address.logic.commands.DeletePersonFromFolderCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditFolderNameCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindFoldersCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListFoldersCommand; import seedu.address.logic.parser.exceptions.ParseException; /** * Parses user input. */ public class AddressBookParser { - /** * Used for initial separation of command word and args. */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + private static final Pattern BASIC_COMMAND_FORMAT = Pattern + .compile("(?\\S+)(?.*)"); /** * Parses user input into command for execution. @@ -40,35 +49,44 @@ public Command parseCommand(String userInput) throws ParseException { 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: + // using if-else and String.startsWith() instead of switch-case + // allows for the flexibility of having commands with multiple words, e.g. rm -contacts + if (StringUtil.startsWithCommand(userInput, AddCommand.COMMAND_WORD)) { return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, EditFolderNameCommand.COMMAND_WORD)) { + return new EditFolderNameCommandParser().parse(arguments); + } else if (StringUtil.startsWithCommand(userInput, DeleteFolderCommand.COMMAND_WORD)) { + return new DeleteFolderCommandParser().parse(arguments); + } else if (StringUtil.startsWithCommand(userInput, EditCommand.COMMAND_WORD)) { return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, ClearCommand.COMMAND_WORD)) { return new ClearCommand(); - - case FindCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, ClearFoldersCommand.COMMAND_WORD)) { + return new ClearFoldersCommand(); + } else if (StringUtil.startsWithCommand(userInput, DeleteCommand.COMMAND_WORD) + || StringUtil.startsWithCommand(userInput, DeletePersonFromFolderCommand.COMMAND_WORD)) { + if (arguments.contains(DeletePersonFromFolderCommand.COMMAND_IDENTIFIER)) { + return new DeletePersonFromFolderCommandParser().parse(arguments); + } + return new DeleteCommandParser().parse(arguments); + } else if (StringUtil.startsWithCommand(userInput, FindFoldersCommand.COMMAND_WORD)) { + return new FindFoldersCommandParser().parse(arguments); + } else if (StringUtil.startsWithCommand(userInput, FindCommand.COMMAND_WORD)) { return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, ListFoldersCommand.COMMAND_WORD)) { + return new ListFoldersCommand(); + } else if (StringUtil.startsWithCommand(userInput, ListCommand.COMMAND_WORD)) { return new ListCommand(); - - case ExitCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, ExitCommand.COMMAND_WORD)) { return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: + } else if (StringUtil.startsWithCommand(userInput, HelpCommand.COMMAND_WORD)) { return new HelpCommand(); - - default: + } else if (StringUtil.startsWithCommand(userInput, CreateFolderCommand.COMMAND_WORD)) { + return new CreateFolderCommandParser().parse(arguments); + } else if (StringUtil.startsWithCommand(userInput, AddToFolderCommand.COMMAND_WORD)) { + return new AddToFolderParser().parse(arguments); + } else { throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..b28a2a78caf 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -3,14 +3,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * Tokenizes arguments string of the form: {@code preamble value value ...}
- * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
- * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
+ * Tokenizes arguments string of the form: {@code preamble value value ...}
+ * e.g. {@code some preamble text -t 11.00 -t 12.00 -k -m July} where prefixes are {@code -t -k -m}.
+ * 1. An argument's value can be an empty string e.g. the value of {@code -k} in the above example.
* 2. Leading and trailing whitespaces of an argument value will be discarded.
- * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} + * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code -t} * in the above example.
*/ public class ArgumentTokenizer { @@ -19,7 +21,7 @@ public class ArgumentTokenizer { * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps prefixes to their * respective argument values. Only the given prefixes will be recognized in the arguments string. * - * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to tokenize the arguments string with * @return ArgumentMultimap object that maps prefixes to their arguments */ @@ -31,7 +33,7 @@ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { /** * Finds all zero-based prefix positions in the given arguments string. * - * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to find in the arguments string * @return List of zero-based prefix positions in the given arguments string */ @@ -59,20 +61,35 @@ private static List findPrefixPositions(String argsString, Prefi /** * Returns the index of the first occurrence of {@code prefix} in - * {@code argsString} starting from index {@code fromIndex}. An occurrence - * is valid if there is a whitespace before {@code prefix}. Returns -1 if no - * such occurrence can be found. + * {@code argsString} starting from index {@code fromIndex}. + * An occurrence is valid if + * - there is a whitespace before {@code prefix} AND + * - there is a whitespace after {@code prefix} OR the prefix is at the end of the string + * Returns -1 if no such occurrence can be found. * - * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns -1 as there are no valid - * occurrences of "p/" with whitespace before it. However, if - * {@code argsString} = "e/hi p/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns 5. + * E.g if {@code argsString} = "-e hi-p900", {@code prefix} = "-p" and {@code fromIndex} = 0, + * this method returns -1 as there are no valid occurrences of "-p" with whitespace before and after it. + * However, if {@code argsString} = "-e hi -p 900", {@code prefix} = "-p" and {@code fromIndex} = 0, + * this method returns 5. + * Also, if {@code argsString} = "-e hi -p", {@code prefix} = "-p" and {@code fromIndex} = 0, + * this method returns 5 as prefixes at the end of the string are allowed to not have a whitespace after it */ private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { - int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); - return prefixIndex == -1 ? -1 - : prefixIndex + 1; // +1 as offset for whitespace + // search for prefix in the middle of the command + int prefixInMiddleIndex = argsString.indexOf(String.format(" %s ", prefix), fromIndex); + if (prefixInMiddleIndex >= 0) { + return prefixInMiddleIndex + 1; // +1 as offset for initial whitespace + } + + // search for prefix at the end of the command + Pattern p = Pattern.compile(String.format(" %s$", prefix)); + Matcher m = p.matcher(argsString.substring(fromIndex)); + if (m.find()) { + return m.start() + fromIndex + 1; // +1 as offset for initial whitespace + } + + // prefix not found + return -1; } /** @@ -80,7 +97,7 @@ private static int findPrefixPosition(String argsString, String prefix, int from * extracted prefixes to their respective arguments. Prefixes are extracted based on their zero-based positions in * {@code argsString}. * - * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} * @return ArgumentMultimap object that maps prefixes to their arguments */ diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..f9b136fcc97 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,10 @@ public class CliSyntax { /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_NAME = new Prefix("-n"); + public static final Prefix PREFIX_PHONE = new Prefix("-p"); + public static final Prefix PREFIX_EMAIL = new Prefix("-e"); + public static final Prefix PREFIX_ADDRESS = new Prefix("-a"); + public static final Prefix PREFIX_TAG = new Prefix("-t"); } diff --git a/src/main/java/seedu/address/logic/parser/CreateFolderCommandParser.java b/src/main/java/seedu/address/logic/parser/CreateFolderCommandParser.java new file mode 100644 index 00000000000..41856015c8c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CreateFolderCommandParser.java @@ -0,0 +1,25 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.CreateFolderCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; + +public class CreateFolderCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CreateFolderCommand + * and returns an CreateFolderCommand object for execution. + * @throws ParseException if the given {@code folderName} is invalid. + */ + public CreateFolderCommand parse(String args) throws ParseException { + if (args.length() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateFolderCommand.MESSAGE_USAGE)); + } + FolderName folderName = ParserUtil.parseFolderName(args); + return new CreateFolderCommand(new Folder(folderName)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 522b93081cc..98beda0f4a1 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -17,13 +17,14 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + if (args.trim().isEmpty()) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); } + + Index index = ParserUtil.parseIndex(args, new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE))); + return new DeleteCommand(index); } } diff --git a/src/main/java/seedu/address/logic/parser/DeleteFolderCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteFolderCommandParser.java new file mode 100644 index 00000000000..1b8604c2275 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteFolderCommandParser.java @@ -0,0 +1,25 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.DeleteFolderCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; + +public class DeleteFolderCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteFolderCommand + * and returns an DeleteFolderCommand object for execution. + * @throws ParseException if the given {@code folderName} is invalid. + */ + public DeleteFolderCommand parse(String args) throws ParseException { + if (args.length() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteFolderCommand.MESSAGE_USAGE)); + } + FolderName folderName = ParserUtil.parseFolderName(args); + return new DeleteFolderCommand(new Folder(folderName)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DeletePersonFromFolderCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePersonFromFolderCommandParser.java new file mode 100644 index 00000000000..e46cdcb747d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeletePersonFromFolderCommandParser.java @@ -0,0 +1,90 @@ +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.DeletePersonFromFolderCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; + +public class DeletePersonFromFolderCommandParser + implements Parser { + + /** + * Parses the given {@code String} of arguments in the + * context of the DeletePersonFromFolderCommand + * and returns an DeletePersonFromFolderCommand object for execution. + * @throws ParseException if the given index and folder name is invalid. + */ + public DeletePersonFromFolderCommand parse(String args) throws ParseException { + if (args.length() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeletePersonFromFolderCommand.MESSAGE_USAGE)); + } + String[] inputsToConvertToCommand = extractIndividualFolderAndIndex(args); + assert inputsToConvertToCommand.length == 2 + : "Should have exactly 2 inputs of Index and Folder strings only!"; + + Index indexToRemove = extractIndex(inputsToConvertToCommand); + Folder folderToRemoveFrom = extractFolder(inputsToConvertToCommand); + + return new DeletePersonFromFolderCommand(indexToRemove, folderToRemoveFrom); + } + + /** + * Sieves out the folder name string and creates and folder object. + * @param inputsToConvertToCommand String[] of index and folder name as string. + * @return Folder as a new folder object created + * @throws ParseException if given folder name is invalid. + */ + private Folder extractFolder(String[] inputsToConvertToCommand) throws ParseException { + String folderNameString = inputsToConvertToCommand[1]; + FolderName currentFolder = ParserUtil.parseFolderName(folderNameString); + return new Folder(currentFolder); + } + + /** + * Sieves out the index string and creates and Index object. + * @param inputsToConvertToCommand String[] of index and folder name as string. + * @return Index object. + * @throws ParseException if the given index given is invalid. + */ + private Index extractIndex( + String[] inputsToConvertToCommand) throws ParseException { + String indexString = inputsToConvertToCommand[0]; + return ParserUtil.parseIndex(indexString, new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeletePersonFromFolderCommand.MESSAGE_USAGE))); + } + + /** + * Segments the joined string of folder name and index into + * individual {@code String[]} objects. + * @param indexAndFolderJoined {@code String} Inputs of + * folder names and index joined together. + * @return An array of Index and folder name string {@code String[]} + * @throws ParseException if either index or folder names are + * of an invalid format. + */ + private String[] extractIndividualFolderAndIndex( + String indexAndFolderJoined) throws ParseException { + int inputsLength = indexAndFolderJoined.length(); + String[] inputsOfIndexAndFolder = indexAndFolderJoined + .split(DeletePersonFromFolderCommand.COMMAND_IDENTIFIER); + + boolean startsWithInvalidInput = indexAndFolderJoined + .startsWith(DeletePersonFromFolderCommand.COMMAND_IDENTIFIER); + boolean endsWithInvalidInput = indexAndFolderJoined + .startsWith(DeletePersonFromFolderCommand.COMMAND_IDENTIFIER, + inputsLength - 2); + boolean isInvalidInputLength = inputsOfIndexAndFolder.length != 2; + + if (startsWithInvalidInput || endsWithInvalidInput || isInvalidInputLength) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeletePersonFromFolderCommand.MESSAGE_USAGE)); + } + + return inputsOfIndexAndFolder; + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..c62b800e796 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -35,13 +35,36 @@ public EditCommand parse(String args) throws ParseException { ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); Index index; + String preamble = argMultimap.getPreamble(); - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + + //Checks boundary cases + if (preamble.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + char firstElement = preamble.charAt(0); + if (preamble.length() == 1 && firstElement == 45) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + // Check for - symbol as first element (or negative index) + if (firstElement == 45) { + throw new ParseException(String.format(ParserUtil.MESSAGE_INVALID_INDEX, preamble)); + } + + // Check for String input as person index + for (int i = 0; i < preamble.length(); i++) { + char element = preamble.charAt(i); + if (!Character.isDigit(element)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditCommand.MESSAGE_USAGE)); + } } + index = ParserUtil.parseIndex(preamble, new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditCommand.MESSAGE_USAGE))); + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); diff --git a/src/main/java/seedu/address/logic/parser/EditFolderNameCommandParser.java b/src/main/java/seedu/address/logic/parser/EditFolderNameCommandParser.java new file mode 100644 index 00000000000..f49508b0be0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditFolderNameCommandParser.java @@ -0,0 +1,64 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.EditFolderNameCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; + +public class EditFolderNameCommandParser implements Parser { + + private static final String EDIT_COMMAND_SEPARATOR = "|"; + public static final String REGEX_FOR_MULTIPLE_FOLDER_SPLIT = "\\" + + EDIT_COMMAND_SEPARATOR; + + /** + * Parses the given {@code String} of arguments in the + * context of the EditFolderNameCommand + * and returns an EditFolderNameCommand object for execution. + * @throws ParseException if the given {@code folderName} is invalid. + */ + public EditFolderNameCommand parse(String args) throws ParseException { + if (args.length() == 0) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, EditFolderNameCommand.MESSAGE_USAGE)); + } + FolderName[] folderNames = extractIndividualFolders(args); + assert folderNames.length == 2 : "Should have exactly 2 folders only!"; + + return new EditFolderNameCommand(new Folder(folderNames[0]), new Folder(folderNames[1])); + } + + /** + * Segments the joined string of folder name into + * individual {@code FolderName[]} objects. + * @param folderStringNames {@code String} Inputs of multiple + * folder names joined together. + * @return An array of folder names {@code FolderName[]} + * @throws ParseException if the 2 given {@code folderNames} are + * of an invalid format. + */ + private FolderName[] extractIndividualFolders(String folderStringNames) throws ParseException { + int foldersLength = folderStringNames.length(); + String[] folders = folderStringNames.split(REGEX_FOR_MULTIPLE_FOLDER_SPLIT); + + boolean startsWithInvalidInput = folderStringNames.startsWith(EDIT_COMMAND_SEPARATOR); + boolean endsWithInvalidInput = folderStringNames.startsWith(EDIT_COMMAND_SEPARATOR, + foldersLength - 1); + boolean isInvalidInputLength = folders.length != 2; + if (startsWithInvalidInput || endsWithInvalidInput || isInvalidInputLength) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditFolderNameCommand.MESSAGE_USAGE)); + } + + FolderName[] allFolderNames = new FolderName[2]; + for (int k = 0; k < folders.length; k++) { + FolderName currentFolder = ParserUtil.parseFolderName(folders[k]); + allFolderNames[k] = currentFolder; + } + + return allFolderNames; + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 4fb71f23103..6b97fc12091 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -4,6 +4,7 @@ import java.util.Arrays; +import seedu.address.commons.util.StringUtil; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; @@ -19,7 +20,7 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); + String trimmedArgs = StringUtil.stripFlags(args.trim()); if (trimmedArgs.isEmpty()) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); diff --git a/src/main/java/seedu/address/logic/parser/FindFoldersCommandParser.java b/src/main/java/seedu/address/logic/parser/FindFoldersCommandParser.java new file mode 100644 index 00000000000..84f052f4960 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindFoldersCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.FindFoldersCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.FolderNameContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindFoldersCommand object + */ +public class FindFoldersCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindFoldersCommand + * and returns a FindFoldersCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindFoldersCommand parse(String args) throws ParseException { + String trimmedArgs = StringUtil.stripFlags(args.trim()); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindFoldersCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindFoldersCommand(new FolderNameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..6146e4bb867 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,6 +9,7 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.folder.FolderName; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -20,17 +21,40 @@ */ public class ParserUtil { - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - + public static final String MESSAGE_INVALID_INDEX = "The index '%1$s' is invalid. It should be a positive integer"; + public static final String MESSAGE_OVERFLOW_INTEGER = "UNIon is unable to handle such a large integer"; /** * 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 { + public static Index parseIndex(String oneBasedIndex, ParseException exception) throws ParseException { String trimmedIndex = oneBasedIndex.trim(); + if (trimmedIndex.equals("")) { + throw exception; + } + char firstElement = trimmedIndex.charAt(0); + + if (firstElement == 45 && trimmedIndex.length() == 1) { + throw exception; + } + + //Check for - symbol or 0 as first element + if (firstElement == 45 || firstElement == 48) { + throw new ParseException(String.format(MESSAGE_INVALID_INDEX, trimmedIndex)); + } + + //Check if all values are integers only + for (int i = 0; i < trimmedIndex.length(); i++) { + char element = trimmedIndex.charAt(i); + if (!Character.isDigit(element)) { + throw exception; + } + } + + //Check if integer input is too big if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); + throw new ParseException(MESSAGE_OVERFLOW_INTEGER); } return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } @@ -50,6 +74,21 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses a {@code String name} into a {@code FolderName}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code folderName} is invalid. + */ + public static FolderName parseFolderName(String folderName) throws ParseException { + requireNonNull(folderName); + String trimmedName = folderName.trim(); + if (!FolderName.isValidName(trimmedName)) { + throw new ParseException(FolderName.MESSAGE_CONSTRAINTS); + } + return new FolderName(trimmedName); + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/address/logic/parser/Prefix.java index c859d5fa5db..6ae76a7283f 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/seedu/address/logic/parser/Prefix.java @@ -2,7 +2,7 @@ /** * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. + * E.g. '-t' in 'add James -t friend'. */ public class Prefix { private final String prefix; @@ -16,7 +16,7 @@ public String getPrefix() { } public String toString() { - return getPrefix(); + return getPrefix() + " "; } @Override diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..944c3daebab 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -1,21 +1,25 @@ package seedu.address.model; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.List; import javafx.collections.ObservableList; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; +import seedu.address.model.folder.UniqueFolderList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed (by .isSamePerson and .isSameFolder comparison) */ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; - + private final UniqueFolderList folders; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html @@ -25,18 +29,49 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + folders = new UniqueFolderList(); } public AddressBook() {} /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Persons and Folders in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); resetData(toBeCopied); } + //// static operations + + /** + * Returns a new address book with empty folders with the copied names from model + * @param model model to copy folder names from + * @return address book with copied folders + */ + public static AddressBook withFolders(Model model) { + ReadOnlyAddressBook addressBook = model.getAddressBook(); + ObservableList folders = addressBook.getFolderList(); + AddressBook newAddressBook = new AddressBook(); + for (Folder folder : folders) { + newAddressBook.addFolder(new Folder(folder.getFolderName())); + } + return newAddressBook; + } + + /** + * Returns a new address book with contacts and zero folders + * @param model model to copy contacts from + * @return address book with contacts + */ + public static AddressBook withContacts(Model model) { + ReadOnlyAddressBook addressBook = model.getAddressBook(); + ObservableList contactList = addressBook.getPersonList(); + AddressBook newAddressBook = new AddressBook(); + newAddressBook.setPersons(contactList); + return newAddressBook; + } + //// list overwrite operations /** @@ -47,6 +82,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the folder list with {@code folders}. + * {@code folders} must not contain duplicate folders. + */ + public void setFolders(List folders) { + this.folders.setFolders(folders); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -54,12 +97,13 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setFolders(newData.getFolderList()); } //// person-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same identity as {@code person} exists in UNIon. */ public boolean hasPerson(Person person) { requireNonNull(person); @@ -67,8 +111,8 @@ public boolean hasPerson(Person person) { } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a person to UNIon. + * The person must not already exist in UNIon. */ public void addPerson(Person p) { persons.add(p); @@ -76,23 +120,102 @@ public void addPerson(Person p) { /** * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * {@code target} must exist in UNIon. + * The person identity of {@code editedPerson} must not be the same as another existing person in UNIon. */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); + // update folder if necessary + folders.updateAllFolders(target, editedPerson); } /** * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. + * {@code key} must exist in UNIon. */ public void removePerson(Person key) { persons.remove(key); + // update folder if necessary + folders.updateAllFolders(key, null); } + //// folder-level operations + + /** + * Returns true if a folder with the same identity as {@code folder} exists in UNIon. + */ + public boolean hasFolder(Folder folder) { + requireNonNull(folder); + return folders.contains(folder); + } + + /** + * Replaces the given folder {@code oldFolder} in the list with {@code newFolder}. + * {@code oldFolder} must exist in UNIon. + * The folder identity of {@code newFolder} must not be the same as + * another existing folder in UNIon. + */ + public void setNewFolder(Folder oldFolder, Folder newFolder) { + requireNonNull(newFolder); + + folders.setFolder(oldFolder, newFolder); + } + + /** + * Checks and returns true if folder already exists + */ + public boolean hasFolderName(FolderName folderName) { + requireNonNull(folderName); + return folders.containsFolderName(folderName); + } + + /** + * Checks and returns true if person has already been added to folder + */ + public boolean folderContainsPerson(Person target, FolderName name) { + requireAllNonNull(target, name); + return folders.folderContainsPerson(target, name); + } + + /** + * Adds a folder to UNIon. + * The folder must not already exist in UNIon. + */ + public void addFolder(Folder f) { + folders.add(f); + } + + /** + * Adds a Contact to the Folder. + * The contact must not already exist in the Folder. + */ + public void addContactToFolder(Person target, FolderName folderName) { + requireAllNonNull(target, folderName); + folders.addContact(target, folderName); + } + + /** + * Removes contact of specified index. + * Contact index and folder must exist. + * @param personToRemove contact to be removed. + * @param targetFolder folder from which contact is to be removed. + */ + public void deletePersonFromIndex( + Person personToRemove, + Folder targetFolder) { + folders.removeFromFolderIndex(personToRemove, targetFolder); + } + + /** + * Deletes a folder in UNIon. + * The folder must already exist in UNIon. + */ + public void deleteFolder(Folder f) { + folders.remove(f); + } + + //// util methods @Override @@ -106,6 +229,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getFolderList() { + return folders.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..d83ecb77a72 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -5,6 +5,8 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; import seedu.address.model.person.Person; /** @@ -13,6 +15,7 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_FOLDERS = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -53,26 +56,26 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same identity as {@code person} exists in UNIon. */ boolean hasPerson(Person person); /** * Deletes the given person. - * The person must exist in the address book. + * The person must exist in UNIon. */ void deletePerson(Person target); /** * Adds the given person. - * {@code person} must not already exist in the address book. + * {@code person} must not already exist in UNIon. */ void addPerson(Person person); /** * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * {@code target} must exist in UNIon. + * The person identity of {@code editedPerson} must not be the same as another existing person in UNIon. */ void setPerson(Person target, Person editedPerson); @@ -84,4 +87,60 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** Returns an unmodifiable view of the filtered folder list */ + ObservableList getFilteredFolderList(); + + /** + * Updates the filter of the filtered folder list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredFolderList(Predicate predicate); + + /** + * Adds the given folder. + * {@code folder} must not already exist in UNIon. + */ + void addFolder(Folder folder); + + /** + * Replaces {@code oldFolder} with {@code newFolder}. + * {@code oldFolder} must exist in UNIon and + * {@code newFolder} must not already exist. + */ + void setNewFolder(Folder oldFolder, Folder newFolder); + + /** + * Adds a Contact to the Folder. + * The contact must not already exist in the Folder. + */ + public void addContactToFolder(Person target, FolderName name); + + /** + * Deletes a contact from its Folder. + * The contact must already exist in the Folder amd + * Folder must already exist as well. + */ + public void deletePersonFromFolder(Person person, Folder folder); + + /** + * Checks and returns true if folder already exists + */ + boolean hasFolderName(FolderName name); + + /** + * Checks and returns true if person has already been added to folder + */ + boolean folderContainsPerson(Person target, FolderName name); + + /** + * Returns true if a folder with the same identity as {@code folder} exists in UNIon. + */ + boolean hasFolder(Folder folder); + + /** + * Deletes the specified folder. + * {@code folder} must already exist in UNIon. + */ + void deleteFolder(Folder folder); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 0650c954f5c..414edd939ca 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -11,10 +11,12 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; import seedu.address.model.person.Person; /** - * Represents the in-memory model of the address book data. + * Represents the in-memory model of UNIon data. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); @@ -22,6 +24,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredFolders; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -35,6 +38,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredFolders = new FilteredList<>(this.addressBook.getFolderList()); } public ModelManager() { @@ -112,6 +116,60 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + @Override + public void addFolder(Folder folder) { + addressBook.addFolder(folder); + updateFilteredFolderList(PREDICATE_SHOW_ALL_FOLDERS); + } + + /** + * Deletes a folder + */ + public void deleteFolder(Folder folder) { + addressBook.deleteFolder(folder); + updateFilteredFolderList(PREDICATE_SHOW_ALL_FOLDERS); + } + + @Override + public void setNewFolder(Folder oldFolder, Folder newFolder) { + requireAllNonNull(oldFolder, newFolder); + addressBook.setNewFolder(oldFolder, newFolder); + updateFilteredFolderList(PREDICATE_SHOW_ALL_FOLDERS); + + } + + @Override + public boolean hasFolder(Folder folder) { + requireNonNull(folder); + return addressBook.hasFolder(folder); + } + + @Override + public void deletePersonFromFolder( + Person personToRemove, + Folder targetFolder) { + requireAllNonNull(personToRemove, targetFolder); + addressBook.deletePersonFromIndex(personToRemove, targetFolder); + } + + @Override + public boolean hasFolderName(FolderName folderName) { + requireNonNull(folderName); + return addressBook.hasFolderName(folderName); + } + + @Override + public boolean folderContainsPerson(Person target, FolderName name) { + requireAllNonNull(target, name); + return addressBook.folderContainsPerson(target, name); + } + + @Override + public void addContactToFolder(Person target, FolderName folderName) { + requireAllNonNull(target, folderName); + addressBook.addContactToFolder(target, folderName); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -129,6 +187,23 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + //=========== Filtered Folder List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Folder} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredFolderList() { + return filteredFolders; + } + + @Override + public void updateFilteredFolderList(Predicate predicate) { + requireNonNull(predicate); + filteredFolders.setPredicate(predicate); + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -145,7 +220,8 @@ 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) + && filteredFolders.equals(other.filteredFolders); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..3f8373a6afb 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.folder.Folder; import seedu.address.model.person.Person; /** @@ -14,4 +15,9 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the folders list. + * This list will not contain any duplicate folders. + */ + ObservableList getFolderList(); } diff --git a/src/main/java/seedu/address/model/folder/Folder.java b/src/main/java/seedu/address/model/folder/Folder.java new file mode 100644 index 00000000000..b1079f997a0 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/Folder.java @@ -0,0 +1,120 @@ +package seedu.address.model.folder; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.model.person.Person; + +/** + * Represents a Folder in UNIon. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Folder { + + // Identity fields + private final FolderName folderName; + + // Data fields + private final Set contacts; + + /** + * Every field must be present and not null. + */ + public Folder(FolderName folderName, Set contacts) { + requireAllNonNull(folderName, contacts); + + this.folderName = folderName; + this.contacts = contacts; + } + + /** + * Every field must be present and not null. + */ + public Folder(FolderName folderName) { + requireAllNonNull(folderName); + + this.folderName = folderName; + this.contacts = new HashSet<>(); + } + + public FolderName getFolderName() { + return this.folderName; + } + + public Set getContacts() { + return this.contacts; + } + + /** + * Checks if folder contains a specific person + * + * @param target + * @return true if person exists in folder + */ + public boolean hasContact(Person target) { + for (Person contact : contacts) { + if (contact.equals(target)) { + return true; + } + } + return false; + } + + /** + * Removes index of contact from the folder. + * @param personToRemove contact to be removed. + */ + public void removePerson(Person personToRemove) { + this.getContacts().remove(personToRemove); + } + + + public void addContacts(Person contact) { + this.contacts.add(contact); + } + + public void setAll(Folder oldFolder) { + this.contacts.addAll(oldFolder.contacts); + } + + /** + * Returns true if both folders have the same name. + * This defines a weaker notion of equality between two folders. + */ + public boolean isSameFolder(Folder otherFolder) { + if (otherFolder == this) { + return true; + } + + return otherFolder != null + && otherFolder.getFolderName().equals(getFolderName()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Folder)) { + return false; + } + + Folder folder = (Folder) other; + return Objects.equals(folderName, folder.folderName); + } + + @Override + public int hashCode() { + return Objects.hash(folderName, contacts); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getFolderName()); + return builder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/folder/FolderName.java b/src/main/java/seedu/address/model/folder/FolderName.java new file mode 100644 index 00000000000..b1370efbd53 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/FolderName.java @@ -0,0 +1,61 @@ +package seedu.address.model.folder; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Folder's name in UNIon. + * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} + */ +public class FolderName { + + public static final String MESSAGE_CONSTRAINTS = + "Names used in the command should only contain" + + " alphanumeric characters and spaces," + + " and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + public final String folderName; + + /** + * Constructs a {@code FolderName}. + * + * @param name A valid folder name. + */ + public FolderName(String name) { + requireNonNull(name); + checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); + folderName = name; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidName(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return folderName; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FolderName // instanceof handles nulls + && folderName.equals(((FolderName) other).folderName)); // state check + } + + @Override + public int hashCode() { + return folderName.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/folder/FolderNameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/folder/FolderNameContainsKeywordsPredicate.java new file mode 100644 index 00000000000..fc79d22cf7a --- /dev/null +++ b/src/main/java/seedu/address/model/folder/FolderNameContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.folder; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Folder}'s {@code Name} matches any of the keywords given. + */ +public class FolderNameContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public FolderNameContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Folder folder) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsTextIgnoreCase(folder.getFolderName().folderName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FolderNameContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((FolderNameContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/folder/UniqueFolderList.java b/src/main/java/seedu/address/model/folder/UniqueFolderList.java new file mode 100644 index 00000000000..4edc102f736 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/UniqueFolderList.java @@ -0,0 +1,231 @@ +package seedu.address.model.folder; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.folder.exceptions.DuplicateFolderException; +import seedu.address.model.folder.exceptions.DuplicatePersonInFolderException; +import seedu.address.model.folder.exceptions.FolderNotFoundException; +import seedu.address.model.person.Person; + +/** + * A list of folders that enforces uniqueness between its elements and does not allow nulls. + * A Folder is considered unique by comparing using {@code Folder#isSameFolder(Folder)}. As such, adding and updating of + * folders uses Folder#isSameFolder(Folder) for equality so as to ensure that the folder being added or updated is + * unique in terms of identity in the UniqueFolderList. However, the removal of a folder uses Folder#equals(Object) so + * as to ensure that the folder with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Folder#isSameFolder(Folder) + */ +public class UniqueFolderList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent folder as the given argument. + */ + public boolean contains(Folder toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameFolder); + } + + /** + * Returns true if the list contains a folder with the same folder name. + */ + public boolean containsFolderName(FolderName name) { + requireNonNull(name); + for (int i = 0; i < internalList.size(); i++) { + Folder folder = internalList.get(i); + if (folder.getFolderName().equals(name)) { + return true; + } + } + return false; + } + + + /** + * Checks and returns true if person has already been added to folder + */ + public boolean folderContainsPerson(Person person, FolderName name) { + requireAllNonNull(person, name); + for (int i = 0; i < internalList.size(); i++) { + Folder folder = internalList.get(i); + if (folder.getFolderName().equals(name) && folder.hasContact(person)) { + return true; + } + } + return false; + } + + /** + * Adds a folder to the list. + * The folder must not already exist in the list. + */ + public void add(Folder toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateFolderException(); + } + internalList.add(toAdd); + } + + /** + * Adds a contact to the folder. + * The contact must not already exist in the folder. + */ + public void addContact(Person contact, FolderName name) { + requireNonNull(contact); + for (int i = 0; i < internalList.size(); i++) { + Folder folder = internalList.get(i); + if (folderContainsPerson(contact, name)) { + throw new DuplicatePersonInFolderException(); + } + if (folder.getFolderName().equals(name)) { + Set newContactList = folder.getContacts(); + newContactList.add(contact); + Folder newFolder = new Folder(name, newContactList); + setFolder(folder, newFolder); + return; + } + } + } + + /** + * Replaces the folder {@code target} in the list with {@code editedFolder}. + * {@code target} must exist in the list. + * The folder identity of {@code editedFolder} must not be + * the same as another existing folder in the list. + */ + public void setFolder(Folder target, Folder editedFolder) { + requireAllNonNull(target, editedFolder); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new FolderNotFoundException(); + } + + if (!target.isSameFolder(editedFolder) && contains(editedFolder)) { + throw new DuplicateFolderException(); + } + + editedFolder.setAll(internalList.get(index)); + internalList.set(index, editedFolder); + } + + /** + * Deletes contact from the specified folder. + * @param personToRemove contact in folder to be removed. + * @param targetFolder folder in which contact is removed from. + */ + public void removeFromFolderIndex( + Person personToRemove, + Folder targetFolder) { + int indexOfFolder = internalList.indexOf(targetFolder); + + if (indexOfFolder == -1) { + throw new FolderNotFoundException(); + } + + Folder actualFolder = internalList.get(indexOfFolder); + actualFolder.removePerson(personToRemove); + targetFolder.setAll(actualFolder); + internalList.set(indexOfFolder, targetFolder); + } + + /** + * Removes the equivalent folder from the list. + * The folder must exist in the list. + */ + public void remove(Folder toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new FolderNotFoundException(); + } + } + + public void setFolders(UniqueFolderList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code folders}. + * {@code folders} must not contain duplicate folders. + */ + public void setFolders(List listFolders) { + requireAllNonNull(listFolders); + if (!foldersAreUnique(listFolders)) { + throw new DuplicateFolderException(); + } + + internalList.setAll(listFolders); + } + + /** + * Updates all the folders when contact is updated, either edit or delete. + * @param originalPerson person that is going to be updated. + * @param updatedPerson person that is updated. + */ + public void updateAllFolders(Person originalPerson, Person updatedPerson) { + for (Folder oldFolder : internalList) { + Set contacts = oldFolder.getContacts(); + if (updatedPerson != null && contacts.contains(originalPerson)) { + contacts.add(updatedPerson); + } + contacts.removeIf(contact -> contact.equals(originalPerson)); + Folder newFolder = new Folder(oldFolder.getFolderName(), contacts); + this.setFolder(oldFolder, newFolder); + } + } + + /** + * 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 UniqueFolderList // instanceof handles nulls + && internalList.equals(((UniqueFolderList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code folders} contains only unique folders. + */ + private boolean foldersAreUnique(List listFolders) { + for (int i = 0; i < listFolders.size() - 1; i++) { + for (int j = i + 1; j < listFolders.size(); j++) { + if (listFolders.get(i).isSameFolder(listFolders.get(j))) { + return false; + } + } + } + return true; + } + + +} diff --git a/src/main/java/seedu/address/model/folder/exceptions/DuplicateFolderException.java b/src/main/java/seedu/address/model/folder/exceptions/DuplicateFolderException.java new file mode 100644 index 00000000000..cd4d1d1f358 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/exceptions/DuplicateFolderException.java @@ -0,0 +1,11 @@ +package seedu.address.model.folder.exceptions; + +/** + * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same + * identity). + */ +public class DuplicateFolderException extends RuntimeException { + public DuplicateFolderException() { + super("Operation would result in duplicate folders"); + } +} diff --git a/src/main/java/seedu/address/model/folder/exceptions/DuplicatePersonInFolderException.java b/src/main/java/seedu/address/model/folder/exceptions/DuplicatePersonInFolderException.java new file mode 100644 index 00000000000..155bb948f53 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/exceptions/DuplicatePersonInFolderException.java @@ -0,0 +1,11 @@ +package seedu.address.model.folder.exceptions; + +/** + * Signals that the contact already exists in folder (Persons are considered duplicates if they have the same + * identity). + */ +public class DuplicatePersonInFolderException extends RuntimeException { + public DuplicatePersonInFolderException() { + super("Folder already contains contact"); + } +} diff --git a/src/main/java/seedu/address/model/folder/exceptions/FolderNotFoundException.java b/src/main/java/seedu/address/model/folder/exceptions/FolderNotFoundException.java new file mode 100644 index 00000000000..a78078242c3 --- /dev/null +++ b/src/main/java/seedu/address/model/folder/exceptions/FolderNotFoundException.java @@ -0,0 +1,10 @@ +package seedu.address.model.folder.exceptions; + +/** + * Signals that the operation is unable to find the specified folder. + */ +public class FolderNotFoundException extends RuntimeException { + public FolderNotFoundException() { + super("Folder not found. Please try again."); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 60472ca22a0..570af128361 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. + * Represents a Person's address in UNIon. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index f866e7133de..cc578a95be3 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. + * Represents a Person's email in UNIon. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 79244d71cf7..8fc627449b9 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Person's name in UNIon. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8ff1d83fe89..73a065631ec 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -10,7 +10,7 @@ import seedu.address.model.tag.Tag; /** - * Represents a Person in the address book. + * Represents a Person in UNIon. * Guarantees: details are present and not null, field values are validated, immutable. */ public class Person { diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index 872c76b382f..e556693301d 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's phone number in the address book. + * Represents a Person's phone number in UNIon. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..6b080736d99 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Tag in the address book. + * Represents a Tag in UNIon. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ public class Tag { diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..a7a870886ac 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -17,6 +17,7 @@ * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), diff --git a/src/main/java/seedu/address/storage/JsonAdaptedFolder.java b/src/main/java/seedu/address/storage/JsonAdaptedFolder.java new file mode 100644 index 00000000000..a79fe912311 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedFolder.java @@ -0,0 +1,69 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.folder.Folder; +import seedu.address.model.folder.FolderName; +import seedu.address.model.person.Person; + +/** + * Jackson-friendly version of {@link Folder}. + */ +public class JsonAdaptedFolder { + + public static final String FOLDER_MISSING_FIELD_MESSAGE_FORMAT = "Folder's %s field is missing!"; + + private final String folderName; + private final List contacts = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedFolder} with the given folder details. + */ + public JsonAdaptedFolder(@JsonProperty("folderName") String folderName, + @JsonProperty("contacts") List contacts) { + this.folderName = folderName; + if (contacts != null) { + this.contacts.addAll(contacts); + } + } + + /** + * Converts a given {@code Folder} into this class for Jackson use. + */ + public JsonAdaptedFolder(Folder source) { + this.folderName = source.getFolderName().toString(); + this.contacts.addAll(source.getContacts().stream() + .map(JsonAdaptedPerson::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted folder object into the model's {@code Folder} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted folder. + */ + public Folder toModelType() throws IllegalValueException { + final List folderContacts = new ArrayList<>(); + for (JsonAdaptedPerson contact : this.contacts) { + folderContacts.add(contact.toModelType()); + } + + if (this.folderName == null) { + throw new IllegalValueException(String.format(FOLDER_MISSING_FIELD_MESSAGE_FORMAT, + FolderName.class.getSimpleName())); + } + if (!FolderName.isValidName(this.folderName)) { + throw new IllegalValueException(FolderName.MESSAGE_CONSTRAINTS); + } + final FolderName modelFolderName = new FolderName(this.folderName); + final Set modelContacts = new HashSet<>(folderContacts); + return new Folder(modelFolderName, modelContacts); + } +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..18058356959 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.folder.Folder; import seedu.address.model.person.Person; /** @@ -20,15 +21,20 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_FOLDER = "Folders list contains duplicate folder(s)."; + private final List persons = new ArrayList<>(); + private final List folders = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and folders. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("folders") List folders) { this.persons.addAll(persons); + this.folders.addAll(folders); } /** @@ -38,6 +44,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + + private static final String FXML = "FolderListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Folder folder; + + @FXML + private HBox cardPane; + @FXML + private Label folderName; + @FXML + private Label id; + @FXML + private FlowPane contacts; + + /** + * Creates a {@code FolderCode} with the given {@code Folder} and index to display. + */ + public FolderCard(Folder folder, int displayedIndex) { + super(FXML); + this.folder = folder; + id.setText(displayedIndex + ". "); + folderName.setText(folder.getFolderName().toString()); + folder.getContacts() + .forEach(person -> contacts.getChildren().add(new Label(person.getName().toString()))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FolderCard)) { + return false; + } + + // state check + FolderCard card = (FolderCard) other; + return id.getText().equals(card.id.getText()) + && folder.equals(card.folder); + } +} diff --git a/src/main/java/seedu/address/ui/FolderListPanel.java b/src/main/java/seedu/address/ui/FolderListPanel.java new file mode 100644 index 00000000000..97bac4487a0 --- /dev/null +++ b/src/main/java/seedu/address/ui/FolderListPanel.java @@ -0,0 +1,51 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.folder.Folder; + +/** + * Panel containing the list of folders. + */ + +public class FolderListPanel extends UiPart { + private static final String FXML = "FolderListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(FolderListPanel.class); + + @FXML + private ListView folderListView; + + /** + * Creates a {@code FolderListPanel} + * with the given {@code ObservableList}. + */ + public FolderListPanel(ObservableList folderList) { + super(FXML); + folderListView.setItems(folderList); + folderListView.setCellFactory(listView -> new FolderListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Folder} using a {@code FolderCard}. + */ + class FolderListViewCell extends ListCell { + @Override + protected void updateItem(Folder folder, boolean empty) { + super.updateItem(folder, empty); + + if (empty || folder == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new FolderCard(folder, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..a976cb37c8d 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,8 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = + "https://ay2122s1-cs2103-t16-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..b1104233e42 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -32,6 +32,7 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private FolderListPanel folderListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -44,6 +45,9 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane folderListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -78,6 +82,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) { @@ -113,6 +118,9 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + folderListPanel = new FolderListPanel(logic.getFilteredFolderList()); + folderListPanelPlaceholder.getChildren().add(folderListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -172,7 +180,8 @@ public PersonListPanel getPersonListPanel() { * * @see seedu.address.logic.Logic#execute(String) */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + private CommandResult executeCommand(String commandText) throws CommandException, + ParseException { try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 882027e4537..0fe095f3e7a 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/UNIon_logo.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/UNIon_logo.png b/src/main/resources/images/UNIon_logo.png new file mode 100644 index 00000000000..da04d151326 Binary files /dev/null and b/src/main/resources/images/UNIon_logo.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 9ce9bcfb569..f47ca655fca 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -350,3 +350,18 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#contacts { + -fx-hgap: 7; + -fx-vgap: 3; + -fx-padding: 6 0 0 0; +} + +#contacts .label { + -fx-text-fill: white; + -fx-background-color: #17b978; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 12; +} diff --git a/src/main/resources/view/FolderListCard.fxml b/src/main/resources/view/FolderListCard.fxml new file mode 100644 index 00000000000..b0388a3c2a0 --- /dev/null +++ b/src/main/resources/view/FolderListCard.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/FolderListPanel.fxml b/src/main/resources/view/FolderListPanel.fxml new file mode 100644 index 00000000000..b113b369eb0 --- /dev/null +++ b/src/main/resources/view/FolderListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 32bcf2c8e70..d3b61a6f26d 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -11,51 +11,65 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + title="UNIon - Manage your wide range of college contacts easily" minWidth="450" minHeight="600" + onCloseRequest="#handleExit"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..601c1923403 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -4,6 +4,6 @@ -