diff --git a/README.md b/README.md
index 16208adb9b6..4a8a9ac2e4e 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,23 @@
-[](https://github.com/se-edu/addressbook-level3/actions)
-
+[](https://github.com/AY2425S2-CS2103T-W13-1/tp/actions)
+# ScoopBook

-* 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/#contributing-to-se-edu) for more info.
+# Overview
+ScoopBook is a lightweight **desktop application** to manage your contacts. Specialised and catered to journalists seeking a faster and offline solution to efficiently manage their contacts.
+
+## Features
+ScoopBook provides journalists with a simple and efficient way to manage their contacts. It allows users to:
+* Add and manage contacts
+* Tag contacts for better organisation
+* Add notes for each contact to keep track of important information
+* Import and export contacts for easy transfer
+
+For more information, check out the [ScoopBook Project Website](https://ay2425s2-cs2103t-w13-1.github.io/tp/).
+
+## Authors
+Find out more about us on our [About Us](https://ay2425s2-cs2103t-w13-1.github.io/tp/AboutUs.html) page!
+
+## Acknowledgements
+This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org).
+We would also like to thank all professors, TAs, coursemates and friends who have helped and supported us throughout the project.
+
diff --git a/build.gradle b/build.gradle
index 0db3743584e..948a0008b32 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,7 +66,11 @@ dependencies {
}
shadowJar {
- archiveFileName = 'addressbook.jar'
+ archiveFileName = 'ScoopBook.jar'
+}
+
+run {
+ enableAssertions = true
}
defaultTasks 'clean', 'test'
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index ff3f04abd02..6910a7ce787 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -5,55 +5,52 @@ title: About Us
We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg).
-You can reach us at the email `seer[at]comp.nus.edu.sg`
+You can reach us at the email `shawn.goh@u.nus.edu`
## Project team
-### John Doe
+### Shawn Goh
-
+
-[[homepage](http://www.comp.nus.edu.sg/~damithch)]
-[[github](https://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[homepage](https://shawnnygoh.github.io/)]
+[[github](https://github.com/shawnnygoh)]
-* Role: Project Advisor
+* Role: Deliverables and Deadlines
-### Jane Doe
+### Freddie Ong
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/souledfigurine)]
-* Role: Team Lead
-* Responsibilities: UI
+* Role: Testing
+* Responsibilities: unconfirmed
-### Johnny Doe
+### Chua Wen Ting
-
+
-[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)]
+[[github](http://github.com/wentingchua)]
* Role: Developer
-* Responsibilities: Data
+* Responsibilities: Scheduling and tracking: In charge of defining, assigning, and tracking project tasks.
-### Jean Doe
+### Soh Tze Jen
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/Meatsushi64)]
-* Role: Developer
-* Responsibilities: Dev Ops + Threading
-### James Doe
+* Role: Developer, Integration
+* Responsibilities: In addition to developing, also in charge of versioning of the code, maintaining the code repository, integrating various parts of the software to create a whole.
+
+### Timothy Tay (tim0tay)
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/tim0tay)]
* Role: Developer
-* Responsibilities: UI
+* Responsibilities: Code Quality
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 743c65a49d2..9d215647702 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -9,7 +9,9 @@ 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}
+* Libraries used: [_JavaFX_](https://openjfx.io/), [_JUnit5_](https://github.com/junit-team/junit5)
+* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org).
+* We would also like to thank all professors, TAs, PE Dry Run Testers, coursemates and friends who have helped and supported us throughout the project.
--------------------------------------------------------------------------------------------------------------------
@@ -127,6 +129,9 @@ The `Model` component,
* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects.
* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components)
+The `Person`,
+* requires a name, and at least one of the following fields: phone number, email, address.
+
: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.
@@ -155,6 +160,59 @@ Classes used by multiple components are in the `seedu.address.commons` package.
This section describes some noteworthy details on how certain features are implemented.
+### Notes feature
+
+The **Notes feature** allows users to write and save plain text notes for each contact. These notes can be opened in a separate window, edited at any time, and are saved to a file on the user's computer. This allows users to keep important information linked to specific people in their contact list.
+
+#### Implementation
+The Notes system spans across the UI, Logic and Storage layers of the application.
+
+#### UI Layer
+When the user executes the command `note INDEX`, the `MainWindow` receives the `CommandResult` from Logic containing the flag `isShowNote == true` and the `targetPerson`. It then calls: `handleNote(commandResult.getTargetPerson())`
+
+This method delegates to `NoteWindowHandler`, which manages the opening and closing of note windows.
+
+Each `NoteWindow` is uniquely associated with a `Person`. Only one note window can be opened per person at a time, tracked by a `Map`.
+
+When the window is initialized, `NoteWindow` calls `logic.readNote(person)` to load the existing note (if any), and displays it in a `TextArea`. When the window is closed, notes are automatically saved by calling `logic.saveNote(person, content)`.
+
+#### Logic Layer
+
+The `LogicManager` connects the UI and Storage layers for the Notes system. It exposes three key methods:
+* `readNote(Person person)`
+* `saveNote(Person person, String content)`
+* `deleteNote(Person person)`
+
+These are called directly by the UI when a `NoteWindow` is opened, closed, or deleted.
+
+Additionally, `LogicManager` performs note cleanup when specific commands are executed:
+* `DeleteCommand` → deletes the note for the removed person
+* `ClearCommand` and `ImportCommand` → delete all notes
+
+These cleanup operations are handled in `handleNoteOperations(Command command)`, which runs after each command execution.
+
+Below are the sequence diagrams for `NoteCommand` and `DeleteNoteCommand` to illustrate the flow of events
+
+1. Sequence diagram for **NoteCommand**:
+
+
+2. Sequence diagram for **DeleteNoteCommand**:
+
+#### Storage Layer
+
+The Notes system uses a dedicated `NotesStorage` interface, implemented by `FileNotesStorage`, to handle note persistence.
+
+Each note is saved as a `.txt` file in a designated notes directory, with filenames based on the `Person`'s unique ID (e.g., `12.txt`). This ensures that notes remain uniquely tied to each contact even if their name changes.
+
+The `FileNotesStorage` class provides three main methods:
+* `readNote(Person person)` — Returns the note content as a string if the file exists, or an empty string otherwise.
+* `saveNote(Person person, String content)` — Saves the note content to a file. If the file does not exist, it is created automatically.
+* `deleteNote(Person person)` — Deletes the note file associated with the given person.
+
+For bulk operations, `deleteAllNotes()` removes all note files from the directory.
+* triggered by `DeleteCommand`& `ClearCommand`.
+
+
### \[Proposed\] Undo/redo feature
#### Proposed Implementation
@@ -239,10 +297,6 @@ The following activity diagram summarizes what happens when a user executes a ne
_{more aspects and alternatives to be added}_
-### \[Proposed\] Data archiving
-
-_{Explain here how the data archiving feature will be implemented}_
-
--------------------------------------------------------------------------------------------------------------------
@@ -260,73 +314,537 @@ _{Explain here how the data archiving feature will be implemented}_
### Product scope
-**Target user profile**:
+**Target user profile**:
+* has a need to manage a significant number of contacts
+* prefers desktop apps over other types
+* can type fast
+* prefers typing to mouse interactions
+* is reasonably comfortable using CLI apps
+* requires a way to categorise and group contacts easily
+* requires a high level of privacy
-* has a need to manage a significant number of contacts
-* prefer desktop apps over other types
-* can type fast
-* prefers typing to mouse interactions
-* is reasonably comfortable using CLI apps
+**Value proposition**: manage contacts faster than a typical mouse/GUI-driven app, keep contacts safe
-**Value proposition**: manage contacts faster than a typical mouse/GUI driven app
-
-### User stories
+### 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 |
+| 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 contact | |
+| `* * *` | user | delete a contact | remove them if they are no longer relevant to my investigations |
+| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list |
+| `* * *` | user | tag contacts by credibility (e.g. verified, unverified, anonymous) | assess reliability quickly |
+| `* * *` | user | search for a contact by tags (e.g. topic, organization) | quickly find relevant contacts |
+| `* * *` | user | access previously saved contacts | contact people that I have saved the contacts of |
+| `* * *` | user | keep my contacts locally on my device | maintain the privacy of my contacts |
+| `* * *` | user | save the home address of a contact | know where to find them if required |
+| `* *` | user | edit previously saved contacts | change contacts if they have any different information |
+| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident |
+| `* *` | user | export my saved contacts to a different device and have them read easily by the same program | transfer my files to different devices easily |
+| `*` | user | log conversation notes with each contact | keep track of all my notes and critical information in one place |
+| `*` | user | view a conversation note | refer back to previously recorded conversation note |
+| `*` | user | delete conversation notes for each contact | delete conversation notes that are no longer relevant to prevent clutter |
+| `*` | user | export my notes to a .txt / .pdf file | share information with my team |
+| `*` | user | create keyboard shortcuts for frequently used actions | work faster |
+| `*` | user | view my existing keyboard shortcuts | remind myself what keyboard shortcuts I currently have |
+| `*` | user | delete an existing keyboard shortcut | update my keyboard shortcuts if I have a change of preference |
+| `*` | user | copy important pieces of information quickly | call or email my contacts |
+| `*` | user | see when a contact was saved | keep track of when I met the contact |
+| `*` | user | set follow-up reminders | do not miss out on getting updates from my contacts |
+| `*` | user | create investigations (groups) | group related contacts together |
+| `*` | user | add a profile photo for contacts | remember their faces too |
+
+
+
+### Use Cases
+
+**Use case: Ask for help - UC1**
+
+**MSS**
+
+1. User asks for help regarding instructions.
+2. ScoopBook directs User to a website with detailed instructions for each functionality.
+
+ Use case ends.
+
+**Use case: Add a contact - UC2**
+
+**MSS**
+
+1. User requests to add a contact with information into the list.
+2. ScoopBook adds contact into the list.
+
+ Use case ends.
+
+**Extensions**
-*{More to be added}*
+* 2a. The input given by User is invalid.
+ * 2a1. ScoopBook shows an error message.
-### Use cases
+ Use case resumes at step 1.
-(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise)
-**Use case: Delete a person**
+**Use case: View contacts - UC3**
**MSS**
-1. User requests to list persons
-2. AddressBook shows a list of persons
-3. User requests to delete a specific person in the list
-4. AddressBook deletes the person
+1. User requests to view list of contacts.
+2. ScoopBook shows a list of contacts.
- Use case ends.
+ Use case ends.
**Extensions**
-* 2a. The list is empty.
+* 2a. Contact list is empty.
Use case ends.
-* 3a. The given index is invalid.
- * 3a1. AddressBook shows an error message.
+
+**Use case: Delete a contact - UC4**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to delete a specific contact in the list.
+3. ScoopBook deletes the contact.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid, or the input is not a number.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Edit a contact - UC5**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to edit a specific contact in the list.
+3. ScoopBook edits that contact.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given input is invalid.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Add tag(s) to a contact - UC6**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to add tags to a specific contact in the list.
+3. ScoopBook adds tags to that contact.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid input.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Remove tag(s) to a contact - UC7**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to remove tag(s) from a specific contact in the list.
+3. ScoopBook removes the specified tags from the specified contact.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given index is invalid, or the input tag does not exist.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Search contact by name - UC8**
+
+**MSS**
+
+1. User requests to search for contacts based on keywords.
+2. ScoopBook displays a list of contacts whose name matches the keywords.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. No contact matches with the keywords.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Search contact by tag - UC9**
+
+**MSS**
+
+1. User requests to search for contacts based on tags.
+2. ScoopBook displays a list of contacts that match the tags.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. No contact matches with the tags.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Export contacts - UC10**
+
+**MSS**
+
+1. User requests to export contacts to a specified file location.
+2. ScoopBook saves the relevant contacts in the export file.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. User provided invalid file location.
+ * 1a1. Scoopbook raises an error
+
+ Use case ends.
+
+
+
+**Use case: Import contacts - UC11**
+
+**MSS**
+
+1. User requests to import contacts from a file location.
+2. ScoopBook imports the contacts from the import file.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. User provided invalid file location.
+ * 1a1. Scoopbook raises an error
+
+ Use case ends.
+
+* 1b. User provided file with invalid format
+ * 1b1. Scoopbook raises an error
+
+ Use case ends.
+
+
+
+**Use case: Create investigation - UC12 (To be implemented)**
+
+**MSS**
+
+1. User requests to create an investigation.
+2. ScoopBook creates the investigation.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The name of the investigation already exists.
+ * 1a1. ScoopBook informs User that there is already an investigation with the same name.
+
+ Use case ends.
+
+
+
+**Use case: Add contact to investigation - UC13 (To be implemented)**
+
+**MSS**
+
+1. User requests to add a contact to an existing investigation.
+2. ScoopBook adds the contact to the specified investigation.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The investigation specified by User does not exist.
+ * 1a1. ScoopBook informs User that no such investigation exists.
+
+ Use case ends.
+
+* 1b. The contact specified by User does not exist.
+ * 1b1. ScoopBook informs User that no such contact exists.
+
+ Use case ends.
+
+* 1c. Multiple contacts contain the keywords specified by User.
+ * 1c1. ScoopBook informs User that there are duplicates, and that the operation cannot be performed.
+
+ Use case ends.
+
+
+
+**Use case: Remove contact from investigation - UC14 (To be implemented)**
+
+**MSS**
+
+1. User requests to remove a contact from an existing investigation.
+2. ScoopBook removes the contact from the specified investigation.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The investigation specified by User does not exist.
+ * 1a1. ScoopBook informs User that no such investigation exists.
+
+ Use case ends.
+
+* 1b. The contact specified by User does not exist in the investigation.
+ * 1b1. ScoopBook informs User that no such contact exists.
+
+ Use case ends.
+
+* 1c. Multiple contacts or investigations contain the keywords specified by User.
+ * 1c1. ScoopBook informs User that there are duplicates, and that the operation cannot be performed.
+
+ Use case ends.
+
+
+
+**Use case: View and edit specific conversation note - UC15**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to view a specific conversation note from a specific contact.
+3. ScoopBook displays the specified conversation note to edit.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given conversation note index or contact index is invalid.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Delete conversation note - UC16**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to delete a specific conversation note by index.
+3. ScoopBook deletes the conversation note.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid input.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Export note - UC17 (To be implemented)**
+
+**MSS**
+
+1. User requests to view contact (UC3).
+2. User requests to export notes by index or export all for a specific contact.
+3. ScoopBook prompts the location to save the export file of notes in.
+4. User confirms the save location.
+5. ScoopBook saves the relevant notes in the export file.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. The given contact index or notes indexes are invalid.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+* 3a. User decides to cancel the operation.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: View keyboard shortcuts - UC18 (To be implemented)**
+
+**MSS**
+
+1. User requests to view keyboard shortcuts.
+2. ScoopBook displays all actions with keyboard shortcuts with their respective shortcuts.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. Keyboard shortcut list is empty.
+ * 2a1. ScoopBook displays an empty list.
+
+ Use case ends.
+
+
+
+**Use case: Create keyboard shortcuts - UC19 (To be implemented)**
+
+**MSS**
+
+1. User requests to view keyboard shortcuts (UC18).
+2. User requests to create keyboard shortcuts.
+3. ScoopBook displays available actions that can be assigned to a shortcut.
+4. User selects a specific action and specifies a key combination.
+5. ScoopBook assigns the shortcut.
+
+ Use case ends.
+
+**Extensions**
+
+* 4a. User provides an invalid action index.
+ * 4a1. ScoopBook shows an error message.
+
+ Use case resumes at step 3.
+
+* 4b. User provides an invalid or existing shortcut combination.
+ * 4b1. ScoopBook shows an error message.
+
+ Use case resumes at step 3.
+
+
+
+**Use case: Delete keyboard shortcuts - UC20 (To be implemented)**
+
+**MSS**
+
+1. User requests to view keyboard shortcuts (UC18).
+2. User requests to delete a specific keyboard shortcut in the list.
+3. ScoopBook deletes the specific keyboard shortcut.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid index.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Set follow-up reminder - UC21 (To be implemented)**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to set a follow-up reminder to specific contact.
+3. ScoopBook sets a follow-up reminder.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid input.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Delete follow-up reminder - UC22 (To be implemented)**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to delete a specific follow-up reminder to a specific contact.
+3. ScoopBook deletes the follow-up reminder.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid input.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+
+
+**Use case: Add profile photo - UC23 (To be implemented)**
+
+**MSS**
+
+1. User requests to view contacts (UC3).
+2. User requests to add a profile photo for a specific contact.
+3. ScoopBook prompts the location to access the profile photo file.
+4. User selects the profile photo.
+5. ScoopBook saves the profile photo.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User provides an invalid input.
+ * 2a1. ScoopBook shows an error message.
+
+ Use case resumes at step 1.
+
+* 4a. User selects an invalid file.
+ * 4a1. ScoopBook shows an error message.
Use case resumes at step 2.
-*{More to be added}*
+
### Non-Functional Requirements
-1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed.
-2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage.
-3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
-
-*{More to be added}*
+* Should work on any mainstream OS as long as it has Java 17 installed.
+* Should be able to store up to 1000 persons with no more than 1s of response time between each command entered.
+* A user with above-average typing speed (50 WPM) for regular English text (i.e., not code, not system admin commands) should be able to accomplish most basic tasks like adding contacts faster using commands than using the mouse.
+* Should not require a login, since ScoopBook is on a user’s own device.
+* All user data must be stored locally and should not require an internet connection for core functionality.
+* The application must be developed using modular, well-documented code to support future feature additions and maintenance.
### Glossary
-* **Mainstream OS**: Windows, Linux, Unix, MacOS
-* **Private contact detail**: A contact detail that is not meant to be shared with others
+**OS**: Operating System. A program that manages both hardware and software on a device.
+
+**CLI**: Command Line Interface. A mechanism through which users interact with their operating system.
+
+**CLI (-based) app**: An app primarily based on the key feature of the CLI, which is text-based typing.
+
+**GUI**: Graphical User Interface. A form of user interface that allows users to interact with electronic devices through graphical icons and visual indicators.
+
+**User**: The person using ScoopBook.
+
+**Attributes**: Relevant information that a contact may have. E.g., “high priority,” “whistleblower,” or “government official.”
+
+**Investigation**: A group of contacts that share some commonality. Users can create investigations and choose who to add to or remove from them.
--------------------------------------------------------------------------------------------------------------------
@@ -345,7 +863,9 @@ testers are expected to do more *exploratory* testing.
1. Download the jar file and copy into an empty folder
- 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
+ 2. Using your computer's terminal, `cd` into the folder in the previous step.
+
+ 3. Use `java -jar ScoopBook.jar` to open the application
1. Saving window preferences
@@ -354,29 +874,210 @@ testers are expected to do more *exploratory* testing.
1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained.
-1. _{ more test cases … }_
+---
+
+### Viewing Help
+
+**Test case:** `help`
+**Expected:** Opens the help window with instructions on how to use the commands.
+
+**Test case:** `help abc`
+**Expected:** Still opens the help window. Extraneous parameter is ignored.
+
+---
+
+### Adding a person
+
+**Prerequisite:** App is launched.
+
+**Test case:** `add n/John Doe p/98765432 e/johnd@example.com a/123 John Street`
+**Expected:** Adds John Doe to the contact list. Details shown in result display.
+
+**Test case:** `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal t/silent`
+**Expected:** Adds Betsy Crowe with multiple tags.
+
+**Test case:** `add n/Johnny Appleseed`
+**Expected:** Error shown. Missing phone number, email, or address.
+
+**Test case:** `add n/John! Doe p/1234567`
+**Expected:** Error shown. Name contains characters that are not accepted. Acceptable characters: alphanumeric characters, whitespaces, `,`, `(`, `)`, `@`, `.`, `-`, `'`.
+
+---
+
+### Listing all persons
+
+**Test case:** `list`
+**Expected:** All persons currently in the address book are displayed.
+
+**Test case:** `list 123`
+**Expected:** All persons currently in the address book are displayed. Extraneous parameters ignored.
+
+---
+
+### Editing a person
+
+**Prerequisite:** At least 2 persons in the list.
+
+**Test case:** `edit 1 p/91234567 e/johndoe@example.com`
+**Expected:** 1st person’s phone number and email are updated.
+
+**Test case:** `edit 2 n/Betsy Crower t/`
+**Expected:** Updates name, clears all tags.
+
+**Test case:** `edit 2`
+**Expected:** Error. No field provided.
+
+**Test case:** `edit 0 p/12345678`
+**Expected:** Error. Index must be a positive integer.
+
+---
+
+### Finding persons by name
+
+**Prerequisite:** Contact list currently contains people with the following names: `John Mary`, `John Doe`, `Alex Yeoh`, `David Lim`, `Hans Solo`.
+
+**Test case:** `find John`
+**Expected:** Displays persons with names containing “John”.
+
+**Test case:** `find Alex David`
+**Expected:** Displays any persons with name containing “Alex” or “David”.
+
+**Test case:** `find Han`
+**Expected:** No match for "Hans". Partial matches not allowed.
+
+---
### Deleting a person
-1. Deleting a person while all persons are being shown
+**Prerequisites:** Should have at least 1 contact in the menu.
+
+**Test case:** `delete 1` (after `list`)
+**Expected:** Deletes first contact.
+
+**Test case:** `delete 0`
+**Expected:** Error. Invalid index.
+
+**Test case:** `delete`
+**Expected:** Error. Missing index.
+
+---
+
+### Adding tags
+
+**Prerequisite:** Should have at least 1 contact in the menu.
+
+**Test case:** `addtag 1 t/friend t/neighbour`
+**Expected:** Adds both tags to person at index 1.
+
+**Test case:** Following the previous test case, `addtag 1 t/friend!`
+**Expected:** Error. Tag contains invalid character.
+
+---
+
+### Removing tags
+
+**Prerequisite:** Should have at least 1 contact in the menu. The first contact should have `friend` as a tag.
+
+**Test case:** `removetag 1 t/friend`
+**Expected:** Removes "friend" tag.
+
+**Test case:** Following the previous test case, `removetag 1 t/Friend`
+**Expected:** No tag removed. Case mismatch.
+
+---
+
+### Finding by tag
+
+**Prerequisite:** Should have at least 5 contacts in the menu. At least two contacts should have `friends` as a tag. At least one of the `friends` contacts should have a `neighbours` tag.
- 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list.
+**Test case:** `findtag t/friends`
+**Expected:** Displays persons with tag "friends", "Friends", etc.
+
+**Test case:** `findtag t/friends t/neighbours`
+**Expected:** Only persons with both tags are displayed.
+
+---
+
+### Notes
+
+**Prerequisite:** Should have at least 1 contact in the menu.
+
+**Test case:** `note 1`
+**Expected:** Opens a note window for person at index 1.
+
+**Test case:** `note 0`
+**Expected:** Error. Invalid index.
+
+---
+
+### Deleting a note
+
+**Prerequisites:** Should have at least 1 contact in the menu.
+**Test case:** `deletenote 1`
+**Expected:** Deletes note from person at index 1.
+
+**Test case:** `deletenote`
+**Expected:** Error. Missing index.
+
+---
- 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.
+### Clearing all entries
- 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same.
+**Test case:** `clear`
+**Expected:** Delete all contacts and notes from the address book.
- 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous.
+**Test case:** `clear abc`
+**Expected:** Still clears all contacts and notes. Extraneous parameter ignored.
-1. _{ more test cases … }_
+---
+
+### Export
+
+**Test case:** `export Contacts.json`
+**Expected:** Export the json file as Contacts.json in the root folder of where the .jar is located at.
+
+**Test case:** `export /invalid/path/Contacts.json`
+**Expected:** Error shown. Invalid path.
+
+---
+
+### Import
+
+**Test case:** `import Contacts.json` (valid exported file)
+**Expected:** Replaces all contacts with imported data. Notes are deleted.
+
+**Test case:** `import corrupted.json`
+**Expected:** Returns error message.
+
+---
+
+### Saving
+
+**Prerequisite:** Should have at least 1 contact in the menu.
+
+**Test case:** `note 1`. Then, add random text. Then, close the note window. Enter `note 1` again.
+**Expected:** Note should show the same random text shown earlier.
+
+**Test case:** `list`. Then, `addtag 1 t/colleague`. Use `exit` to exit the application. Launch the application again.
+**Expected:** Upon opening the application, the first contact should have the tag `colleague`.
+
+---
+
+### Exit
+
+**Test case:** `exit`
+**Expected:** Application closes.
+
+
+--------------------------------------------------------------------------------------------------------------------
-### Saving data
+## **Appendix: Planned Enhancements**
-1. Dealing with missing/corrupted data files
+Team size: 5
- 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_
+1. **Removing placeholder values for Person:** Currently Person uses a placeholder value for optional fields in the contact. I.e., if Alex Yeoh has a phone number only, the email and address fields will be replaced with a placeholder value instead of being blank. We plan to wrap each field in an Optional class so that it is able to handle empty values rather than leaving it with a placeholder value.
+2. **Person cards to be more visible for longer texts:** Currently, Person fields truncates long text fields (i.e., Name, Phone Number, Email, Address). For example, if the name exceeds a certain length, it will be truncated with a "...". It shall be updated in future iterations to be able to display the whole text field by using a scrolling mechanic to see the whole text field for all fields (Name, Phone Number, Email, Address, Tags).
+3. **Standardise error messages for commands that use Index referencing:** Currently, commands that use index referencing do not return the same error message for different cases of index errors. Invalid cases include: out of bounds for addressbook, non-positive integers, non-integers. We plan to standardise the index error messages to the different cases of invalid index accordingly.
+4. **Improve search commands**: `find` command currently uses an OR operator to search. (i.e. `find John Mary` will return contacts with names `John`, `Mary`, and `John Mary`, but `John Mary` does not appear as the first contact). `find` shall be updated to return results where the first contact is the most relevant contact (i.e Names with `John Mary`) followed by other contacts.
+5. **Allow user to copy from display**: Currently, the display does not allow users to copy text from the display. This may cause inconvenience or introduce delays for journalists who may want to copy text from the display. We plan to introduce support for copying for ease of use.
-1. _{ more test cases … }_
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 27c2d1cf16c..c97c5238f5c 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -3,8 +3,29 @@ 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.
+## :trophy: Our Goal
+**ScoopBook** is built to help journalists efficiently manage the contacts of their sources, witnesses, and other key individuals they interact with on the job.
+
+## :bust_in_silhouette: Target Audience
+
+ScoopBook is designed for **journalists** who need a fast, efficient and offline way to manage sources and contacts. We assume that these journalists are comfortable with the keyboard, but **no technical background is required**.
+
+## :dart: Problems We’re Solving
+
+- Traditional contact apps (like those on mobile phones) often have clunky interfaces that make it difficult for journalists to handle large number of contacts, sources and witnesses.
+- Journalists frequently juggle multiple tools just to do simple tasks (like saving a contact or jotting down notes) wasting valuable time when the next big scoop is on the line.
+
+## :mag: How does ScoopBook work?
+
+**ScoopBook** is a **desktop app** designed with journalists in mind. It combines the speed and precision of a **Command Line Interface (CLI)** with the ease of a **Graphical User Interface (GUI)**—so if you can type fast, you can work fast.
+
+With ScoopBook, you get:
+- **Blazing-fast** contact entry: Offering a Typing-based interface.
+- **Smart categorization** of contacts (e.g. sources, leads, officials)
+- **Powerful, instant search** to find the right sources, right contacts, fast.
+
+## :memo: Table of Contents
* Table of Contents
{:toc}
@@ -15,28 +36,28 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
1. Ensure you have Java `17` or above installed in your Computer.
**Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html).
-1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases).
+2. Download the latest `.jar` file from [here](https://github.com/AY2425S2-CS2103T-W13-1/tp/releases).
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook.
+3. Copy the file to the folder you want to use as the _home folder_ for your ScoopBook.
-1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
+4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar ScoopBook.jar` command to run the application.
A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.

-1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try:
+5. Type the command into the command box and press `Enter` to execute it. e.g. typing `help` and pressing `Enter` will open the help window.
+ Here are some example commands you can try:
- * `list` : Lists all contacts.
+ * `list` : Lists all contacts.
- * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book.
+ * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the address book.
- * `delete 3` : Deletes the 3rd contact shown in the current list.
+ * `delete 3` : Deletes the 3rd contact shown in the current list.
- * `clear` : Deletes all contacts.
+ * `clear` : Deletes all contacts.
- * `exit` : Exits the app.
+ * `exit` : Exits the app.
-1. Refer to the [Features](#features) below for details of each command.
+6. Refer to the [Features](#features) below for details of each command.
--------------------------------------------------------------------------------------------------------------------
@@ -46,6 +67,9 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
**:information_source: Notes about the command format:**
+* Commands are case-sensitive.
+ e.g. `LIST`command will not work.
+
* 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`.
@@ -66,117 +90,385 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
### Viewing help : `help`
-Shows a message explaning how to access the help page.
-
-
-
-Format: `help`
-
+Shows the user guide, containing instructions on how to use the command.
+```
+help
+```
+
### Adding a person: `add`
Adds a person to the address book.
-Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…`
+```
+add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG] [t/MORE_TAGS]
+```
-
:bulb: **Tip:**
-A person can have any number of tags (including 0)
+
+
+
+
+**:information_source: Note:**
+* The add command **must** have a name, and one of the following fields: phone number, email, address.
+ i.e. `add n/Johnny Appleseed` does not work because there is no phone number, email or address.
+* A person can have any number of tags (including 0).
+* A person's name can only contain alphanumeric characters (numbers or letters only), whitespaces, and the following special characters: `,`, `(`, `)`, `@`, `.`, `-`, `'`.
+* A person's tags can only contain alphanumeric characters (numbers or letters only, no special characters).
+* If a contact is added with the following values, they will not be displayed in the contact list, as they are used as internal placeholders:
+ - Phone Number: `000`
+ - Email: `unknown@example.com`
+ - Address: `Unknown address`
+ This ensures that every contact has a placeholder value for these fields if left empty.
-Examples:
+:paperclip: 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`
+* `add n/Betsy Crowe t/Witness e/betsycrowe@example.com a/Newgate Prison p/1234567 t/Criminal`
### Listing all persons : `list`
Shows a list of all persons in the address book.
-Format: `list`
+```
+list
+```
+
### Editing a person : `edit`
-Edits an existing person in the address book.
+Edits an existing person in the address book at specified index.
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
+```
+edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG] [t/MORE_TAGS]
+```
+
+
+
+**:information_source: Note:**
* 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.
+* Similar to the `add` command, the aforementioned placeholder values will not be displayed in the contact list.
+
+
+
-Examples:
+:bulb: TIP: You can remove all the person’s tags by typing `edit INDEX t/` without specifying any tags after it.
+
+
+:paperclip: 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 1 t/friends t/colleagues` Removes all existing tags of the 1st person, and sets the 1st person's tag to `friends` and `colleagues` only.
* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags.
### Locating persons by name: `find`
Finds persons whose names contain any of the given keywords.
-Format: `find KEYWORD [MORE_KEYWORDS]`
+```
+find KEYWORD [MORE_KEYWORDS]
+```
+
+
+
+**:information_source: Note:**
* The search is case-insensitive. e.g `hans` will match `Hans`
* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans`
* Only the name is searched.
* Only full words will be matched e.g. `Han` will not match `Hans`
* Persons matching at least one keyword will be returned (i.e. `OR` search).
e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`
+
-Examples:
+:paperclip: Examples:
* `find John` returns `john` and `John Doe`
* `find alex david` returns `Alex Yeoh`, `David Li`
- 
### Deleting a person : `delete`
Deletes the specified person from the address book.
-Format: `delete INDEX`
+```
+delete INDEX
+```
+
+
+
+**:information_source: Note:**
* Deletes the person at the specified `INDEX`.
* The index refers to the index number shown in the displayed person list.
* The index **must be a positive integer** 1, 2, 3, …
+
-Examples:
+:paperclip: 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.
+### Adding tags to a contact: `addtag`
+
+Adds the tag(s) typed in to the specified person.
+
+```
+addtag INDEX t/TAG1 [t/MORE_TAGS]
+```
+
+
+
+**:information_source: Note:**
+* Adds the specified tags to 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, …
+* Multiple tags in a single `addtag` command is supported.
+ i.e. `addtag 1 t/Witness t/HomeAffairs` will tag the 1st person with both "Witness" and "HomeAffairs".
+* Tags can only contain alphanumeric characters (numbers or letters only, no special characters or spaces).
+* Tags are case-sensitive.
+ i.e. `addtag 1 t/Witness` will add the tag "Witness" while `addtag 1 t/witness` will add the tag "witness".
+
+
+:paperclip: Examples:
+* `list` followed by `addtag 2 t/Education` tags the 2nd person with "Education" in the address book.
+* `find Betsy` followed by `addtag 1 t/Victim` tags the 1st person in the results of the `find` command with "Victim".
+
+### Removing tag from a contact: `removetag`
+
+Removes the specified tag(s) from the specified person.
+
+```
+removetag INDEX t/TAG1 [t/MORE_TAGS]
+```
+
+
+
+
+**:information_source: Note:**
+* Removes the specified tags from 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, …
+* Multiple tags in a single `removetag` command is supported. i.e. `removetag 1 t/Witness t/Local` will remove both the "Witness" and "Local" tag for the 1st person.
+* Tags are case-sensitive. The typed tag must match the tag on the person exactly.
+ i.e. `removetag 1 t/witness` will not remove the tag "Witness".
+
+
+
+:bulb: TIP: Use `edit INDEX t/` instead to remove all tags for specified contact.
+
+
+:paperclip: Examples:
+* `list` followed by `removetag 2 t/friend` removes the "friend" tag from the 2nd person in the address book.
+* `find Betsy` followed by `removetag 1 t/friend` removes the "friend" tag from the 1st person in the results of the `find` command.
+
+### Finding people with tags: `findtag`
+
+Find persons who have all the specified tags.
+
+```
+findtag t/TAG1 [t/MORE_TAGS]
+```
+
+
+
+**:information_source: Note:**
+* The searching of tags is case-insensitive. e.g `friends` will match `Friends`
+* The order of the tags does not matter. i.e. As long as the person has the listed tags, they will be shown.
+* Only the tags are searched.
+* Only full words will be matched e.g. `Friend` will not match `Friends`
+* Only persons matching all the tags will be returned (i.e. `AND` search).
+
+
+:paperclip: Examples:
+* `findtag t/witness` returns people with tag `witness`, `Witness`, `WitNeSs` (due to case insensitivity).
+* `findtag t/Witness t/HomeAffairs` returns people with tag `Witness` **and** `HomeAffairs` only.
+
+### Opening Note for Person: `note`
+
+Open a window for the user to add notes to.
+If the person at the specified `INDEX` already has a note, the note will be displayed and the user can edit it in the window.
+
+If no note exists for the person, a new note will be created and displayed in the window for editing.
+
+```
+note INDEX
+```
+
+
+
+
+**:information_source: Note:**
+* Opens a window for the user to add notes to the person at the specified `INDEX`.
+ * :exclamation: Please use only this opened window to edit the note (see [#Known issues](#known-issues) section below)
+* The index refers to the index number shown in the displayed person list.
+* The index must be a positive integer 1, 2, 3, …
+* The note will be saved when the window is closed.
+
+
+:paperclip: Examples:
+* `list` followed by `note 2` opens a note window for the 2nd person in the address book.
+* `find Betsy` followed by `note 1` opens a note window for the 1st person in the results of the `find` command.
+
+### Deleting Note from Person: `deletenote`
+
+Deletes the note from the person.
+
+```
+deletenote INDEX
+```
+
+
+
+
+**:information_source: Note:**
+* Deletes note for 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, …
+
+
+:paperclip: Examples:
+* `list` followed by `deletenote 2` deletes the note for the 2nd person in the address book, if the note exists.
+* `find Betsy` followed by `deletenote 1` deletes the note for the 1st person in the results of the `find` command, if the note exists.
+
### Clearing all entries : `clear`
Clears all entries from the address book.
-Format: `clear`
+```
+clear
+```
+
:exclamation: **Caution:**
+This clears all contacts, notes & `.txt` files from the address book.
+
+
+### Exporting your contacts: `export`
+
+Exports the contacts in a .json file to the target path.
+
+```
+export TARGET_PATH
+```
+
+
+
+
+**:information_source: Note:**
+- The `export` command only exports your contacts. It does not export the notes tagged to them.
+- Before executing the `export` command, add at least 1 contact using the `add` command.
+- `export` command is case-insensitive. If `sAmPle.json` already exists (in the folder the `ScoopBook.jar` is located at), `export sample.json` will overwrite `sAmPle.json`.
+- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the `TARGET_PATH`.
+
+
+
+:bulb: TIP: If you are running into issues with TARGET_PATH, use `export sample.json` to export it directly to the root folder with of the ScoopBook.jar file. Then, move the .json file to wherever you want it to be.
+
+
+:paperclip: Examples:
+* For Windows: `export C:/Users/username/Desktop/MyContacts.json`
+ * saves the json file as `MyContacts.json` in the `Users/username/Desktop` folder.
+
+
+* For macOS: `export /Users/username/Desktop/MyContacts.json`
+ * saves the json file as `MyContacts.json` in the `Users/username/Desktop` folder.
+
+
+* For Linux: `export /home/user/desktop/MyContacts.json`
+ * saves the json file as `MyContacts.json` in the `home/user/desktop` folder.
+
+
+* For all OS: `export Contacts.json`
+ * saves the json file as `Contacts.json` in the root folder of where `ScoopBook.jar` is located at.
+
+### Importing your contacts: `import`
+
+Imports contacts from the external .json file located at the specified path into the application.
+
+```
+import TARGET_PATH
+```
+
+
+
:exclamation: **Caution:**
+This command overwrites existing contacts and remove all notes.
+
+
+
+
+**:information_source: Note:**
+- Only import .json files exported using the `export` command.
+- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the `TARGET_PATH`.
+
+
+:paperclip: Examples:
+* For Windows: `import C:/Users/username/Desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `Users/username/Desktop` folder.
+* For macOS: `import /Users/username/Desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `Users/username/Desktop` folder.
+* For Linux: `import /home/user/desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `home/user/desktop` folder.
+* `import Contacts.json` imports the json file named `Contacts.json` from the root folder of where ScoopBook.jar is located at.
### Exiting the program : `exit`
Exits the program.
-Format: `exit`
+```
+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.
+ScoopBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually.
### Editing the data file
-AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
+ScoopBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
-
:exclamation: **Caution:**
-If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
-
+**Unsure where to find the JSON file? No worries! Follow these instructions:**
-### Archiving data files `[coming in v2.0]`
+1. In ScoopBook, type the following command: `export temp.json`
+2. `temp.json` will be saved in your JAR file location. Open it in an editor of your choice.
+3. Edit the fields while adhering to the format of the file. Save the JSON file.
+4. In ScoopBook, type the following command: `import temp.json`
+5. Done!
-_Details coming soon ..._
+
:exclamation: **Caution:**
+If your changes to the data file makes its format invalid, ScoopBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the ScoopBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
+
--------------------------------------------------------------------------------------------------------------------
## 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.
+:question: **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 ScoopBook home folder.
+
+:question: **Q**: **What are considered duplicate contacts?**
+**A**: Duplicate contacts are contacts with names that match exactly. We do not allow the addition of duplicate contacts in our app. For example, `John Doe` and `John Doe` are considered duplicate contacts, and we will not allow the addition of the second contact if the first has already been added.
+
+Names that differ in lower and upper case letters are not considered as duplicate contacts even if the same exact letters are used. For example, `John Doe` and `john doe` are not considered duplicate contacts.
+
+Additionally, names with different amount of spaces between them are also not considered duplicate contacts. For example, `John Doe` and John Doe are not considered duplicate contacts.
+
+This way, we leave room for flexibility in deciding contact names, with the bare minimum of preventing the addition of duplicates as specified.
+
+:question: **Q**: **Why didn't my `add` command work?**
+**A**: Ensure that you entered at least a `name` and **one of the following**: phone number, email, or address.
+- Does your name contain special characters? Only whitespace, `,`, `(`, `)`, `@`, `.`, `-`, `'` are allowed.
+
+:question: **Q**: **Why didn't my `edit`, `addtag`, `removetag` `delete` `note` or `deletenote` command work?**
+**A**: Check your INDEX! Is your index within range?
+
+:question: **Q**: **My `import` command did not work. What went wrong?**
+**A**: Make sure that the file was originally exported from ScoopBook. Do not import unrelated `.json` files or edit them outside of ScoopBook!
+- Did you also check your filepath?
+
+
+
+:bulb: TIP: If you are unsure, test with a sample `export` first:
+```
+export temp.json
+import temp.json
+```
+
--------------------------------------------------------------------------------------------------------------------
@@ -184,6 +476,12 @@ _Details coming soon ..._
1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again.
2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window.
+3. **If you minimize the Note Window**, and then run the `note` command again, the original Note Window will remain minimized, and no new Note Window will appear. The remedy is to manually restore the minimized Note Window.
+4. **If you use any other means apart from the note window that ScoopBook opens to edit a note**, (eg. notepad) we cannot guarantee that your edits will be saved. This may be because of an encoding incompatibility between your text editor and ScoopBook's. Please use the note window that ScoopBook opens to edit the note.
+5. **Text fields in the GUI**: Currently, text fields that are too long may be cut off in the GUI. We will introduce scrolling as a feature to enable viewing these fields in full in future releases.
+6. **Adding a contact with placeholder values**: Currently, we do not prevent the user from adding a contact with placeholder values. This is because we want to allow the user to add a contact with only a name and one other field, and we chose these placeholder values as unlikely values that would be used for a contact.
+ 1. Regardless, we acknowledge that this may lead to confusion as these contact fields deliberately added with placeholder values will not be displayed in the contact list. We will fix this in future releases.
+7. **Finding a contact**: Currently, the `find` command performs an `OR` search. While all contacts matching at least one keyword will be returned, they are not sorted according to the highest similarity or match. We will improve this in future releases.
--------------------------------------------------------------------------------------------------------------------
@@ -191,10 +489,17 @@ _Details coming soon ..._
Action | Format, Examples
--------|------------------
-**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…` e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague`
+**Add** | `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/Witness t/Local`
**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`
+**Add Tag** | `addtag INDEX t/TAG1 [t/MORE_TAGS]…` e.g., `addtag 2 t/Witness`
+**Find Tag** | `findtag t/TAG1 [t/MORE_TAGS]…` e.g., `findtag t/Witness`
+**Remove Tag** | `removetag INDEX t/TAG1 [t/MORE_TAGS]…` e.g., `removetag 2 t/Witness`
+**Note** | `note INDEX` e.g., `note 2`
+**Delete Note** | `deletenote INDEX` e.g., `deletenote 3`
+**Export Contacts** | `export TARGET_PATH` e.g., `export backup.json`
+**Import Contacts** | `import TARGET_PATH` e.g., `import previousVer.json`
**List** | `list`
**Help** | `help`
diff --git a/docs/_config.yml b/docs/_config.yml
index 6bd245d8f4e..89f78868dd6 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -1,4 +1,4 @@
-title: "AB-3"
+title: "ScoopBook"
theme: minima
header_pages:
@@ -8,7 +8,7 @@ header_pages:
markdown: kramdown
-repository: "se-edu/addressbook-level3"
+repository: "AY2425S2-CS2103T-W13-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..f4bf6e2e1df 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: "ScoopBook";
font-size: 32px;
}
}
diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml
index 598474a5c82..b914e1c14cc 100644
--- a/docs/diagrams/BetterModelClassDiagram.puml
+++ b/docs/diagrams/BetterModelClassDiagram.puml
@@ -4,8 +4,8 @@ skinparam arrowThickness 1.1
skinparam arrowColor MODEL_COLOR
skinparam classBackgroundColor MODEL_COLOR
-AddressBook *-right-> "1" UniquePersonList
-AddressBook *-right-> "1" UniqueTagList
+AddressBook -right-> "1" UniquePersonList
+AddressBook -right-> "1" UniqueTagList
UniqueTagList -[hidden]down- UniquePersonList
UniqueTagList -[hidden]down- UniquePersonList
@@ -14,8 +14,9 @@ UniquePersonList -right-> Person
Person -up-> "*" Tag
-Person *--> Name
-Person *--> Phone
-Person *--> Email
-Person *--> Address
+Person --> "1" Name
+Person --> "0..1" Phone
+Person --> "1" PersonId
+Person --> "0..1" Email
+Person --> "0..1" Address
@enduml
diff --git a/docs/diagrams/DeleteNoteSequenceDiagram.puml b/docs/diagrams/DeleteNoteSequenceDiagram.puml
new file mode 100644
index 00000000000..b88c663c780
--- /dev/null
+++ b/docs/diagrams/DeleteNoteSequenceDiagram.puml
@@ -0,0 +1,76 @@
+@startuml
+!include style.puml
+skinparam ArrowFontStyle plain
+
+box Logic LOGIC_COLOR_T1
+participant ":LogicManager" as LogicManager LOGIC_COLOR
+participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR
+participant ":DeleteNoteCommandParser" as DeleteNoteCommandParser LOGIC_COLOR
+participant "d:DeleteNoteCommand" as DeleteNoteCommand LOGIC_COLOR
+participant "r:CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+Box Storage STORAGE_COLOR_T1
+participant "s:Storage" as Storage STORAGE_COLOR
+end box
+
+
+[-> LogicManager : execute("deletenote 1")
+activate LogicManager
+
+LogicManager -> AddressBookParser : parseCommand("deletenote 1")
+activate AddressBookParser
+
+create DeleteNoteCommandParser
+AddressBookParser -> DeleteNoteCommandParser
+activate DeleteNoteCommandParser
+
+DeleteNoteCommandParser --> AddressBookParser
+deactivate DeleteNoteCommandParser
+
+AddressBookParser -> DeleteNoteCommandParser : parse("1")
+activate DeleteNoteCommandParser
+
+create DeleteNoteCommand
+DeleteNoteCommandParser -> DeleteNoteCommand
+activate DeleteNoteCommand
+
+DeleteNoteCommand --> DeleteNoteCommandParser :
+deactivate DeleteNoteCommand
+
+DeleteNoteCommandParser --> AddressBookParser : d
+deactivate DeleteNoteCommandParser
+'Hidden arrow to position the destroy marker below the end of the activation bar.
+DeleteNoteCommandParser -[hidden]-> AddressBookParser
+destroy DeleteNoteCommandParser
+
+AddressBookParser --> LogicManager : d
+deactivate AddressBookParser
+
+LogicManager -> DeleteNoteCommand: setStorage(s)
+activate DeleteNoteCommand
+DeleteNoteCommand --> LogicManager
+deactivate DeleteNoteCommand
+
+LogicManager -> DeleteNoteCommand : execute(m)
+activate DeleteNoteCommand
+
+DeleteNoteCommand -> Storage : deleteNote(targetPerson)
+activate Storage
+
+Storage --> DeleteNoteCommand
+deactivate Storage
+
+create CommandResult
+DeleteNoteCommand -> CommandResult
+activate CommandResult
+
+CommandResult --> DeleteNoteCommand
+deactivate CommandResult
+
+DeleteNoteCommand --> LogicManager : r
+deactivate DeleteNoteCommand
+
+[<--LogicManager
+deactivate LogicManager
+@enduml
diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml
index 5241e79d7da..854e098b5c9 100644
--- a/docs/diagrams/DeleteSequenceDiagram.puml
+++ b/docs/diagrams/DeleteSequenceDiagram.puml
@@ -14,6 +14,11 @@ box Model MODEL_COLOR_T1
participant "m:Model" as Model MODEL_COLOR
end box
+Box Storage STORAGE_COLOR_T1
+participant "s.Storage" as Storage STORAGE_COLOR
+end box
+
+
[-> LogicManager : execute("delete 1")
activate LogicManager
@@ -65,6 +70,14 @@ deactivate CommandResult
DeleteCommand --> LogicManager : r
deactivate DeleteCommand
+LogicManager -> LogicManager : handleNoteOperation(d)
+
+LogicManager -> Storage : deleteNote(personDeleted)
+activate Storage
+
+Storage --> LogicManager
+deactivate Storage
+
[<--LogicManager
deactivate LogicManager
@enduml
diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml
index 0de5673070d..a191479445a 100644
--- a/docs/diagrams/ModelClassDiagram.puml
+++ b/docs/diagrams/ModelClassDiagram.puml
@@ -18,6 +18,7 @@ Class Address
Class Email
Class Name
Class Phone
+Class PersonId
Class Tag
Class I #FFFFFF
@@ -35,18 +36,19 @@ ModelManager -left-> "1" AddressBook
ModelManager -right-> "1" UserPrefs
UserPrefs .up.|> ReadOnlyUserPrefs
-AddressBook *--> "1" UniquePersonList
+AddressBook --> "1" UniquePersonList
UniquePersonList --> "~* all" Person
-Person *--> Name
-Person *--> Phone
-Person *--> Email
-Person *--> Address
-Person *--> "*" Tag
+Person --> "1" Name
+Person --> "0..1" Phone
+Person --> "1" PersonId
+Person --> "0..1" Email
+Person --> "0..1" Address
+Person --> "*" Tag
Person -[hidden]up--> I
UniquePersonList -[hidden]right-> I
-Name -[hidden]right-> Phone
+Name -[hidden]right-> PersonId
Phone -[hidden]right-> Address
Address -[hidden]right-> Email
diff --git a/docs/diagrams/NoteSequenceDiagram.puml b/docs/diagrams/NoteSequenceDiagram.puml
new file mode 100644
index 00000000000..1bd4065c61f
--- /dev/null
+++ b/docs/diagrams/NoteSequenceDiagram.puml
@@ -0,0 +1,83 @@
+@startuml
+!include style.puml
+skinparam ArrowFontStyle plain
+
+box Ui UI_COLOR_T1
+participant "u:Ui" as Ui UI_COLOR
+end box
+
+box Logic LOGIC_COLOR_T1
+participant ":LogicManager" as LogicManager LOGIC_COLOR
+participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR
+participant ":NoteCommandParser" as NoteCommandParser LOGIC_COLOR
+participant "n:NoteCommand" as NoteCommand LOGIC_COLOR
+participant "r:CommandResult" as CommandResult LOGIC_COLOR
+end box
+
+Box Storage STORAGE_COLOR_T1
+participant "s.Storage" as Storage STORAGE_COLOR
+end box
+
+[-> Ui : note 1
+activate Ui
+
+Ui -> LogicManager : execute("note 1")
+activate LogicManager
+
+LogicManager -> AddressBookParser : parseCommand("note 1")
+activate AddressBookParser
+
+create NoteCommandParser
+AddressBookParser -> NoteCommandParser
+activate NoteCommandParser
+
+NoteCommandParser --> AddressBookParser
+deactivate NoteCommandParser
+
+AddressBookParser -> NoteCommandParser : parse("1")
+activate NoteCommandParser
+
+create NoteCommand
+NoteCommandParser -> NoteCommand
+activate NoteCommand
+
+NoteCommand --> NoteCommandParser :
+deactivate NoteCommand
+
+NoteCommandParser --> AddressBookParser : n
+deactivate NoteCommandParser
+'Hidden arrow to position the destroy marker below the end of the activation bar.
+NoteCommandParser -[hidden]-> AddressBookParser
+destroy NoteCommandParser
+
+AddressBookParser --> LogicManager : n
+deactivate AddressBookParser
+
+LogicManager -> NoteCommand : execute(m)
+activate NoteCommand
+
+create CommandResult
+NoteCommand -> CommandResult
+activate CommandResult
+
+CommandResult --> NoteCommand
+deactivate CommandResult
+
+NoteCommand --> LogicManager : r
+deactivate NoteCommand
+
+Ui <--LogicManager
+deactivate LogicManager
+
+Ui -> LogicManager : readNote(person)
+activate LogicManager
+
+LogicManager -> Storage : readNote(person)
+activate Storage
+
+Storage --> LogicManager : note content
+deactivate Storage
+
+LogicManager --> Ui : note content
+deactivate LogicManager
+@enduml
diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml
index a821e06458c..f73e17dbe29 100644
--- a/docs/diagrams/StorageClassDiagram.puml
+++ b/docs/diagrams/StorageClassDiagram.puml
@@ -22,6 +22,11 @@ Class JsonAdaptedPerson
Class JsonAdaptedTag
}
+package "Notes Storage" #F4F6F6{
+Class "<>\nNotesStorage" as NotesStorage
+Class FilesNotesStorage
+}
+
}
Class HiddenOutside #FFFFFF
@@ -30,12 +35,15 @@ HiddenOutside ..> Storage
StorageManager .up.|> Storage
StorageManager -up-> "1" UserPrefsStorage
StorageManager -up-> "1" AddressBookStorage
+StorageManager -up-> "1" NotesStorage
Storage -left-|> UserPrefsStorage
Storage -right-|> AddressBookStorage
+Storage -down-|> NotesStorage
JsonUserPrefsStorage .up.|> UserPrefsStorage
JsonAddressBookStorage .up.|> AddressBookStorage
+FilesNotesStorage .up.|> NotesStorage
JsonAddressBookStorage ..> JsonSerializableAddressBook
JsonSerializableAddressBook --> "*" JsonAdaptedPerson
JsonAdaptedPerson --> "*" JsonAdaptedTag
diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml
index 95473d5aa19..1fa57f4e648 100644
--- a/docs/diagrams/UiClassDiagram.puml
+++ b/docs/diagrams/UiClassDiagram.puml
@@ -10,7 +10,10 @@ Class "{abstract}\nUiPart" as UiPart
Class UiManager
Class MainWindow
Class HelpWindow
+Class NoteWindow
+Class NoteWindowHandler
Class ResultDisplay
+Class DialogBox
Class PersonListPanel
Class PersonCard
Class StatusBarFooter
@@ -30,22 +33,26 @@ HiddenOutside ..> Ui
UiManager .left.|> Ui
UiManager -down-> "1" MainWindow
-MainWindow *-down-> "1" CommandBox
-MainWindow *-down-> "1" ResultDisplay
-MainWindow *-down-> "1" PersonListPanel
-MainWindow *-down-> "1" StatusBarFooter
+MainWindow -down-> "1" CommandBox
+MainWindow -down-> "1" ResultDisplay
+MainWindow -down-> "1" StatusBarFooter
MainWindow --> "0..1" HelpWindow
-
-PersonListPanel -down-> "*" PersonCard
-
+MainWindow -down-> "1" NoteWindowHandler
+NoteWindowHandler -down-> "*" NoteWindow
+ResultDisplay -down-> "*" DialogBox
MainWindow -left-|> UiPart
+MainWindow -down-> "1" PersonListPanel
+PersonListPanel --> "*" PersonCard
+
ResultDisplay --|> UiPart
CommandBox --|> UiPart
PersonListPanel --|> UiPart
PersonCard --|> UiPart
StatusBarFooter --|> UiPart
HelpWindow --|> UiPart
+DialogBox --|> UiPart
+NoteWindow --|> UiPart
PersonCard ..> Model
UiManager -right-> Logic
diff --git a/docs/images/AddCommand.png b/docs/images/AddCommand.png
new file mode 100644
index 00000000000..c9cc85022fc
Binary files /dev/null and b/docs/images/AddCommand.png differ
diff --git a/docs/images/AddTagCommand.png b/docs/images/AddTagCommand.png
new file mode 100644
index 00000000000..c6283e39bc0
Binary files /dev/null and b/docs/images/AddTagCommand.png differ
diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png
index 02a42e35e76..3fb40f252c7 100644
Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ
diff --git a/docs/images/DeleteCommand.png b/docs/images/DeleteCommand.png
new file mode 100644
index 00000000000..771c20bdcce
Binary files /dev/null and b/docs/images/DeleteCommand.png differ
diff --git a/docs/images/DeleteNoteCommand.png b/docs/images/DeleteNoteCommand.png
new file mode 100644
index 00000000000..55af4a9ef26
Binary files /dev/null and b/docs/images/DeleteNoteCommand.png differ
diff --git a/docs/images/DeleteNoteSequenceDiagram.png b/docs/images/DeleteNoteSequenceDiagram.png
new file mode 100644
index 00000000000..9a4e55d0c60
Binary files /dev/null and b/docs/images/DeleteNoteSequenceDiagram.png differ
diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png
index ac2ae217c51..651b94f3997 100644
Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ
diff --git a/docs/images/EditCommand.png b/docs/images/EditCommand.png
new file mode 100644
index 00000000000..ac0a1b7798d
Binary files /dev/null and b/docs/images/EditCommand.png differ
diff --git a/docs/images/ExportCommand.png b/docs/images/ExportCommand.png
new file mode 100644
index 00000000000..0f76559a152
Binary files /dev/null and b/docs/images/ExportCommand.png differ
diff --git a/docs/images/FindTagCommand.png b/docs/images/FindTagCommand.png
new file mode 100644
index 00000000000..0507aebca59
Binary files /dev/null and b/docs/images/FindTagCommand.png differ
diff --git a/docs/images/HelpCommand.png b/docs/images/HelpCommand.png
new file mode 100644
index 00000000000..6d95fc6ca5d
Binary files /dev/null and b/docs/images/HelpCommand.png differ
diff --git a/docs/images/ImportCommand.png b/docs/images/ImportCommand.png
new file mode 100644
index 00000000000..140825dbeee
Binary files /dev/null and b/docs/images/ImportCommand.png differ
diff --git a/docs/images/ListCommand.png b/docs/images/ListCommand.png
new file mode 100644
index 00000000000..41eb25adc17
Binary files /dev/null and b/docs/images/ListCommand.png differ
diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png
index a19fb1b4ac8..3cad6067cec 100644
Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ
diff --git a/docs/images/NoteCommand.png b/docs/images/NoteCommand.png
new file mode 100644
index 00000000000..a67ca07c8b1
Binary files /dev/null and b/docs/images/NoteCommand.png differ
diff --git a/docs/images/NoteSequenceDiagram.png b/docs/images/NoteSequenceDiagram.png
new file mode 100644
index 00000000000..bc64ab8c0e3
Binary files /dev/null and b/docs/images/NoteSequenceDiagram.png differ
diff --git a/docs/images/RemoveTagCommand.png b/docs/images/RemoveTagCommand.png
new file mode 100644
index 00000000000..def62b69b96
Binary files /dev/null and b/docs/images/RemoveTagCommand.png differ
diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png
index 18fa4d0d51f..9ce736f197a 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 5bd77847aa2..ecca22369dc 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 11f06d68671..9803c8886f4 100644
Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ
diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png
index 235da1c273e..e2273375e4c 100644
Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ
diff --git a/docs/images/meatsushi64.png b/docs/images/meatsushi64.png
new file mode 100644
index 00000000000..c9b085dc726
Binary files /dev/null and b/docs/images/meatsushi64.png differ
diff --git a/docs/images/shawnnygoh.png b/docs/images/shawnnygoh.png
new file mode 100644
index 00000000000..5ebe3027ece
Binary files /dev/null and b/docs/images/shawnnygoh.png differ
diff --git a/docs/images/souledfigurine.png b/docs/images/souledfigurine.png
new file mode 100644
index 00000000000..a7dadab9d79
Binary files /dev/null and b/docs/images/souledfigurine.png differ
diff --git a/docs/images/tim0tay.png b/docs/images/tim0tay.png
new file mode 100644
index 00000000000..2d16765712a
Binary files /dev/null and b/docs/images/tim0tay.png differ
diff --git a/docs/images/wentingchua.png b/docs/images/wentingchua.png
new file mode 100644
index 00000000000..b6aed0d27dd
Binary files /dev/null and b/docs/images/wentingchua.png differ
diff --git a/docs/img.png b/docs/img.png
new file mode 100644
index 00000000000..b96c36a90fa
Binary files /dev/null and b/docs/img.png differ
diff --git a/docs/index.md b/docs/index.md
index 7601dbaad0d..b84eea3313f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
---
layout: page
-title: AddressBook Level-3
+title: ScoopBook
---
[](https://github.com/se-edu/addressbook-level3/actions)
@@ -8,10 +8,10 @@ title: AddressBook Level-3

-**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).
+**ScoopBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface).
-* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start).
-* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start.
+* If you are interested in using ScoopBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start).
+* If you are interested about developing ScoopBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start.
**Acknowledgements**
diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java
index 9461d6da769..76fd4d9262a 100644
--- a/src/main/java/seedu/address/Main.java
+++ b/src/main/java/seedu/address/Main.java
@@ -34,7 +34,6 @@ public static void main(String[] args) {
// The warning however, can be safely ignored. Thus, the following log informs
// the user (if looking at the log output) that the said warning appearing in the log
// can be ignored.
-
logger.warning("The warning about Unsupported JavaFX configuration below (if any) can be ignored.");
Application.launch(MainApp.class, args);
}
diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java
index 678ddc8c218..89587b7eecc 100644
--- a/src/main/java/seedu/address/MainApp.java
+++ b/src/main/java/seedu/address/MainApp.java
@@ -36,7 +36,7 @@
*/
public class MainApp extends Application {
- public static final Version VERSION = new Version(0, 2, 2, true);
+ public static final Version VERSION = new Version(1, 5, 1, true);
private static final Logger logger = LogsCenter.getLogger(MainApp.class);
@@ -77,14 +77,21 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) {
Optional addressBookOptional;
ReadOnlyAddressBook initialData;
+ boolean isSampleDataUsed = false;
try {
addressBookOptional = storage.readAddressBook();
if (!addressBookOptional.isPresent()) {
logger.info("Creating a new data file " + storage.getAddressBookFilePath()
+ " populated with a sample AddressBook.");
+ isSampleDataUsed = true;
}
initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook);
- } catch (DataLoadingException e) {
+
+ if (isSampleDataUsed) {
+ storage.saveAddressBook(initialData);
+ logger.info("Sample AddressBook saved to disk.");
+ }
+ } catch (DataLoadingException | IOException e) {
logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded."
+ " Will be starting with an empty AddressBook.");
initialData = new AddressBook();
diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java
index 92cd8fa605a..78cc706d1d2 100644
--- a/src/main/java/seedu/address/logic/Logic.java
+++ b/src/main/java/seedu/address/logic/Logic.java
@@ -1,5 +1,6 @@
package seedu.address.logic;
+import java.io.IOException;
import java.nio.file.Path;
import javafx.collections.ObservableList;
@@ -9,6 +10,7 @@
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.person.Person;
+import seedu.address.storage.Storage;
/**
* API of the Logic component
@@ -47,4 +49,24 @@ public interface Logic {
* Set the user prefs' GUI settings.
*/
void setGuiSettings(GuiSettings guiSettings);
+
+ /**
+ * Returns the storage.
+ */
+ Storage getStorage();
+
+ /**
+ * Reads the note for a person.
+ */
+ String readNote(Person person) throws IOException;
+
+ /**
+ * Saves a note for a person.
+ */
+ void saveNote(Person person, String content) throws IOException;
+
+ /**
+ * Deletes a note for a person.
+ */
+ void deleteNote(Person person) throws IOException;
}
diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java
index 5aa3b91c7d0..368bb43ff6f 100644
--- a/src/main/java/seedu/address/logic/LogicManager.java
+++ b/src/main/java/seedu/address/logic/LogicManager.java
@@ -8,8 +8,12 @@
import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
+import seedu.address.logic.commands.ClearCommand;
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.commands.DeleteNoteCommand;
+import seedu.address.logic.commands.ImportCommand;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.AddressBookParser;
import seedu.address.logic.parser.exceptions.ParseException;
@@ -48,10 +52,15 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
CommandResult commandResult;
Command command = addressBookParser.parseCommand(commandText);
- commandResult = command.execute(model);
+ // Inject storage manually if it's DeleteNoteCommand
+ if (command instanceof DeleteNoteCommand castCommand) {
+ castCommand.setStorage(storage);
+ }
+ commandResult = command.execute(model);
try {
storage.saveAddressBook(model.getAddressBook());
+ handleNoteOperations(command);
} catch (AccessDeniedException e) {
throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e);
} catch (IOException ioe) {
@@ -85,4 +94,47 @@ public GuiSettings getGuiSettings() {
public void setGuiSettings(GuiSettings guiSettings) {
model.setGuiSettings(guiSettings);
}
+
+ @Override
+ public Storage getStorage() {
+ return storage;
+ }
+
+ @Override
+ public String readNote(Person person) throws IOException {
+ return storage.readNote(person);
+ }
+
+ @Override
+ public void saveNote(Person person, String content) throws IOException {
+ storage.saveNote(person, content);
+ }
+
+ @Override
+ public void deleteNote(Person person) throws IOException {
+ storage.deleteNote(person);
+ }
+
+ /**
+ * Handles note operations based on the command type.
+ *
+ * @param command The command that was executed
+ * @throws IOException If there is an issue with file operations
+ */
+ private void handleNoteOperations(Command command) throws IOException {
+ if (command instanceof DeleteCommand) {
+ DeleteCommand deleteCommand = (DeleteCommand) command;
+ Person personDeleted = deleteCommand.getTargetPerson();
+ if (personDeleted != null) {
+ storage.deleteNote(personDeleted);
+ }
+ } else if (command instanceof ClearCommand) {
+ ClearCommand clearCommand = (ClearCommand) command;
+ if (clearCommand.hasCleared()) {
+ storage.deleteAllNotes();
+ }
+ } else if (command instanceof ImportCommand) {
+ storage.deleteAllNotes();
+ }
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java
index 5d7185a9680..64ccdbdbb8e 100644
--- a/src/main/java/seedu/address/logic/commands/AddCommand.java
+++ b/src/main/java/seedu/address/logic/commands/AddCommand.java
@@ -22,7 +22,8 @@ public class AddCommand extends Command {
public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. "
+ "Parameters: "
- + PREFIX_NAME + "NAME "
+ + PREFIX_NAME + "NAME (alphanumeric characters, spaces and , ( ) . @ - ' only) "
+ + "and at least one of the following: "
+ PREFIX_PHONE + "PHONE "
+ PREFIX_EMAIL + "EMAIL "
+ PREFIX_ADDRESS + "ADDRESS "
diff --git a/src/main/java/seedu/address/logic/commands/AddTagCommand.java b/src/main/java/seedu/address/logic/commands/AddTagCommand.java
new file mode 100644
index 00000000000..c8a541e8b62
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/AddTagCommand.java
@@ -0,0 +1,118 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
+import seedu.address.model.person.Phone;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Adds one or more tags to a person in the address book.
+ */
+public class AddTagCommand extends Command {
+ public static final String COMMAND_WORD = "addtag";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a tag to a person in the address book. "
+ + "Parameters: "
+ + "INDEX (must be a positive integer) "
+ + PREFIX_TAG + "TAG (need not be a single word) "
+ + "[" + PREFIX_TAG + "TAG]...\n"
+ + "Example: " + COMMAND_WORD + " 1 " + PREFIX_TAG + "friends "
+ + PREFIX_TAG + "owesMoney";
+
+ public static final String MESSAGE_SUCCESS = "Tag added to person: %1$s";
+
+ public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty";
+
+ private final Index index;
+ private final PersonDescriptor personDescriptor;
+
+ /**
+ * @param targetIndex of the person in the filtered person list to edit
+ * @param personDescriptor details to edit the person with
+ */
+ public AddTagCommand(Index targetIndex, PersonDescriptor personDescriptor) {
+ this.index = targetIndex;
+ this.personDescriptor = personDescriptor;
+ }
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (index.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ Person personToAddTag = lastShownList.get(index.getZeroBased());
+ Person personWithTags = createPersonWithAddedTags(personToAddTag, personDescriptor);
+
+ model.setPerson(personToAddTag, personWithTags);
+ model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personWithTags)));
+ }
+
+
+ /**
+ * Creates and returns a {@code Person} with the details of {@code personToAddTag}
+ * edited with {@code personDescriptor}.
+ */
+ private static Person createPersonWithAddedTags(Person personToAddTag,
+ PersonDescriptor personDescriptor) {
+ assert personToAddTag != null;
+
+ Name updatedName = personToAddTag.getName();
+ Phone updatedPhone = personToAddTag.getPhone();
+ Email updatedEmail = personToAddTag.getEmail();
+ Address updatedAddress = personToAddTag.getAddress();
+ // gets current tags for personToAddTag
+ Set updatedTags = new HashSet<>(personToAddTag.getTags());
+ // add new tags from personDescriptor
+ updatedTags.addAll(personDescriptor.getTags().orElse(Collections.emptySet()));
+ PersonId personId = personToAddTag.getId();
+
+ return new Person(
+ updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, personId);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddTagCommand)) {
+ return false;
+ }
+
+ AddTagCommand otherAddTagCommand = (AddTagCommand) other;
+ return index.equals(otherAddTagCommand.index)
+ && personDescriptor.equals(otherAddTagCommand.personDescriptor);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("index", index)
+ .add("personDescriptor", personDescriptor)
+ .toString();
+ }
+}
+
diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java
index 9c86b1fa6e4..c1f30ff9fe2 100644
--- a/src/main/java/seedu/address/logic/commands/ClearCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java
@@ -4,6 +4,7 @@
import seedu.address.model.AddressBook;
import seedu.address.model.Model;
+import seedu.address.model.person.PersonId;
/**
* Clears the address book.
@@ -12,12 +13,23 @@ public class ClearCommand extends Command {
public static final String COMMAND_WORD = "clear";
public static final String MESSAGE_SUCCESS = "Address book has been cleared!";
+ private boolean hasCleared = false;
@Override
public CommandResult execute(Model model) {
requireNonNull(model);
model.setAddressBook(new AddressBook());
- return new CommandResult(MESSAGE_SUCCESS);
+ hasCleared = true;
+ PersonId.reset();
+ return new CommandResult(MESSAGE_SUCCESS, false, false,
+ false, NoteCloseInstruction.CLOSE_ALL);
+ }
+
+ /**
+ * Returns whether this command has cleared the address book.
+ */
+ public boolean hasCleared() {
+ return hasCleared;
}
}
diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java
index 249b6072d0d..ff9b5d9a898 100644
--- a/src/main/java/seedu/address/logic/commands/CommandResult.java
+++ b/src/main/java/seedu/address/logic/commands/CommandResult.java
@@ -1,10 +1,12 @@
package seedu.address.logic.commands;
import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.commands.NoteCloseInstruction.CLOSE_NONE;
import java.util.Objects;
import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.person.Person;
/**
* Represents the result of a command execution.
@@ -17,15 +19,27 @@ public class CommandResult {
private final boolean showHelp;
/** The application should exit. */
- private final boolean exit;
+ private final boolean shouldExit;
+
+ /** Note should be shown to the user. */
+ private final boolean showNote;
+ /** Note should be deleted. */
+ private final NoteCloseInstruction shouldDeleteNote;
+
+ /** The person to show the note for. */
+ private final Person targetPerson;
/**
* Constructs a {@code CommandResult} with the specified fields.
*/
- public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) {
+ public CommandResult(String feedbackToUser, boolean showHelp, boolean shouldExit,
+ boolean showNote, NoteCloseInstruction shouldDeleteNote, Person targetPerson) {
this.feedbackToUser = requireNonNull(feedbackToUser);
this.showHelp = showHelp;
- this.exit = exit;
+ this.shouldExit = shouldExit;
+ this.showNote = showNote;
+ this.shouldDeleteNote = shouldDeleteNote;
+ this.targetPerson = targetPerson;
}
/**
@@ -33,7 +47,20 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) {
* and other fields set to their default value.
*/
public CommandResult(String feedbackToUser) {
- this(feedbackToUser, false, false);
+ this(feedbackToUser, false, false, false, CLOSE_NONE, null);
+ }
+
+ /**
+ * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, {@code showHelp}, {@code exit},
+ * and {@code showNote}, and other fields set to their default value.
+ */
+ public CommandResult(String feedbackToUser, boolean showHelp, boolean shouldExit,
+ boolean showNote, NoteCloseInstruction shouldDeleteNote) {
+ this(feedbackToUser, showHelp, shouldExit, showNote, shouldDeleteNote, null);
+ }
+
+ public Person getTargetPerson() {
+ return targetPerson;
}
public String getFeedbackToUser() {
@@ -45,7 +72,14 @@ public boolean isShowHelp() {
}
public boolean isExit() {
- return exit;
+ return shouldExit;
+ }
+
+ public boolean isShowNote() {
+ return showNote;
+ }
+ public NoteCloseInstruction shouldDeleteNote() {
+ return shouldDeleteNote;
}
@Override
@@ -62,12 +96,16 @@ public boolean equals(Object other) {
CommandResult otherCommandResult = (CommandResult) other;
return feedbackToUser.equals(otherCommandResult.feedbackToUser)
&& showHelp == otherCommandResult.showHelp
- && exit == otherCommandResult.exit;
+ && shouldExit == otherCommandResult.shouldExit
+ && showNote == otherCommandResult.showNote
+ && shouldDeleteNote == otherCommandResult.shouldDeleteNote
+ && Objects.equals(targetPerson, otherCommandResult.targetPerson);
}
@Override
public int hashCode() {
- return Objects.hash(feedbackToUser, showHelp, exit);
+ return Objects.hash(feedbackToUser, showHelp, shouldExit,
+ showNote, shouldDeleteNote, targetPerson);
}
@Override
@@ -75,7 +113,9 @@ public String toString() {
return new ToStringBuilder(this)
.add("feedbackToUser", feedbackToUser)
.add("showHelp", showHelp)
- .add("exit", exit)
+ .add("exit", shouldExit)
+ .add("showNote", showNote)
+ .add("shouldDeleteNote", shouldDeleteNote)
.toString();
}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
index 1135ac19b74..1c26a90a004 100644
--- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java
+++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
@@ -26,6 +26,7 @@ public class DeleteCommand extends Command {
public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s";
private final Index targetIndex;
+ private Person targetPerson;
public DeleteCommand(Index targetIndex) {
this.targetIndex = targetIndex;
@@ -41,8 +42,11 @@ public CommandResult execute(Model model) throws CommandException {
}
Person personToDelete = lastShownList.get(targetIndex.getZeroBased());
+ this.targetPerson = personToDelete;
model.deletePerson(personToDelete);
- return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete)));
+
+ return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete)),
+ false, false, false, NoteCloseInstruction.CLOSE_ONE, personToDelete);
}
@Override
@@ -66,4 +70,11 @@ public String toString() {
.add("targetIndex", targetIndex)
.toString();
}
+
+ /**
+ * Returns the target person to be deleted.
+ */
+ public Person getTargetPerson() {
+ return targetPerson;
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java
new file mode 100644
index 00000000000..40daf150d71
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java
@@ -0,0 +1,112 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.person.Person;
+import seedu.address.storage.Storage;
+
+/**
+ * Deletes the note tagged to the contact identified using it's displayed index from the address book.
+ */
+public class DeleteNoteCommand extends Command {
+
+ public static final String COMMAND_WORD = "deletenote";
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Deletes the note of the person identified by the index number used in the displayed person list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+ public static final String MESSAGE_DELETENOTE_SUCCESS = "Note deleted for: %1$s";
+ public static final String MESSAGE_NO_NOTE = "No note found for: %1$s";
+ private final Index targetIndex;
+ private Storage storage;
+ private Person targetPerson;
+
+ /**
+ * Constructs a {@code DeleteNoteCommand} that targets a person by their index in the filtered person list.
+ *
+ * @param targetIndex The index of the person in the filtered list.
+ */
+ public DeleteNoteCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ /**
+ * Executes the delete note command by attempting to remove a note from the target person.
+ *
+ * @param model The model containing the current state of the address book.
+ * @return A {@code CommandResult} indicating success or failure, along with instructions to close the note view.
+ * @throws CommandException If the target index is invalid or an I/O error occurs during deletion.
+ */
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+ Person personToDeleteNote = lastShownList.get(targetIndex.getZeroBased());
+ this.targetPerson = personToDeleteNote;
+
+ boolean deleted;
+ try {
+ deleted = storage.deleteNote(targetPerson);
+ } catch (IOException e) {
+ throw new CommandException("Failed to delete note: " + e.getMessage(), e);
+ }
+
+ String message = deleted
+ ? String.format(MESSAGE_DELETENOTE_SUCCESS, Messages.format(personToDeleteNote))
+ : String.format(MESSAGE_NO_NOTE, Messages.format(personToDeleteNote));
+
+ return new CommandResult(message, false, false, false,
+ NoteCloseInstruction.CLOSE_ONE, personToDeleteNote);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ //instanceof handles nulls
+ if (!(other instanceof DeleteNoteCommand)) {
+ return false;
+ }
+
+ DeleteNoteCommand otherDeleteNoteCommand = (DeleteNoteCommand) other;
+ return targetIndex.equals(otherDeleteNoteCommand.targetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .toString();
+ }
+
+ /**
+ * Returns the person associated with this command at execution time.
+ *
+ * @return The target {@code Person} whose note was attempted to be deleted.
+ */
+ public Person getTargetPerson() {
+ return targetPerson;
+ }
+ /**
+ * Sets the {@code Storage} instance required to perform note deletion.
+ *
+ * @param storage The storage implementation to use.
+ */
+ public void setStorage(Storage storage) {
+ this.storage = storage;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java
index 4b581c7331e..a6a1a7fbe9b 100644
--- a/src/main/java/seedu/address/logic/commands/EditCommand.java
+++ b/src/main/java/seedu/address/logic/commands/EditCommand.java
@@ -8,15 +8,10 @@
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
import seedu.address.commons.core.index.Index;
-import seedu.address.commons.util.CollectionUtil;
import seedu.address.commons.util.ToStringBuilder;
import seedu.address.logic.Messages;
import seedu.address.logic.commands.exceptions.CommandException;
@@ -25,6 +20,7 @@
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
import seedu.address.model.person.Phone;
import seedu.address.model.tag.Tag;
@@ -53,18 +49,18 @@ public class EditCommand extends Command {
public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book.";
private final Index index;
- private final EditPersonDescriptor editPersonDescriptor;
+ private final PersonDescriptor personDescriptor;
/**
* @param index of the person in the filtered person list to edit
- * @param editPersonDescriptor details to edit the person with
+ * @param personDescriptor details to edit the person with
*/
- public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) {
+ public EditCommand(Index index, PersonDescriptor personDescriptor) {
requireNonNull(index);
- requireNonNull(editPersonDescriptor);
+ requireNonNull(personDescriptor);
this.index = index;
- this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor);
+ this.personDescriptor = new PersonDescriptor(personDescriptor);
}
@Override
@@ -77,7 +73,7 @@ public CommandResult execute(Model model) throws CommandException {
}
Person personToEdit = lastShownList.get(index.getZeroBased());
- Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor);
+ Person editedPerson = createEditedPerson(personToEdit, personDescriptor);
if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) {
throw new CommandException(MESSAGE_DUPLICATE_PERSON);
@@ -90,18 +86,19 @@ public CommandResult execute(Model model) throws CommandException {
/**
* Creates and returns a {@code Person} with the details of {@code personToEdit}
- * edited with {@code editPersonDescriptor}.
+ * edited with {@code personDescriptor}.
*/
- private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) {
+ private static Person createEditedPerson(Person personToEdit, PersonDescriptor personDescriptor) {
assert personToEdit != null;
- Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName());
- Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone());
- Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail());
- Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress());
- Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags());
+ Name updatedName = personDescriptor.getName().orElse(personToEdit.getName());
+ Phone updatedPhone = personDescriptor.getPhone().orElse(personToEdit.getPhone());
+ Email updatedEmail = personDescriptor.getEmail().orElse(personToEdit.getEmail());
+ Address updatedAddress = personDescriptor.getAddress().orElse(personToEdit.getAddress());
+ Set updatedTags = personDescriptor.getTags().orElse(personToEdit.getTags());
+ PersonId personId = personToEdit.getId();
- return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags);
+ return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, personId);
}
@Override
@@ -117,126 +114,14 @@ public boolean equals(Object other) {
EditCommand otherEditCommand = (EditCommand) other;
return index.equals(otherEditCommand.index)
- && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor);
+ && personDescriptor.equals(otherEditCommand.personDescriptor);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.add("index", index)
- .add("editPersonDescriptor", editPersonDescriptor)
+ .add("personDescriptor", personDescriptor)
.toString();
}
-
- /**
- * Stores the details to edit the person with. Each non-empty field value will replace the
- * corresponding field value of the person.
- */
- public static class EditPersonDescriptor {
- private Name name;
- private Phone phone;
- private Email email;
- private Address address;
- private Set tags;
-
- public EditPersonDescriptor() {}
-
- /**
- * Copy constructor.
- * A defensive copy of {@code tags} is used internally.
- */
- public EditPersonDescriptor(EditPersonDescriptor toCopy) {
- setName(toCopy.name);
- setPhone(toCopy.phone);
- setEmail(toCopy.email);
- setAddress(toCopy.address);
- setTags(toCopy.tags);
- }
-
- /**
- * Returns true if at least one field is edited.
- */
- public boolean isAnyFieldEdited() {
- return CollectionUtil.isAnyNonNull(name, phone, email, address, tags);
- }
-
- public void setName(Name name) {
- this.name = name;
- }
-
- public Optional getName() {
- return Optional.ofNullable(name);
- }
-
- public void setPhone(Phone phone) {
- this.phone = phone;
- }
-
- public Optional getPhone() {
- return Optional.ofNullable(phone);
- }
-
- public void setEmail(Email email) {
- this.email = email;
- }
-
- public Optional getEmail() {
- return Optional.ofNullable(email);
- }
-
- public void setAddress(Address address) {
- this.address = address;
- }
-
- public Optional getAddress() {
- return Optional.ofNullable(address);
- }
-
- /**
- * Sets {@code tags} to this object's {@code tags}.
- * A defensive copy of {@code tags} is used internally.
- */
- public void setTags(Set tags) {
- this.tags = (tags != null) ? new HashSet<>(tags) : null;
- }
-
- /**
- * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException}
- * if modification is attempted.
- * Returns {@code Optional#empty()} if {@code tags} is null.
- */
- public Optional> getTags() {
- return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
-
- // instanceof handles nulls
- if (!(other instanceof EditPersonDescriptor)) {
- return false;
- }
-
- EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other;
- return Objects.equals(name, otherEditPersonDescriptor.name)
- && Objects.equals(phone, otherEditPersonDescriptor.phone)
- && Objects.equals(email, otherEditPersonDescriptor.email)
- && Objects.equals(address, otherEditPersonDescriptor.address)
- && Objects.equals(tags, otherEditPersonDescriptor.tags);
- }
-
- @Override
- public String toString() {
- return new ToStringBuilder(this)
- .add("name", name)
- .add("phone", phone)
- .add("email", email)
- .add("address", address)
- .add("tags", tags)
- .toString();
- }
- }
}
diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java
index 3dd85a8ba90..deac69450f1 100644
--- a/src/main/java/seedu/address/logic/commands/ExitCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java
@@ -13,7 +13,8 @@ public class ExitCommand extends Command {
@Override
public CommandResult execute(Model model) {
- return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true);
+ return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true,
+ false, NoteCloseInstruction.CLOSE_NONE);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java
new file mode 100644
index 00000000000..98e6160b2b4
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java
@@ -0,0 +1,89 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+
+/**
+ * Exports contacts into a json file.
+ */
+public class ExportCommand extends Command {
+
+ public static final String COMMAND_WORD = "export";
+ public static final String MESSAGE_SOURCE_FILE_NOT_FOUND = "Address book not found."
+ + " Start adding contacts to form your address book.";
+ public static final String MESSAGE_INVALID_FILE_FORMAT = "Export failed. Target file must have a .json extension.";
+ public static final String MESSAGE_EXPORT_SUCCESS = "Exported Address Book to %1$s as requested ...";
+ public static final String MESSAGE_EXPORT_FAILURE = "Failed to export json file.";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exports the address book data at the specified path.\n"
+ + "Parameters: FILEPATH (must be a valid file path)\n"
+ + "Example (Windows): " + COMMAND_WORD + " C:/Users/username/Desktop/exported_data.json\n"
+ + "Example (Mac): " + COMMAND_WORD + " /Users/username/Desktop/exported_data.json\n"
+ + "Example (Linux): " + COMMAND_WORD + " /home/user/desktop/exported_data.json";
+ private static final Path SOURCE_JSON_PATH = Paths.get("data/addressbook.json");
+ private final Path targetPath;
+ private boolean sourcePathValid = true;
+
+ /**
+ * Creates an ExportCommand to export json file to specified target path
+ * @param targetPath
+ */
+ public ExportCommand(Path targetPath) {
+ this.targetPath = targetPath;
+ }
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(targetPath);
+
+ if (!targetPath.toString().toLowerCase().endsWith(".json")) {
+ throw new CommandException(MESSAGE_INVALID_FILE_FORMAT);
+ }
+ if (!sourcePathValid) {
+ throw new CommandException(MESSAGE_SOURCE_FILE_NOT_FOUND);
+ }
+ try {
+ // Ensure parent directory exists
+ if (targetPath.getParent() != null) {
+ Files.createDirectories(targetPath.getParent());
+ }
+
+ // Perform the export
+ Files.copy(SOURCE_JSON_PATH, targetPath, StandardCopyOption.REPLACE_EXISTING);
+ return new CommandResult(String.format(MESSAGE_EXPORT_SUCCESS, targetPath.toAbsolutePath()));
+
+ } catch (IOException e) {
+ throw new CommandException(MESSAGE_EXPORT_FAILURE);
+ }
+ }
+
+ /**
+ * Method to check if the json source file exists and
+ * store boolean in variable
+ */
+ public void sourceFileExists(String s) {
+ if (s.equals("simulate invalid source file")) {
+ sourcePathValid = false;
+ } else {
+ sourcePathValid = Files.exists(SOURCE_JSON_PATH);
+ }
+ }
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ExportCommand that = (ExportCommand) obj;
+ return targetPath.equals(that.targetPath); // Compare the file path
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/FindTagCommand.java b/src/main/java/seedu/address/logic/commands/FindTagCommand.java
new file mode 100644
index 00000000000..a02be6fa514
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/FindTagCommand.java
@@ -0,0 +1,59 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.model.Model;
+import seedu.address.model.person.TagNamesContainsTagsPredicate;
+
+/**
+ * Finds and lists all contacts in ScoopBook whose tag(s) match(es) all of the argument tag(s).
+ * Tag matching is case insensitive.
+ */
+public class FindTagCommand extends Command {
+
+ public static final String COMMAND_WORD = "findtag";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts whose tag(s) match(es) all of "
+ + "the specified tags (case-insensitive) and displays them as a list with index numbers.\n"
+ + "Parameters: t/TAG [t/MORE_TAGS]...\n"
+ + "Example: " + COMMAND_WORD + " t/friend t/reporter t/government";
+
+ public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty.";
+
+ private final TagNamesContainsTagsPredicate predicate;
+
+ public FindTagCommand(TagNamesContainsTagsPredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.updateFilteredPersonList(predicate);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size()));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof FindTagCommand)) {
+ return false;
+ }
+
+ FindTagCommand otherFindTagCommand = (FindTagCommand) other;
+ return predicate.equals(otherFindTagCommand.predicate);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("predicate", predicate)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java
index bf824f91bd0..a6ce5df38e4 100644
--- a/src/main/java/seedu/address/logic/commands/HelpCommand.java
+++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java
@@ -16,6 +16,7 @@ public class HelpCommand extends Command {
@Override
public CommandResult execute(Model model) {
- return new CommandResult(SHOWING_HELP_MESSAGE, true, false);
+ return new CommandResult(SHOWING_HELP_MESSAGE, true, false,
+ false, NoteCloseInstruction.CLOSE_NONE);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java
new file mode 100644
index 00000000000..d229e1ecb94
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java
@@ -0,0 +1,124 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.person.Person;
+import seedu.address.storage.JsonAddressBookStorage;
+
+/**
+ * Imports contacts from a specified JSON file.
+ */
+public class ImportCommand extends Command {
+
+ public static final String COMMAND_WORD = "import";
+ public static final String MESSAGE_INVALID_FILE_FORMAT = "Import failed. Target file must have a .json extension.";
+ public static final String MESSAGE_IMPORT_SUCCESS = "Imported contacts from %1$s as requested ...";
+ public static final String MESSAGE_IMPORT_FAILURE = "Failed to import json file.";
+ public static final String MESSAGE_INVALID_JSON = "JSON file does not follow required format.";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports contacts from the specified path.\n"
+ + "Parameters: FILEPATH (must be a valid file path)\n"
+ + "Example (Windows): " + COMMAND_WORD + " C:/Users/username/Desktop/exported_data.json\n"
+ + "Example (Mac): " + COMMAND_WORD + " /Users/username/Desktop/exported_data.json\n"
+ + "Example (Linux): " + COMMAND_WORD + " /home/user/desktop/exported_data.json";
+ private int previousIdNum = 0;
+ private final Path targetPath;
+
+ /**
+ * Imports contacts from specified .json file from {@param targetPath}
+ */
+ public ImportCommand(Path targetPath) {
+ this.targetPath = targetPath;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ requireNonNull(targetPath);
+
+ if (!targetPath.toString().toLowerCase().endsWith(".json")) {
+ throw new CommandException(MESSAGE_INVALID_FILE_FORMAT);
+ }
+
+ JsonAddressBookStorage jsonStorage = new JsonAddressBookStorage(targetPath);
+
+ try {
+ ReadOnlyAddressBook importedAddressBook = jsonStorage.readAddressBook()
+ .orElseThrow(() -> new CommandException(MESSAGE_IMPORT_FAILURE));
+
+ validateImportedContacts(importedAddressBook);
+ model.setAddressBook(importedAddressBook);
+
+ Path savePath = Paths.get("data", "addressbook.json");
+ new JsonAddressBookStorage(savePath).saveAddressBook(model.getAddressBook());
+
+ return new CommandResult(String.format(MESSAGE_IMPORT_SUCCESS, targetPath));
+
+ } catch (DataLoadingException | IOException e) {
+ throw new CommandException("Invalid JSON file: " + shortenErrorMessage(e));
+ }
+ }
+
+ /**
+ * Method to shorten error message
+ * @param e
+ * @return shortened error message
+ */
+ private String shortenErrorMessage(Exception e) {
+ if (e.getCause() instanceof IllegalValueException) {
+ IllegalValueException ive = (IllegalValueException) e.getCause();
+ return ive.getMessage();
+ }
+ return MESSAGE_INVALID_JSON;
+ }
+
+ /**
+ * Checks that at least one of "phone", "email", or "address" is different from placeholder values.
+ * Checks that personId of the first person is 1 and increment sequentially.
+ */
+ private void validateImportedContacts(ReadOnlyAddressBook addressBook) throws CommandException {
+ for (Person person : addressBook.getPersonList()) {
+ if (isAllPlaceholderValues(person)) {
+ throw new CommandException("Invalid JSON file: Person '" + person.getName()
+ + "' does not have any added phone, email, or address.");
+ }
+ int personId = person.getId().getIntId();
+ if (personId > previousIdNum) {
+ previousIdNum = personId;
+ } else {
+ throw new CommandException("Invalid JSON file: Person '" + person.getName()
+ + "' has either out-of-order OR duplicate person ID.");
+ }
+ }
+ }
+
+ /**
+ * Returns true if all three fields (phone, email, address) are set to placeholder values.
+ */
+ private boolean isAllPlaceholderValues(Person person) {
+ return "000".equals(person.getPhone().value)
+ && "unknown@example.com".equals(person.getEmail().value)
+ && "Unknown address".equals(person.getAddress().value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ImportCommand that = (ImportCommand) obj;
+ return targetPath.equals(that.targetPath); // Compare the file path
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java b/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java
new file mode 100644
index 00000000000..3b1c170ea57
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java
@@ -0,0 +1,7 @@
+package seedu.address.logic.commands;
+/**
+ * Represents the instruction for deletion of notes.
+ */
+public enum NoteCloseInstruction {
+ CLOSE_ONE, CLOSE_ALL, CLOSE_NONE
+}
diff --git a/src/main/java/seedu/address/logic/commands/NoteCommand.java b/src/main/java/seedu/address/logic/commands/NoteCommand.java
new file mode 100644
index 00000000000..b82da674d41
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/NoteCommand.java
@@ -0,0 +1,57 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.person.Person;
+
+/**
+ * Creates a .txt file tagged to contact identified using it's displayed index from the address book.
+ */
+public class NoteCommand extends Command {
+
+ public static final String COMMAND_WORD = "note";
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Adds a note to the person identified by the index number used in the displayed person list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+ public static final String MESSAGE_NOTE_PERSON_SUCCESS = "Viewing note for: %1$s";
+
+ private final Index targetIndex;
+ public NoteCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+ Person personToAddNote = lastShownList.get(targetIndex.getZeroBased());
+ return new CommandResult(String.format(MESSAGE_NOTE_PERSON_SUCCESS, Messages.format(personToAddNote)),
+ false, false, true, NoteCloseInstruction.CLOSE_NONE, personToAddNote);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof NoteCommand)) {
+ return false;
+ }
+
+ NoteCommand otherNoteCommand = (NoteCommand) other;
+ return targetIndex.equals(otherNoteCommand.targetIndex);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/PersonDescriptor.java b/src/main/java/seedu/address/logic/commands/PersonDescriptor.java
new file mode 100644
index 00000000000..c0338e40d40
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/PersonDescriptor.java
@@ -0,0 +1,129 @@
+package seedu.address.logic.commands;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.util.CollectionUtil;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Phone;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Stores the details to tag the person with. Each non-empty field value will replace the
+ * corresponding field value of the person.
+ */
+public class PersonDescriptor {
+ private Name name;
+ private Phone phone;
+ private Email email;
+ private Address address;
+ private Set tags;
+
+ public PersonDescriptor() {
+ }
+
+ /**
+ * Copy constructor.
+ * A defensive copy of {@code tags} is used internally.
+ */
+ public PersonDescriptor(PersonDescriptor toCopy) {
+ setName(toCopy.name);
+ setPhone(toCopy.phone);
+ setEmail(toCopy.email);
+ setAddress(toCopy.address);
+ setTags(toCopy.tags);
+ }
+
+ /**
+ * Returns true if at least one field is edited.
+ */
+ public boolean isAnyFieldEdited() {
+ return CollectionUtil.isAnyNonNull(name, phone, email, address, tags);
+ }
+
+ public void setName(Name name) {
+ this.name = name;
+ }
+
+ public Optional getName() {
+ return Optional.ofNullable(name);
+ }
+
+ public void setPhone(Phone phone) {
+ this.phone = phone;
+ }
+
+ public Optional getPhone() {
+ return Optional.ofNullable(phone);
+ }
+
+ public void setEmail(Email email) {
+ this.email = email;
+ }
+
+ public Optional getEmail() {
+ return Optional.ofNullable(email);
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public Optional getAddress() {
+ return Optional.ofNullable(address);
+ }
+
+ /**
+ * Sets {@code tags} to this object's {@code tags}.
+ * A defensive copy of {@code tags} is used internally.
+ */
+ public void setTags(Set tags) {
+ this.tags = (tags != null) ? new HashSet<>(tags) : null;
+ }
+
+ /**
+ * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException}
+ * if modification is attempted.
+ * Returns {@code Optional#empty()} if {@code tags} is null.
+ */
+ public Optional> getTags() {
+ return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof PersonDescriptor)) {
+ return false;
+ }
+
+ PersonDescriptor otherPersonDescriptor =
+ (PersonDescriptor) other;
+ return Objects.equals(name, otherPersonDescriptor.name)
+ && Objects.equals(phone, otherPersonDescriptor.phone)
+ && Objects.equals(email, otherPersonDescriptor.email)
+ && Objects.equals(address, otherPersonDescriptor.address)
+ && Objects.equals(tags, otherPersonDescriptor.tags);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("name", name)
+ .add("phone", phone)
+ .add("email", email)
+ .add("address", address)
+ .add("tags", tags)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java b/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java
new file mode 100644
index 00000000000..e82477362f6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java
@@ -0,0 +1,195 @@
+package seedu.address.logic.commands;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
+import seedu.address.model.person.Phone;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Removes specified tags from a person in the address book.
+ */
+public class RemoveTagCommand extends Command {
+
+ public static final String COMMAND_WORD = "removetag";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Removes the specified tag(s) from the person in the address book.\n"
+ + "Parameters: INDEX (must be a positive integer) t/TAG [t/TAG]...\n"
+ + "Example: " + COMMAND_WORD + " 1 t/Friend t/Colleague";
+ public static final String MESSAGE_REMOVE_TAG_SUCCESS = "Tag(s) %2$s removed for person: %1$s";
+ public static final String MESSAGE_INVALID_TAGS = "Tag(s) %2$s do not exist for this person.";
+ public static final String MESSAGE_TAG_NOT_FOUND = "Tag(s) do not exist for this person.";
+ public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty";
+ private final Index targetIndex;
+ private final PersonDescriptor personDescriptor;
+
+ /**
+ * @param targetIndex of the person in the filtered person list to edit
+ * @param personDescriptor details to edit the person with
+ */
+ public RemoveTagCommand(Index targetIndex, PersonDescriptor personDescriptor) {
+ this.targetIndex = targetIndex;
+ this.personDescriptor = new PersonDescriptor(personDescriptor);
+ }
+
+ /**
+ * Executes the RemoveTagCommand by removing specified tags from the selected person.
+ * Displays which tags were removed and which were invalid (not found).
+ *
+ * @param model The current model containing the address book.
+ * @return CommandResult containing feedback to the user.
+ * @throws CommandException if the index is invalid or no valid tags were found.
+ */
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ Person personToRemoveTag = lastShownList.get(targetIndex.getZeroBased());
+ TagRemovalResult result = createTagRemovalResult(personToRemoveTag, personDescriptor);
+ Person removedTagPerson = result.updatedPerson;
+
+ if (result.validTags.isEmpty()) {
+ throw new CommandException(MESSAGE_TAG_NOT_FOUND);
+ }
+
+ String message = messageBuilder(result);
+
+ model.setPerson(personToRemoveTag, removedTagPerson);
+ return new CommandResult(message);
+ }
+
+ /**
+ * Creates and returns a {@code Person}, removing from {@code personToRemoveTag}
+ * only the tags specified in {@code personDescriptor}.
+ */
+ private static TagRemovalResult createTagRemovalResult(Person personToRemoveTag,
+ PersonDescriptor personDescriptor) {
+ assert personToRemoveTag != null;
+
+ Name name = personToRemoveTag.getName();
+ Phone phone = personToRemoveTag.getPhone();
+ Email email = personToRemoveTag.getEmail();
+ Address address = personToRemoveTag.getAddress();
+
+ // Start with the existing set of Tags
+ Set existingTags = new HashSet<>(personToRemoveTag.getTags());
+ Set validTags = new HashSet<>();
+ Set invalidTags = new HashSet<>();
+ Optional> tagsToRemove = personDescriptor.getTags();
+ PersonId personId = personToRemoveTag.getId();
+
+ //Sort and remove valid tags
+ for (Tag tag : tagsToRemove.orElse(Collections.emptySet())) {
+ if (existingTags.contains(tag)) {
+ existingTags.remove(tag);
+ validTags.add(tag);
+ } else {
+ invalidTags.add(tag);
+ }
+ }
+
+ Person removedTagPerson = new Person(name, phone, email, address, existingTags, personId);
+ return new TagRemovalResult(removedTagPerson, validTags, invalidTags);
+ }
+
+ /**
+ * Constructs a user-friendly message summarizing which tags were removed
+ * and which tags were not found on the person.
+ *
+ * @param result The result of the tag removal operation.
+ * @return A formatted message to be displayed to the user.
+ */
+ private static String messageBuilder(TagRemovalResult result) {
+ Person removedTagPerson = result.updatedPerson;
+
+ String validTagString = formatTagSet(result.validTags);
+ String invalidTagString = formatTagSet(result.invalidTags);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(MESSAGE_REMOVE_TAG_SUCCESS, Messages.format(removedTagPerson), validTagString));
+ if (!result.invalidTags.isEmpty()) {
+ sb.append("\n\n").append(String.format(MESSAGE_INVALID_TAGS,
+ Messages.format(removedTagPerson), invalidTagString));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Formats a set of tags into a comma-separated string.
+ *
+ * @param tags Set of tags to format.
+ * @return A string representation of the tag set, or "none" if empty.
+ */
+ private static String formatTagSet(Set tags) {
+ return tags.stream()
+ .map(Tag::toString)
+ .sorted()
+ .reduce((a, b) -> a + ", " + b)
+ .orElse("none");
+ }
+
+
+ /**
+ * A container for the result of a removeTag operation.
+ * Stores the updated {@code Person} object after attempting to remove tags,
+ * along with sets of tags that were successfully removed and tags that were not found.
+ */
+ private static class TagRemovalResult {
+ private final Person updatedPerson;
+ private final Set validTags;
+ private final Set invalidTags;
+
+ /**
+ * Constructs a {@code TagRemovalResult}.
+ *
+ * @param updatedPerson The updated person after tag removal.
+ * @param validTags Tags that were successfully removed.
+ * @param invalidTags Tags that were not found on the person.
+ */
+ public TagRemovalResult(Person updatedPerson, Set validTags, Set invalidTags) {
+ this.updatedPerson = updatedPerson;
+ this.validTags = validTags;
+ this.invalidTags = invalidTags;
+ }
+ }
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof RemoveTagCommand otherCommand)) {
+ return false;
+ }
+
+ return targetIndex.equals(otherCommand.targetIndex)
+ && personDescriptor.equals(otherCommand.personDescriptor);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .add("personDescriptor", personDescriptor)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java
index 4ff1a97ed77..80fc59400f6 100644
--- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java
@@ -33,16 +33,19 @@ public AddCommand parse(String args) throws ParseException {
ArgumentMultimap argMultimap =
ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG);
- if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL)
+ if ((!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS)
+ && !arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE)
+ && !arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_EMAIL))
|| !argMultimap.getPreamble().isEmpty()) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE));
}
argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS);
Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get());
- Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get());
- Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get());
- Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get());
+ Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).orElseGet(() -> Phone.EMPTY_PHONE));
+ Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).orElseGet(() -> Email.EMPTY_EMAIL));
+ Address address =
+ ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).orElseGet(() -> Address.EMPTY_ADDRESS));
Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG));
Person person = new Person(name, phone, email, address, tagList);
diff --git a/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java
new file mode 100644
index 00000000000..b5d856bfbc6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java
@@ -0,0 +1,67 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.AddTagCommand;
+import seedu.address.logic.commands.PersonDescriptor;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a new AddTagCommand object
+ */
+public class AddTagCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddTagCommand
+ * and returns an AddTagCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+
+ public AddTagCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG);
+
+ Index index;
+
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE), pe);
+ }
+ if (!arePrefixesPresent(argMultimap, PREFIX_TAG)) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE));
+ }
+ PersonDescriptor personDescriptor = new PersonDescriptor();
+ // If no tags are provided, throw an exception
+ parseTags(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personDescriptor::setTags);
+ return new AddTagCommand(index, personDescriptor);
+ }
+
+ /**
+ * Returns true if none of the prefixes contains empty {@code Optional} values in the given
+ * {@code ArgumentMultimap}.
+ */
+ private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) {
+ return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent());
+ }
+
+ private Optional> parseTags(Collection tags) throws ParseException {
+ assert tags != null;
+
+ if (tags.isEmpty() || tags.contains("")) {
+ throw new ParseException(AddTagCommand.MESSAGE_EMPTY_TAG);
+ }
+ Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags;
+ return Optional.of(ParserUtil.parseTags(tagSet));
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java
index 3149ee07e0b..1621b0ce9e5 100644
--- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java
+++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java
@@ -9,14 +9,21 @@
import seedu.address.commons.core.LogsCenter;
import seedu.address.logic.commands.AddCommand;
+import seedu.address.logic.commands.AddTagCommand;
import seedu.address.logic.commands.ClearCommand;
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.commands.DeleteNoteCommand;
import seedu.address.logic.commands.EditCommand;
import seedu.address.logic.commands.ExitCommand;
+import seedu.address.logic.commands.ExportCommand;
import seedu.address.logic.commands.FindCommand;
+import seedu.address.logic.commands.FindTagCommand;
import seedu.address.logic.commands.HelpCommand;
+import seedu.address.logic.commands.ImportCommand;
import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.NoteCommand;
+import seedu.address.logic.commands.RemoveTagCommand;
import seedu.address.logic.parser.exceptions.ParseException;
/**
@@ -68,6 +75,9 @@ public Command parseCommand(String userInput) throws ParseException {
case FindCommand.COMMAND_WORD:
return new FindCommandParser().parse(arguments);
+ case FindTagCommand.COMMAND_WORD:
+ return new FindTagCommandParser().parse(arguments);
+
case ListCommand.COMMAND_WORD:
return new ListCommand();
@@ -77,6 +87,24 @@ public Command parseCommand(String userInput) throws ParseException {
case HelpCommand.COMMAND_WORD:
return new HelpCommand();
+ case RemoveTagCommand.COMMAND_WORD:
+ return new RemoveTagCommandParser().parse(arguments);
+
+ case AddTagCommand.COMMAND_WORD:
+ return new AddTagCommandParser().parse(arguments);
+
+ case ExportCommand.COMMAND_WORD:
+ return new ExportCommandParser().parse(arguments);
+
+ case NoteCommand.COMMAND_WORD:
+ return new NoteCommandParser().parse(arguments);
+
+ case DeleteNoteCommand.COMMAND_WORD:
+ return new DeleteNoteCommandParser().parse(arguments);
+
+ case ImportCommand.COMMAND_WORD:
+ return new ImportCommandParser().parse(arguments);
+
default:
logger.finer("This user input caused a ParseException: " + userInput);
throw new ParseException(MESSAGE_UNKNOWN_COMMAND);
diff --git a/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java
new file mode 100644
index 00000000000..753534e79ae
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java
@@ -0,0 +1,28 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.DeleteNoteCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteNoteCommand object
+ */
+public class DeleteNoteCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteNoteCommand
+ * and returns a DeleteNoteCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteNoteCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteNoteCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteNoteCommand.MESSAGE_USAGE), pe);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
index 46b3309a78b..c97037adbac 100644
--- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
@@ -15,7 +15,7 @@
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.EditCommand;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
+import seedu.address.logic.commands.PersonDescriptor;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.tag.Tag;
@@ -44,27 +44,27 @@ public EditCommand parse(String args) throws ParseException {
argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS);
- EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor();
+ PersonDescriptor personDescriptor = new PersonDescriptor();
if (argMultimap.getValue(PREFIX_NAME).isPresent()) {
- editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()));
+ personDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()));
}
if (argMultimap.getValue(PREFIX_PHONE).isPresent()) {
- editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()));
+ personDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()));
}
if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) {
- editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()));
+ personDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()));
}
if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) {
- editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()));
+ personDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()));
}
- parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags);
+ parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personDescriptor::setTags);
- if (!editPersonDescriptor.isAnyFieldEdited()) {
+ if (!personDescriptor.isAnyFieldEdited()) {
throw new ParseException(EditCommand.MESSAGE_NOT_EDITED);
}
- return new EditCommand(index, editPersonDescriptor);
+ return new EditCommand(index, personDescriptor);
}
/**
diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java
new file mode 100644
index 00000000000..df3d2b55638
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java
@@ -0,0 +1,39 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import seedu.address.logic.commands.ExportCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+/**
+ * Parses input arguments and creates a new ExportCommand object
+ */
+public class ExportCommandParser implements Parser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the ExportCommand
+ * and returns an ExportCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public ExportCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE));
+ }
+ String[] parts = trimmedArgs.split("\\s+", 2);
+ if (parts.length > 1) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE));
+ }
+ try {
+ Path targetPath = Paths.get(trimmedArgs);
+ return new ExportCommand(targetPath);
+ } catch (InvalidPathException e) {
+ throw new ParseException(ExportCommand.MESSAGE_EXPORT_FAILURE + " Invalid path: "
+ + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java
new file mode 100644
index 00000000000..4450b1f2e6d
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java
@@ -0,0 +1,45 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.List;
+
+import seedu.address.logic.commands.FindTagCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.TagNamesContainsTagsPredicate;
+
+/**
+ * Parses input arguments and creates a new FindTagCommand object
+ */
+public class FindTagCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the FindTagCommand
+ * and returns a FindTagCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public FindTagCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG);
+
+ List tagNames = argMultimap.getAllValues(PREFIX_TAG);
+
+ if (tagNames.isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE));
+ }
+
+ if (tagNames.contains("")) {
+ throw new ParseException(FindTagCommand.MESSAGE_EMPTY_TAG);
+ }
+
+ try {
+ ParserUtil.parseTags(tagNames);
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE), pe);
+ }
+
+ return new FindTagCommand(new TagNamesContainsTagsPredicate(tagNames));
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java
new file mode 100644
index 00000000000..96f19c445ba
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java
@@ -0,0 +1,40 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import seedu.address.logic.commands.ImportCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new ImportCommand object
+ */
+public class ImportCommandParser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the ImportCommand
+ * and returns an ImportCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public ImportCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE));
+ }
+ String[] parts = trimmedArgs.split("\\s+", 2);
+ if (parts.length > 1) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE));
+ }
+ try {
+ Path targetPath = Paths.get(trimmedArgs);
+ return new ImportCommand(targetPath);
+ } catch (InvalidPathException e) {
+ throw new ParseException(ImportCommand.MESSAGE_IMPORT_FAILURE + " Invalid path: "
+ + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/NoteCommandParser.java b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java
new file mode 100644
index 00000000000..69e362ccba4
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java
@@ -0,0 +1,28 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.NoteCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new NoteCommand object
+ */
+public class NoteCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the NoteCommand
+ * and returns a NoteCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public NoteCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new NoteCommand(index);
+ } catch (ParseException pe) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, NoteCommand.MESSAGE_USAGE), pe);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java b/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java
new file mode 100644
index 00000000000..826b0652725
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java
@@ -0,0 +1,62 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.PersonDescriptor;
+import seedu.address.logic.commands.RemoveTagCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.tag.Tag;
+/**
+ * Parses input arguments and creates a new RemoveTagCommand object
+ */
+public class RemoveTagCommandParser implements Parser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the RemoveTagCommand
+ * and returns a RemoveTagCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public RemoveTagCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_TAG);
+
+ Index index;
+
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE), pe);
+ }
+
+ if (!argMultimap.getValue(PREFIX_TAG).isPresent()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE));
+ }
+
+ PersonDescriptor removeTagPersonDescriptor = new PersonDescriptor();
+
+ parseTags(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(removeTagPersonDescriptor::setTags);
+ return new RemoveTagCommand(index, removeTagPersonDescriptor);
+ }
+
+ /**
+ * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty.
+ * If {@code tags} contain only one element which is an empty string, it will be parsed into a
+ * {@code Set} containing zero tags.
+ */
+ private Optional> parseTags(Collection tags) throws ParseException {
+ assert tags != null;
+
+ if (tags.isEmpty() || tags.contains("")) {
+ throw new ParseException(RemoveTagCommand.MESSAGE_EMPTY_TAG);
+ }
+
+ Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags;
+ return Optional.of(ParserUtil.parseTags(tagSet));
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java
index 469a2cc9a1e..f82cfbb7827 100644
--- a/src/main/java/seedu/address/model/person/Address.java
+++ b/src/main/java/seedu/address/model/person/Address.java
@@ -16,6 +16,7 @@ public class Address {
* otherwise " " (a blank string) becomes a valid input.
*/
public static final String VALIDATION_REGEX = "[^\\s].*";
+ public static final String EMPTY_ADDRESS = "Unknown address";
public final String value;
@@ -39,7 +40,7 @@ public static boolean isValidAddress(String test) {
@Override
public String toString() {
- return value;
+ return value.equals(EMPTY_ADDRESS) ? "" : value;
}
@Override
diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java
index c62e512bc29..85b513cb379 100644
--- a/src/main/java/seedu/address/model/person/Email.java
+++ b/src/main/java/seedu/address/model/person/Email.java
@@ -9,6 +9,7 @@
*/
public class Email {
+ public static final String EMPTY_EMAIL = "unknown@example.com";
private static final String SPECIAL_CHARACTERS = "+_.-";
public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain "
+ "and adhere to the following constraints:\n"
@@ -53,7 +54,7 @@ public static boolean isValidEmail(String test) {
@Override
public String toString() {
- return value;
+ return value.equals(EMPTY_EMAIL) ? "" : value;
}
@Override
diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java
index 173f15b9b00..1483dc603c2 100644
--- a/src/main/java/seedu/address/model/person/Name.java
+++ b/src/main/java/seedu/address/model/person/Name.java
@@ -10,13 +10,14 @@
public class Name {
public static final String MESSAGE_CONSTRAINTS =
- "Names should only contain alphanumeric characters and spaces, and it should not be blank";
+ "Names should only contain alphanumeric characters, spaces, and the following special characters "
+ + ", ( ) . @ - ' \nIt should also 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 static final String VALIDATION_REGEX = "[\\p{Alnum},.'()@-][\\p{Alnum} ,.'()@-]*";
public final String fullName;
diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java
index abe8c46b535..4c667431282 100644
--- a/src/main/java/seedu/address/model/person/Person.java
+++ b/src/main/java/seedu/address/model/person/Person.java
@@ -25,8 +25,12 @@ public class Person {
private final Address address;
private final Set tags = new HashSet<>();
+ // For internal usage; ID field
+ private final PersonId id;
+
/**
* Every field must be present and not null.
+ * This constructor is used when creating a new person.
*/
public Person(Name name, Phone phone, Email email, Address address, Set tags) {
requireAllNonNull(name, phone, email, address, tags);
@@ -35,6 +39,21 @@ public Person(Name name, Phone phone, Email email, Address address, Set tag
this.email = email;
this.address = address;
this.tags.addAll(tags);
+ this.id = new PersonId();
+ }
+
+ /**
+ * Every field must be present and not null.
+ * This constructor is used when loading data from storage.
+ */
+ public Person(Name name, Phone phone, Email email, Address address, Set tags, PersonId id) {
+ requireAllNonNull(name, phone, email, address, tags, id);
+ this.name = name;
+ this.phone = phone;
+ this.email = email;
+ this.address = address;
+ this.tags.addAll(tags);
+ this.id = id;
}
public Name getName() {
@@ -53,6 +72,10 @@ public Address getAddress() {
return address;
}
+ public PersonId getId() {
+ return id;
+ }
+
/**
* Returns an immutable tag set, which throws {@code UnsupportedOperationException}
* if modification is attempted.
@@ -100,7 +123,7 @@ public boolean equals(Object other) {
@Override
public int hashCode() {
// use this method for custom fields hashing instead of implementing your own
- return Objects.hash(name, phone, email, address, tags);
+ return Objects.hash(name, phone, email, address, tags, id);
}
@Override
@@ -111,6 +134,7 @@ public String toString() {
.add("email", email)
.add("address", address)
.add("tags", tags)
+ .add("id", id)
.toString();
}
diff --git a/src/main/java/seedu/address/model/person/PersonId.java b/src/main/java/seedu/address/model/person/PersonId.java
new file mode 100644
index 00000000000..b4f58bc07bc
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/PersonId.java
@@ -0,0 +1,91 @@
+package seedu.address.model.person;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
+/**
+ * Represents a Person's ID in the address book.
+ * Guarantees: immutable; is valid as declared in {@link #isValidId(String)}
+ * For internal use and reference only.
+ */
+public class PersonId {
+
+ public static final String MESSAGE_CONSTRAINTS =
+ "PersonId should be an integer"; // The person id must be a valid integer
+ public static final String VALIDATION_REGEX = "^0|[1-9]\\d*$";
+
+ private static int counter = 0;
+ public final String value;
+
+ /**
+ * Constructs a {@code PersonId}.
+ * When a new {@code Person} is created, the {@code Person} is assigned a unique ID.
+ */
+ public PersonId() {
+ PersonId.counter++;
+ this.value = String.valueOf(PersonId.counter);
+ }
+
+ /**
+ * Constructs a {@code PersonId} with a specified ID.
+ * Used when loading data from storage.
+ */
+ public PersonId(String id) {
+ requireNonNull(id);
+ checkArgument(isValidId(id), MESSAGE_CONSTRAINTS);
+ value = id;
+ }
+
+ /**
+ * Returns true if a given string is a valid person id.
+ */
+ public static boolean isValidId(String test) {
+ return test.matches(VALIDATION_REGEX);
+ }
+
+ /**
+ * Resets the counter to 0.
+ * Called in conjunction with {@code ClearCommand#execute()}.
+ */
+ public static int reset() {
+ PersonId.counter = 0;
+ return PersonId.counter;
+ }
+
+ /**
+ * Sets the counter to the specified value.
+ * Called in conjunction with {@code Storage#readAddressBook()}.
+ */
+ public static int setCounter(int maxId) {
+ checkArgument(isValidId(String.valueOf(maxId)), MESSAGE_CONSTRAINTS);
+ PersonId.counter = maxId;
+ return PersonId.counter;
+ }
+
+ /**
+ * Getter to obtain person ID
+ * @return the person ID in integer
+ */
+ public int getIntId() {
+ return Integer.parseInt(this.value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof PersonId)) {
+ return false;
+ }
+
+ PersonId otherId = (PersonId) other;
+ return value.equals(otherId.value);
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java
index d733f63d739..5b4b09fde2e 100644
--- a/src/main/java/seedu/address/model/person/Phone.java
+++ b/src/main/java/seedu/address/model/person/Phone.java
@@ -13,6 +13,7 @@ public class Phone {
public static final String MESSAGE_CONSTRAINTS =
"Phone numbers should only contain numbers, and it should be at least 3 digits long";
public static final String VALIDATION_REGEX = "\\d{3,}";
+ public static final String EMPTY_PHONE = "000";
public final String value;
/**
@@ -33,9 +34,17 @@ public static boolean isValidPhone(String test) {
return test.matches(VALIDATION_REGEX);
}
+ /**
+ * Returns a string representation of the phone number.
+ * If the phone number is not present, it returns "Unknown number".
+ */
+ public String getPhoneNumber() {
+ return value.equals(EMPTY_PHONE) ? "Unknown number" : value;
+ }
+
@Override
public String toString() {
- return value;
+ return value.equals(EMPTY_PHONE) ? "" : value;
}
@Override
diff --git a/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java b/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java
new file mode 100644
index 00000000000..dff15899f33
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java
@@ -0,0 +1,45 @@
+package seedu.address.model.person;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.ToStringBuilder;
+
+/**
+ * Tests that a {@code Person}'s tags contain all the tag(s) given.
+ * Tag matching is case-insensitive.
+ */
+public class TagNamesContainsTagsPredicate implements Predicate {
+ private final List tags;
+
+ public TagNamesContainsTagsPredicate(List tags) {
+ this.tags = tags;
+ }
+
+ @Override
+ public boolean test(Person person) {
+ return tags.stream()
+ .allMatch(tagName -> person.getTags().stream()
+ .anyMatch(tag -> tag.tagName.equalsIgnoreCase(tagName)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof TagNamesContainsTagsPredicate)) {
+ return false;
+ }
+
+ TagNamesContainsTagsPredicate otherTagNamesContainsTagsPredicate = (TagNamesContainsTagsPredicate) other;
+ return tags.equals(otherTagNamesContainsTagsPredicate.tags);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).add("tags", tags).toString();
+ }
+}
diff --git a/src/main/java/seedu/address/storage/FileNotesStorage.java b/src/main/java/seedu/address/storage/FileNotesStorage.java
new file mode 100644
index 00000000000..a0e252551d8
--- /dev/null
+++ b/src/main/java/seedu/address/storage/FileNotesStorage.java
@@ -0,0 +1,77 @@
+package seedu.address.storage;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.model.person.Person;
+
+/**
+ * A class to manage notes storage in the file system.
+ */
+public class FileNotesStorage implements NotesStorage {
+ private static final Logger logger = LogsCenter.getLogger(FileNotesStorage.class);
+ private final Path notesDir;
+
+ /**
+ * Creates a new FileNotesStorage.
+ *
+ * @param notesDir The directory to store notes in
+ */
+ public FileNotesStorage(Path notesDir) {
+ this.notesDir = notesDir;
+ createNotesDirectory();
+ }
+
+ private void createNotesDirectory() {
+ try {
+ Files.createDirectories(notesDir);
+ } catch (IOException e) {
+ logger.warning("Failed to create notes directory: " + e.getMessage());
+ }
+ }
+
+ private Path getNoteFilePath(Person person) {
+ return notesDir.resolve(person.getId().value + ".txt");
+ }
+
+ @Override
+ public String readNote(Person person) throws IOException {
+ Path notePath = getNoteFilePath(person);
+ if (Files.exists(notePath)) {
+ return Files.readString(notePath);
+ }
+ return "";
+ }
+
+ @Override
+ public void saveNote(Person person, String content) throws IOException {
+ Files.createDirectories(notesDir);
+ Files.writeString(getNoteFilePath(person), content);
+ }
+
+ @Override
+ public boolean deleteNote(Person person) throws IOException {
+ Path notePath = getNoteFilePath(person);
+ if (Files.exists(notePath)) {
+ Files.delete(notePath);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void deleteAllNotes() throws IOException {
+ if (Files.exists(notesDir)) {
+ Files.list(notesDir).filter(path -> path.toString().endsWith(".txt")).forEach(path -> {
+ try {
+ Files.delete(path);
+ } catch (IOException e) {
+ logger.warning("Failed to delete note: " + e.getMessage());
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
index bd1ca0f56c8..9a0253e4228 100644
--- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
+++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
@@ -14,6 +14,7 @@
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
import seedu.address.model.person.Phone;
import seedu.address.model.tag.Tag;
@@ -29,6 +30,7 @@ class JsonAdaptedPerson {
private final String email;
private final String address;
private final List tags = new ArrayList<>();
+ private final String personId;
/**
* Constructs a {@code JsonAdaptedPerson} with the given person details.
@@ -36,7 +38,7 @@ class JsonAdaptedPerson {
@JsonCreator
public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone,
@JsonProperty("email") String email, @JsonProperty("address") String address,
- @JsonProperty("tags") List tags) {
+ @JsonProperty("tags") List tags, @JsonProperty("personId") String personId) {
this.name = name;
this.phone = phone;
this.email = email;
@@ -44,6 +46,7 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone
if (tags != null) {
this.tags.addAll(tags);
}
+ this.personId = personId;
}
/**
@@ -57,6 +60,7 @@ public JsonAdaptedPerson(Person source) {
tags.addAll(source.getTags().stream()
.map(JsonAdaptedTag::new)
.collect(Collectors.toList()));
+ personId = source.getId().value;
}
/**
@@ -101,9 +105,24 @@ public Person toModelType() throws IllegalValueException {
throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS);
}
final Address modelAddress = new Address(address);
+ if (personId == null) {
+ throw new IllegalValueException(
+ String.format(MISSING_FIELD_MESSAGE_FORMAT, PersonId.class.getSimpleName()));
+ }
+ if (!PersonId.isValidId(personId)) {
+ throw new IllegalValueException(PersonId.MESSAGE_CONSTRAINTS);
+ }
+ final PersonId modelPersonId = new PersonId(personId);
final Set modelTags = new HashSet<>(personTags);
- return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags);
+ return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelPersonId);
+ }
+
+ /**
+ * Returns the personId of the person.
+ */
+ public String getPersonId() {
+ return personId;
}
}
diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java
index 5efd834091d..bbeff4bf99e 100644
--- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java
+++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java
@@ -12,6 +12,7 @@
import seedu.address.model.AddressBook;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
/**
* An Immutable AddressBook that is serializable to JSON format.
@@ -54,6 +55,13 @@ public AddressBook toModelType() throws IllegalValueException {
}
addressBook.addPerson(person);
}
+
+ // Set the id counter to the maximum id in the address book
+ int maxId = persons.stream()
+ .mapToInt(jsonAdaptedPerson -> Integer.parseInt(jsonAdaptedPerson.getPersonId()))
+ .max()
+ .orElse(0);
+ PersonId.setCounter(maxId);
return addressBook;
}
diff --git a/src/main/java/seedu/address/storage/NotesStorage.java b/src/main/java/seedu/address/storage/NotesStorage.java
new file mode 100644
index 00000000000..8346c642418
--- /dev/null
+++ b/src/main/java/seedu/address/storage/NotesStorage.java
@@ -0,0 +1,41 @@
+package seedu.address.storage;
+
+import java.io.IOException;
+
+import seedu.address.model.person.Person;
+
+/**
+ * Represents a storage for Notes.
+ */
+public interface NotesStorage {
+
+ /**
+ * Reads the note for a person.
+ * @param person The person to read the note for.
+ * @return The content of the note.
+ * @throws IOException If an error occurs during reading.
+ */
+ String readNote(Person person) throws IOException;
+
+ /**
+ * Saves a note for a person.
+ * @param person The person to save the note for.
+ * @param content The content of the note.
+ * @throws IOException If an error occurs during saving.
+ */
+ void saveNote(Person person, String content) throws IOException;
+
+ /**
+ * Deletes the note for a person if it exists.
+ * @param person The person to delete the note for.
+ * @return true if the note was found and successfully deleted, false if no note was found.
+ * @throws IOException If an error occurs during deletion.
+ */
+ boolean deleteNote(Person person) throws IOException;
+
+ /**
+ * Deletes all notes.
+ * @throws IOException If an error occurs during deletion.
+ */
+ void deleteAllNotes() throws IOException;
+}
diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java
index 9fba0c7a1d6..f7f6eba5b85 100644
--- a/src/main/java/seedu/address/storage/Storage.java
+++ b/src/main/java/seedu/address/storage/Storage.java
@@ -8,11 +8,12 @@
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
/**
* API of the Storage component
*/
-public interface Storage extends AddressBookStorage, UserPrefsStorage {
+public interface Storage extends AddressBookStorage, UserPrefsStorage, NotesStorage {
@Override
Optional readUserPrefs() throws DataLoadingException;
@@ -29,4 +30,15 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage {
@Override
void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException;
+ @Override
+ String readNote(Person person) throws IOException;
+
+ @Override
+ void saveNote(Person person, String content) throws IOException;
+
+ @Override
+ boolean deleteNote(Person person) throws IOException;
+
+ @Override
+ void deleteAllNotes() throws IOException;
}
diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java
index 8b84a9024d5..266b22d5eb4 100644
--- a/src/main/java/seedu/address/storage/StorageManager.java
+++ b/src/main/java/seedu/address/storage/StorageManager.java
@@ -2,6 +2,7 @@
import java.io.IOException;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Optional;
import java.util.logging.Logger;
@@ -10,6 +11,7 @@
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
/**
* Manages storage of AddressBook data in local storage.
@@ -19,13 +21,26 @@ public class StorageManager implements Storage {
private static final Logger logger = LogsCenter.getLogger(StorageManager.class);
private AddressBookStorage addressBookStorage;
private UserPrefsStorage userPrefsStorage;
+ private final NotesStorage notesStorage;
/**
* Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}.
*/
public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) {
+ this(addressBookStorage, userPrefsStorage,
+ new FileNotesStorage(Paths.get("data/notes")));
+ }
+
+ /**
+ * Creates a {@code StorageManager} with the given {@code AddressBookStorage}, {@code UserPrefStorage} and
+ * {@code NotesStorage}.
+ */
+ public StorageManager(AddressBookStorage addressBookStorage,
+ UserPrefsStorage userPrefsStorage,
+ NotesStorage notesStorage) {
this.addressBookStorage = addressBookStorage;
this.userPrefsStorage = userPrefsStorage;
+ this.notesStorage = notesStorage;
}
// ================ UserPrefs methods ==============================
@@ -75,4 +90,25 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro
addressBookStorage.saveAddressBook(addressBook, filePath);
}
+ // ================ Notes methods ==============================
+
+ @Override
+ public String readNote(Person person) throws IOException {
+ return notesStorage.readNote(person);
+ }
+
+ @Override
+ public void saveNote(Person person, String content) throws IOException {
+ notesStorage.saveNote(person, content);
+ }
+
+ @Override
+ public boolean deleteNote(Person person) throws IOException {
+ return notesStorage.deleteNote(person);
+ }
+
+ @Override
+ public void deleteAllNotes() throws IOException {
+ notesStorage.deleteAllNotes();
+ }
}
diff --git a/src/main/java/seedu/address/ui/DialogBox.java b/src/main/java/seedu/address/ui/DialogBox.java
new file mode 100644
index 00000000000..1cf80e37f0e
--- /dev/null
+++ b/src/main/java/seedu/address/ui/DialogBox.java
@@ -0,0 +1,47 @@
+package seedu.address.ui;
+
+import static java.util.Objects.requireNonNull;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+
+/**
+ * A custom control using FXML that represents a dialog box.
+ */
+public class DialogBox extends UiPart {
+ private static final String FXML = "DialogBox.fxml";
+ @FXML
+ private Label text;
+
+ /**
+ * Constructor for DialogBox.
+ * @param s
+ */
+ public DialogBox(String s) {
+ super(FXML);
+ text.setText(s);
+ }
+
+ public void setDialogBox(String textToSet) {
+ requireNonNull(textToSet);
+ text.setText(textToSet);
+ }
+
+
+
+ public static DialogBox getUserDialog(String text) {
+ return new DialogBox(text);
+ }
+
+ public static DialogBox getScoopBookDialog(String text) {
+ var db = new DialogBox(text);
+ setScoopBookDialogStyle(db);
+ return db;
+ }
+ private static void setScoopBookDialogStyle(DialogBox db) {
+ db.getRoot().setScaleX(-1);
+ db.text.setScaleX(-1);
+ db.text.styleProperty().set("-fx-background-color: #9c9c9c");
+ }
+}
diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java
index 3f16b2fcf26..07eb84eaf77 100644
--- a/src/main/java/seedu/address/ui/HelpWindow.java
+++ b/src/main/java/seedu/address/ui/HelpWindow.java
@@ -1,10 +1,15 @@
package seedu.address.ui;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.util.logging.Logger;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.stage.Stage;
@@ -15,7 +20,7 @@
*/
public class HelpWindow extends UiPart {
- public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html";
+ public static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-w13-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);
@@ -27,6 +32,9 @@ public class HelpWindow extends UiPart {
@FXML
private Label helpMessage;
+ @FXML
+ private TextArea localGuideTextArea;
+
/**
* Creates a new HelpWindow.
*
@@ -35,6 +43,7 @@ public class HelpWindow extends UiPart {
public HelpWindow(Stage root) {
super(FXML, root);
helpMessage.setText(HELP_MESSAGE);
+ loadLocalGuide();
}
/**
@@ -99,4 +108,37 @@ private void copyUrl() {
url.putString(USERGUIDE_URL);
clipboard.setContent(url);
}
+
+ /**
+ * Loads the local user guide from a text file
+ */
+ private void loadLocalGuide() {
+ String content = loadResourceFile("help/local_userguide.txt");
+ if (content != null) {
+ localGuideTextArea.setText(content);
+ } else {
+ localGuideTextArea.setText("Local user guide not found.");
+ }
+ }
+
+ /**
+ * Loads a resource file from the classpath.
+ *
+ * @param filename The filename of the resource.
+ * @return The content of the file, or null if an error occurs.
+ */
+ public String loadResourceFile(String filename) {
+ StringBuilder content = new StringBuilder();
+ try (InputStream inputStream = getClass().getResourceAsStream("/" + filename);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+ return content.toString();
+ } catch (IOException | NullPointerException e) {
+ logger.warning("Error loading resource file: " + filename);
+ return null;
+ }
+ }
}
diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java
index 79e74ef37c0..99c6842256a 100644
--- a/src/main/java/seedu/address/ui/MainWindow.java
+++ b/src/main/java/seedu/address/ui/MainWindow.java
@@ -5,10 +5,12 @@
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
@@ -16,6 +18,7 @@
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.Person;
/**
* The Main Window. Provides the basic application layout containing
@@ -34,9 +37,11 @@ public class MainWindow extends UiPart {
private PersonListPanel personListPanel;
private ResultDisplay resultDisplay;
private HelpWindow helpWindow;
-
+ private NoteWindowHandler noteWindowHandler;
@FXML
private StackPane commandBoxPlaceholder;
+ @FXML
+ private ScrollPane scrollPane;
@FXML
private MenuItem helpMenuItem;
@@ -45,11 +50,14 @@ public class MainWindow extends UiPart {
private StackPane personListPanelPlaceholder;
@FXML
- private StackPane resultDisplayPlaceholder;
+ private VBox resultDisplayPlaceholder;
@FXML
private StackPane statusbarPlaceholder;
+ @FXML
+ private DialogBox dialogBox;
+
/**
* Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}.
*/
@@ -66,6 +74,7 @@ public MainWindow(Stage primaryStage, Logic logic) {
setAccelerators();
helpWindow = new HelpWindow();
+ noteWindowHandler = new NoteWindowHandler(logic);
}
public Stage getPrimaryStage() {
@@ -109,12 +118,14 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) {
/**
* Fills up all the placeholders of this window.
*/
+ @SuppressWarnings("checkstyle:Regexp")
void fillInnerParts() {
personListPanel = new PersonListPanel(logic.getFilteredPersonList());
personListPanelPlaceholder.getChildren().add(personListPanel.getRoot());
-
resultDisplay = new ResultDisplay();
- resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot());
+
+ resultDisplayPlaceholder.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0));
+ scrollPane.setContent(resultDisplayPlaceholder);
StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath());
statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot());
@@ -147,6 +158,14 @@ public void handleHelp() {
}
}
+ /**
+ * Opens the note window for a specific person.
+ */
+ @FXML
+ public void handleNote(Person person) {
+ noteWindowHandler.openNoteWindow(person);
+ }
+
void show() {
primaryStage.show();
}
@@ -160,6 +179,7 @@ private void handleExit() {
(int) primaryStage.getX(), (int) primaryStage.getY());
logic.setGuiSettings(guiSettings);
helpWindow.hide();
+ noteWindowHandler.closeAllNoteWindows(true);
primaryStage.hide();
}
@@ -176,8 +196,14 @@ private CommandResult executeCommand(String commandText) throws CommandException
try {
CommandResult commandResult = logic.execute(commandText);
logger.info("Result: " + commandResult.getFeedbackToUser());
+ // prints the user's input
+ resultDisplayPlaceholder.getChildren().add(DialogBox.getUserDialog(commandText).getRoot());
+ // print the result of the command
+ resultDisplayPlaceholder.getChildren().add(
+ DialogBox.getScoopBookDialog(commandResult.getFeedbackToUser()).getRoot());
resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());
+
if (commandResult.isShowHelp()) {
handleHelp();
}
@@ -186,10 +212,30 @@ private CommandResult executeCommand(String commandText) throws CommandException
handleExit();
}
+ if (commandResult.isShowNote()) {
+ handleNote(commandResult.getTargetPerson());
+ }
+
+ switch (commandResult.shouldDeleteNote()) {
+ case CLOSE_ONE:
+ noteWindowHandler.closeNoteWindowWithoutSaving(commandResult.getTargetPerson());
+ break;
+ case CLOSE_ALL:
+ // For clear command, close all note windows without saving
+ noteWindowHandler.closeAllNoteWindows(false);
+ break;
+ default:
+ break;
+ }
return commandResult;
} catch (CommandException | ParseException e) {
logger.info("An error occurred while executing command: " + commandText);
resultDisplay.setFeedbackToUser(e.getMessage());
+ // prints the user's input
+ resultDisplayPlaceholder.getChildren().add(DialogBox.getUserDialog(commandText).getRoot());
+ // print the result of the command
+ resultDisplayPlaceholder.getChildren().add(
+ DialogBox.getScoopBookDialog(e.getMessage()).getRoot());
throw e;
}
}
diff --git a/src/main/java/seedu/address/ui/NoteWindow.java b/src/main/java/seedu/address/ui/NoteWindow.java
new file mode 100644
index 00000000000..2c4ac422eca
--- /dev/null
+++ b/src/main/java/seedu/address/ui/NoteWindow.java
@@ -0,0 +1,153 @@
+package seedu.address.ui;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.TextArea;
+import javafx.stage.Stage;
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.logic.Logic;
+import seedu.address.model.person.Person;
+
+/**
+ * Controller for a note page
+ */
+public class NoteWindow extends UiPart {
+
+ private static final Logger logger = LogsCenter.getLogger(NoteWindow.class);
+ private static final String FXML = "NoteWindow.fxml";
+
+ @FXML
+ private TextArea noteTextArea;
+
+ private Person person;
+ private Logic logic;
+
+ /**
+ * Creates a new NoteWindow.
+ *
+ * @param root Stage to use as the root of the NoteWindow.
+ */
+ public NoteWindow(Stage root) {
+ super(FXML, root);
+ }
+
+ /**
+ * Creates a new NoteWindow with a new Stage.
+ */
+ public NoteWindow() {
+ this(new Stage());
+ }
+
+ /**
+ * Creates a new NoteWindow for a specific person.
+ *
+ * @param person The person to create notes for
+ */
+ public NoteWindow(Person person, Logic logic) {
+ this();
+ this.person = person;
+ this.logic = logic;
+ if (person != null) {
+ getRoot().setTitle("Notes for " + person.getName().fullName);
+ loadExistingNotes();
+ }
+ }
+
+ /**
+ * Sets up the close and hide handler for the Note window.
+ */
+ public void setupCloseAndHideHandler(Runnable handler) {
+ getRoot().setOnCloseRequest(event -> {
+ saveNotes();
+ handler.run();
+ });
+ getRoot().setOnHidden(event -> {
+ saveNotes();
+ handler.run();
+ });
+ }
+ /**
+ * Closes the notes for a specific person if available.
+ */
+ public void closeWithoutSaving() {
+ getRoot().setOnCloseRequest(event -> {});
+ getRoot().setOnHidden(event -> {});
+ getRoot().close();
+ }
+
+ /**
+ * Loads existing notes for a specific person if available.
+ */
+ private void loadExistingNotes() {
+ try {
+ String content = logic.readNote(person);
+ noteTextArea.setText(content);
+ logger.info("Loaded notes for person: " + person.getName().fullName);
+ } catch (IOException e) {
+ logger.warning("Failed to load notes for person: " + person.getName().fullName);
+ }
+ }
+
+ /**
+ * Saves the notes for a specific person to a file.
+ */
+ private void saveNotes() {
+ try {
+ logic.saveNote(person, noteTextArea.getText());
+ logger.info("Saved notes for person: " + person.getName().fullName);
+ } catch (IOException e) {
+ logger.warning("Failed to save notes: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Delete the notes for a specific person if available.
+ */
+ private void deleteNotes() {
+ try {
+ logic.deleteNote(person);
+ logger.info("Deleted notes for person: " + person.getName().fullName);
+ } catch (IOException e) {
+ logger.warning("Failed to delete notes: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Shows the Note window.
+ */
+ public void show() {
+ logger.fine("Showing Note window.");
+ getRoot().show();
+ getRoot().centerOnScreen();
+ }
+
+ /**
+ * Hides the Note window.
+ */
+ public void hide() {
+ getRoot().hide();
+ }
+
+ /**
+ * Focuses on the Note window.
+ */
+ public void focus() {
+ getRoot().requestFocus();
+ }
+
+ /**
+ * Sets the text in the note area.
+ */
+ public void setText(String text) {
+ noteTextArea.setText(text);
+ }
+
+ /**
+ * Gets the text from the note area.
+ */
+ public String getText() {
+ return noteTextArea.getText();
+ }
+}
diff --git a/src/main/java/seedu/address/ui/NoteWindowHandler.java b/src/main/java/seedu/address/ui/NoteWindowHandler.java
new file mode 100644
index 00000000000..fc02b70f393
--- /dev/null
+++ b/src/main/java/seedu/address/ui/NoteWindowHandler.java
@@ -0,0 +1,72 @@
+package seedu.address.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import seedu.address.logic.Logic;
+import seedu.address.model.person.Person;
+/**
+ * Handles the opening and closing of NoteWindows.
+ */
+public class NoteWindowHandler {
+ private final Map openNoteWindows = new HashMap<>();
+ private final Logic logic;
+ public NoteWindowHandler(Logic logic) {
+ this.logic = logic;
+ }
+
+ /**
+ * Opens a NoteWindow for the specified person. If a NoteWindow is already open for the person, it will be focused.
+ * @param person
+ */
+ public void openNoteWindow(Person person) {
+ if (openNoteWindows.containsKey(person)) {
+ NoteWindow existingNoteWindow = openNoteWindows.get(person);
+ existingNoteWindow.focus();
+ } else {
+ NoteWindow newNoteWindow = new NoteWindow(person, logic);
+ newNoteWindow.setupCloseAndHideHandler(() -> closeNoteWindowWithSaving(person));
+ openNoteWindows.put(person, newNoteWindow);
+ newNoteWindow.show();
+ }
+ }
+
+ /**
+ * Closes the NoteWindow for the specified person, saving the notes.
+ * @param person
+ */
+ public void closeNoteWindowWithSaving(Person person) {
+ // Since the notes are set to save on close/hide by default,
+ // we only need to close/hide the window to save the notes.
+ NoteWindow noteWindow = openNoteWindows.get(person);
+ if (noteWindow != null) {
+ noteWindow.hide();
+ openNoteWindows.remove(person);
+ }
+ }
+ /**
+ * Closes the NoteWindow for the specified person without saving the notes.
+ * @param person
+ */
+ public void closeNoteWindowWithoutSaving(Person person) {
+ // Since the notes are set to save on close/hide by default,
+ // we need to reset the handler, then close/hide the window to save the notes.
+ NoteWindow noteWindow = openNoteWindows.get(person);
+ if (noteWindow != null) {
+ noteWindow.closeWithoutSaving();
+ openNoteWindows.remove(person);
+ }
+ }
+
+ /**
+ * Closes all open NoteWindows.
+ */
+ public void closeAllNoteWindows(boolean isToSave) {
+ if (isToSave) {
+ new ArrayList<>(openNoteWindows.keySet()).forEach(this::closeNoteWindowWithSaving);
+ } else {
+ new ArrayList<>(openNoteWindows.keySet()).forEach(this::closeNoteWindowWithoutSaving);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java
index 094c42cda82..483569a8498 100644
--- a/src/main/java/seedu/address/ui/PersonCard.java
+++ b/src/main/java/seedu/address/ui/PersonCard.java
@@ -1,5 +1,9 @@
package seedu.address.ui;
+import static seedu.address.model.person.Address.EMPTY_ADDRESS;
+import static seedu.address.model.person.Email.EMPTY_EMAIL;
+import static seedu.address.model.person.Phone.EMPTY_PHONE;
+
import java.util.Comparator;
import javafx.fxml.FXML;
@@ -52,8 +56,23 @@ public PersonCard(Person person, int displayedIndex) {
phone.setText(person.getPhone().value);
address.setText(person.getAddress().value);
email.setText(person.getEmail().value);
+
+ // Apply visibility settings for optional fields
+ updateVisibility(phone, phone.getText(), EMPTY_PHONE);
+ updateVisibility(address, address.getText(), EMPTY_ADDRESS);
+ updateVisibility(email, email.getText(), EMPTY_EMAIL);
+
person.getTags().stream()
.sorted(Comparator.comparing(tag -> tag.tagName))
.forEach(tag -> tags.getChildren().add(new Label(tag.tagName)));
}
+
+ /**
+ * Updates the visibility of a label based on whether the field is empty.
+ */
+ public void updateVisibility(Label label, String text, String emptyText) {
+ boolean isEmpty = text.equals(emptyText);
+ label.setVisible(!isEmpty);
+ label.setManaged(!isEmpty);
+ }
}
diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java
index fdf024138bc..d638530f59e 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/ScoopBook.png";
private Logic logic;
private MainWindow mainWindow;
diff --git a/src/main/resources/help/local_userguide.txt b/src/main/resources/help/local_userguide.txt
new file mode 100644
index 00000000000..ad9866fe8da
--- /dev/null
+++ b/src/main/resources/help/local_userguide.txt
@@ -0,0 +1,224 @@
+How to use ScoopBook?
+
+1. Adding a contact: add
+
+Adds a contact to the address book.
+
+Format: add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]
+
+Examples:
+- add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01
+- add n/Betsy Crowe e/betsycrowe@example.com a/Newgate Prison p/1234567 t/Criminal
+
+Note:
+- The add command must have a name, and one of the following fields: phone number, email, address.
+ i.e. 'add n/Johnny Appleseed' does not work because there is no phone number, email or address.
+- A person can have any number of tags (including 0).
+- A person's name can only contain alphanumeric characters (numbers or letters only), whitespaces, and the following
+ special characters , ( ) @ . - '
+- A person's tags can only contain alphanumeric characters (numbers or letters only, no special characters).
+- If a contact is added with the following values, they will not be displayed in the contact list,
+ as they are used as internal placeholders:
+ - Phone Number: `000`
+ - Email: `unknown@example.com`
+ - Address: `Unknown address`
+ This ensures that every contact has a placeholder value for these fields if left empty.
+
+2. Listing all contacts: list
+
+Shows a list of all contacts in the address book.
+
+Format: list
+
+3. Editing a contact: edit
+
+Edits an existing person in the address book at specified index.
+
+Format: edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...
+
+Note:
+- Edits the contact at the specified INDEX. The index refers to the index number shown in the displayed contact list.
+ The index must be a positive integer (1, 2, 3, …).
+- At least one of the optional fields must be provided.
+- Existing values will be updated to the input values.
+- 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 contact’s tags by typing t/ without specifying any tags after it.
+- Similar to the add command, the aforementioned placeholder values will not be displayed in the contact list.
+
+
+Examples:
+- 'edit 1 p/91234567 e/johndoe@example.com' edits the phone number and email address of the 1st person to "91234567" and
+ "johndoe@example.com".
+- 'edit 2 n/Betsy Crower t/' edits the name of the 2nd person to "Betsy Crower" and clears all existing tags.
+
+4. Locating contacts by name: find
+
+Finds persons whose names contain any of the given keywords.
+
+Format: find KEYWORD [MORE_KEYWORDS]
+
+Note:
+- The search is case-insensitive. For example, "hans" will match "Hans".
+- The order of the keywords does not matter. For example, "Hans Bo" will match "Bo Hans".
+- Only the name is searched.
+- Only full words will be matched. For example, "Han" will not match "Hans".
+- Persons matching at least one keyword will be returned (OR search). For example, "Hans Bo" will return "Hans Gruber"
+ and "Bo Yang".
+
+5. Deleting a person: delete
+
+Deletes the specified person from the address book.
+
+Format: delete INDEX
+
+Note:
+- Deletes the person at the specified INDEX.
+- The index refers to the index number shown in the displayed person list.
+- The index must be a positive integer (1, 2, 3, ...).
+
+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.
+
+6. Adding tags to a contact: addtag
+
+Adds the tag(s) typed in to the specified person.
+
+Format: addtag INDEX t/TAG1 [t/MORETAGS]
+
+Note:
+- Adds the specified tags to 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, ...
+- Multiple tags in a single addtag command is supported. i.e. 'addtag 1 t/Witness t/HomeAffairs' will tag the 1st person
+ with both "Witness" and "HomeAffairs".
+- Tags can only contain alphanumeric characters (numbers or letters only, no special characters or spaces).
+- Tags are case-sensitive. i.e. 'addtag 1 t/witness' will add the tag "witness" while 'addtag 1 t/Witness'
+ will add the tag "Witness".
+
+Examples:
+- 'list' followed by 'addtag 2 t/Education' tags the 2nd person with "Education" in the address book.
+- 'find Betsy' followed by 'addtag 1 t/Victim' tags the 1st person in the results of the find command with "Victim".
+
+7. Removing tag from a contact: removetag
+
+Removes the specified tag(s) from the specified person.
+
+Format: removetag INDEX t/TAG1 [t/MORE_TAGS]
+
+Note:
+- Removes the specified tag(s) from 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, ...
+- Multiple tags in a single removetag command is supported. i.e. 'removetag 1 t/Witness t/Local' will remove
+ both the "Witness" and "Local" tag for the 1st person.
+- Tags are case-sensitive. The typed tag must match the tag on the person exactly. i.e. 'removetag 1 t/witness' will
+ not remove the tag "Witness".
+
+Examples:
+- 'list' followed by 'removetag 2 t/Witness' removes the "Witness" tag from the 2nd person in the address book.
+- 'find Betsy' followed by 'removetag 1 t/Victim' removes the "Victim" tag from the 1st person in the results of
+ the find command.
+
+8. Finding people with tags: findtag
+
+Find persons who have all the specified tag(s).
+
+Format: findtag t/TAG1 [t/MORE_TAGS]
+
+Note:
+- The searching of tags is case-insensitive. e.g "friends" will match "Friends"
+- The order of the tags does not matter. i.e. As long as the person has the listed tags, they will be shown.
+- Only the tags are searched.
+- Only full words will be matched e.g. "Friend" will not match "Friends"
+- Only persons matching all the tags will be returned (i.e. AND search).
+
+Examples:
+- 'findtag t/witness' returns people with tag "witness", "Witness", "witNeSs" (due to case insensitivity).
+- 'findtag t/witness t/HomeAffairs' returns people with tag "Witness" and "HomeAffairs" only.
+
+9. Opening Note for Person: note
+
+Opens a window for the user to add notes to the person at the specified INDEX.
+
+Format: note INDEX
+
+Note:
+- Opens a window for the user to add notes to the person at the specified INDEX.
+- Please use only this opened window to edit the note.
+- The index refers to the index number shown in the displayed person list.
+- The index must be a positive integer 1, 2, 3, ...
+- The note will be saved when the window is closed.
+
+Examples:
+* 'list' followed by 'note 2' opens a note window for the 2nd person in the address book.
+* 'find Betsy' followed by 'note 1' opens a note window for the 1st person in the results of the find command.
+
+10. Deleting Note from Person: deletenote
+
+Deletes the note from the person.
+
+Format: deletenote INDEX
+
+Note:
+- Deletes note for 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, ...
+
+11. Exporting your contacts: export
+
+Exports the contacts in a .json file to the target path.
+
+Format: export TARGET_PATH
+
+Note:
+- The export command only exports your contacts. It does not export the notes tagged to them.
+- Before executing the export command, add at least 1 contact using the add command.
+- Export command is case-insensitive. If 'sAmPle.json' already exists (in the folder the 'ScoopBook.jar'
+ is located at), export sample.json` will overwrite 'sAmPle.json'.
+- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the TARGET_PATH.
+- TIP: If you are running into issues with TARGET_PATH, use export sample.json to export it directly to the root
+ folder with of the ScoopBook.jar file. Then, move the .json file to wherever you want it to be.
+
+Examples:
+- For Windows: 'export C:/Users/username/Desktop/MyContacts.json' saves the json file as MyContacts.json in the
+ Users/username/Desktop folder.
+- For macOS: 'export /Users/username/Desktop/MyContacts.json' saves the json file as MyContacts.json in the
+ Users/username/Desktop folder.
+- For Linux: 'export /home/user/desktop/MyContacts.json' saves the json file as MyContacts.json in the
+ home/user/desktop folder.
+- For all OS: 'export Contacts.json' saves the json file as Contacts.json in the root folder of where
+ ScoopBook.jar is located at.
+
+12. Importing your contacts: import
+
+Imports the external .json file from target path into the application.
+
+Format: import TARGET_PATH
+
+Note:
+- WARNING: This command overwrites existing contacts and remove all notes.
+- Only import .json files exported using the export command.
+- Ensure that there are no special characters (E.g. *!<>) or spaces in the TARGET_PATH.
+
+Examples:
+- For Windows: 'import C:/Users/username/Desktop/MyContacts.json' imports the json file from MyContacts.json in the
+ Users/username/Desktop folder.
+- For macOS: 'import /Users/username/Desktop/MyContacts.json' imports the json file from MyContacts.json in the
+ Users/username/Desktop folder.
+- For Linux: 'import /home/user/desktop/MyContacts.json' imports the json file from MyContacts.json in the
+ home/user/desktop folder.
+- For all OS: 'import Contacts.json' imports the json file named Contacts.json from the root folder of where
+ ScoopBook.jar is located at.
+
+13. Clearing all entries: clear
+
+Warning: this clears all contacts, notes & .txt files from the address book.
+
+Format: clear
+
+14. Exiting the program: exit
+
+Exits the program.
+
+Format: exit
diff --git a/src/main/resources/images/ScoopBook.png b/src/main/resources/images/ScoopBook.png
new file mode 100644
index 00000000000..4401557641a
Binary files /dev/null and b/src/main/resources/images/ScoopBook.png differ
diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css
index 36e6b001cd8..e19f5fe8328 100644
--- a/src/main/resources/view/DarkTheme.css
+++ b/src/main/resources/view/DarkTheme.css
@@ -100,11 +100,11 @@
}
.list-cell:filled:even {
- -fx-background-color: #3c3e3f;
+ -fx-background-color: #3f3c3f;
}
.list-cell:filled:odd {
- -fx-background-color: #515658;
+ -fx-background-color: #585156;
}
.list-cell:filled:selected {
@@ -344,7 +344,7 @@
#tags .label {
-fx-text-fill: white;
- -fx-background-color: #3e7b91;
+ -fx-background-color: #eb73dd;
-fx-padding: 1 3 1 3;
-fx-border-radius: 2;
-fx-background-radius: 2;
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 00000000000..c8fe5db94c1
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css
index 17e8a8722cd..09e61156a28 100644
--- a/src/main/resources/view/HelpWindow.css
+++ b/src/main/resources/view/HelpWindow.css
@@ -1,4 +1,4 @@
-#copyButton, #helpMessage {
+#copyButton, #helpMessage, #noInternetMessage {
-fx-text-fill: white;
}
diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml
index e01f330de33..d9b23cec784 100644
--- a/src/main/resources/view/HelpWindow.fxml
+++ b/src/main/resources/view/HelpWindow.fxml
@@ -1,15 +1,15 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
@@ -19,26 +19,32 @@
-
+
-
-
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
index 7778f666a0a..eb649f2f79f 100644
--- a/src/main/resources/view/MainWindow.fxml
+++ b/src/main/resources/view/MainWindow.fxml
@@ -6,15 +6,17 @@
+
+
+
-
+
-
+
@@ -23,8 +25,8 @@
-
-
+
+
@@ -32,28 +34,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/main/resources/view/NoteWindow.fxml b/src/main/resources/view/NoteWindow.fxml
new file mode 100644
index 00000000000..89072e9d595
--- /dev/null
+++ b/src/main/resources/view/NoteWindow.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/data/ImportCommandTest/duplicatePersonId.json b/src/test/data/ImportCommandTest/duplicatePersonId.json
new file mode 100644
index 00000000000..2155b186327
--- /dev/null
+++ b/src/test/data/ImportCommandTest/duplicatePersonId.json
@@ -0,0 +1,45 @@
+{
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "personId" : "1"
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "personId" : "1"
+ }, {
+ "name" : "Charlotte Oliveiro",
+ "phone" : "93210283",
+ "email" : "charlotte@example.com",
+ "address" : "Blk 11 Ang Mo Kio Street 74, #11-04",
+ "tags" : [ "neighbours" ],
+ "personId" : "3"
+ }, {
+ "name" : "David Li",
+ "phone" : "91031282",
+ "email" : "lidavid@example.com",
+ "address" : "Blk 436 Serangoon Gardens Street 26, #16-43",
+ "tags" : [ "family" ],
+ "personId" : "4"
+ }, {
+ "name" : "Irfan Ibrahim",
+ "phone" : "92492021",
+ "email" : "irfan@example.com",
+ "address" : "Blk 47 Tampines Street 20, #17-35",
+ "tags" : [ "classmates" ],
+ "personId" : "5"
+ }, {
+ "name" : "Roy Balakrishnan",
+ "phone" : "92624417",
+ "email" : "royb@example.com",
+ "address" : "Blk 45 Aljunied Street 85, #11-31",
+ "tags" : [ "colleagues" ],
+ "personId" : "6"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/invalidAddressBook.txt b/src/test/data/ImportCommandTest/invalidAddressBook.txt
new file mode 100644
index 00000000000..896bc934f6e
--- /dev/null
+++ b/src/test/data/ImportCommandTest/invalidAddressBook.txt
@@ -0,0 +1,45 @@
+{
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "personId" : "1"
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "personId" : "2"
+ }, {
+ "name" : "Charlotte Oliveiro",
+ "phone" : "93210283",
+ "email" : "charlotte@example.com",
+ "address" : "Blk 11 Ang Mo Kio Street 74, #11-04",
+ "tags" : [ "neighbours" ],
+ "personId" : "3"
+ }, {
+ "name" : "David Li",
+ "phone" : "91031282",
+ "email" : "lidavid@example.com",
+ "address" : "Blk 436 Serangoon Gardens Street 26, #16-43",
+ "tags" : [ "family" ],
+ "personId" : "4"
+ }, {
+ "name" : "Irfan Ibrahim",
+ "phone" : "92492021",
+ "email" : "irfan@example.com",
+ "address" : "Blk 47 Tampines Street 20, #17-35",
+ "tags" : [ "classmates" ],
+ "personId" : "5"
+ }, {
+ "name" : "Roy Balakrishnan",
+ "phone" : "92624417",
+ "email" : "royb@example.com",
+ "address" : "Blk 45 Aljunied Street 85, #11-31",
+ "tags" : [ "colleagues" ],
+ "personId" : "6"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noAddressField.json b/src/test/data/ImportCommandTest/noAddressField.json
new file mode 100644
index 00000000000..09c8cac07ff
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noAddressField.json
@@ -0,0 +1,9 @@
+{
+ "persons" : [ {
+ "name" : "alex",
+ "phone" : "000",
+ "email" : "unknown@example.com",
+ "tags" : [ ],
+ "personId" : "1"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noCustomPhoneEmailAddress.json b/src/test/data/ImportCommandTest/noCustomPhoneEmailAddress.json
new file mode 100644
index 00000000000..f5d54859c01
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noCustomPhoneEmailAddress.json
@@ -0,0 +1,10 @@
+{
+ "persons" : [ {
+ "name" : "alex",
+ "phone" : "000",
+ "email" : "unknown@example.com",
+ "address" : "Unknown address",
+ "tags" : [ ],
+ "personId" : "1"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noEmailField.json b/src/test/data/ImportCommandTest/noEmailField.json
new file mode 100644
index 00000000000..a172a46cf1d
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noEmailField.json
@@ -0,0 +1,9 @@
+{
+ "persons" : [ {
+ "name" : "alex",
+ "phone" : "000",
+ "address" : "Unknown address",
+ "tags" : [ ],
+ "personId" : "1"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noNameField.json b/src/test/data/ImportCommandTest/noNameField.json
new file mode 100644
index 00000000000..f15a6011266
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noNameField.json
@@ -0,0 +1,9 @@
+{
+ "persons" : [ {
+ "phone" : "100",
+ "email" : "unknown@example.com",
+ "address" : "Unknown address",
+ "tags" : [ ],
+ "personId" : "1"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noPersonIdField.json b/src/test/data/ImportCommandTest/noPersonIdField.json
new file mode 100644
index 00000000000..c364b3dc00f
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noPersonIdField.json
@@ -0,0 +1,9 @@
+{
+ "persons" : [ {
+ "name" : "alex",
+ "phone" : "100",
+ "email" : "unknown@example.com",
+ "address" : "Unknown address",
+ "tags" : [ ]
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/noPhoneField.json b/src/test/data/ImportCommandTest/noPhoneField.json
new file mode 100644
index 00000000000..e0f1fdc8963
--- /dev/null
+++ b/src/test/data/ImportCommandTest/noPhoneField.json
@@ -0,0 +1,9 @@
+{
+ "persons" : [ {
+ "name" : "alex",
+ "email" : "unknown@example.com",
+ "address" : "Unknown address",
+ "tags" : [ ],
+ "personId" : "1"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/outOfOrderPersonId.json b/src/test/data/ImportCommandTest/outOfOrderPersonId.json
new file mode 100644
index 00000000000..89d90389ef0
--- /dev/null
+++ b/src/test/data/ImportCommandTest/outOfOrderPersonId.json
@@ -0,0 +1,45 @@
+{
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "personId" : "1"
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "personId" : "2"
+ }, {
+ "name" : "Charlotte Oliveiro",
+ "phone" : "93210283",
+ "email" : "charlotte@example.com",
+ "address" : "Blk 11 Ang Mo Kio Street 74, #11-04",
+ "tags" : [ "neighbours" ],
+ "personId" : "3"
+ }, {
+ "name" : "David Li",
+ "phone" : "91031282",
+ "email" : "lidavid@example.com",
+ "address" : "Blk 436 Serangoon Gardens Street 26, #16-43",
+ "tags" : [ "family" ],
+ "personId" : "4"
+ }, {
+ "name" : "Irfan Ibrahim",
+ "phone" : "92492021",
+ "email" : "irfan@example.com",
+ "address" : "Blk 47 Tampines Street 20, #17-35",
+ "tags" : [ "classmates" ],
+ "personId" : "7"
+ }, {
+ "name" : "Roy Balakrishnan",
+ "phone" : "92624417",
+ "email" : "royb@example.com",
+ "address" : "Blk 45 Aljunied Street 85, #11-31",
+ "tags" : [ "colleagues" ],
+ "personId" : "6"
+ } ]
+}
diff --git a/src/test/data/ImportCommandTest/validAddressBook.json b/src/test/data/ImportCommandTest/validAddressBook.json
new file mode 100644
index 00000000000..896bc934f6e
--- /dev/null
+++ b/src/test/data/ImportCommandTest/validAddressBook.json
@@ -0,0 +1,45 @@
+{
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "personId" : "1"
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "personId" : "2"
+ }, {
+ "name" : "Charlotte Oliveiro",
+ "phone" : "93210283",
+ "email" : "charlotte@example.com",
+ "address" : "Blk 11 Ang Mo Kio Street 74, #11-04",
+ "tags" : [ "neighbours" ],
+ "personId" : "3"
+ }, {
+ "name" : "David Li",
+ "phone" : "91031282",
+ "email" : "lidavid@example.com",
+ "address" : "Blk 436 Serangoon Gardens Street 26, #16-43",
+ "tags" : [ "family" ],
+ "personId" : "4"
+ }, {
+ "name" : "Irfan Ibrahim",
+ "phone" : "92492021",
+ "email" : "irfan@example.com",
+ "address" : "Blk 47 Tampines Street 20, #17-35",
+ "tags" : [ "classmates" ],
+ "personId" : "5"
+ }, {
+ "name" : "Roy Balakrishnan",
+ "phone" : "92624417",
+ "email" : "royb@example.com",
+ "address" : "Blk 45 Aljunied Street 85, #11-31",
+ "tags" : [ "colleagues" ],
+ "personId" : "6"
+ } ]
+}
diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
index a7427fe7aa2..59ff178ed95 100644
--- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
+++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
@@ -4,11 +4,13 @@
"phone": "94351253",
"email": "alice@example.com",
"address": "123, Jurong West Ave 6, #08-111",
- "tags": [ "friends" ]
+ "tags": [ "friends" ],
+ "personId": "1"
}, {
"name": "Alice Pauline",
"phone": "94351253",
"email": "pauline@example.com",
- "address": "4th street"
+ "address": "4th street",
+ "personId": "1"
} ]
}
diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
index 72262099d35..508112edb97 100644
--- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
+++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
@@ -5,42 +5,49 @@
"phone" : "94351253",
"email" : "alice@example.com",
"address" : "123, Jurong West Ave 6, #08-111",
- "tags" : [ "friends" ]
+ "tags" : [ "friends" ],
+ "personId" : "1"
}, {
"name" : "Benson Meier",
"phone" : "98765432",
"email" : "johnd@example.com",
"address" : "311, Clementi Ave 2, #02-25",
- "tags" : [ "owesMoney", "friends" ]
+ "tags" : [ "owesMoney", "friends" ],
+ "personId" : "2"
}, {
"name" : "Carl Kurz",
"phone" : "95352563",
"email" : "heinz@example.com",
"address" : "wall street",
- "tags" : [ ]
+ "tags" : [ ],
+ "personId" : "3"
}, {
"name" : "Daniel Meier",
"phone" : "87652533",
"email" : "cornelia@example.com",
"address" : "10th street",
- "tags" : [ "friends" ]
+ "tags" : [ "friends" ],
+ "personId" : "4"
}, {
"name" : "Elle Meyer",
"phone" : "9482224",
"email" : "werner@example.com",
"address" : "michegan ave",
- "tags" : [ ]
+ "tags" : [ ],
+ "personId" : "5"
}, {
"name" : "Fiona Kunz",
"phone" : "9482427",
"email" : "lydia@example.com",
"address" : "little tokyo",
- "tags" : [ ]
+ "tags" : [ ],
+ "personId" : "6"
}, {
"name" : "George Best",
"phone" : "9482442",
"email" : "anna@example.com",
"address" : "4th street",
- "tags" : [ ]
+ "tags" : [ ],
+ "personId" : "7"
} ]
}
diff --git a/src/test/java/seedu/address/commons/util/AppUtilTest.java b/src/test/java/seedu/address/commons/util/AppUtilTest.java
index 594de1e6365..001bf9124f6 100644
--- a/src/test/java/seedu/address/commons/util/AppUtilTest.java
+++ b/src/test/java/seedu/address/commons/util/AppUtilTest.java
@@ -9,7 +9,7 @@ public class AppUtilTest {
@Test
public void getImage_exitingImage() {
- assertNotNull(AppUtil.getImage("/images/address_book_32.png"));
+ assertNotNull(AppUtil.getImage("/images/ScoopBook.png"));
}
@Test
diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java
index baf8ce336a2..1c2c3e5b5ac 100644
--- a/src/test/java/seedu/address/logic/LogicManagerTest.java
+++ b/src/test/java/seedu/address/logic/LogicManagerTest.java
@@ -18,6 +18,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import seedu.address.commons.core.GuiSettings;
import seedu.address.logic.commands.AddCommand;
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.ListCommand;
@@ -64,6 +65,13 @@ public void execute_commandExecutionError_throwsCommandException() {
assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
+ @Test
+ public void execute_deleteNoteCommandStorageInjection_success() throws Exception {
+ String deleteNoteCommand = "deletenote 1";
+ assertCommandFailure(deleteNoteCommand, CommandException.class, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, model);
+ }
+
+
@Test
public void execute_validCommand_success() throws Exception {
String listCommand = ListCommand.COMMAND_WORD;
@@ -87,6 +95,123 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException
assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredPersonList().remove(0));
}
+ @Test
+ public void getAddressBook_returnsCorrectAddressBook() {
+ assertEquals(model.getAddressBook(), logic.getAddressBook());
+ }
+
+ @Test
+ public void getAddressBookFilePath_returnsCorrectPath() {
+ assertEquals(model.getAddressBookFilePath(), logic.getAddressBookFilePath());
+ }
+
+ @Test
+ public void getGuiSettings_returnsCorrectSettings() {
+ assertEquals(model.getGuiSettings(), logic.getGuiSettings());
+ }
+
+ @Test
+ public void setGuiSettings_updatesModelSettings() {
+ GuiSettings newSettings = new GuiSettings(800, 600, 10, 20);
+ logic.setGuiSettings(newSettings);
+ assertEquals(newSettings, model.getGuiSettings());
+ }
+
+ @Test
+ public void getStorage_returnsStorageInstance() {
+ StorageManager expectedStorage = (StorageManager) ((LogicManager) logic).getStorage();
+ assertEquals(expectedStorage, logic.getStorage());
+ }
+
+ @Test
+ public void execute_deleteNote_success() throws Exception {
+ Person person = new PersonBuilder().build();
+ model.addPerson(person);
+
+ String noteContent = "Test note";
+ logic.saveNote(person, noteContent);
+
+ String deleteNoteCommand = "deletenote 1";
+ logic.execute(deleteNoteCommand);
+
+ String deletedContent = logic.readNote(person);
+ assertEquals("", deletedContent);
+ }
+
+ @Test
+ public void saveNote_validPersonAndContent_savesCorrectly() throws Exception {
+ Person person = new PersonBuilder().build();
+ model.addPerson(person);
+
+ String noteContent = "Test note content";
+ logic.saveNote(person, noteContent);
+
+ String readContent = logic.readNote(person);
+ assertEquals(noteContent, readContent);
+ }
+
+ @Test
+ public void saveNote_updatingExistingNote_overwritesPrevious() throws Exception {
+ Person person = new PersonBuilder().build();
+ model.addPerson(person);
+
+ logic.saveNote(person, "Initial content");
+
+ String newContent = "Updated content";
+ logic.saveNote(person, newContent);
+
+ assertEquals(newContent, logic.readNote(person));
+ }
+
+ @Test
+ public void deleteNote_existingNote_removesNote() throws Exception {
+ Person person = new PersonBuilder().build();
+ model.addPerson(person);
+
+ String noteContent = "Note to be deleted";
+ logic.saveNote(person, noteContent);
+
+ logic.deleteNote(person);
+
+ String afterDelete = logic.readNote(person);
+ assertEquals("", afterDelete);
+ }
+
+ @Test
+ public void handleNoteOperations_deleteCommand_deletesNote() throws Exception {
+ Person person = new PersonBuilder().build();
+ model.addPerson(person);
+
+ String noteContent = "Note for deletion test";
+ logic.saveNote(person, noteContent);
+
+ logic.execute("delete 1");
+
+ model.addPerson(person);
+ String result = logic.readNote(person);
+ assertEquals("", result);
+ }
+
+ @Test
+ public void handleNoteOperations_clearCommand_deletesAllNotes() throws Exception {
+ Person person1 = new PersonBuilder().withName("Alice").build();
+ Person person2 = new PersonBuilder().withName("Bob").build();
+ model.addPerson(person1);
+ model.addPerson(person2);
+
+ logic.saveNote(person1, "Alice's note");
+ logic.saveNote(person2, "Bob's note");
+
+ String clearCommand = "clear";
+ logic.execute(clearCommand);
+
+ model.addPerson(person1);
+ model.addPerson(person2);
+
+ assertEquals("", logic.readNote(person1));
+ assertEquals("", logic.readNote(person2));
+ }
+
/**
* Executes the command and confirms that
* - no exceptions are thrown
diff --git a/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java b/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java
new file mode 100644
index 00000000000..2bfd13fd569
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/AddTagCommandTest.java
@@ -0,0 +1,80 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.Messages;
+import seedu.address.model.AddressBook;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
+
+public class AddTagCommandTest {
+ private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ @Test
+ public void execute_allFieldsSpecifiedUnfilteredList_success() {
+ // execute with valid index and tags
+ Person currentPerson = model.getFilteredPersonList().get(0);
+ Person editedPerson = new PersonBuilder(currentPerson).withTags("colleague", "friends").build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(editedPerson).build();
+ AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, descriptor);
+ String expectedMessage = String.format(AddTagCommand.MESSAGE_SUCCESS, Messages.format(editedPerson));
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ expectedModel.setPerson(currentPerson, editedPerson);
+ assertCommandSuccess(addTagCommand, model, expectedMessage, expectedModel);
+ }
+
+ @Test
+ public void execute_invalidIndex_throwsCommandException() {
+ Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
+ Person personForDescriptor = new PersonBuilder().build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(personForDescriptor).build();
+ AddTagCommand addTagCommand = new AddTagCommand(outOfBoundIndex, descriptor);
+ assertCommandFailure(addTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void equals_testAllFields() {
+ Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(firstPerson).build();
+ Person firstPersonOnListDifferentTags = new PersonBuilder().withTags("tester").build();
+ PersonDescriptor descriptor2 = new PersonDescriptorBuilder(firstPersonOnListDifferentTags)
+ .build();
+ AddTagCommand addTagCommand = new AddTagCommand(INDEX_FIRST_PERSON, descriptor);
+ AddTagCommand addTagCommandCopy = new AddTagCommand(INDEX_FIRST_PERSON, descriptor);
+ AddTagCommand addTagCommandDifferentIndex = new AddTagCommand(Index.fromOneBased(2), descriptor);
+ AddTagCommand addTagCommandDifferentDescriptor = new AddTagCommand(INDEX_FIRST_PERSON,
+ descriptor2);
+ // same object -> returns true
+ assert(addTagCommand.equals(addTagCommand));
+ // same values -> returns true
+ assert(addTagCommand.equals(addTagCommandCopy));
+ // different types -> returns false
+ assert(!addTagCommand.equals(1));
+ // null -> returns false
+ assert(!addTagCommand.equals(null));
+ // different index -> returns false
+ assert(!addTagCommand.equals(addTagCommandDifferentIndex));
+ // different descriptor -> returns false
+ assert(!addTagCommand.equals(addTagCommandDifferentDescriptor));
+ }
+
+ @Test
+ public void toString_test() {
+ Index index = Index.fromOneBased(1);
+ PersonDescriptor addTagPersonDescriptor = new PersonDescriptor();
+ AddTagCommand addTagCommand = new AddTagCommand(index, addTagPersonDescriptor);
+ String expected = AddTagCommand.class.getCanonicalName() + "{index=" + index + ", personDescriptor="
+ + addTagPersonDescriptor + "}";
+ assertEquals(expected, addTagCommand.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
index 80d9110c03a..8095421ee86 100644
--- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
@@ -1,5 +1,7 @@
package seedu.address.logic.commands;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
@@ -16,8 +18,10 @@ public class ClearCommandTest {
public void execute_emptyAddressBook_success() {
Model model = new ModelManager();
Model expectedModel = new ModelManager();
+ CommandResult expectedCommandResult = new CommandResult(ClearCommand.MESSAGE_SUCCESS,
+ false, false, false, NoteCloseInstruction.CLOSE_ALL);
- assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel);
+ assertCommandSuccess(new ClearCommand(), model, expectedCommandResult, expectedModel);
}
@Test
@@ -25,8 +29,22 @@ public void execute_nonEmptyAddressBook_success() {
Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs());
expectedModel.setAddressBook(new AddressBook());
+ CommandResult expectedCommandResult = new CommandResult(ClearCommand.MESSAGE_SUCCESS,
+ false, false, false, NoteCloseInstruction.CLOSE_ALL);
- assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel);
+ assertCommandSuccess(new ClearCommand(), model, expectedCommandResult, expectedModel);
}
+ @Test
+ public void hasCleared_afterExecution_returnsTrue() {
+ Model model = new ModelManager();
+ ClearCommand clearCommand = new ClearCommand();
+
+ // Before execution
+ assertFalse(clearCommand.hasCleared());
+
+ // After execution
+ clearCommand.execute(model);
+ assertTrue(clearCommand.hasCleared());
+ }
}
diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java
index 7b8c7cd4546..cd6ba2f96fc 100644
--- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java
+++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java
@@ -7,6 +7,9 @@
import org.junit.jupiter.api.Test;
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
+
public class CommandResultTest {
@Test
public void equals() {
@@ -14,7 +17,8 @@ public void equals() {
// same values -> returns true
assertTrue(commandResult.equals(new CommandResult("feedback")));
- assertTrue(commandResult.equals(new CommandResult("feedback", false, false)));
+ assertTrue(commandResult.equals(new CommandResult("feedback", false, false,
+ false, NoteCloseInstruction.CLOSE_NONE)));
// same object -> returns true
assertTrue(commandResult.equals(commandResult));
@@ -29,10 +33,17 @@ public void equals() {
assertFalse(commandResult.equals(new CommandResult("different")));
// different showHelp value -> returns false
- assertFalse(commandResult.equals(new CommandResult("feedback", true, false)));
-
+ assertFalse(commandResult.equals(new CommandResult("feedback", true, false,
+ false, NoteCloseInstruction.CLOSE_NONE)));
// different exit value -> returns false
- assertFalse(commandResult.equals(new CommandResult("feedback", false, true)));
+ assertFalse(commandResult.equals(new CommandResult("feedback", false, true,
+ false, NoteCloseInstruction.CLOSE_NONE)));
+ // different showNote value -> returns false
+ assertFalse(commandResult.equals(new CommandResult("feedback", false, false,
+ true, NoteCloseInstruction.CLOSE_NONE)));
+ // different shouldDeleteNote value -> returns false
+ assertFalse(commandResult.equals(new CommandResult("feedback", false, false,
+ false, NoteCloseInstruction.CLOSE_ALL)));
}
@Test
@@ -46,10 +57,19 @@ public void hashcode() {
assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode());
// different showHelp value -> returns different hashcode
- assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode());
+ assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true,
+ false, false, NoteCloseInstruction.CLOSE_NONE).hashCode());
// different exit value -> returns different hashcode
- assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode());
+ assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false,
+ true, false, NoteCloseInstruction.CLOSE_NONE).hashCode());
+
+ // different showNote value -> returns different hashcode
+ assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false,
+ false, true, NoteCloseInstruction.CLOSE_NONE).hashCode());
+ // different shouldDeleteNote value -> returns different hashcode
+ assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false,
+ false, false, NoteCloseInstruction.CLOSE_ALL).hashCode());
}
@Test
@@ -57,7 +77,29 @@ public void toStringMethod() {
CommandResult commandResult = new CommandResult("feedback");
String expected = CommandResult.class.getCanonicalName() + "{feedbackToUser="
+ commandResult.getFeedbackToUser() + ", showHelp=" + commandResult.isShowHelp()
- + ", exit=" + commandResult.isExit() + "}";
+ + ", exit=" + commandResult.isExit()
+ + ", showNote=" + commandResult.isShowNote()
+ + ", shouldDeleteNote=" + commandResult.shouldDeleteNote() + "}";
assertEquals(expected, commandResult.toString());
}
+
+ @Test
+ public void equals_differentTargetPerson_returnsFalse() {
+ Person person1 = new PersonBuilder().withName("Alice").build();
+ Person person2 = new PersonBuilder().withName("Bob").build();
+ CommandResult commandResult = new CommandResult("feedback", false, false,
+ true, NoteCloseInstruction.CLOSE_NONE, person1);
+
+ // different targetPerson -> returns false
+ assertFalse(commandResult.equals(new CommandResult("feedback", false, false,
+ true, NoteCloseInstruction.CLOSE_NONE, person2)));
+ }
+
+ @Test
+ public void getTargetPerson_returnsCorrectPerson() {
+ Person person = new PersonBuilder().withName("Alice").build();
+ CommandResult commandResult = new CommandResult("feedback", false, false,
+ true, NoteCloseInstruction.CLOSE_NONE, person);
+ assertEquals(person, commandResult.getTargetPerson());
+ }
}
diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
index 643a1d08069..bde577bb057 100644
--- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
+++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
@@ -19,7 +19,7 @@
import seedu.address.model.Model;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.Person;
-import seedu.address.testutil.EditPersonDescriptorBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
/**
* Contains helper methods for testing commands.
@@ -28,10 +28,13 @@ public class CommandTestUtil {
public static final String VALID_NAME_AMY = "Amy Bee";
public static final String VALID_NAME_BOB = "Bob Choo";
+ public static final String VALID_EMPTY_PHONE = "000";
public static final String VALID_PHONE_AMY = "11111111";
public static final String VALID_PHONE_BOB = "22222222";
+ public static final String VALID_EMPTY_EMAIL = "unknown@example.com";
public static final String VALID_EMAIL_AMY = "amy@example.com";
public static final String VALID_EMAIL_BOB = "bob@example.com";
+ public static final String VALID_EMPTY_ADDRESS = "Unknown address";
public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1";
public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3";
public static final String VALID_TAG_HUSBAND = "husband";
@@ -57,14 +60,14 @@ public class CommandTestUtil {
public static final String PREAMBLE_WHITESPACE = "\t \r \n";
public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble";
- public static final EditCommand.EditPersonDescriptor DESC_AMY;
- public static final EditCommand.EditPersonDescriptor DESC_BOB;
+ public static final PersonDescriptor DESC_AMY;
+ public static final PersonDescriptor DESC_BOB;
static {
- DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY)
+ DESC_AMY = new PersonDescriptorBuilder().withName(VALID_NAME_AMY)
.withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY)
.withTags(VALID_TAG_FRIEND).build();
- DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB)
+ DESC_BOB = new PersonDescriptorBuilder().withName(VALID_NAME_BOB)
.withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB)
.withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build();
}
diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
index b6f332eabca..da888670122 100644
--- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
@@ -10,22 +10,44 @@
import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
import seedu.address.model.person.Person;
+import seedu.address.storage.FileNotesStorage;
+import seedu.address.storage.JsonAddressBookStorage;
+import seedu.address.storage.JsonUserPrefsStorage;
+import seedu.address.storage.NotesStorage;
+import seedu.address.storage.StorageManager;
/**
* Contains integration tests (interaction with the Model) and unit tests for
* {@code DeleteCommand}.
*/
public class DeleteCommandTest {
-
+ @TempDir
+ public Path testFolder;
private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ private StorageManager storageManager;
+ @BeforeEach
+ public void setUp() {
+ JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab"));
+ JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs"));
+ NotesStorage notesStorage = new FileNotesStorage(getTempFilePath("notes"));
+ storageManager = new StorageManager(addressBookStorage, userPrefsStorage);
+ }
+ private Path getTempFilePath(String fileName) {
+ return testFolder.resolve(fileName);
+ }
@Test
public void execute_validIndexUnfilteredList_success() {
@@ -34,17 +56,20 @@ public void execute_validIndexUnfilteredList_success() {
String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS,
Messages.format(personToDelete));
+ CommandResult expectedCommandResult = new CommandResult(expectedMessage, false, false,
+ false, NoteCloseInstruction.CLOSE_ONE, personToDelete);
ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
expectedModel.deletePerson(personToDelete);
- assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel);
+ assertCommandSuccess(deleteCommand, model, expectedCommandResult, expectedModel);
}
@Test
public void execute_invalidIndexUnfilteredList_throwsCommandException() {
Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex);
+ // Manually inject storageManager into deleteCommand
assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
@@ -55,15 +80,17 @@ public void execute_validIndexFilteredList_success() {
Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON);
-
String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS,
Messages.format(personToDelete));
+ CommandResult expectedCommandResult = new CommandResult(expectedMessage, false,
+ false, false, NoteCloseInstruction.CLOSE_ONE, personToDelete);
+
Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
expectedModel.deletePerson(personToDelete);
showNoPerson(expectedModel);
- assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel);
+ assertCommandSuccess(deleteCommand, model, expectedCommandResult, expectedModel);
}
@Test
@@ -109,6 +136,19 @@ public void toStringMethod() {
assertEquals(expected, deleteCommand.toString());
}
+ @Test
+ public void getTargetPerson_afterExecution_returnsCorrectPerson() throws CommandException {
+ Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON);
+
+ // Execute the command
+ deleteCommand.execute(model);
+
+ // Verify that getTargetPerson returns the person that was deleted
+ assertEquals(personToDelete, deleteCommand.getTargetPerson());
+ }
+
/**
* Updates {@code model}'s filtered list to show no one.
*/
diff --git a/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java
new file mode 100644
index 00000000000..05168100e44
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java
@@ -0,0 +1,157 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.logic.Messages;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.ReadOnlyUserPrefs;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.storage.Storage;
+
+public class DeleteNoteCommandTest {
+ private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+
+ @Test
+ public void execute_deleteNote_success() {
+ Person personToDeleteNote = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(INDEX_FIRST_PERSON);
+ deleteNoteCommand.setStorage(new StorageStub(true));
+
+ String expectedMessage = String.format(DeleteNoteCommand.MESSAGE_DELETENOTE_SUCCESS,
+ Messages.format(personToDeleteNote));
+
+ CommandResult expectedCommandResult = new CommandResult(expectedMessage, false, false,
+ false, NoteCloseInstruction.CLOSE_ONE, personToDeleteNote);
+
+ assertCommandSuccess(deleteNoteCommand, model, expectedCommandResult, model);
+ assertEquals(personToDeleteNote, deleteNoteCommand.getTargetPerson());
+ }
+
+ @Test
+ public void execute_deleteNote_noNoteFound() {
+ Person personToDeleteNote = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(INDEX_FIRST_PERSON);
+ deleteNoteCommand.setStorage(new StorageStub(false));
+
+ String expectedMessage = String.format(DeleteNoteCommand.MESSAGE_NO_NOTE,
+ Messages.format(personToDeleteNote));
+
+ CommandResult expectedCommandResult = new CommandResult(expectedMessage, false, false,
+ false, NoteCloseInstruction.CLOSE_ONE, personToDeleteNote);
+
+ assertCommandSuccess(deleteNoteCommand, model, expectedCommandResult, model);
+ assertEquals(personToDeleteNote, deleteNoteCommand.getTargetPerson());
+ }
+ @Test
+ public void execute_invalidIndex_throwsCommandException() {
+ DeleteNoteCommand command = new DeleteNoteCommand(INDEX_FIRST_PERSON);
+ command.setStorage(new StorageStub(true));
+
+ Model emptyModel = new ModelManager();
+
+ assertCommandFailure(command, emptyModel, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+ @Test
+ public void equals() {
+ DeleteNoteCommand deleteNoteFirstCommand = new DeleteNoteCommand(INDEX_FIRST_PERSON);
+ DeleteNoteCommand deleteNoteSecondCommand = new DeleteNoteCommand(INDEX_SECOND_PERSON);
+
+ // same object -> returns true
+ assertTrue(deleteNoteFirstCommand.equals(deleteNoteFirstCommand));
+
+ // same values -> returns true
+ DeleteNoteCommand deleteNoteFirstCommandCopy = new DeleteNoteCommand(INDEX_FIRST_PERSON);
+ assertTrue(deleteNoteFirstCommand.equals(deleteNoteFirstCommandCopy));
+
+ // different types -> returns false
+ assertFalse(deleteNoteFirstCommand.equals(1));
+
+ // null -> returns false
+ assertFalse(deleteNoteFirstCommand.equals(null));
+
+ // different person -> returns false
+ assertFalse(deleteNoteFirstCommand.equals(deleteNoteSecondCommand));
+ }
+ @Test
+ public void toStringMethod() {
+ Index index = Index.fromOneBased(1);
+ DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(index);
+ String expected = DeleteNoteCommand.class.getCanonicalName() + "{targetIndex=" + index + "}";
+ assertEquals(expected, deleteNoteCommand.toString());
+ }
+
+ private static class StorageStub implements Storage {
+ private final boolean shouldDelete;
+
+ StorageStub(boolean shouldDelete) {
+ this.shouldDelete = shouldDelete;
+ }
+ @Override
+ public boolean deleteNote(Person person) {
+ return shouldDelete;
+ }
+ // Throw for unimplemented methods to catch accidental calls
+ @Override
+ public String readNote(Person person) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void saveNote(Person person, String content) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void deleteAllNotes() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public Path getUserPrefsFilePath() {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public Optional readUserPrefs() {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public Path getAddressBookFilePath() {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public Optional readAddressBook() {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public Optional readAddressBook(Path filePath) throws DataLoadingException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
index 469dd97daa7..cc2dbc52d8e 100644
--- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
@@ -19,14 +19,13 @@
import seedu.address.commons.core.index.Index;
import seedu.address.logic.Messages;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
import seedu.address.model.AddressBook;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
import seedu.address.model.person.Person;
-import seedu.address.testutil.EditPersonDescriptorBuilder;
import seedu.address.testutil.PersonBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
/**
* Contains integration tests (interaction with the Model) and unit tests for EditCommand.
@@ -38,7 +37,7 @@ public class EditCommandTest {
@Test
public void execute_allFieldsSpecifiedUnfilteredList_success() {
Person editedPerson = new PersonBuilder().build();
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(editedPerson).build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(editedPerson).build();
EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, descriptor);
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
@@ -58,7 +57,7 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() {
Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB)
.withTags(VALID_TAG_HUSBAND).build();
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB)
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withName(VALID_NAME_BOB)
.withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build();
EditCommand editCommand = new EditCommand(indexLastPerson, descriptor);
@@ -72,7 +71,7 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() {
@Test
public void execute_noFieldSpecifiedUnfilteredList_success() {
- EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, new EditPersonDescriptor());
+ EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, new PersonDescriptor());
Person editedPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
@@ -89,7 +88,7 @@ public void execute_filteredList_success() {
Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
Person editedPerson = new PersonBuilder(personInFilteredList).withName(VALID_NAME_BOB).build();
EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON,
- new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build());
+ new PersonDescriptorBuilder().withName(VALID_NAME_BOB).build());
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
@@ -102,7 +101,7 @@ public void execute_filteredList_success() {
@Test
public void execute_duplicatePersonUnfilteredList_failure() {
Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(firstPerson).build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(firstPerson).build();
EditCommand editCommand = new EditCommand(INDEX_SECOND_PERSON, descriptor);
assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON);
@@ -115,7 +114,7 @@ public void execute_duplicatePersonFilteredList_failure() {
// edit person in filtered list into a duplicate in address book
Person personInList = model.getAddressBook().getPersonList().get(INDEX_SECOND_PERSON.getZeroBased());
EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON,
- new EditPersonDescriptorBuilder(personInList).build());
+ new PersonDescriptorBuilder(personInList).build());
assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON);
}
@@ -123,7 +122,7 @@ public void execute_duplicatePersonFilteredList_failure() {
@Test
public void execute_invalidPersonIndexUnfilteredList_failure() {
Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withName(VALID_NAME_BOB).build();
EditCommand editCommand = new EditCommand(outOfBoundIndex, descriptor);
assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
@@ -141,7 +140,7 @@ public void execute_invalidPersonIndexFilteredList_failure() {
assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size());
EditCommand editCommand = new EditCommand(outOfBoundIndex,
- new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build());
+ new PersonDescriptorBuilder().withName(VALID_NAME_BOB).build());
assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
@@ -151,7 +150,7 @@ public void equals() {
final EditCommand standardCommand = new EditCommand(INDEX_FIRST_PERSON, DESC_AMY);
// same values -> returns true
- EditPersonDescriptor copyDescriptor = new EditPersonDescriptor(DESC_AMY);
+ PersonDescriptor copyDescriptor = new PersonDescriptor(DESC_AMY);
EditCommand commandWithSameValues = new EditCommand(INDEX_FIRST_PERSON, copyDescriptor);
assertTrue(standardCommand.equals(commandWithSameValues));
@@ -174,9 +173,9 @@ public void equals() {
@Test
public void toStringMethod() {
Index index = Index.fromOneBased(1);
- EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor();
+ PersonDescriptor editPersonDescriptor = new PersonDescriptor();
EditCommand editCommand = new EditCommand(index, editPersonDescriptor);
- String expected = EditCommand.class.getCanonicalName() + "{index=" + index + ", editPersonDescriptor="
+ String expected = EditCommand.class.getCanonicalName() + "{index=" + index + ", personDescriptor="
+ editPersonDescriptor + "}";
assertEquals(expected, editCommand.toString());
}
diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java
index 9533c473875..39267df93f4 100644
--- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java
@@ -14,7 +14,8 @@ public class ExitCommandTest {
@Test
public void execute_exit_success() {
- CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true);
+ CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false,
+ true, false, NoteCloseInstruction.CLOSE_NONE);
assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel);
}
}
diff --git a/src/test/java/seedu/address/logic/commands/ExportCommandTest.java b/src/test/java/seedu/address/logic/commands/ExportCommandTest.java
new file mode 100644
index 00000000000..249bb38b1ee
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/ExportCommandTest.java
@@ -0,0 +1,135 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+
+/**
+ * Contains integration tests (interaction with the Model) and unit tests for ExportCommand.
+ */
+public class ExportCommandTest {
+ @TempDir
+ public Path temporaryFolder; // JUnit will create a temporary directory
+
+ private final Model model = new ModelManager();
+ @Test
+ public void execute_exportSuccess() throws Exception {
+ // Create source directory and file for testing
+ Path sourceDir = Files.createDirectories(Paths.get("data"));
+ Path sourcePath = sourceDir.resolve("addressbook.json");
+ Files.write(sourcePath, "{}".getBytes()); // Write empty JSON object
+
+ Path targetPath = temporaryFolder.resolve("exported_data.json");
+ ExportCommand exportCommand = new ExportCommand(targetPath);
+
+ try {
+ CommandResult result = exportCommand.execute(model);
+ assertEquals(String.format(ExportCommand.MESSAGE_EXPORT_SUCCESS, targetPath.toAbsolutePath()),
+ result.getFeedbackToUser());
+ assertEquals(true, Files.exists(targetPath));
+ } catch (CommandException e) {
+ System.out.println("Error message: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ @Test
+ public void execute_invalidFileFormat_throwsCommandException() {
+ Path invalidPath = temporaryFolder.resolve("exported_data.txt"); // Not a .json file
+ ExportCommand exportCommand = new ExportCommand(invalidPath);
+ exportCommand.sourceFileExists("");
+ CommandException exception = assertThrows(CommandException.class, () -> {
+ exportCommand.execute(model);
+ });
+ assertEquals(ExportCommand.MESSAGE_INVALID_FILE_FORMAT, exception.getMessage());
+ }
+
+ @Test
+ public void execute_sourceFileNotFound_throwsCommandException() {
+ Path destPath = temporaryFolder.resolve("testing.json");
+ ExportCommand exportCommand = new ExportCommand(destPath);
+ exportCommand.sourceFileExists("simulate invalid source file");
+
+ CommandException exception = assertThrows(CommandException.class, () -> {
+ exportCommand.execute(model);
+ });
+ assertEquals(ExportCommand.MESSAGE_SOURCE_FILE_NOT_FOUND, exception.getMessage());
+ }
+
+ @Test
+ public void equals_sameObject_returnsTrue() {
+ Path targetPath = Paths.get("test.json");
+ ExportCommand exportCommand = new ExportCommand(targetPath);
+
+ // Same object reference should be equal
+ assertTrue(exportCommand.equals(exportCommand));
+ }
+
+ @Test
+ public void equals_nullObject_returnsFalse() {
+ Path targetPath = Paths.get("test.json");
+ ExportCommand exportCommand = new ExportCommand(targetPath);
+
+ // Null comparison should return false
+ assertFalse(exportCommand.equals(null));
+ }
+
+ @Test
+ public void equals_differentClass_returnsFalse() {
+ Path targetPath = Paths.get("test.json");
+ ExportCommand exportCommand = new ExportCommand(targetPath);
+
+ // Different class comparison should return false
+ assertFalse(exportCommand.equals("string"));
+ }
+
+ @Test
+ public void equals_sameTargetPath_returnsTrue() {
+ Path targetPath1 = Paths.get("test.json");
+ Path targetPath2 = Paths.get("test.json");
+
+ ExportCommand exportCommand1 = new ExportCommand(targetPath1);
+ ExportCommand exportCommand2 = new ExportCommand(targetPath2);
+
+ // Commands with same target path should be equal
+ assertTrue(exportCommand1.equals(exportCommand2));
+ assertTrue(exportCommand2.equals(exportCommand1)); // Test symmetry
+ }
+
+ @Test
+ public void equals_differentTargetPath_returnsFalse() {
+ Path targetPath1 = Paths.get("test1.json");
+ Path targetPath2 = Paths.get("test2.json");
+
+ ExportCommand exportCommand1 = new ExportCommand(targetPath1);
+ ExportCommand exportCommand2 = new ExportCommand(targetPath2);
+
+ // Commands with different target paths should not be equal
+ assertFalse(exportCommand1.equals(exportCommand2));
+ assertFalse(exportCommand2.equals(exportCommand1)); // Test symmetry
+ }
+
+ @Test
+ public void equals_differentPathFormat_dependsOnPathImplementation() {
+
+ Path targetPath1 = Paths.get("./folder/test.json");
+ Path targetPath2 = Paths.get("folder/test.json");
+
+ ExportCommand exportCommand1 = new ExportCommand(targetPath1);
+ ExportCommand exportCommand2 = new ExportCommand(targetPath2);
+ assertEquals(targetPath1.equals(targetPath2),
+ exportCommand1.equals(exportCommand2));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java
new file mode 100644
index 00000000000..ace9577b772
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java
@@ -0,0 +1,127 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
+import static seedu.address.testutil.TypicalPersons.ALICE;
+import static seedu.address.testutil.TypicalPersons.BENSON;
+import static seedu.address.testutil.TypicalPersons.DANIEL;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.TagNamesContainsTagsPredicate;
+
+/**
+ * Contains integration tests (interaction with the Model) for {@code FindTagCommand}.
+ */
+public class FindTagCommandTest {
+ private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+
+ @Test
+ public void equals() {
+ TagNamesContainsTagsPredicate firstPredicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("first"));
+ TagNamesContainsTagsPredicate secondPredicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("second"));
+
+ FindTagCommand findTagFirstCommand = new FindTagCommand(firstPredicate);
+ FindTagCommand findTagSecondCommand = new FindTagCommand(secondPredicate);
+
+ // same object -> returns true
+ assertTrue(findTagFirstCommand.equals(findTagFirstCommand));
+
+ // same values -> returns true
+ FindTagCommand findTagFirstCommandCopy = new FindTagCommand(firstPredicate);
+ assertTrue(findTagFirstCommand.equals(findTagFirstCommandCopy));
+
+ // different types -> returns false
+ assertFalse(findTagFirstCommand.equals(1));
+
+ // null -> returns false
+ assertFalse(findTagFirstCommand.equals(null));
+
+ // different tag list -> returns false
+ assertFalse(findTagFirstCommand.equals(findTagSecondCommand));
+ }
+
+ @Test
+ public void execute_oneTag_multiplePersonsFound() {
+ String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3);
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("friends"));
+ FindTagCommand command = new FindTagCommand(predicate);
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, expectedMessage, expectedModel);
+
+ List expectedPersons = Arrays.asList(ALICE, BENSON, DANIEL);
+ assertEquals(expectedPersons, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_oneTag_onePersonFound() {
+ String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1);
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("owesMoney"));
+ FindTagCommand command = new FindTagCommand(predicate);
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, expectedMessage, expectedModel);
+
+ assertEquals(Collections.singletonList(BENSON), model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_oneTag_noPersonFound() {
+ String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0);
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("nonExistentTag"));
+ FindTagCommand command = new FindTagCommand(predicate);
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, expectedMessage, expectedModel);
+
+ assertEquals(Collections.emptyList(), model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_multipleTags_onePersonFound() {
+ String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1);
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Arrays.asList("friends", "owesMoney"));
+ FindTagCommand command = new FindTagCommand(predicate);
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, expectedMessage, expectedModel);
+
+ assertEquals(Collections.singletonList(BENSON), model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_multipleTags_noPersonFound() {
+ String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0);
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Arrays.asList("friends", "nonExistentTag"));
+ FindTagCommand command = new FindTagCommand(predicate);
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, expectedMessage, expectedModel);
+
+ assertEquals(Collections.emptyList(), model.getFilteredPersonList());
+ }
+
+ @Test
+ public void toStringMethod() {
+ TagNamesContainsTagsPredicate predicate = new TagNamesContainsTagsPredicate(List.of("tags"));
+ FindTagCommand findTagCommand = new FindTagCommand(predicate);
+ String expected = FindTagCommand.class.getCanonicalName() + "{predicate=" + predicate + "}";
+ assertEquals(expected, findTagCommand.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java
index 4904fc4352e..f66d01da34e 100644
--- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java
@@ -1,10 +1,18 @@
package seedu.address.logic.commands;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
import static seedu.address.logic.commands.HelpCommand.SHOWING_HELP_MESSAGE;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
import org.junit.jupiter.api.Test;
+import seedu.address.commons.core.LogsCenter;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
@@ -14,7 +22,33 @@ public class HelpCommandTest {
@Test
public void execute_help_success() {
- CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false);
+ CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false,
+ false, NoteCloseInstruction.CLOSE_NONE);
assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel);
}
+ @Test
+ public void loadResourceFile_validFile_returnsContent() {
+ String content = loadResourceFile("help/local_userguide.txt");
+ assertNotNull(content);
+ assertTrue(content.length() > 0);
+ }
+
+ /**
+ * Loads a resource file from the classpath.
+ * This is a copy of the method from HelpWindow to test it independently of JavaFX.
+ */
+ private String loadResourceFile(String filename) {
+ StringBuilder content = new StringBuilder();
+ try (InputStream inputStream = getClass().getResourceAsStream("/" + filename);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+ return content.toString();
+ } catch (IOException | NullPointerException e) {
+ LogsCenter.getLogger(HelpCommandTest.class).warning("Error loading resource file: " + filename);
+ return null;
+ }
+ }
}
diff --git a/src/test/java/seedu/address/logic/commands/ImportCommandTest.java b/src/test/java/seedu/address/logic/commands/ImportCommandTest.java
new file mode 100644
index 00000000000..999e74f9d8a
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/ImportCommandTest.java
@@ -0,0 +1,144 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+
+/**
+ * Contains integration tests (interaction with the Model) and unit tests for ImportCommand
+ */
+public class ImportCommandTest {
+ private final Model model = new ModelManager();
+
+ @Test
+ public void execute_importSuccess() throws Exception {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/validAddressBook.json").toAbsolutePath();
+ ImportCommand importcommand = new ImportCommand(targetPath);
+ try {
+ CommandResult result = importcommand.execute(model);
+ assertEquals(String.format(ImportCommand.MESSAGE_IMPORT_SUCCESS, targetPath.toAbsolutePath()),
+ result.getFeedbackToUser());
+ } catch (CommandException e) {
+ System.out.println("Error message: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ @Test
+ public void execute_notJsonFile_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/invalidAddressBook.txt").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ assertEquals(ImportCommand.MESSAGE_INVALID_FILE_FORMAT, exception.getMessage());
+ }
+
+ @Test
+ public void execute_nonExistentJsonFile_throwsCommandException() {
+ Path targetPath = Paths.get("h.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ assertEquals(ImportCommand.MESSAGE_IMPORT_FAILURE, exception.getMessage());
+ }
+
+ @Test
+ public void execute_noCustomPhoneEmailAddress_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noCustomPhoneEmailAddress.json")
+ .toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person 'alex' does not have any added phone, email, or address.";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_missingNameField_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noNameField.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person's Name field is missing!";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_missingPhoneField_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noPhoneField.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person's Phone field is missing!";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_missingEmailField_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noEmailField.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person's Email field is missing!";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_missingAddressField_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noAddressField.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person's Address field is missing!";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_missingPersonIdField_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/noPersonIdField.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person's PersonId field is missing!";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_duplicatePersonId_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/duplicatePersonId.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person 'Bernice Yu' has either out-of-order OR duplicate person ID.";
+ assertEquals(expected, exception.getMessage());
+ }
+
+ @Test
+ public void execute_outOfOrderPersonId_throwsCommandException() {
+ Path targetPath = Paths.get("src/test/data/ImportCommandTest/outOfOrderPersonId.json").toAbsolutePath();
+ ImportCommand importCommand = new ImportCommand(targetPath);
+ CommandException exception = Assertions.assertThrows(CommandException.class, () -> {
+ importCommand.execute(model);
+ });
+ String expected = "Invalid JSON file: Person 'Roy Balakrishnan' has either out-of-order "
+ + "OR duplicate person ID.";
+ assertEquals(expected, exception.getMessage());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/NoteCommandTest.java b/src/test/java/seedu/address/logic/commands/NoteCommandTest.java
new file mode 100644
index 00000000000..b821073fec4
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/NoteCommandTest.java
@@ -0,0 +1,85 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
+import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.Messages;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+
+/**
+ * Contains integration tests (interaction with the Model) and unit tests for
+ * {@code NoteCommand}.
+ */
+public class NoteCommandTest {
+
+ private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+
+ @Test
+ public void execute_invalidIndexUnfilteredList_throwsCommandException() {
+ Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
+ NoteCommand noteCommand = new NoteCommand(outOfBoundIndex);
+
+ assertCommandFailure(noteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void execute_invalidIndexFilteredList_throwsCommandException() {
+ showPersonAtIndex(model, INDEX_FIRST_PERSON);
+
+ Index outOfBoundIndex = INDEX_SECOND_PERSON;
+ // ensures that outOfBoundIndex is still in bounds of address book list
+ assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size());
+
+ NoteCommand noteCommand = new NoteCommand(outOfBoundIndex);
+
+ assertCommandFailure(noteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void equals() {
+ NoteCommand noteFirstCommand = new NoteCommand(INDEX_FIRST_PERSON);
+ NoteCommand noteSecondCommand = new NoteCommand(INDEX_SECOND_PERSON);
+
+ // same object -> returns true
+ assertTrue(noteFirstCommand.equals(noteFirstCommand));
+
+ // same values -> returns true
+ NoteCommand noteFirstCommandCopy = new NoteCommand(INDEX_FIRST_PERSON);
+ assertTrue(noteFirstCommand.equals(noteFirstCommandCopy));
+
+ // different types -> returns false
+ assertFalse(noteFirstCommand.equals(1));
+
+ // null -> returns false
+ assertFalse(noteFirstCommand.equals(null));
+
+ // different person -> returns false
+ assertFalse(noteFirstCommand.equals(noteSecondCommand));
+ }
+
+ @Test
+ public void execute_validIndex_returnsCorrectCommandResult() throws Exception {
+ Person targetPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ NoteCommand noteCommand = new NoteCommand(INDEX_FIRST_PERSON);
+
+ CommandResult result = noteCommand.execute(model);
+
+ // Verify the CommandResult has showNote=true and the correct target person
+ assertTrue(result.isShowNote());
+ assertEquals(targetPerson, result.getTargetPerson());
+ assertEquals(String.format(NoteCommand.MESSAGE_NOTE_PERSON_SUCCESS, Messages.format(targetPerson)),
+ result.getFeedbackToUser());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/PersonDescriptorTest.java
similarity index 70%
rename from src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java
rename to src/test/java/seedu/address/logic/commands/PersonDescriptorTest.java
index b17c1f3d5c2..ff8ca0ba0a2 100644
--- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java
+++ b/src/test/java/seedu/address/logic/commands/PersonDescriptorTest.java
@@ -13,15 +13,14 @@
import org.junit.jupiter.api.Test;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
-import seedu.address.testutil.EditPersonDescriptorBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
-public class EditPersonDescriptorTest {
+public class PersonDescriptorTest {
@Test
public void equals() {
// same values -> returns true
- EditPersonDescriptor descriptorWithSameValues = new EditPersonDescriptor(DESC_AMY);
+ PersonDescriptor descriptorWithSameValues = new PersonDescriptor(DESC_AMY);
assertTrue(DESC_AMY.equals(descriptorWithSameValues));
// same object -> returns true
@@ -37,30 +36,30 @@ public void equals() {
assertFalse(DESC_AMY.equals(DESC_BOB));
// different name -> returns false
- EditPersonDescriptor editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build();
+ PersonDescriptor editedAmy = new PersonDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build();
assertFalse(DESC_AMY.equals(editedAmy));
// different phone -> returns false
- editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withPhone(VALID_PHONE_BOB).build();
+ editedAmy = new PersonDescriptorBuilder(DESC_AMY).withPhone(VALID_PHONE_BOB).build();
assertFalse(DESC_AMY.equals(editedAmy));
// different email -> returns false
- editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withEmail(VALID_EMAIL_BOB).build();
+ editedAmy = new PersonDescriptorBuilder(DESC_AMY).withEmail(VALID_EMAIL_BOB).build();
assertFalse(DESC_AMY.equals(editedAmy));
// different address -> returns false
- editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build();
+ editedAmy = new PersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build();
assertFalse(DESC_AMY.equals(editedAmy));
// different tags -> returns false
- editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build();
+ editedAmy = new PersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build();
assertFalse(DESC_AMY.equals(editedAmy));
}
@Test
public void toStringMethod() {
- EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor();
- String expected = EditPersonDescriptor.class.getCanonicalName() + "{name="
+ PersonDescriptor editPersonDescriptor = new PersonDescriptor();
+ String expected = PersonDescriptor.class.getCanonicalName() + "{name="
+ editPersonDescriptor.getName().orElse(null) + ", phone="
+ editPersonDescriptor.getPhone().orElse(null) + ", email="
+ editPersonDescriptor.getEmail().orElse(null) + ", address="
diff --git a/src/test/java/seedu/address/logic/commands/RemoveTagCommandTest.java b/src/test/java/seedu/address/logic/commands/RemoveTagCommandTest.java
new file mode 100644
index 00000000000..ad29c30daf3
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/RemoveTagCommandTest.java
@@ -0,0 +1,108 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
+import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.Messages;
+import seedu.address.model.AddressBook;
+import seedu.address.model.Model;
+import seedu.address.model.ModelManager;
+import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
+
+public class RemoveTagCommandTest {
+ private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());
+ @Test
+ public void execute_allFieldsSpecifiedUnfilteredList_success() {
+ model.setAddressBook(new AddressBook()); // Clear previous data
+
+ // Create person with tags "collegaue" and "friends"
+ Person currentPerson = new PersonBuilder().withName("removeTagTestAllFields")
+ .withTags("colleague", "friends").build();
+ model.addPerson(currentPerson);
+
+ Person editedPerson = new PersonBuilder(currentPerson).withTags("friends").build();
+
+ // Descriptor that specifies which tag to remove
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withTags("colleague").build();
+
+ RemoveTagCommand removeTagCommand = new RemoveTagCommand(INDEX_FIRST_PERSON, descriptor);
+
+ String expectedMessage = String.format(RemoveTagCommand.MESSAGE_REMOVE_TAG_SUCCESS,
+ Messages.format(editedPerson), "[colleague]");
+
+ Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs());
+ expectedModel.setPerson(currentPerson, editedPerson);
+
+ assertCommandSuccess(removeTagCommand, model, expectedMessage, expectedModel);
+ }
+ @Test
+ public void execute_invalidIndex_throwsCommandException() {
+ Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
+ Person personForDescriptor = new PersonBuilder().build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(personForDescriptor).build();
+ RemoveTagCommand removeTagCommand = new RemoveTagCommand(outOfBoundIndex, descriptor);
+ assertCommandFailure(removeTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ @Test
+ public void execute_invalidTag_throwsCommandException() {
+ model.setAddressBook(new AddressBook()); // Clear previous data
+
+ // Create person with tags "collegaue" and "friends"
+ Person currentPerson = new PersonBuilder().withName("removeTagTestAllFields")
+ .withTags("colleague", "friends").build();
+ model.addPerson(currentPerson);
+
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withTags("nonExistentTag").build();
+ RemoveTagCommand removeTagCommand = new RemoveTagCommand(INDEX_FIRST_PERSON, descriptor);
+
+ assertCommandFailure(removeTagCommand, model, RemoveTagCommand.MESSAGE_TAG_NOT_FOUND);
+ }
+
+ @Test
+ public void equals_testAllFields() {
+ Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased());
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(firstPerson).build();
+ Person firstPersonOnListDifferentTags = new PersonBuilder().withTags("tester").build();
+ PersonDescriptor descriptor2 = new PersonDescriptorBuilder(firstPersonOnListDifferentTags)
+ .build();
+ RemoveTagCommand removeTagCommand = new RemoveTagCommand(INDEX_FIRST_PERSON, descriptor);
+ RemoveTagCommand removeTagCommandCopy = new RemoveTagCommand(INDEX_FIRST_PERSON, descriptor);
+ RemoveTagCommand removeTagCommandDifferentIndex = new RemoveTagCommand(Index.fromOneBased(2), descriptor);
+ RemoveTagCommand removeTagCommandDifferentDescriptor = new RemoveTagCommand(INDEX_FIRST_PERSON,
+ descriptor2);
+ // same object -> returns true
+ assertTrue(removeTagCommand.equals(removeTagCommand));
+ // same values -> returns true
+ assertTrue(removeTagCommand.equals(removeTagCommandCopy));
+ // different types -> returns false
+ assertFalse(removeTagCommand.equals(1));
+ // null -> returns false
+ assertFalse(removeTagCommand.equals(null));
+ // different index -> returns false
+ assertFalse(removeTagCommand.equals(removeTagCommandDifferentIndex));
+ // different descriptor -> returns false
+ assertFalse(removeTagCommand.equals(removeTagCommandDifferentDescriptor));
+ }
+
+ @Test
+ public void toString_test() {
+ Index index = Index.fromOneBased(1);
+ PersonDescriptor descriptor = new PersonDescriptor();
+ RemoveTagCommand removeTagCommand = new RemoveTagCommand(index, descriptor);
+ String expected = RemoveTagCommand.class.getCanonicalName() + "{targetIndex=" + index + ", personDescriptor="
+ + descriptor + "}";
+ assertEquals(expected, removeTagCommand.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
index 5bc11d3cdaa..2730eead456 100644
--- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
@@ -20,6 +20,9 @@
import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_EMPTY_ADDRESS;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_EMPTY_EMAIL;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_EMPTY_PHONE;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
@@ -135,6 +138,30 @@ public void parse_optionalFieldsMissing_success() {
Person expectedPerson = new PersonBuilder(AMY).withTags().build();
assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY,
new AddCommand(expectedPerson));
+
+ // name and phone only
+ Person expectedPersonNameAndPhone = new PersonBuilder(BOB)
+ .withAddress(VALID_EMPTY_ADDRESS)
+ .withEmail(VALID_EMPTY_EMAIL)
+ .withTags().build();
+ assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB,
+ new AddCommand(expectedPersonNameAndPhone));
+
+ // name and email only
+ Person expectedPersonNameAndEmail = new PersonBuilder(BOB)
+ .withAddress(VALID_EMPTY_ADDRESS)
+ .withPhone(VALID_EMPTY_PHONE)
+ .withTags().build();
+ assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + EMAIL_DESC_BOB,
+ new AddCommand(expectedPersonNameAndEmail));
+
+ // name and address only
+ Person expectedPersonNameAndAddress = new PersonBuilder(BOB)
+ .withEmail(VALID_EMPTY_EMAIL)
+ .withPhone(VALID_EMPTY_PHONE)
+ .withTags().build();
+ assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + ADDRESS_DESC_BOB,
+ new AddCommand(expectedPersonNameAndAddress));
}
@Test
@@ -145,18 +172,6 @@ public void parse_compulsoryFieldMissing_failure() {
assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB,
expectedMessage);
- // missing phone prefix
- assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB,
- expectedMessage);
-
- // missing email prefix
- assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB,
- expectedMessage);
-
- // missing address prefix
- assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB,
- expectedMessage);
-
// all prefixes missing
assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB,
expectedMessage);
diff --git a/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java
new file mode 100644
index 00000000000..d0660f500cf
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/AddTagCommandParserTest.java
@@ -0,0 +1,54 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.AddTagCommand;
+import seedu.address.testutil.PersonDescriptorBuilder;
+
+public class AddTagCommandParserTest {
+ private AddTagCommandParser parser = new AddTagCommandParser();
+
+ @Test
+ public void parse_allFieldsPresent_success() {
+ // whitespace only preamble
+ assertParseSuccess(parser, " 1 t/friends", new AddTagCommand(INDEX_FIRST_PERSON, new PersonDescriptorBuilder()
+ .withTags("friends").build()));
+
+ // multiple tags - all accepted
+ assertParseSuccess(parser, " 1 t/friends t/colleagues",
+ new AddTagCommand(INDEX_FIRST_PERSON, new PersonDescriptorBuilder().withTags("friends", "colleagues")
+ .build()));
+ }
+
+
+ @Test
+ public void parse_noTagsPresent_failure() {
+ // no tags at all
+ assertParseFailure(parser, " 1",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE));
+ // tag prefix present but no tags
+ assertParseFailure(parser, " 1 t/", AddTagCommand.MESSAGE_EMPTY_TAG);
+ // tag prefix present but only spaces
+ assertParseFailure(parser, " 1 t/ ", AddTagCommand.MESSAGE_EMPTY_TAG);
+ }
+
+ @Test
+ public void parse_invalidIndex_failure() {
+ // invalid index
+ assertParseFailure(parser, " a t/friends",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE));
+ // index not present
+ assertParseFailure(parser, " t/friends",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_multipleTagsWithOneEmpty_throwsParseException() {
+ assertParseFailure(parser, "1 t/valid t/", AddTagCommand.MESSAGE_EMPTY_TAG);
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
index 5a1ab3dbc0c..964784a2b7f 100644
--- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
@@ -7,6 +7,7 @@
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -17,16 +18,19 @@
import seedu.address.logic.commands.ClearCommand;
import seedu.address.logic.commands.DeleteCommand;
import seedu.address.logic.commands.EditCommand;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
import seedu.address.logic.commands.ExitCommand;
+import seedu.address.logic.commands.ExportCommand;
import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.commands.HelpCommand;
+import seedu.address.logic.commands.ImportCommand;
import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.NoteCommand;
+import seedu.address.logic.commands.PersonDescriptor;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.Person;
-import seedu.address.testutil.EditPersonDescriptorBuilder;
import seedu.address.testutil.PersonBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
import seedu.address.testutil.PersonUtil;
public class AddressBookParserTest {
@@ -56,7 +60,7 @@ public void parseCommand_delete() throws Exception {
@Test
public void parseCommand_edit() throws Exception {
Person person = new PersonBuilder().build();
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder(person).build();
EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " "
+ INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor));
assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command);
@@ -88,6 +92,28 @@ public void parseCommand_list() throws Exception {
assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand);
}
+ @Test
+ public void parseCommand_export() throws Exception {
+ String userInput = ExportCommand.COMMAND_WORD + " exported_data.json";
+ ExportCommand expectedCommand = new ExportCommand(Paths.get("exported_data.json"));
+ ExportCommand command = (ExportCommand) parser.parseCommand(userInput); //using AddressBookParser.java
+ assertEquals(expectedCommand, command);
+ }
+
+ @Test
+ public void parseCommand_import() throws Exception {
+ String userInput = ImportCommand.COMMAND_WORD + " src/test/data/ImportCommandTest/validAddressBook.json";
+ ImportCommand expectedCommand = new ImportCommand(Paths.get("src/test/data/ImportCommandTest"
+ + "/validAddressBook.json"));
+ ImportCommand command = (ImportCommand) parser.parseCommand(userInput);
+ assertEquals(expectedCommand, command);
+ }
+
+ @Test
+ public void parseCommand_note() throws Exception {
+ assertTrue(parser.parseCommand(NoteCommand.COMMAND_WORD + " 3") instanceof NoteCommand);
+ }
+
@Test
public void parseCommand_unrecognisedInput_throwsParseException() {
assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), ()
diff --git a/src/test/java/seedu/address/logic/parser/DeleteNoteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteNoteCommandParserTest.java
new file mode 100644
index 00000000000..522ceb14995
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/DeleteNoteCommandParserTest.java
@@ -0,0 +1,23 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.DeleteNoteCommand;
+public class DeleteNoteCommandParserTest {
+ private DeleteNoteCommandParser parser = new DeleteNoteCommandParser();
+
+ @Test
+ public void parse_validArgs_returnsDeleteNoteCommand() {
+ assertParseSuccess(parser, "1", new DeleteNoteCommand(INDEX_FIRST_PERSON));
+ }
+ @Test
+ public void parse_invalidArgs_throwsParseException() {
+ assertParseFailure(parser, "a",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteNoteCommand.MESSAGE_USAGE));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
index cc7175172d4..6c9e9fa71a9 100644
--- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
@@ -37,13 +37,13 @@
import seedu.address.commons.core.index.Index;
import seedu.address.logic.Messages;
import seedu.address.logic.commands.EditCommand;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
+import seedu.address.logic.commands.PersonDescriptor;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
import seedu.address.model.person.Phone;
import seedu.address.model.tag.Tag;
-import seedu.address.testutil.EditPersonDescriptorBuilder;
+import seedu.address.testutil.PersonDescriptorBuilder;
public class EditCommandParserTest {
@@ -109,7 +109,7 @@ public void parse_allFieldsSpecified_success() {
String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND
+ EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND;
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY)
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withName(VALID_NAME_AMY)
.withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY)
.withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build();
EditCommand expectedCommand = new EditCommand(targetIndex, descriptor);
@@ -122,7 +122,7 @@ public void parse_someFieldsSpecified_success() {
Index targetIndex = INDEX_FIRST_PERSON;
String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + EMAIL_DESC_AMY;
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB)
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withPhone(VALID_PHONE_BOB)
.withEmail(VALID_EMAIL_AMY).build();
EditCommand expectedCommand = new EditCommand(targetIndex, descriptor);
@@ -134,31 +134,31 @@ public void parse_oneFieldSpecified_success() {
// name
Index targetIndex = INDEX_THIRD_PERSON;
String userInput = targetIndex.getOneBased() + NAME_DESC_AMY;
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY).build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withName(VALID_NAME_AMY).build();
EditCommand expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
// phone
userInput = targetIndex.getOneBased() + PHONE_DESC_AMY;
- descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_AMY).build();
+ descriptor = new PersonDescriptorBuilder().withPhone(VALID_PHONE_AMY).build();
expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
// email
userInput = targetIndex.getOneBased() + EMAIL_DESC_AMY;
- descriptor = new EditPersonDescriptorBuilder().withEmail(VALID_EMAIL_AMY).build();
+ descriptor = new PersonDescriptorBuilder().withEmail(VALID_EMAIL_AMY).build();
expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
// address
userInput = targetIndex.getOneBased() + ADDRESS_DESC_AMY;
- descriptor = new EditPersonDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build();
+ descriptor = new PersonDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build();
expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
// tags
userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND;
- descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build();
+ descriptor = new PersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build();
expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
}
@@ -179,7 +179,7 @@ public void parse_multipleRepeatedFields_failure() {
assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE));
- // mulltiple valid fields repeated
+ // multiple valid fields repeated
userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY
+ TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND
+ PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND;
@@ -200,7 +200,7 @@ public void parse_resetTags_success() {
Index targetIndex = INDEX_THIRD_PERSON;
String userInput = targetIndex.getOneBased() + TAG_EMPTY;
- EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withTags().build();
+ PersonDescriptor descriptor = new PersonDescriptorBuilder().withTags().build();
EditCommand expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
diff --git a/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java
new file mode 100644
index 00000000000..2d6d03f31e4
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/ExportCommandParserTest.java
@@ -0,0 +1,44 @@
+package seedu.address.logic.parser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.ExportCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+public class ExportCommandParserTest {
+ private final ExportCommandParser parser = new ExportCommandParser();
+ @Test
+ public void parse_validArgs_returnsExportCommand() throws Exception {
+ String userInput = "exported_data.json";
+ ExportCommand expectedCommand = new ExportCommand(Paths.get(userInput));
+ ExportCommand result = parser.parse(userInput);
+ assertEquals(expectedCommand, result);
+ }
+ @Test
+ public void parse_missingArgs_throwsParseException() {
+ String userInput = " ";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_tooManyArgs_throwsParseException() {
+ String userInput = "export exported_data.json";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_argContainSpecialCharacter_throwsParseException() {
+ String userInput = "export ??.json";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(ExportCommand.MESSAGE_EXPORT_FAILURE
+ + " Invalid path: Illegal char > at index 0: ??.json"));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java
new file mode 100644
index 00000000000..b29211c015b
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java
@@ -0,0 +1,58 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.FindTagCommand;
+import seedu.address.model.person.TagNamesContainsTagsPredicate;
+
+public class FindTagCommandParserTest {
+ private FindTagCommandParser parser = new FindTagCommandParser();
+
+ @Test
+ public void parse_emptyArg_throwsParseException() {
+ assertParseFailure(parser, " ",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_noTagPrefix_throwsParseException() {
+ assertParseFailure(parser, "friends",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_validArgs_returnsFindTagCommand() {
+ // Test with a single tag
+ FindTagCommand expectedSingleTagCommand =
+ new FindTagCommand(new TagNamesContainsTagsPredicate(Collections.singletonList("friends")));
+ assertParseSuccess(parser, " t/friends", expectedSingleTagCommand);
+
+ // Test with multiple tags
+ FindTagCommand expectedMultipleTagsCommand =
+ new FindTagCommand(new TagNamesContainsTagsPredicate(Arrays.asList("friends", "colleagues")));
+ assertParseSuccess(parser, " t/friends t/colleagues", expectedMultipleTagsCommand);
+ }
+
+ @Test
+ public void parse_invalidTagFormat_throwsParseException() {
+ assertParseFailure(parser, " t/invalid*tag",
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_emptyTagName_throwsParseException() {
+ assertParseFailure(parser, " t/", FindTagCommand.MESSAGE_EMPTY_TAG);
+ }
+
+ @Test
+ public void parse_multipleTagsWithOneEmpty_throwsParseException() {
+ assertParseFailure(parser, " t/valid t/", FindTagCommand.MESSAGE_EMPTY_TAG);
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java
new file mode 100644
index 00000000000..d7474cba6eb
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/ImportCommandParserTest.java
@@ -0,0 +1,46 @@
+package seedu.address.logic.parser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.ImportCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+public class ImportCommandParserTest {
+ private final ImportCommandParser parser = new ImportCommandParser();
+
+ @Test
+ public void parse_validArgs_returnImportCommand() throws Exception {
+ String userInput = "src/test/data/ImportCommandTest/validAddressBook.json";
+ ImportCommand expectedCommand = new ImportCommand(Paths.get(userInput));
+ ImportCommand result = parser.parse(userInput);
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void parse_missingArgs_throwParseException() {
+ String userInput = " ";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_tooManyArgs_throwParseException() {
+ String userInput = "import validAddressBook.json";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_argContainSpecialCharacter_throwsParseException() {
+ String userInput = "import ??.json";
+ assertThrows(ParseException.class, () -> parser.parse(userInput),
+ String.format(ImportCommand.MESSAGE_IMPORT_FAILURE
+ + " Invalid path: Illegal char > at index 0: ??.json"));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/NoteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/NoteCommandParserTest.java
new file mode 100644
index 00000000000..c84cb1f2ba8
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/NoteCommandParserTest.java
@@ -0,0 +1,25 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.NoteCommand;
+
+public class NoteCommandParserTest {
+
+ private NoteCommandParser parser = new NoteCommandParser();
+
+ @Test
+ public void parse_validArgs_returnsNoteCommand() {
+ assertParseSuccess(parser, "1", new NoteCommand(INDEX_FIRST_PERSON));
+ }
+
+ @Test
+ public void parse_invalidArgs_throwsParseException() {
+ assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, NoteCommand.MESSAGE_USAGE));
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
index 4256788b1a7..10e05636407 100644
--- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
+++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
@@ -21,7 +21,7 @@
import seedu.address.model.tag.Tag;
public class ParserUtilTest {
- private static final String INVALID_NAME = "R@chel";
+ private static final String INVALID_NAME = "R/chel";
private static final String INVALID_PHONE = "+651234";
private static final String INVALID_ADDRESS = " ";
private static final String INVALID_EMAIL = "example.com";
diff --git a/src/test/java/seedu/address/logic/parser/RemoveTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/RemoveTagCommandParserTest.java
new file mode 100644
index 00000000000..ed40156a207
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/RemoveTagCommandParserTest.java
@@ -0,0 +1,75 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.PersonDescriptor;
+import seedu.address.logic.commands.RemoveTagCommand;
+import seedu.address.model.tag.Tag;
+
+
+public class RemoveTagCommandParserTest {
+ private RemoveTagCommandParser parser = new RemoveTagCommandParser();
+ @Test
+ public void parse_validArgsMultipleTags_returnsRemoveTagCommand() {
+ //valid input with multiple tags
+ String userInput = "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND;
+ Index expectedIndex = Index.fromOneBased(1);
+ PersonDescriptor expectedDescriptor = new PersonDescriptor();
+ expectedDescriptor.setTags(Set.of(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND)));
+
+ RemoveTagCommand expectedCommand = new RemoveTagCommand(expectedIndex, expectedDescriptor);
+ assertParseSuccess(parser, userInput, expectedCommand);
+ }
+
+ @Test
+ public void parse_validArgsSingleTag_returnsRemoveTagCommand() {
+ String userInput = "1 " + TAG_DESC_FRIEND;
+ Index expectedIndex = Index.fromOneBased(1);
+ PersonDescriptor expectedDescriptor = new PersonDescriptor();
+ expectedDescriptor.setTags(Set.of(new Tag(VALID_TAG_FRIEND)));
+
+ RemoveTagCommand expectedCommand = new RemoveTagCommand(expectedIndex, expectedDescriptor);
+ assertParseSuccess(parser, userInput, expectedCommand);
+ }
+
+ @Test
+ public void parse_missingIndex_throwsParseException() {
+ String userInput = TAG_DESC_FRIEND;
+ assertParseFailure(parser, userInput, String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ RemoveTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_missingTag_throwsParseException() {
+ String userInput = "1";
+ assertParseFailure(parser, userInput, String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ RemoveTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_invalidIndex_throwsParseException() {
+ String userInput = "abc" + TAG_DESC_FRIEND;
+ assertParseFailure(parser, userInput, String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ RemoveTagCommand.MESSAGE_USAGE));
+ }
+
+ @Test
+ public void parse_emptyTagField_returnsEmptyTagErrorMessage() {
+ String userInput = "1 t/";
+ Index expectedIndex = Index.fromOneBased(1);
+ assertParseFailure(parser, userInput, RemoveTagCommand.MESSAGE_EMPTY_TAG);
+ }
+}
+
+
diff --git a/src/test/java/seedu/address/model/person/AddressTest.java b/src/test/java/seedu/address/model/person/AddressTest.java
index 314885eca26..6d1c1ad7d92 100644
--- a/src/test/java/seedu/address/model/person/AddressTest.java
+++ b/src/test/java/seedu/address/model/person/AddressTest.java
@@ -53,4 +53,15 @@ public void equals() {
// different values -> returns false
assertFalse(address.equals(new Address("Other Valid Address")));
}
+
+ @Test
+ public void toStringTest() {
+ // valid address
+ Address address = new Address("Valid Address");
+ assertTrue(address.toString().equals("Valid Address"));
+
+ // empty address
+ Address emptyAddress = new Address("Unknown address");
+ assertTrue(emptyAddress.toString().equals(""));
+ }
}
diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java
index f08cdff0a64..beb5df545a2 100644
--- a/src/test/java/seedu/address/model/person/EmailTest.java
+++ b/src/test/java/seedu/address/model/person/EmailTest.java
@@ -85,4 +85,15 @@ public void equals() {
// different values -> returns false
assertFalse(email.equals(new Email("other.valid@email")));
}
+
+ @Test
+ public void toStringTest() {
+ // valid email
+ Email email = new Email("valid@email");
+ assertTrue(email.toString().equals("valid@email"));
+
+ // empty email
+ Email email_empty = new Email("unknown@example.com");
+ assertTrue(email_empty.toString().equals(""));
+ }
}
diff --git a/src/test/java/seedu/address/model/person/PersonIdTest.java b/src/test/java/seedu/address/model/person/PersonIdTest.java
new file mode 100644
index 00000000000..309a27d88dd
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/PersonIdTest.java
@@ -0,0 +1,91 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class PersonIdTest {
+ @Test
+ public void constructor_validPersonId() {
+ PersonId personId = new PersonId("1");
+ assertEquals("1", personId.value);
+ }
+
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new PersonId(null));
+ }
+
+ @Test
+ public void constructor_invalidPersonId_throwsIllegalArgumentException() {
+ String invalidPersonIdEmpty = "";
+ assertThrows(IllegalArgumentException.class, () -> new PersonId(invalidPersonIdEmpty));
+
+ String invalidPersonIdString = "test";
+ assertThrows(IllegalArgumentException.class, () -> new PersonId(invalidPersonIdString));
+ }
+
+ @Test
+ public void isValidId() {
+ // null person id
+ assertThrows(NullPointerException.class, () -> PersonId.isValidId(null));
+
+ // blank person id
+ assertFalse(PersonId.isValidId("")); // empty string
+ assertFalse(PersonId.isValidId(" ")); // spaces only
+
+ // invalid person id
+ assertFalse(PersonId.isValidId("test")); // non-integer
+ assertFalse(PersonId.isValidId("123.45")); // non-integer
+ assertFalse(PersonId.isValidId("123-45")); // non-integer
+ assertFalse(PersonId.isValidId("123 45")); // non-integer
+ assertFalse(PersonId.isValidId("-1")); // negative integer
+
+ // valid person id
+ assertTrue(PersonId.isValidId("0")); // zero
+ assertTrue(PersonId.isValidId("1")); // positive integer
+ assertTrue(PersonId.isValidId("1234567890")); // long integer
+ }
+
+ @Test
+ public void reset() {
+ assertEquals(0, PersonId.reset());
+ }
+
+ @Test
+ public void setCounter() {
+ // valid input
+ assertEquals(5, PersonId.setCounter(5));
+ assertEquals(0, PersonId.setCounter(0));
+ assertEquals(Integer.MAX_VALUE, PersonId.setCounter(Integer.MAX_VALUE));
+
+ // invalid input
+ assertThrows(IllegalArgumentException.class, () -> PersonId.setCounter(-1));
+ assertThrows(IllegalArgumentException.class, () -> PersonId.setCounter(Integer.MIN_VALUE));
+ }
+
+ @Test
+ public void equals() {
+ PersonId personId1 = new PersonId("1");
+ PersonId personId2 = new PersonId("2");
+ PersonId personId1Copy = new PersonId("1");
+
+ // same object -> returns true
+ assertTrue(personId1.equals(personId1));
+
+ // same values -> returns true
+ assertTrue(personId1.equals(personId1Copy));
+
+ // different types -> returns false
+ assertFalse(personId1.equals(1));
+
+ // null -> returns false
+ assertFalse(personId1.equals(null));
+
+ // different person id -> returns false
+ assertFalse(personId1.equals(personId2));
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java
index 31a10d156c9..ac905a8b249 100644
--- a/src/test/java/seedu/address/model/person/PersonTest.java
+++ b/src/test/java/seedu/address/model/person/PersonTest.java
@@ -93,7 +93,8 @@ public void equals() {
@Test
public void toStringMethod() {
String expected = Person.class.getCanonicalName() + "{name=" + ALICE.getName() + ", phone=" + ALICE.getPhone()
- + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}";
+ + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags()
+ + ", id=" + ALICE.getId() + "}";
assertEquals(expected, ALICE.toString());
}
}
diff --git a/src/test/java/seedu/address/model/person/PhoneTest.java b/src/test/java/seedu/address/model/person/PhoneTest.java
index deaaa5ba190..2d0a7930aea 100644
--- a/src/test/java/seedu/address/model/person/PhoneTest.java
+++ b/src/test/java/seedu/address/model/person/PhoneTest.java
@@ -38,6 +38,17 @@ public void isValidPhone() {
assertTrue(Phone.isValidPhone("124293842033123")); // long phone numbers
}
+ @Test
+ public void getPhoneNumber() {
+ // valid phone number
+ Phone phone = new Phone("999");
+ assertTrue(phone.getPhoneNumber().equals("999"));
+
+ // empty phone number
+ Phone emptyPhone = new Phone("000");
+ assertTrue(emptyPhone.getPhoneNumber().equals("Unknown number"));
+ }
+
@Test
public void equals() {
Phone phone = new Phone("999");
@@ -57,4 +68,15 @@ public void equals() {
// different values -> returns false
assertFalse(phone.equals(new Phone("995")));
}
+
+ @Test
+ public void toStringTest() {
+ // valid phone number
+ Phone phone = new Phone("999");
+ assertTrue(phone.toString().equals("999"));
+
+ // empty phone number
+ Phone emptyPhone = new Phone("000");
+ assertTrue(emptyPhone.toString().equals(""));
+ }
}
diff --git a/src/test/java/seedu/address/model/person/TagNamesContainsTagsPredicateTest.java b/src/test/java/seedu/address/model/person/TagNamesContainsTagsPredicateTest.java
new file mode 100644
index 00000000000..c218c1ebda8
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TagNamesContainsTagsPredicateTest.java
@@ -0,0 +1,83 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.testutil.PersonBuilder;
+
+public class TagNamesContainsTagsPredicateTest {
+
+ @Test
+ public void equals() {
+ List firstPredicateTagList = Collections.singletonList("first");
+ List secondPredicateTagList = Arrays.asList("first", "second");
+
+ TagNamesContainsTagsPredicate firstPredicate = new TagNamesContainsTagsPredicate(firstPredicateTagList);
+ TagNamesContainsTagsPredicate secondPredicate = new TagNamesContainsTagsPredicate(secondPredicateTagList);
+
+ // same object -> returns true
+ assertTrue(firstPredicate.equals(firstPredicate));
+
+ // same values -> returns true
+ TagNamesContainsTagsPredicate firstPredicateCopy = new TagNamesContainsTagsPredicate(firstPredicateTagList);
+ assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+ // different types -> returns false
+ assertFalse(firstPredicate.equals(1));
+
+ // null -> returns false
+ assertFalse(firstPredicate.equals(null));
+
+ // different tag list -> returns false
+ assertFalse(firstPredicate.equals(secondPredicate));
+ }
+
+ @Test
+ public void test_personHasAllTags_returnsTrue() {
+ // Person with exact tag match
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Collections.singletonList("friends"));
+ assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friends").build()));
+
+ // Person with all requested tags (multiple tags)
+ predicate = new TagNamesContainsTagsPredicate(Arrays.asList("friends", "neighbor"));
+ assertTrue(predicate.test(
+ new PersonBuilder().withName("Alice Bob").withTags("friends", "neighbor", "family").build()));
+
+ // Case-insensitive matching
+ predicate = new TagNamesContainsTagsPredicate(List.of("FRIENDS"));
+ assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friends").build()));
+ }
+
+ @Test
+ public void test_personDoesNotHaveAllTags_returnsFalse() {
+ // Person missing some of the requested tags
+ TagNamesContainsTagsPredicate predicate =
+ new TagNamesContainsTagsPredicate(Arrays.asList("friends", "colleague"));
+ assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friends").build()));
+
+ // Person has none of the requested tags
+ predicate = new TagNamesContainsTagsPredicate(List.of("colleague"));
+ assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").withTags("friends", "family").build()));
+
+ // Person has no tags
+ predicate = new TagNamesContainsTagsPredicate(List.of("friends"));
+ assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").withTags().build()));
+ }
+
+ @Test
+ public void toStringMethod() {
+ List tags = Arrays.asList("tag1", "tag2");
+ TagNamesContainsTagsPredicate predicate = new TagNamesContainsTagsPredicate(tags);
+
+ String expected = TagNamesContainsTagsPredicate.class.getCanonicalName() + "{tags=" + tags + "}";
+ assertEquals(expected, predicate.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/storage/FileNotesStorageTest.java b/src/test/java/seedu/address/storage/FileNotesStorageTest.java
new file mode 100644
index 00000000000..a0c6ace8e77
--- /dev/null
+++ b/src/test/java/seedu/address/storage/FileNotesStorageTest.java
@@ -0,0 +1,100 @@
+package seedu.address.storage;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
+
+public class FileNotesStorageTest {
+
+ @TempDir
+ public Path testFolder;
+
+ private FileNotesStorage notesStorage;
+ private Person testPerson;
+
+ @BeforeEach
+ public void setUp() {
+ notesStorage = new FileNotesStorage(testFolder);
+ testPerson = new PersonBuilder().withName("Test Person").withPersonId("123").build();
+ }
+
+ @Test
+ public void readNote_nonExistentNote_returnsEmptyString() throws IOException {
+ assertEquals("", notesStorage.readNote(testPerson));
+ }
+
+ @Test
+ public void saveAndReadNote_validInput_success() throws IOException {
+ String testContent = "This is a test note";
+ notesStorage.saveNote(testPerson, testContent);
+
+ // Check if the file exists
+ File noteFile = new File(testFolder.toString(), "123.txt");
+ assertTrue(noteFile.exists());
+
+ // Verify content
+ String readContent = notesStorage.readNote(testPerson);
+ assertEquals(testContent, readContent);
+ }
+
+ @Test
+ public void deleteNote_nonExistentNote_returnsFalse() throws IOException {
+ boolean result = notesStorage.deleteNote(testPerson);
+ assertFalse(result);
+ }
+
+ @Test
+ public void deleteNote_existingNote_success() throws IOException {
+ // Create a note first
+ notesStorage.saveNote(testPerson, "Note to delete");
+ File noteFile = new File(testFolder.toString(), "123.txt");
+ assertTrue(noteFile.exists());
+
+ // Delete the note
+ notesStorage.deleteNote(testPerson);
+ assertFalse(noteFile.exists());
+ }
+
+ @Test
+ public void deleteAllNotes_multipleNotes_success() throws IOException {
+ // Create multiple notes
+ Person person1 = new PersonBuilder().withPersonId("1").build();
+ Person person2 = new PersonBuilder().withPersonId("2").build();
+
+ notesStorage.saveNote(person1, "Note 1");
+ notesStorage.saveNote(person2, "Note 2");
+
+ // Verify files exist
+ assertTrue(new File(testFolder.toString(), "1.txt").exists());
+ assertTrue(new File(testFolder.toString(), "2.txt").exists());
+
+ // Delete all notes
+ notesStorage.deleteAllNotes();
+
+ // Verify files are deleted
+ assertFalse(new File(testFolder.toString(), "1.txt").exists());
+ assertFalse(new File(testFolder.toString(), "2.txt").exists());
+ }
+
+ @Test
+ public void constructor_cannotCreateDirectory_handlesException() throws Exception {
+ Path blockingFile = testFolder.resolve("blocked-dir");
+ Files.createFile(blockingFile);
+
+ new FileNotesStorage(blockingFile);
+
+ assertTrue(Files.isRegularFile(blockingFile));
+ }
+}
diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
index 83b11331cdb..5fc4acb56ba 100644
--- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
+++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
@@ -15,14 +15,16 @@
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
+import seedu.address.model.person.PersonId;
import seedu.address.model.person.Phone;
public class JsonAdaptedPersonTest {
- private static final String INVALID_NAME = "R@chel";
+ private static final String INVALID_NAME = "R/chel";
private static final String INVALID_PHONE = "+651234";
private static final String INVALID_ADDRESS = " ";
private static final String INVALID_EMAIL = "example.com";
private static final String INVALID_TAG = "#friend";
+ private static final String INVALID_PERSON_ID = "five";
private static final String VALID_NAME = BENSON.getName().toString();
private static final String VALID_PHONE = BENSON.getPhone().toString();
@@ -31,6 +33,7 @@ public class JsonAdaptedPersonTest {
private static final List VALID_TAGS = BENSON.getTags().stream()
.map(JsonAdaptedTag::new)
.collect(Collectors.toList());
+ private static final String VALID_PERSON_ID = BENSON.getId().toString();
@Test
public void toModelType_validPersonDetails_returnsPerson() throws Exception {
@@ -40,60 +43,64 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception {
@Test
public void toModelType_invalidName_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = Name.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullName_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidPhone_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = Phone.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullPhone_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidEmail_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = Email.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullEmail_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidAddress_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = Address.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullAddress_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS, VALID_PERSON_ID);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@@ -102,9 +109,25 @@ public void toModelType_nullAddress_throwsIllegalValueException() {
public void toModelType_invalidTags_throwsIllegalValueException() {
List invalidTags = new ArrayList<>(VALID_TAGS);
invalidTags.add(new JsonAdaptedTag(INVALID_TAG));
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags, VALID_PERSON_ID);
assertThrows(IllegalValueException.class, person::toModelType);
}
+ @Test
+ public void toModelType_invalidPersonId_throwsIllegalValueException() {
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, INVALID_PERSON_ID);
+ String expectedMessage = PersonId.MESSAGE_CONSTRAINTS;
+ assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
+ }
+
+ @Test
+ public void toModelType_nullPersonId_throwsIllegalValueException() {
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS, null);
+ String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, PersonId.class.getSimpleName());
+ assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
+ }
+
}
diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/seedu/address/storage/StorageManagerTest.java
index 99a16548970..76f968f3486 100644
--- a/src/test/java/seedu/address/storage/StorageManagerTest.java
+++ b/src/test/java/seedu/address/storage/StorageManagerTest.java
@@ -14,6 +14,8 @@
import seedu.address.model.AddressBook;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
public class StorageManagerTest {
@@ -26,6 +28,7 @@ public class StorageManagerTest {
public void setUp() {
JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab"));
JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs"));
+ NotesStorage notesStorage = new FileNotesStorage(getTempFilePath("notes"));
storageManager = new StorageManager(addressBookStorage, userPrefsStorage);
}
@@ -65,4 +68,28 @@ public void getAddressBookFilePath() {
assertNotNull(storageManager.getAddressBookFilePath());
}
+ @Test
+ public void notesReadSave() throws Exception {
+ // Note-related tests
+ Person person = new PersonBuilder().build();
+ String noteContent = "This is a test note";
+
+ // Test saving and reading a note
+ storageManager.saveNote(person, noteContent);
+ assertEquals(noteContent, storageManager.readNote(person));
+
+ // Test deleting a note
+ storageManager.deleteNote(person);
+ assertEquals("", storageManager.readNote(person));
+
+ // Test saving multiple notes and deleting all
+ Person person1 = new PersonBuilder().withPersonId("1").build();
+ Person person2 = new PersonBuilder().withPersonId("2").build();
+ storageManager.saveNote(person1, "Note 1");
+ storageManager.saveNote(person2, "Note 2");
+
+ storageManager.deleteAllNotes();
+ assertEquals("", storageManager.readNote(person1));
+ assertEquals("", storageManager.readNote(person2));
+ }
}
diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java
index 6be381d39ba..af31e041fdb 100644
--- a/src/test/java/seedu/address/testutil/PersonBuilder.java
+++ b/src/test/java/seedu/address/testutil/PersonBuilder.java
@@ -7,6 +7,7 @@
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
import seedu.address.model.person.Person;
+import seedu.address.model.person.PersonId;
import seedu.address.model.person.Phone;
import seedu.address.model.tag.Tag;
import seedu.address.model.util.SampleDataUtil;
@@ -20,12 +21,14 @@ public class PersonBuilder {
public static final String DEFAULT_PHONE = "85355255";
public static final String DEFAULT_EMAIL = "amy@gmail.com";
public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111";
+ public static final String DEFAULT_PERSON_ID = "1";
private Name name;
private Phone phone;
private Email email;
private Address address;
private Set tags;
+ private PersonId personId;
/**
* Creates a {@code PersonBuilder} with the default details.
@@ -36,6 +39,7 @@ public PersonBuilder() {
email = new Email(DEFAULT_EMAIL);
address = new Address(DEFAULT_ADDRESS);
tags = new HashSet<>();
+ personId = new PersonId(DEFAULT_PERSON_ID);
}
/**
@@ -47,6 +51,7 @@ public PersonBuilder(Person personToCopy) {
email = personToCopy.getEmail();
address = personToCopy.getAddress();
tags = new HashSet<>(personToCopy.getTags());
+ personId = personToCopy.getId();
}
/**
@@ -89,8 +94,16 @@ public PersonBuilder withEmail(String email) {
return this;
}
+ /**
+ * Sets the (@code PersonId) of the (@code Person) that we are building.
+ */
+ public PersonBuilder withPersonId(String personId) {
+ this.personId = new PersonId(personId);
+ return this;
+ }
+
public Person build() {
- return new Person(name, phone, email, address, tags);
+ return new Person(name, phone, email, address, tags, personId);
}
}
diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/PersonDescriptorBuilder.java
similarity index 68%
rename from src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java
rename to src/test/java/seedu/address/testutil/PersonDescriptorBuilder.java
index 4584bd5044e..45bb3a744ee 100644
--- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java
+++ b/src/test/java/seedu/address/testutil/PersonDescriptorBuilder.java
@@ -4,7 +4,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
+import seedu.address.logic.commands.PersonDescriptor;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
@@ -15,23 +15,23 @@
/**
* A utility class to help with building EditPersonDescriptor objects.
*/
-public class EditPersonDescriptorBuilder {
+public class PersonDescriptorBuilder {
- private EditPersonDescriptor descriptor;
+ private PersonDescriptor descriptor;
- public EditPersonDescriptorBuilder() {
- descriptor = new EditPersonDescriptor();
+ public PersonDescriptorBuilder() {
+ descriptor = new PersonDescriptor();
}
- public EditPersonDescriptorBuilder(EditPersonDescriptor descriptor) {
- this.descriptor = new EditPersonDescriptor(descriptor);
+ public PersonDescriptorBuilder(PersonDescriptor descriptor) {
+ this.descriptor = new PersonDescriptor(descriptor);
}
/**
* Returns an {@code EditPersonDescriptor} with fields containing {@code person}'s details
*/
- public EditPersonDescriptorBuilder(Person person) {
- descriptor = new EditPersonDescriptor();
+ public PersonDescriptorBuilder(Person person) {
+ descriptor = new PersonDescriptor();
descriptor.setName(person.getName());
descriptor.setPhone(person.getPhone());
descriptor.setEmail(person.getEmail());
@@ -42,7 +42,7 @@ public EditPersonDescriptorBuilder(Person person) {
/**
* Sets the {@code Name} of the {@code EditPersonDescriptor} that we are building.
*/
- public EditPersonDescriptorBuilder withName(String name) {
+ public PersonDescriptorBuilder withName(String name) {
descriptor.setName(new Name(name));
return this;
}
@@ -50,7 +50,7 @@ public EditPersonDescriptorBuilder withName(String name) {
/**
* Sets the {@code Phone} of the {@code EditPersonDescriptor} that we are building.
*/
- public EditPersonDescriptorBuilder withPhone(String phone) {
+ public PersonDescriptorBuilder withPhone(String phone) {
descriptor.setPhone(new Phone(phone));
return this;
}
@@ -58,7 +58,7 @@ public EditPersonDescriptorBuilder withPhone(String phone) {
/**
* Sets the {@code Email} of the {@code EditPersonDescriptor} that we are building.
*/
- public EditPersonDescriptorBuilder withEmail(String email) {
+ public PersonDescriptorBuilder withEmail(String email) {
descriptor.setEmail(new Email(email));
return this;
}
@@ -66,7 +66,7 @@ public EditPersonDescriptorBuilder withEmail(String email) {
/**
* Sets the {@code Address} of the {@code EditPersonDescriptor} that we are building.
*/
- public EditPersonDescriptorBuilder withAddress(String address) {
+ public PersonDescriptorBuilder withAddress(String address) {
descriptor.setAddress(new Address(address));
return this;
}
@@ -75,13 +75,13 @@ public EditPersonDescriptorBuilder withAddress(String address) {
* Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor}
* that we are building.
*/
- public EditPersonDescriptorBuilder withTags(String... tags) {
+ public PersonDescriptorBuilder withTags(String... tags) {
Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet());
descriptor.setTags(tagSet);
return this;
}
- public EditPersonDescriptor build() {
+ public PersonDescriptor build() {
return descriptor;
}
}
diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java
index 90849945183..9807dceb626 100644
--- a/src/test/java/seedu/address/testutil/PersonUtil.java
+++ b/src/test/java/seedu/address/testutil/PersonUtil.java
@@ -9,7 +9,7 @@
import java.util.Set;
import seedu.address.logic.commands.AddCommand;
-import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
+import seedu.address.logic.commands.PersonDescriptor;
import seedu.address.model.person.Person;
import seedu.address.model.tag.Tag;
@@ -31,7 +31,7 @@ public static String getAddCommand(Person person) {
public static String getPersonDetails(Person person) {
StringBuilder sb = new StringBuilder();
sb.append(PREFIX_NAME + person.getName().fullName + " ");
- sb.append(PREFIX_PHONE + person.getPhone().value + " ");
+ sb.append(PREFIX_PHONE + person.getPhone().getPhoneNumber() + " ");
sb.append(PREFIX_EMAIL + person.getEmail().value + " ");
sb.append(PREFIX_ADDRESS + person.getAddress().value + " ");
person.getTags().stream().forEach(
@@ -41,14 +41,16 @@ public static String getPersonDetails(Person person) {
}
/**
- * Returns the part of command string for the given {@code EditPersonDescriptor}'s details.
+ * Returns the part of command string for the given {@code PersonDescriptor}'s details.
*/
- public static String getEditPersonDescriptorDetails(EditPersonDescriptor descriptor) {
+ public static String getEditPersonDescriptorDetails(PersonDescriptor descriptor) {
StringBuilder sb = new StringBuilder();
descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" "));
- descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" "));
+ descriptor.getPhone().ifPresent(
+ phone -> sb.append(PREFIX_PHONE).append(phone.getPhoneNumber()).append(" "));
descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" "));
- descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" "));
+ descriptor.getAddress().ifPresent(
+ address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" "));
if (descriptor.getTags().isPresent()) {
Set tags = descriptor.getTags().get();
if (tags.isEmpty()) {
diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java
index fec76fb7129..b01406453a4 100644
--- a/src/test/java/seedu/address/testutil/TypicalPersons.java
+++ b/src/test/java/seedu/address/testutil/TypicalPersons.java
@@ -26,21 +26,39 @@ public class TypicalPersons {
public static final Person ALICE = new PersonBuilder().withName("Alice Pauline")
.withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com")
.withPhone("94351253")
- .withTags("friends").build();
+ .withTags("friends")
+ .withPersonId("1").build();
public static final Person BENSON = new PersonBuilder().withName("Benson Meier")
.withAddress("311, Clementi Ave 2, #02-25")
.withEmail("johnd@example.com").withPhone("98765432")
- .withTags("owesMoney", "friends").build();
- public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563")
- .withEmail("heinz@example.com").withAddress("wall street").build();
- public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533")
- .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build();
- public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224")
- .withEmail("werner@example.com").withAddress("michegan ave").build();
- public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427")
- .withEmail("lydia@example.com").withAddress("little tokyo").build();
- public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442")
- .withEmail("anna@example.com").withAddress("4th street").build();
+ .withTags("owesMoney", "friends")
+ .withPersonId("2").build();
+ public static final Person CARL = new PersonBuilder().withName("Carl Kurz")
+ .withPhone("95352563")
+ .withEmail("heinz@example.com")
+ .withAddress("wall street")
+ .withPersonId("3").build();
+ public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier")
+ .withPhone("87652533")
+ .withEmail("cornelia@example.com")
+ .withAddress("10th street")
+ .withTags("friends")
+ .withPersonId("4").build();
+ public static final Person ELLE = new PersonBuilder().withName("Elle Meyer")
+ .withPhone("9482224")
+ .withEmail("werner@example.com")
+ .withAddress("michegan ave")
+ .withPersonId("5").build();
+ public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz")
+ .withPhone("9482427")
+ .withEmail("lydia@example.com")
+ .withAddress("little tokyo")
+ .withPersonId("6").build();
+ public static final Person GEORGE = new PersonBuilder().withName("George Best")
+ .withPhone("9482442")
+ .withEmail("anna@example.com")
+ .withAddress("4th street")
+ .withPersonId("7").build();
// Manually added
public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424")