diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index aabbbdbaf33..a476e014cc5 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -41,6 +41,7 @@ jobs: - uses: codecov/codecov-action@v2 if: runner.os == 'Linux' with: + token: cfaa373b-2c7a-40b1-a4c1-11023db5e91f directory: ${{ github.workspace }}/build/reports/jacoco/coverage files: coverage.xml fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 71c9194e8bd..82435a1fa98 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + diff --git a/README.md b/README.md index 13f5c77403f..6c70e5b077c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S1-CS2103-F13-1/tp) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +* DevEnable is a desktop application for **Software Developers** to manage client projects.
+ Developers can: + * view all projects in one place + * tag and manage tasks based on deadlines + * tag and manage clients for each project +* It is optimized for use via a **Command Line Interface (CLI)**. +* For the detailed documentation of this project, see the [DevEnable Product Website](https://ay2223s1-cs2103-f13-1.github.io/tp/). + +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index 108397716bd..4e0042a4307 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,10 @@ checkstyle { toolVersion = '10.2' } +run { + enableAssertions = true +} + test { useJUnitPlatform() finalizedBy jacocoTestReport @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'devenable.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..20ce2dcdceb 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,54 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` - ## Project team -### John Doe +### Aishwarya Hariharan Iyer - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Aishwarya-Hariharan-Iyer)] +[[portfolio](team/aishwarya-hariharan-iyer.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: UI -### Jane Doe +### Lew Kian Loong, Conrad - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/ConradLew)] +[[portfolio](team/conradlew.md)] * Role: Team Lead -* Responsibilities: UI +* Responsibilities: Data -### Johnny Doe +### Crystal Phua - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/crvstalphua)] +[[portfolio](team/crvstalphua.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: UI -### Jean Doe +### Donovan Singh - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Donovan9617)] +[[portfolio](team/donovan9617.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: UI -### James Doe +### Kim Yong Beom - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Yongbeom-Kim)] +[[portfolio](team/yongbeom-kim.md)] * Role: Developer * Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..d5b2a997d48 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -5,11 +5,11 @@ title: Developer Guide * Table of Contents {:toc} --------------------------------------------------------------------------------------------------------------------- +--- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +- This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +23,7 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/docs/diagrams) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
### Architecture @@ -36,7 +36,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** has two classes called [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, +**`Main`** has two classes called [`Main`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/MainApp.java). It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. @@ -52,7 +52,7 @@ The rest of the App consists of four components. **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `project -d 1`. @@ -69,34 +69,37 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +**API**: [`Ui.java`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`ProjectListPanel`, `IssueListPanel`, `ClientListPanel`, `ResultDisplay`, `HelpWindow` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src//main/resources/view/MainWindow.fxml) The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to add a person). -1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command to result in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddClientCommand`). More specifically, + 1. If the command is specific to an entity (e.g. `Project`, `Client` or `Issue`), this results in a `CommandParser` object (More specifically, an object of one of its subclasses e.g., `ClientCommandParser`). + 1. This `XYZCommandParser` object then yields a `Command` object (more specifically, a derived class of `ProjectCommand`, `ClientCommand` or `IssueCommand` respectively). + 2. Otherwise, it results in a `Command` Object directly. +2. The resulting `Command` object is then executed by the `LogicManager`. +3. The command can communicate with the `Model` when it is executed (e.g. to add a person). +4. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. @@ -110,39 +113,43 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `ClientCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddClientCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `ClientCommandParser`, `IssueCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. -### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +#### Entity Initialisation - +* In some instances, we note that an entity object (`Project`, `Issue` or `Client`) object cannot be created together with the `XYZCommand` object, since we may need access to the `model` object at the point of initialisation (for instance, to generate the next ID number when creating an entity). +* Given this, we opt to _partially initialise_ entity objects using the clas `XYZWithoutModel`. +* These are done mainly when creating the `AddXYZCommand` objects. +The sequence diagram below illustrates the partial initialisation logic during the creation and execution of a `AddProjectCommand` object. -The `Model` component, + -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores 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) +### Model component +**API** : [`Model.java`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/model/Model.java) -
: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.
+ - +The `Model` component, -
+* stores the project book data i.e., all `Project`, `Client`, and `Issue` objects (which are contained in separate `UniqueEntityList` objects). +* stores the currently 'selected' `Project`, `Client`, or `Issue` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList`, `ObservableList` or `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. +* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2223S1-CS2103-F13-1/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) - + The `Storage` component, -* can save both address book data and user preference data in json format, and read them back into corresponding objects. +* can save both project book data and user preference data in json format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`). +* handles all parsing of objects in `StorageUtil`. ### Common classes @@ -152,92 +159,512 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa ## **Implementation** -This section describes some noteworthy details on how certain features are implemented. +This section describes some details on how certain features are implemented. + +### List Command Feature + +The list mechanism is facilitated by `MainWindow`. It contains a `CommandBox` which listens for user command input and a JavaFX `StackPane` which holds the current entity list to be displayed. Upon the execution of either a `ListProjectCommand`, `ListClientCommand` or `ListIssueCommand`, the following operations are carried out: +* `MainWindow#swapProjectListDisplay()` — Changes the current display to a list of projects. +* `MainWindow#swapClientListDisplay()` — Changes the current display to a list of clients. +* `MainWindow#swapIssueListDisplay()` — Changes the current display to a list of issues. + +Given below is an example usage scenario and how the list mechanism behaves at each step. -### \[Proposed\] Undo/redo feature +Step 1. The user launches the application for the first time. The `StackPane` will be initialized by default with a list of projects. -#### Proposed Implementation +Step 2. The user executes `client -l` to view a list of clients. The `ListClientCommand` is executed and calls `MainWindow#swapClientListDisplay()`, clearing the current collection of child nodes of the `StackPane` and adds the root of a `ClientListPanel` to the emptied child nodes. The list of clients is now shown in the UI. -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +
+:information_source: **Note:** If the current display is already the same as that requested by the user, it will still call the respective setter methods, although the actual UI display will not change. +
-* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +The following sequence diagram shows how the list operation works: -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +![ListSequenceDiagram](images/ListSequenceDiagram.png) -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +
+:information_source: **Note:** The lifeline for `ListClientCommand` +should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
-Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +The following activity diagram summarizes what happens when a user executes a list command: -![UndoRedoState0](images/UndoRedoState0.png) +![ListActivityDiagram](images/ListActivityDiagram.png) -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +
+:information_source: **Note:** The diagrams above are generalized to `Client` but work similarly for other entity types (`Project`, `Issue`). +
-![UndoRedoState1](images/UndoRedoState1.png) +#### Design considerations -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +**Aspect: How list executes:** -![UndoRedoState2](images/UndoRedoState2.png) +* **Alternative 1 (current choice):** The children nodes of the `StackPane` are cleared and the root of the desired list type is added every time a list `Command` is executed. + * Pros: Easier to implement. + * Cons: The same `StackPane` is being reused for different entities. Misses out on potential polymorphism benefits. -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +* **Alternative 2:** The children nodes of the `StackPane` are never cleared and holds a single list of entities (`Project`, `Client`, `Issue`) and the list is filtered for the desired instance type for each list `Command`. + * Pros: Less duplication of code. + * Cons: Leads to more `instanceof` checks. Not much common behaviour between the entity classes to be abstracted via polymorphism. -
+### Default View Command Feature + +The default view mechanism is facilitated by `GuiSettings`. It is stored internally as a `DefaultView` enumeration which can take either of three values, `PROJECT`, `CLIENT` or `ISSUE`. Upon the execution of either a `SetProjectDefaultViewCommand`, `SetClientDefaultViewCommand` or `SetIssueDefaultViewCommand`, the following operation is called: +* `GuiSettings#setDefaultView()` — Sets the default view variable to the specified `DefaultView` type. + +This operation is exposed in the Model interface as `Model#setDefaultView()`. + +Given below is an example usage scenario and how the default view mechanism behaves at each step. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +Step 1. The user launches the application for the first time. The list of projects is shown by default. -![UndoRedoState3](images/UndoRedoState3.png) +Step 2. The user executes `client -v` to set the default view to clients. The `SetClientDefaultViewCommand` is executed and calls `Model#setDefaultView()`, setting the default view to the list of clients. -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +Step 3. The next time the user launches the application, the list of clients is shown by default. +The following sequence diagram shows how the default view operation works: + +![DefaultViewSequenceDiagram](images/DefaultViewSequenceDiagram.png) + +
+:information_source: **Note:** The lifeline for `SetClientDefaultViewCommand` +should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-The following sequence diagram shows how the undo operation works: +The following activity diagram summarizes what happens when a user executes a default view command: -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +![DefaultViewActivityDiagram](images/DefaultViewActivityDiagram.png) -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+:information_source: **Note:** The diagrams above are generalized to `Client` but work similarly for other entity types (`Project`, `Issue`). +
+ +### Add Command Feature + +A key functionality of DevEnable is the ability to add projects, issues, and clients into the system. The command word for adding will be `project`, `issue`, or `client`, depending on which entity is being added. +This is followed by the flag `-a`, representing an Add command. Next, it is followed by a series of prefixes-value pairs to initialise the entity, some of which are compulsory while others are optional. +When a user enters a valid Add command in the interface, `AddressBookParser#parseCommand` will be called which processes the inputs, creates an instance of a command parser, and calls the `ProjectCommandParser#parse`, +`IssueCommandParser#parse` or `ClientCommandParser#parse` method, depending on which entity is being added. Within this method, the flag `-a` will be detected, calling `ProjectCommandParser#parseAddProjectCommand`, +`IssueCommandParser#parseAddIssueCommand`, or `ClientCommandParser#parseAddClientCommand`, depending on which entity is added, which checks for input and prefix-value pair validity with methods in `ParserUtil`. +Finally, the parsed arguments are passed into and returned in an instance of the Add Command entity and the `AddProjectCommand#execute`, `AddIssueCommand#execute`, or `AddClientCommand#execute` is called depending +on which entity is added, which retrieves the respective entity list from the system, adds the entity into the list to update it, and have the UI display the updated filtered entity list. + +#### Add Project Command +Compulsory prefixes: n/VALID_NAME +Optional prefixes: c/VALID_CLIENT_ID, r/VALID_REPOSITORY, d/VALID_DEADLINE +Example Use: `project -a n/John c/1 r/JohnDoe/tp d/2022-03-05` + +#### Add Issue Command +Compulsory prefixes: p/VALID_PROJECT_ID, t/VALID_TITLE +Optional prefixes: d/VALID_DEADLINE, u/VALID_URGENCY +Example Use: `issue p/1 t/To create a person class which stores all relevant person data d/2022-12-10 u/0` + +#### Add Client Command +Compulsory prefixes: n/VALID_NAME, p/VALID_PROJECT_ID +Optional prefixes: m/VALID_MOBILE_NUMBER, e/VALID_EMAIL +Example Use: `client -a n/John Doe m/98765432 e/johnd@example.com p/1` + +The following sequence diagram shows how the add command operation works for adding a project entity: +Example: `project -a n/Team Project` + +![AddSequenceDiagram](images/AddSequenceDiagram.png) + +
+:information_source: **Note:** The lifeline for `AddProjectCommand` +should end at destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+#### Design considerations: + +**Aspect: Add command access to the model:** + +**Alternative 1: (current choice)** Only `AddProjectCommand:execute`, `IssueCommandParser:execute` and `ClientCommandParser:execute` have access to the Model. +* Pros: No coupling between Parser class and Model class. +* Cons: Mappings could not be performed within the parser. + +**Alternative 2:** Refactor `ProjectCommandParser:parseAddProjectCommand`, `IssueCommandParser:parseAddIssueCommand` and `ClientCommandParser:parseAddClientCommand` to have access to the Model. + * Pros: Mappings could be performed within the parser which fitted its responsibility. + * Cons: May result in extra coupling between Parser class and Model class. + +Taking into consideration the extra coupling involved, Alternative 1 was chosen as the current design for add command access to the model. + +### Delete Command Feature + +A key functionality of DevEnable is the ability to delete projects, issues, and clients into the system. The command +word for deleting will be `project`, `issue`, or `client`, depending on which entity is being deleted. +This is followed by the flag `-d`, representing a Delete Command. Next, it is followed by a compulsory argument to +initialise the entity. +When a user enters a valid Delete Command in the interface, `AddressBookParser#parseCommand` will be called which +processes the inputs, creates an instance of a command parser, and calls the `ProjectCommandParser#parse`, +`IssueCommandParser#parse` or `ClientCommandParser#parse` method, depending on which entity is being added. Within +this method, the flag `-d` will be detected, calling `ProjectCommandParser#parseDeleteProjectCommand`, +`IssueCommandParser#parseDeleteIssueCommand`, or `ClientCommandParser#parseDeleteClientCommand`, depending on which +entity is deleted, which checks for input argument validity with methods in `ParserUtil`. +Finally, the parsed arguments are passed into and returned in an instance of the Delete Command entity and the +`DeleteProjectCommand#execute`, `DeleteIssueCommand#execute`, or `AddClientCommand#execute` is called depending +on which entity is deleted, which retrieves the respective entity list from the system, deletes the entity from the +list to update it, and have the UI display the updated filtered entity list. + +#### Delete Project Command + +Compulsory argument: VALID_PROJECT_ID +Example Use: `project -d 1` + +#### Delete Issue Command +Compulsory argument: VALID_ISSUE_ID +Example Use: `issue -d 2` + +#### Delete Client Command +Compulsory argument: VALID_CLIENT_ID +Example Use: `client -d 3` + +The following sequence diagram shows how the delete command operation works for deleting a client entity: +Example: `client -d 1` + +![DeleteSequenceDiagram](images/DeleteSequenceDiagram.png) + +
+:information_source: **Note:** The lifeline for `DeleteClientCommand` +should end at destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +#### Design considerations: + +**Aspect: Delete Command's access to the model:** + +**Alternative 1: (current choice)** Only `ProjectCommand:execute`, `IssueCommandParser:execute` and `ClientCommandParser:execute` have access to the Model. +* Pros: No coupling between Parser class and Model class. +* Cons: Mappings could not be performed within the parser. + +**Alternative 2:** Refactor `ProjectCommandParser:parseDeleteProjectCommand`,`IssueCommandParser:parseDeleteIssueCommand` and `ClientCommandParser:parseDeleteClientCommand` to have access to the Model. +* Pros: Mappings could be performed within the parser which fitted its responsibility. +* Cons: May result in extra coupling between Parser class and Model class. + +Taking into consideration the extra coupling involved, Alternative 1 was chosen as the current design for Delete +Command's access to the model. + +### Edit Command Feature + +A key functionality of DevEnable is the ability to edit projects, issues and clients currently in the system. The command word for editing will be `project`, `issue`, or `client`, depending on which entity it being edited. +This is followed by the flag `-e`, representing an Edit command. Next, it is followed by a series of prefixes-value pairs, one compulsory pair for identifying the entity to be edited, and at least one pair indicating the fields to be edited. +When a user enters a valid Edit command in the interface, `AddressBookParser#parseCommand` will be called which processes the inputs, creates an instance and calls the `ProjectCommandParser#parse`, +`IssueCommandParser#parse` or `ClientCommandParser#parse` method, depending on which entity is being edited. Within this method, the flag `-e` will be detected, calling `ProjectCommandParser#parseEditProjectCommand`, +`IssueCommandParser#parseEditIssueCommand`, or `ClientCommandParser#parseEditClientCommand`, depending on which entity is edited, which checks for input and prefix-pair validity with methods in `ParserUtil`. +Finally, the parsed arguments are passed into and returned in an instance of the Edit Command entity and the `EditProjectCommand#execute`, `EditIssueCommand#execute`, or `EditClientCommand#execute` is called depending on +which entity is edited, which retrieves the respective entity from its entity list in the system, edits the fields of the entity, updates it, and have the UI display the updated filtered entity list. + +#### Edit Project Command +Compulsory prefix: p/VALID_PROJECT_ID +Optional prefixes (at least one to be included): n/VALID_NAME, c/VALID_CLIENT_ID, r/VALID_REPOSITORY, d/VALID_DEADLINE +Example Use: `project -e p/1 n/Jeff c/1 r/Jeffrey/tp d/2022-07-05` + +#### Edit Issue Command +Compulsory prefix: i/VALID_ISSUE_ID +Optional prefixes (at least one to be included): t/VALID_TITLE, d/VALID_DEADLINE, u/VALID_URGENCY +Example Use: `issue -e i/1 t/To edit issue command d/2022-04-09 u/1` + +#### Edit Client Command +Compulsory prefix: c/VALID_CLIENT_ID +Optional prefixes (at least one to be included): n/VALID_NAME, m/VALID_MOBILE_NUMBER, e/VALID_EMAIL, p/VALID_PROJECT_ID +Example Use: `client -e c/1 n/BenTen m/12345678 e/Ben10@gmail.com p/1` + +The following sequence diagram shows how the edit command operation works for editing an issue entity: +Example: `issue -e i/1 t/To edit issue command d/2022-04-09 u/1` + +![EditSequenceDiagram](images/EditSequenceDiagram.png) + +#### Design considerations: + +**Aspect: Editing of entity fields:** + +**Alternative 1: (current choice)** For each possible field to be edited, a new object of that field, with the parsed argument(if any) or null value, is created in `ProjectCommandParser#parseEditProjectCommand`, `IssueCommandParser#parseEditIssueCommand` or `ClientCommandParser#parseEditClientCommand`, then passed as arguments into `EditProjectCommand`, `EditIssueCommand` or `EditClientCommand`. +Within `EditProjectCommand#execute`, `EditIssueCommand#execute` and `EditClientCommand#execute`, set the fields to the new field objects. +* Pros: No creation of a new entity object +* Cons: Requires use of multiple accessors for various fields of each entity. + +**Alternative 2:** For each possible field to be edited, a new object of that field, with the parsed argument(if any) or null value, is created in `ProjectCommandParser#parseEditProjectCommand`, `IssueCommandParser#parseEditIssueCommand` or `ClientCommandParser#parseEditClientCommand`, then passed as arguments into `EditProjectCommand`, `EditIssueCommand` or `EditClientCommand`. +Within `EditProjectCommand#execute`, `EditIssueCommand#execute` and `EditClientCommand#execute`, retrieve the entity to be edited and create a new entity with the new field objects and the original fields not to be edited. +* Pros: Only requires getters for fields, preventing field values from being easily edited. +* Cons: Creation of a new entity object, requiring modification of entity list. + +Taking into consideration the potential issues with ID that came with modifying the entity lists, Alternative 1 was chosen as the current design for editing fields of an entity. + +### Sort Command Feature + +The sort feature sorts the entities in their respective entity lists in the Model according to a specified `key` and `order`. The View pulls the new entity lists from the Model and displays them. Upon the execution of +either a `SortProjectCommand`, `SortIssueCommand` or `SortClientCommand`, the `AddressBook#sortXXXByYYY()` is invoked (where XXX is the `entity` and YYY is the `key` to be sorted by) which obtains the entity class's +modifiable `ObservableList` as imported from the JavaFX collections, and calls its `sorted()` method that, depending on the specified `order`, takes in a comparator function specifying how to sort the entities. + +#### Sort Project Command +Keys (exactly one key to be included): p, d, i, n +Orders (exactly one order to be included): 0, 1 +General Form: `project -s KEY/ORDER` +Example Use: `project -s d/0` + +#### Sort Issue Command +Keys (exactly one key to be included): i, d, u +Orders (exactly one order to be included): 0, 1 +General Form: `issue -s KEY/ORDER` +Example Use: `issue -s u/1` + +#### Sort Client Command +Keys (exactly one key to be included): c, n +Orders (exactly one order to be included): 0, 1 +General Form: `client -s KEY/ORDER` +Example Use: `client -s c/1` + +#### Design considerations: + +**Aspect: How sorted entities are stored in the Model:** + +**Alternative 1 (current choice):** Sort entities directly on their original entity lists. After `SortProjectCommand`, `SortIssueCommand` or `SortClientCommand`, the original entity list gets manipulated and is rendered to the View. +* Pros: Saves lots of space +* Cons: Sort commands manipulate the original entity list in order to change the display on view + +**Alternative 2:** Maintain a separate sorted entity list for each entity and their purpose is to store each entity in their sorted order. After `SortProjectCommand`, `SortIssueCommand` or `SortClientCommand`, the respective sorted entity list gets manipulated and is rendered to the View. +* Pros: The original entity lists will not be affected by manipulations made through sorting in order to change the display on view +* Cons: To maintain such a sorted entity list for Project, Issue and Client will take up considerable space + +Alternative 1 was chosen because it saves space when sorting entities. The command to set default view of each entity helped overcome the cons of directly manipulating of the original list. This meant rebooting the app removed the previous entity sort order and revert to the default order. + +### Pin Command Feature -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +The pin mechanism is facilitated by `AddressBook`. It contains a `UniqueEntityList` for each entity type. Upon the execution of either a `PinProjectCommand`, `PinClientCommand` or `PinIssueCommand`, the following operations are carried out: +* `AddressBook#sortProjectsByPin()` — Sorts the current project list according to pin. +* `AddressBook#sortClientsByPin()`, `AddressBook#sortIssuesByPin()` — Similar function as above, but for clients and issues. +* `AddressBook#sortProjectsByCurrentCategory()`  — Sorts the current project list according to the last known sorting category. +* `AddressBook#sortClientsByCurrentCategory()`, `AddressBook#sortIssuesByCurrentCategory()` — Similar function as above, but for clients and issues. +These operations are exposed in the Model interface as methods with the same name e.g. `Model#sortProjectsByPin()`, `Model#sortProjectsByCurrentCategory()`. + +Given below is an example usage scenario and how the pin mechanism behaves at each step. + +Step 1. The user creates an entity with a unique ID. The entity is unpinned by default and will be displayed according to the current sorting order. + +Step 2. The user executes `client -p 3` to pin the 3rd client in the project book. The `PinClientCommand` is executed and calls `Client#togglePin()`, toggling the `Pin` attribute of the 5th client from `false` to `true`. This is followed by a call to `Model#sortClientsByCurrentCategory()` and `Model#sortClientsByPin()`, which displays the sorted client list with pinned clients (now including the 4th client) at the top. + +
+:information_source: **Note:** If the current client is already pinned, `Client#togglePin()` will toggle the `Pin` attribute of the client from `true` to `false` and call the latest sort order, causing the client to be displayed in its original position.
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +The following sequence diagram shows how the pin operation works: -![UndoRedoState4](images/UndoRedoState4.png) +![PinSequenceDiagram](images/PinSequenceDiagram.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +
+:information_source: **Note:** The lifeline for `PinClientCommand` +should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +The following activity diagram summarizes what happens when a user executes a pin command: -![UndoRedoState5](images/UndoRedoState5.png) +![PinActivityDiagram](images/PinActivityDiagram.png) -The following activity diagram summarizes what happens when a user executes a new command: +
+:information_source: **Note:** The diagrams above are generalized to `Client` but work similarly for other entity types (`Project`, `Issue`). +
- +#### Design considerations + +**Aspect: How entities can be unpinned:** + +**Alternative 1 (current choice):** The same command e.g. `PinClientCommand` used to pin the entity is also used to unpin the entity. +* Pros: Less duplication of code and less commands for the user to remember. +* Cons: Lesser separation of responsibilities as the same command is used for different (but similar) functionality. + +**Alternative 2:** An additional separate unpin command is created e.g. `UnpinClientCommand`. +* Pros: Better separation of responsibilities as one command is used to pin and the other is used to unpin the entity. There is no overlap. +* Cons: More duplication of code, additional command for user to remember with roughly the same functionality. + +### Find Command Feature + +A key functionality of DevEnable is the ability to find projects, issues and clients by searching for specific +keywords in different attributes of these entities. The command word for finding will be `project`, `issue`, or +`client`, depending on which entity it being edited. This is followed by the flag `-f`, representing a Find command. +Next, it is followed by a series of prefixes-value pairs, least one which is compulsory, representing the fields and +keywords to search and thereby identifying the entity to be found. When a user enters a valid Find command in the +interface,`AddressBookParser#parseCommand` will be called which processes the inputs, creates an instance and calls +the `ProjectCommandParser#parse`, `IssueCommandParser#parse` or `ClientCommandParser#parse` method, depending on +which entity is being found. Within this method, the flag `-f` will be detected, calling +`ProjectCommandParser#parseFindProjectCommand`, `IssueCommandParser#parseFindIssueCommand`, or +`ClientCommandParser#parseFindClientCommand`, depending on which entity is found, which checks for input and +prefix-pair validity with methods in `ParserUtil`. Finally, the parsed arguments are passed into and returned in an +instance of the Find Command entity and the `FindProjectCommand#execute`, `FindIssueCommand#execute`, or +`FindClientCommand#execute` is called depending on which entity is found, which retrieves the respective entity from +its entity list in the system, searches for the keywords the fields of the entity, matches it so as to filter the +entities with such fields, and have the UI display the updated filtered entity list. + +#### Find Project Command +Optional prefixes (at least one to be included): n/VALID_PROJECT_NAME, p/VALID_PROJECT_ID, c/VALID_CLIENT_ID, +r/VALID_REPOSITORY, l/VALID_CLIENT_NAME +Example Use: `project -f p/1 n/DevEnable c/2 r/Jeffrey/tp l/Jeffrey` + +#### Find Issue Command +Optional prefixes (at least one to be included): t/VALID_TITLE, s/VALID_STATUS, u/VALID_URGENCY, +n/VALID_PROJECT_NAME, p/VALID_PROJECT_ID, i/VALID_ISSUE_ID +Example Use: `issue -f t/Documentation s/Incomplete u/LOW n/DevEnable p/1 i/3` + +#### Find Client Command +Optional prefixes (at least one to be included): n/VALID_CLIENT_NAME, c/VALID_CLIENT_ID, e/VALID_EMAIL, m/VALID_MOBILE +Example Use: `client -f n/BenTen c/1 m/12345678 e/Ben10@gmail.com` + +The following sequence diagram shows how the find command operation works for finding a client: +Example: `client -f n/Harry` + +![FindSequenceDiagram](images/FindSequenceDiagram.png) + +
+:information_source: **Note:** The lifeline for `FindCommand` +should end at destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
#### Design considerations: -**Aspect: How undo & redo executes:** +**Aspect: Find Command's access to the model:** + +**Alternative 1: (current choice)** Only `ProjectCommand:execute`, `IssueCommandParser:execute` and `ClientCommandParser:execute` have access to the Model. +* Pros: No coupling between Parser class and Model class. +* Cons: Mappings could not be performed within the parser. + +**Alternative 2:** Refactor `ProjectCommandParser:parseFindProjectCommand`,`IssueCommandParser:parseFindIssueCommand` +and `ClientCommandParser:parseFindlientCommand` to have access to the Model. +* Pros: Mappings could be performed within the parser which fitted its responsibility. +* Cons: May result in extra coupling between Parser class and Model class. + +Taking into consideration the extra coupling involved, Alternative 1 was chosen as the current design for Find +Command's access to the model. + +**Aspect: Find Command's validation of input keywords:** + +**Alternative 1: (current choice)** All keywords must pass the validation check based on their respective prefixes. +* Pros: Helps to differentiate between no items being listed because none matched the search criteria and because the + criteria was invalid. +* Cons: The user can only search by valid keywords and thus has less flexibility in choosing inputs. + +**Alternative 2:** The keywords need not pass the validation check for their respective prefixes. +* Pros: The user has greater freedom in choosing the inputs which is a more familiar experience with respect to + other such apps. +* Cons: There is ambiguity in cases where no items are listed as to whether it is because such an item can never exist + in the list or if it does not exist at the time of search. + +Taking into consideration the need for clear system feedback to the user, Alternative 1 was chosen as the current +design for Find Command's validation of input keywords. + +**Aspect: How Find Command matches a keyword against the target in a given field:** + +**Alternative 1: (current choice)** At least one word in the target must match exactly with the keyword. +* Pros: The search result is more precise and concise which makes it easier for the user to navigate and the keyword + can be validated for each prefix which makes it easier for the user to use a variety of prefixes and keywords. +* Cons: Partial searches are not supported as the user needs to search by whole words. + +**Alternative 2:** At least a part of the target must match with the keyword. +* Pros: The user can make partial searches and search for parts of a word. +* Cons: The filtered list might be more cluttered and input validation might not be supported. + +Taking into account the ease of use and the benefits of input validation, Alternative 1 was chosen as the current +design to match keywords against targets. + +**Aspect: How Find Command handles multiple keywords for name and title prefixes:** + +**Alternative 1: (current choice)** At least one word in the target must match exactly with at least one keyword. +* Pros: The user can search for many keywords at once such that the user can filter the list based on multiple criteria. +* Cons: The user can not search by sentences or phrases. + +**Alternative 2:** At least one word in the target must match exactly with all keywords. +* Pros: The user can search for exact sentences or phrases. +* Cons: The user can not search based on many keywords and may not be able to remember long phrases to use this + effectively. + +Taking into account the ease of use when the user can search for long titles or names by remembering non-continuous +words as opposed to a long phrase and the relevance of the design decision being specific to the name and title +prefixes, Alternative 1 as chosen as the current design to handles multiple keywords for the name and title prefixes. + +**Aspect: How Find Command handles multiple arguments with the same prefix:** + +**Alternative 1: (current choice)** At least one word in the target is in the union of the set of keywords +from multiple arguments with the same prefix. +* Pros: The user can search for many keywords at once such that the user can filter the list based on multiple + criteria and more convenient to expand search criteria without having to alter prefix arguments already typed. +* Cons: It might be redundant with the multi keyword search for arguments with name and title prefixes. + +**Alternative 2:** At least one word in the target matches with the keyword from the last argument with the same prefix. +* Pros: It prevents redundancy with the search made when multiple keywords are present for an argument with the name + and title prefixes. +* Cons: It does not allow the user to easily expand his search criteria and might be harder to remember. + +Taking into account the benefits of improving flexibility of use by allowing the user to expand the search criteria +as he types without having to edit a part of the command already typed and the redundancy being restricted to only +certain prefixes, Alternative 1 was chosen as the current design to handle multiple arguments with the same prefix. + +**Aspect: How Find Command handles multiple arguments with different prefixes:** -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +**Alternative 1: (current choice)** Find all items that fulfil the search criteria for all prefixes in the input. +* Pros: The user can search based on criteria across different fields to obtain a more specific list of items. +* Cons: A mistake in specifying the keyword for one prefix can result in the desired item not appearing on the list. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +**Alternative 2:** Find all items that fulfil the search criteria for at least one prefixes in the input. +* Pros: A mistake in specifying the search criteria for one prefix may not result in the desired item being filtered + out. +* Cons: May be hard for the user to find specific items on the list that fulfill different criteria. -_{more aspects and alternatives to be added}_ +Taking into account the benefits of improving specificity of use by allowing the user to search based on multiple +criteria all of which must be fulfilled, Alternative 1 was chosen as the current design to handle multiple arguments +with different prefixes. -### \[Proposed\] Data archiving +### Mark/Unmark Issue Command Feature -_{Explain here how the data archiving feature will be implemented}_ +An important feature of DevEnable is the ability to mark issues as completed or incomplete. +The command word will be `issue`, followed by the flag `-m`, representing a Mark command, which marks the issue as completed, or `-u` representing an Unmark command, which marks the issue as incomplete. +Next, it is followed by a compulsory argument, representing the `issueId`, to initialise the issue. +#### Mark Issue Command + +When a user enters a valid Mark command in the interface, `AddressBookParser#parseCommand` will be called which processes the inputs, creates an instance of a command parser and calls the `IssueCommandParser#parse` method. +Within this method, the `-m` flag is detected, calling `IssueCommandParser#parseMarkIssueCommand`, which checks for input argument validity (only positive integers) with the `ParserUtil#parseIssueID` method. +A new `status` object, initialised with `isCompleted` equals `true` is created. The parsed issueId and new status object are passed into and returned in an instance of the `MarkIssueCommand`, `MarkIssueCommand#execute` is called, +which retrieves the issue with the parsed issueId from the `IssueList` in the system. The status of the issue is set to the new status object, list is updated and the UI displays the updated filtered issue list. + +Compulsory argument: VALID_ ISSUE_ID +Example use: `issue -m 1` + +#### Unmark Issue Command + +When a user enters a valid Unmark command in the interface, `AddressBookParser#parseCommand` will be called which processes the inputs, creates an instance of a command parser and calls the `IssueCommandParser#parse` method. +Within this method, the `-u` flag is detected, calling `IssueCommandParser#parseUnmarkIssueCommand`, which checks for input argument validity (only positive integers) with the `ParserUtil#parseIssueID` method. +A new `status` object, initialised with `isCompleted` equals `false` is created. The parsed issueId and new status object are passed into and returned in an instance of the `UnmarkIssueCommand`, `UnmarkIssueCommand#execute` is called, +which retrieves the issue with the parsed issueId from the `IssueList` in the system. The status of the issue is set to the new status object, list is updated and the UI displays the updated filtered issue list. + +Compulsory argument: VALID_ISSUE_ID +Example use: `issue -u 2` + +The following sequence diagram shows how the mark command operation works for marking an issue entity (unmark works in the same manner): +Example: `issue -m 1` + +![MarkSequenceDiagram](images/MarkSequenceDiagram.png) + +
+:information_source: **Note:** The lifeline for `MarkIssueCommand` +should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +#### Design Considerations + +**Aspect: Method to mark and unmark issues** + +**Alternative 1: (current choice)** Separate Mark command to mark status as completed and Unmark command to mark status as incomplete, regardless of current status of issue. +* Pros: Requires minimal user input and user can set desired status without reference to current status. Responsibilities of each command are separate and clear. +* Cons: More commands for the user to remember and more duplication of code. + +**Alternative 2:** A single Mark command which flips the status of the issue (completed issues become incomplete and vice versa). +* Pros: Requires minimal user input, only one command to remember and less duplication of code. +* Cons: Requires user to refer to the current state in order to set it as the desired state. Less separation in terms of responsibilities. + +**Alternative 3:** A single Mark command which takes in issue id as well as the completion status to mark the issue as (completed or incomplete) +* Pros: Only one command to remember, less duplication of code and user can set desired status without reference to current status. +* Cons: Requires user to remember the valid input state (`completed` and `incomplete`). Less separation in terms of responsibilities. + +Alternative 1 was chosen as the design method since it allowed users to set the desired status regardless of current status +with the shortest and easiest to type command possible. It also allowed for command purposes and responsibilities to be kept clear and separate. -------------------------------------------------------------------------------------------------------------------- @@ -255,73 +682,565 @@ _{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 +**Steve** is a web developer who: +* has a need to manage a significant projects and stakeholders related to said projects * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: manage projects and project contacts faster than a typical mouse/GUI driven app. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | ------------------- | ------------------------------------------------------------------------------------------ |---------------------------------------------------------------------------------------------| +| `* * *` | student developer | track multiple projects spread across different websites in one place. | | +| `* * *` | developer | see the projects automatically sorted in accordance with the deadline | manage and clear those with a higher urgency first | +| `* * *` | user | add projects to the application | | +| `* * *` | user | delete projects from the application | keep my data accurate if I make a mistake in entering data. | +| `* * *` | user | edit projects from the application | handle changes in my projects. | +| `* * *` | user | tag clients to each project | know which clients each project is under. | +| `* * *` | new user | view a guide | learn about the functionalities of the application. | +| `* * *` | user | add deadlines to the projects | prioritize accordingly. | +| `* * *` | user | add the contact numbers and email addresses of each client to the projects | contact them more efficiently. | +| `* * *` | user | link my projects to their repositories | easily navigate to them. | +| `* * *` | user | find projects by fields such as their name, id and repository | easily view specific projects | +| `* * *` | user | find clients by fields such as their name, id, mobile and email | easily view specific clients | +| `* * *` | user | find issues by fields such as their title, id, urgency, and status | easily view specific issues | +| `* * *` | user | edit client details | change and update client details when I have more information. | +| `* * *` | user | add issues to projects | keep track of what features/bugs need to be worked on. | +| `* * *` | user | delete issues from projects | remove issues added by mistake/no longer needed. | +| `* * *` | user | find certain issues based on fields | better keep track of, and retrieve the specific issue I want. | +| `* * *` | user | mark issues as completed | keep track of what issues are completed | +| `* * *` | user | unmark completed issues as not completed | keep track of what issues are not done | +| `* *` | developer | choose to ‘pin’ certain projects | quickly access them | +| `* *` | developer | see all the issues/room for improvements of the website that my clients have in one place, | know what features/bugs to work on for them | +| `* *` | new user | view dummy data | learn how to use the application. | +| `* *` | new user | tag ongoing bugs to a project | allocate my time to bug fixes in an efficient manner. | +| `* *` | developer | sort the projects | see which projects require more urgency when the number of projects becomes too long. | +| `* *` | developer | clear all data using a single command | | +| `* *` | user | split the project tiles into different categories | organize my workspace better. | +| `* *` | user | see a list of all clients I am currently working with | keep an overview of my entire client base. | +| `* *` | user | sort a list of clients based on name or other fields | see a list of my clients in an organised fashion. | +| `*` | user | configure the app to list projects/clients/issues on startup | use the app more efficiently. | ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is `DevEnable` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +**Use case: UC1 - List all projects** **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list projects +2. DevEnable shows a list of projects. Use case ends. **Extensions** -* 2a. The list is empty. +* 1a. The list is empty. + * 1a1. DevEnable displays a default message. - Use case ends. + Use case ends. -* 3a. The given index is invalid. +**Use case: UC2 - Add project** - * 3a1. AddressBook shows an error message. +**MSS** + +1. User requests to add a project. +2. DevEnable adds the project to the list. + + Use case ends. - Use case resumes at step 2. +**Extensions** + +* 1a. The project already exists within the list. + * 1a1. DevEnable displays an error message that the project already exists. + + Use case ends. + +**Use case: UC3 - Delete project** + +**MSS** + +1. User views the list of projects (UC1). +2. User requests to delete a specific project in the list. +3. DevEnable deletes the project from the list. + + Use case ends. + +**Extensions** + +* 2a. The project does not exist. + * 2a1. DevEnable displays an error message that the project does not exist. + + Use case ends. + +**Use case: UC4 - Edit project** + +**MSS** + +1. User views the list of projects (UC1). +2. User requests to edit a specific project in the list. +3. DevEnable edits the project in the list. + + Use case ends. + +**Extensions** + +* 2a. The project does not exist. + * 2a1. DevEnable displays an error message that the project does not exist. + + Use case ends. + +**Use case: UC5 - List all clients** + +**MSS** + +1. User requests to list clients +2. DevEnable shows a list of clients. + + Use case ends. + +**Extensions** + +* 1a. The list is empty. + * 1a1. DevEnable displays a default message. + + Use case ends. + +**Use case: UC6 - Add Client to DevEnable** + +**MSS** + +1. User requests to add a client. +2. DevEnable adds the client to the list, and attaches it to a project.. + + Use case ends. + +**Extensions** + +* 2a. The project that the client will be attached to does not exist. + * 2a1. DevEnable displays an error message that the project does not exist. + + Use case ends. + +**Use case: UC7 - Delete client** + +**MSS** + +1. User requests to delete a client. +2. DevEnable deletes the client from the project in the list. + + Use case ends. + +**Extensions** + +* 2a. The client does not exist. + * 2a1. DevEnable displays an error message that the client does not exist. + + Use case ends. + +**Use case: UC8 - Edit client** + +**MSS** + +1. User requests to edit a client of a specific project in the list. +2. DevEnable edits the client of the project in the list. + + Use case ends. + +**Extensions** + +* 2a. The client does not exist. + * 2a1. DevEnable displays an error message that the client does not exist. + + Use case ends. + +**Use case: UC9 - Edit details of project** + +**MSS** + +1. User views the list of projects (UC1). +2. User requests to edit some details of a specific project in the list. +3. DevEnable adds the deadline to the project in the list. + + Use case ends. + +**Extensions** + +* 2a. The project does not exist. + * 2a1. DevEnable displays an error message that the project does not exist. + + Use case ends. + +* 2b. The user input is not in the correct format. + * 2b1. DevEnable displays an error message with the required format. + + Use case ends. + +**Use case: UC10 - Add client** + +**MSS** + +1. User requests to add a client. +2. DevEnable adds the client to the client list. + + Use case ends. + +**Extensions** + +* 1a. The client already exists in the list. + * 1a1. DevEnable displays an error message that the client already exists. + + Use case ends. + +**Use case: UC11 - Delete client** + +**MSS** + +1. User views the list of clients. +2. User requests to delete a specific client in the list. +3. DevEnable deletes the client from the list. + + Use case ends. + +**Extensions** + +* 2a. DevEnable detects that the client does not exist. + * 2a1. DevEnable displays an error message that the client does not exist. + + Use case ends. + +**Use case: UC12 - Edit client** + +**MSS** + +1. User views the list of clients. +2. User requests to edit a specific client in the list. +3. DevEnable edits the client in the list. + + Use case ends. + +**Extensions** + +* 2a. DevEnable detects that the client does not exist. + * 2a1. DevEnable displays an error message that the client does not exist. + + Use case ends. + +**Use case: UC13 - Find projects** + +**MSS** + +1. User views views the list of projects (UC1). +2. User requests to filter the list based on specific keywords. +3. DevEnable finds matching projects in the list. + + Use case ends. + +**Extensions** + +* 2a. A project that matches the search does not exist. + * 2a1. DevEnable displays an error message that such a project does not exist. + + Use case ends. + +**Use case: UC14 - Find clients** + +**MSS** -*{More to be added}* +1. User views the list of clients. +2. User requests to filter the list based on specific keywords. +3. DevEnable finds matching clients in the list. + Use case ends. + +**Extensions** + +* 2a. A client that matches the search does not exist. + * 2a1. DevEnable displays an error message that such a client does not exist. + + Use case ends. + +**Use case: UC15 - Add issue to DevEnable** + +**MSS** + +1. User requests to add a issue. +2. DevEnable adds the issue to the list, and attaches it to a project.. + + Use case ends. + +**Extensions** + +* 2a. The project specified for the issue to be attached to does not exist. + * 2a1. DevEnable displays an error message that the project does not exist. + + Use case ends. + +**Use case: UC16 - Delete issue from project** + +**MSS** + +1. User requests to delete an issue. +2. DevEnable deletes the issue. + + Use case ends. + +**Extensions** + +* 2a. The issue does not exist + * 2a1. DevEnable displays an error message. + + Use case ends. + +**Use case: UC17 - Edit issue of project** + +**MSS** + +1. User requests to edit an issue. +2. DevEnable edits the issue as requested. + + Use case ends. + +**Extensions** + +* 2a. The issue does not exist. + * 2a1. DevEnable displays an error message that the issue does not exist. + + Use case ends. + +**Use case: UC18 - List all issues** + +**MSS** + +1. User requests to list issues +2. DevEnable shows a list of issues. + + Use case ends. + +**Extensions** + +* 1a. The list is empty. + * 1a1. DevEnable displays a default message. + + Use case ends. + +**Use case: UC19 - Mark issue as complete** + +**MSS** + +1. User requests to mark an issue as complete +2. DevEnable marks said issue as complete + +**Extensions** +- 1a. There are no issues/issue is not found. + - 1a1. DevEnable displays an error message + + Use Case ends. + +- 1b. The issue is already complete + - 1b1. DevEnable displays an error message + + Use Case ends. + +**Use case: UC20 - Mark issue as incomplete** + +**MSS** + +1. User requests to mark an issue as incomplete +2. DevEnable marks said issue as incomplete + +**Extensions** +- 1a. There are no issues/issue is not found. + - 1a1. DevEnable displays an error message + + Use Case ends. + +- 1b. The issue is incomplete + - 1b1. DevEnable displays an error message + + Use Case ends. + +**Use case: UC21 - Find issues** + +**MSS** + +1. User views the list of issues. +2. User requests to filter the list based on specific keywords. +3. DevEnable finds matching issues in the list. + + Use case ends. + +**Extensions** + +* 2a. DevEnable detects that such an issue does not exist. + * 2a1. DevEnable displays an error message that such an issue does not exist. + + Use case ends. + + +**Use Case: UC22 - View a sorted list of projects** + +**MSS** + +1. User requests to view a sorted list of projects with parameters. +2. DevEnable displays a sorted list of projects with said parameters. + + Use Case ends + +**Use Case: UC23 - View a sorted list of clients** + +**MSS** + +1. User requests to view a sorted list of clients with parameters. +2. DevEnable displays a sorted list of clients with said parameters. + + Use Case ends + +**Use Case: UC24 - View a sorted list of issues** + +**MSS** + +1. User requests to view a sorted list of issues with parameters. +2. DevEnable displays a sorted list of issues with said parameters. + + Use Case ends + +**Use Case: UC25 - Pin/Unpin a project** + +**MSS** + +1. User requests to pin a project +2. DevEnable pins/unpins said project in the list, depending on whether it was pinned initially. + + Use Case ends + +**Extensions** +- 1a. The project requested for by the user does not exist + - 1a1. DevEnable displays an error message that the project does not exist. + + Use Case ends. + + +**Use Case: UC26 - Pin/Unpin a issue** + +**MSS** + +1. User requests to pin a issue +2. DevEnable pins/unpins said issue in the list, depending on whether it was pinned initially. + + Use Case ends + +**Extensions** +- 1a. The issue requested for by the user does not exist + - 1a1. DevEnable displays an error message that the issue does not exist. + + Use Case ends. + +**Use Case: UC27 - Pin/Unpin a client** + +**MSS** + +1. User requests to pin a client +2. DevEnable pins/unpins said client in the list, depending on whether it was pinned initially. + + Use Case ends + +**Extensions** +- 1a. The client requested for by the user does not exist + - 1a1. DevEnable displays an error message that the client does not exist. + + Use Case resumes at Step 1. + +**Use Case: UC28 - Set default view of DevEnable to project list** + +**Guarantees:** On startup, DevEnable shows project list initially. + +**MSS** + +1. User requests for the project list to be the default view +2. DevEnable sets the default view to be the project list. + +**Use Case: UC29 - Set default view of DevEnable to client list** + +**Guarantees:** On startup, DevEnable shows client list initially. + +**MSS** + +1. User requests for the client list to be the default view +2. DevEnable sets the default view to be the client list. + +**Use Case: UC30 - Set default view of DevEnable to issue list** + +**Guarantees:** On startup, DevEnable shows issue list initially. + +**MSS** + +1. User requests for the issue list to be the default view +2. DevEnable sets the default view to be the issue list. + + +**Use case: UC31 - View list of commands** + +**MSS** + +1. User requests to list all commands. +2. DevEnable shows the list of commands. + + Use case ends. + +**Use Case: UC32 - Clear project book** + +**Guarantees:** Project book is empty after clear + +**MSS** + +1. User requests to clear project book +2. DevEnable clears project book + + Use case ends + +**Use Case: UC33 - Exit project book** + +**MSS** + +1. User requests to exit project book +2. DevEnable exits + + Use Case ends + ### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +1. The product should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. The product should be able to hold up to 200 projects 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}* +4. The product should work only for a single user. +5. The data should be stored locally and should be in a human editable text file. +6. The GUI should work well for standard screen resolutions 1920x1080 and higher and for screen scales 100% and 125%. +7. The GUI should be usable for resolutions 1280x720 and higher and for screen scales 150%. +8. The product file size should not exceed 100MB. +9. The document file size should not exceed 15MB. +10. The DG and UG should be PDF-friendly. +11. The product needs to be developed in a breadth-first incremental manner. +12. The product should not use a DBMS to store data. +13. The data should be saved every time a command alters the data. ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Mainstream OS**: Windows, Linux, Unix, OS-X. +* **Client**: A contact detail that is attached to a project. +* **Project**: A project that has many clients, which typically has deliverables with deadlines. +* **Issue**: A deliverable or a task to be done for a project. +* **Entity**: A Client, Project or Issue. -------------------------------------------------------------------------------------------------------------------- @@ -338,40 +1257,297 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the jar file and copy into an empty folder. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. Double-click the jar file.
+ Expected: Shows the GUI with a set of sample projects. The window size may not be optimum. -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 2. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. + 3. _{ more test cases …​ }_ + +### Listing an entity + +1. Listing an entity while any list of entities is currently being shown + + 1. Test case: `project -l`
+ Expected: All project entities are listed regardless of the initial display. Details of the pinned project shown in the status message. + + 2. Test case: `project -l 012345`
+ Expected: Similar to previous. Extraneous parameters are ignored. + + 3. Test case: `project l`
+ Expected: Displayed list does not change. Error details shown in the status message. Status bar remains the same. + + 4. Other incorrect list commands to try: `project list`, `project -list`, `...`. Same for entities `issue` and `client`.
+ Expected: Similar to previous. + +### Setting default view + +1. Setting the default view to any list of entities + + 1. Prerequisites: Current default view not set to clients (DevEnable sets the default view to project for first time users) + + 2. Test case: `client -v`
+ Expected: On reopening the application, the list of clients will be displayed. Details of the changed default view shown in the status message. + + 3. Test case: `client -v 012345`
+ Expected: Similar to previous. Extraneous parameters are ignored. + + 4. Test case: `client v`
+ Expected: List displayed by default does not change. Error details shown in the status message. Status bar remains the same. + + 5. Other incorrect default view commands to try: `client view`, `client -dv`, `...`. Same for entities `issue` and `project`.
+ Expected: Similar to previous. + +### Adding an entity + +1. Adding an entity while any list of entities is being shown + + 1. Prerequisites: At least one project in project list to test issue and client command. + + 2. Test case: `project -a n/Home Project`
+ Expected: Project with `name` Home Project is added to project list. View of project list is shown. + + 3. Test case: `issue -s t/Has bugs p/1`
+ Expected: Issue with `title` Has bugs is added to issue list. View of issue list is shown. -1. _{ more test cases …​ }_ + 4. Test case: `client -a n/John Doe p/1`
+ Expected: Client with `name` John Doe is added to client list. View of client list is shown. -### Deleting a person + 5. Other incorrect add commands to try: `project -a`, `project -a r/Project/Home`, `project -a n/Project d/x`, `project -a n/Project y/`, `...` (where x is improperly formatted, and y is an invalid prefix). Same for entities `issue` and `client`.
+ Expected: No adding occurs. Error details shown in the status message. Status bar remains the same. -1. Deleting a person while all persons are being shown +### Deleting an entity - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +1. Deleting an entity while all entities are being shown - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 1. Prerequisites: List all entities using the respective entity list command. Multiple entities in the list. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 2. Test case: `client -d 1`
+ Expected: Client with `clientId` 1 is deleted from the list. Details of the deleted client shown in the status message. View of client list is shown. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 3. Test case: `issue -d 0`
+ Expected: No issue is deleted. Error details shown in the status message. + + 4. Other incorrect delete commands to try: `project -d`, `project -d x`, `project -d word` (where x is larger than the list + size). Same for entities `issue` and `client`.
Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Editing an entity +1. Editing an entity while all entities are being shown + + 1. Prerequisites: List all entities using the respective entity list command. Multiple entities in the list. + + 2. Test case: `client -e c/1 n/Charles m/92345678 e/charles@gmail.com`
+ Expected: Client with `clientId` 1 is edited to have new `ame` Charles, new `mobile` 92345678 and new `email` + charles@gmail.com. New `name` of the edited client shown in the status message. View of client list is shown. + + 3. Test case: `issue -e i/2 t/Finish Work u/2`
+ Expected: Issue with `issueId` 2 is edited to have new `title` Finish Work and new `urgency` medium. New `title` + of the edited issue shown in the status message. View of issue list is shown. + + 4. Test case: `project -e p/1 d/2019-12-12`
+ Expected: Project with `projectId` 1 is edited to have new `deadline` 12 Dec 2019. The `name` of edited project + shown in the status message. View of project list is shown. + + 5. Test case: `client -e c/0 n/Barry m/12345678 e/harry@gmail.com`
+ Expected: No client is edited. Error details shown in the status message. + + 6. Other incorrect edit commands to try: `client -e`, `client -e c/1`, `client -e c/1 p/&&123456`, `client -e + n/Harry c/x`, `...` (where x is larger than the client list size). Same for entities `issue` and `project`.
+ Expected: Similar to previous. + +### Sorting an entity + +1. Sorting an entity while any list of entities is being shown + + 1. Prerequisites: Optional parameters of various entities have values. + + 2. Test case: `project -s d/1`
+ Expected: Projects sorted in reverse chronological order. View of project list is shown. + + 3. Test case: `issue -s u/1`
+ Expected: Issues sorted in descending levels of `urgency`. View of issue list is shown. + + 4. Test case: `client -s n/0`
+ Expected: Clients sorted in alphabetical order of `name`. View of client list is shown. + + 5. Other incorrect sort commands to try: `project -s`, `project -s d/x`, `project -s y/0`, `project -s i/0 i/1 d/0`, `...` (where x is not 0 or 1, andy is not a valid key). Same for entities `issue` and `client`.
+ Expected: No sorting occurs. Error details shown in the status message. Status bar remains the same. + +### Pinning an entity + +1. Pinning an entity while any list of entities is being shown + + 1. Prerequisites: List all projects using the `project -l` command. Multiple projects in the list. + + 2. Test case: `project -p 3`
+ Expected: Project with `projectId` 3 appears with a pin symbol at the top of the list. Details of the pinned project shown in the status message. + + 3. Test case: Repeat `project -p 2` twice.
+ Expected: On the first enter of the command, the project with `projectId` 2 appears as in 2. On the second enter of the command, the project with `projectId` 2 is no longer at the top of the list and does not have any pin symbol in its display. Details of the unpinned project shown in the status message. + + 4. Test case: `project -p 0`
+ Expected: No project is pinned. Error details shown in the status message. Status bar remains the same. + + 5. Other incorrect pin commands to try: `project -p`, `project -p x`, `...` (where x is a project ID not in the list). Same for entities `issue` and `client`
+ Expected: Similar to previous. + +### Finding an entity + +1. Find client(s) while all clients are being shown + + 1. Prerequisites: List all clients using the `client -l` command. Multiple clients in the list, at least one of + which has the `name` Harry. + + 2. Test case: `client -f n/Harry Ginny`
+ Expected: All clients whose `name` has the word Harry or Ginny are listed. Number of listed clients + shown in the status message. + + 3. Test case: `client -f n/Harry Ginny e/harry@gmail.com m/12345 65432`
+ Expected: All clients, if any, whose `name` has the word Harry or Ginny and whose `email` is harry@gmail.com + and whose `mobile` is 12345 or 65432 are listed. Number of listed clients shown in the status message. + + 4. Test case: `client -f n/Harry c/1 n/Ginny`
+ Expected: All clients, if any, whose `name` has the word Harry or Ginny and whose `clientId` is 1 are listed. + Number of listed clients shown in the status message. + + 5. Test case: `client -f n/&&invalidname`
+ Expected: No client is listed. Error details shown in the status message. + + 6. Test case: `client -f abc`
+ Expected: No client is listed. Error details shown in the status message. + + 7. Other incorrect find commands to try: `client -f`, `client -f c/x`, `...` (where x is larger than the list + size)
+ Expected: Similar to previous. + +2. Find project(s) while all projects are being shown + + 1. Prerequisites: List all projects using the `project -l` command. Multiple projects in the list, at least one of + which has the name DevEnable. + + 2. Test case: `project -f n/DevEnable AB3`
+ Expected: All projects whose `name` has the word DevEnable or AB3 are listed. Number of listed projects shown + in the status message. + + 3. Test case: `project -f n/DevEnable AB3 r/dev/tp l/Harry`
+ Expected: All projects, if any, whose `name` has the word DevEnable or AB3 and `repository` is dev/tp and whose + client has the `name` Harry are listed. Number of listed projects shown in the status message. + + 4. Test case: `client -f n/DevEnable c/2 p/1 n/AB3`
+ Expected: All projects, if any, whose `name` has the word DevEnable or AB3 and `projectId` is 1 and whose client + has the `clientId` 2 are listed. Number of listed projects shown in the status message. + + 5. Test case: `project -f r/invalid-repo`
+ Expected: No project is listed. Error details shown in the status message. + + 6. Test case: `project -f abc`
+ Expected: No project is listed. Error details shown in the status message. + + 7. Other incorrect find commands to try: `project -f`, `project -f p/x`, `...` (where x is larger than the list + size)
+ Expected: Similar to previous. + +3. Find issue(s) while all issues are being shown + + 1. Prerequisites: List all issues using the `issue -l` command. Multiple issues in the list, at least one of + which has the `title` Testing. + + 2. Test case: `issue -f t/Testing Documentation`
+ Expected: All issues whose `title` has the word Testing or Documentation are listed. Number of listed issues + shown in the status message. + + 3. Test case: `issue -f t/Testing s/Incomplete u/NONE n/DevEnable`
+ Expected: All issues, if any, whose `title` has the word Testing and `status` is incomplete and `urgency` is NONE + and whose project has the `name` DevEnable are listed. Number of listed projects shown in the status message. + + 4. Test case: `issue -f n/Testing i/1 p/2 t/Documentation`
+ Expected: All issues, if any, whose `title` has the word Testing or Documentation and `issueId` is 1 and whose + project has the `projectId` 2 are listed. Number of listed projects shown in the status message. + + 5. Test case: `issue -f s/thisIsAnInvalidStatus`
+ Expected: No issue is listed. Error details shown in the status message. + + 6. Test case: `issue -f abc`
+ Expected: No issue is listed. Error details shown in the status message. + + 7. Other incorrect find commands to try: `issue -f`, `issue -f i/x`, `...` (where x is larger than the list + size)
+ Expected: Similar to previous. + +### Marking/Unmarking an issue + +1. Marking an issue while all issues are being shown + + 1. Prerequisites: List all issues using the `issue -l` command. Multiple issues in the list. + + 2. Test case: `issue -m 1`
+ Expected: The status of the issue with `issueId` 1 is set as completed. Details of the marked issue shown in the status message. + + 3. Test case: `issue -m 0`
+ Expected: No issue is marked. Error details shown in the status message. + + 4. Other incorrect mark commands to try: `issue -m`, `issue -m x`, `issue -m word`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Unmarking an issue while all issues are being shown + + 1. Prerequisites: List all issues using the `issue -l` command. Multiple issues in the list. + + 2. Test case: `issue -u 1`
+ Expected: The status of the issue with `issueId` 1 is set as incomplete. Details of the unmarked issue shown in the status message. + + 3. Test case: `issue -u 0`
+ Expected: No issue is unmarked. Error details shown in the status message. + + 4. Other incorrect unmark commands to try: `issue -u`, `issue -u x`, `issue -u word`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + ### Saving data -1. Dealing with missing/corrupted data files +1. Editing the data file + + 1. Prerequisites: Obtain sample data file from running the application for the first time. + + 2. Test case: Change the `pin` attribute for any `project` or `issue` object from `false` to `true` or vice-versa.
+ Expected: Application starts with the corresponding `project` or `issue` pinned to the top of their respective lists. + + 3. Test case: Change the `mobile` attribute for **all** instances of the `client` object with name `Alex Yeoh`.
+ Expected: Application starts with the respective change in the `mobile` attribute. + +2. Dealing with missing data file + + 1. Test case: Delete the `addressbook.json` data file and open up the application.
+ Expected: Application starts up with sample data. Details of the missing data file is logged in `addressbook.log.0` + +3. Dealing with corrupted data files + + 1. Prerequisites: Same as that for editing the data file. + + 2. Test case: Remove the `pin` attribute from any JSON object and open up the application.
+ Expected: Application starts with empty data. Error details logged in `addressbook.log.0` + + 3. Test case: Change the `deadline` attribute to an invalid deadline string e.g. `2022-50-04`.
+ Expected: Similar to previous. + + 4. Test case: Change the `projectId` attribute of the first project object (with `name` attribute `Individual Project`) to `2` such that there is a duplicate project ID.
+ Expected: Similar to previous. + + 5. Test case: Change the `mobile` attribute of the first client object (with `name` attribute `Alex Yeoh`) to `91111111` such that it does not tally with the second copy of `Alex Yeoh`.
+ Expected: Similar to previous. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 6. Test case: Change the `name` attribute of the first project object (with `name` attribute `Individual Project`) to `Team Project` such that there is a duplicate project name.
+ Expected: Similar to previous. + + 7. Test case: Add an extra comma `,` after any other comma e.g. `"name" : "Individual Project",,` such that the data file is in the wrong format.
+ Expected: Similar to previous. -1. _{ more test cases …​ }_ + 8. Test case: Add an extraneous attribute e.g. `"remark" : "likes to eat"` after any other attribute in the file.
+ Expected: The extraneous attribute is ignored and the application starts up as per normal with the correct data. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..7902c4c3d82 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,41 +3,41 @@ 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. +DevEnable is a **desktop app for developers to manager their projects, 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, DevEnable can get your project management tasks done faster than traditional GUI apps. * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- +
## Quick start 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `devenable.jar`. -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 DevEnable. -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +4. Double-click the file to start the app. -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: - * **`list`** : Lists all contacts. + * **`project -l`** : Lists all projects. - * **`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. + * **`project -a`**`n/Orbital` : Adds a project named `Orbital` to the project list. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + * **`project -d`**`3` : Deletes the project with project id `3`. - * **`clear`** : Deletes all contacts. + * **`clear`** : Deletes all data from all lists. - * **`exit`** : Exits the app. - -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +
+ ## Features
@@ -45,148 +45,357 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. + e.g. in `project -a n/PROJECT_NAME`, `PROJECT_NAME` is a parameter which can be used as `project -a n/PROJECT_NAME`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g. `n/PROJECT_NAME [r/REPOSITORY]` can be used as `project -a n/ProjectY r/AgentX/ProjectY` or + `project -a n/ProjectY`. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Commands are in the following format: `COMMAND FLAG [ID/ARGUMENTS]`. Every command starts with a command keyword, + followed by the command flag, then the arguments for said command. Typically, flags start with a dash `-`, and + arguments start with an identifier `ID/ARGUMENT`. * Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. + e.g. if the command specifies `client -a p/PROJECT_ID n/CLIENT_NAME [m/CLIENT_MOBILE] [e/CLIENT_EMAIL]`, + `client -a p/PROJECT_ID n/CLIENT_NAME [e/CLIENT_EMAIL] [m/CLIENT_MOBILE]` are both acceptable. -* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. +* If a parameter is expected only once in the command, but you specified it multiple times, only the last occurrence + of the parameter will be taken. (Does not apply for Find and Sort Commands)
+ e.g. if you specify `m/12341234 m/56785678`, only `m/56785678` will be taken. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Extraneous parameters for commands that do not take in parameters (such as `help`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Trailing extraneous input for commands that take in parameters will be considered a part of the last valid + argument identifier unless the argument identifier takes in an index parameter . + e.g. if the command specifies `project -e n/New Project x/this is extra`, `New Project x/this is extra` will be + taken as the argument for `n/` but if the command specifies `project -e n/New Project p/2 x/this is extra`, then + only `2` will be taken as the argument for `p/`. +
+
### Viewing help : `help` -Shows a message explaning how to access the help page. - -![help message](images/helpMessage.png) +Displays a list of commands and functionalities. Format: `help` +### Clearing all entries : `clear` -### Adding a person: `add` +Clears all entries from the list. -Adds a person to the address book. +Format: `clear` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +### Exiting the project book: `exit` -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+Exits the project book. +Format: `exit` +
+ +### Field Details + +| PREFIX/ARGUMENT | Note | +|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| n/PROJECT_NAME or n/CLIENT_NAME or l/CLIENT_LABEL | must only contain alphanumeric characters (cannot be empty or start with a space) | +| p/PROJECT_ID or c/CLIENT_ID or i/ISSUE_ID | must exist and be a positive integer (1, 2, 3 ...​) | +| t/TITLE | cannot be empty or start with a space | +| d/DEADLINE | must be in the format yyyy-mm-dd | +| r/REPOSITORY | must be in the form USERNAME/REPO_NAME
NOTE: Clicking the Repository Link of a project will copy it to the clipboard | +| m/CLIENT_MOBILE | must only contain numbers and must be more than 3 digits long such that any number with a countrycode can be added without any prefix or connecting symbol | +| e/CLIENT_EMAIL | must be in the format LOCAL_NAME@DOMAIN_NAME.com (local name must be longer than 3 characters) | +| u/URGENCY | must be an integer from 0 to 3, 0 for NONE, 1 for LOW, 2 for MEDIUM and 3 for HIGH | +| s/STATUS | must be either `Incomplete` or `Completed` | + +> NOTE: Multiple issues can have the exact same fields (other than the issue id), as it is possible for a project to +> have multiple of the same issues. +
+ +### Add Command: `-a` + +Adds a project, client, or issue to the project book. A unique ID will be automatically generated. + + + + +
+ +Format: +* Project: `project -a n/PROJECT_NAME [r/REPOSITORY] [c/CLIENT_ID] [d/DEADLINE]` + * Adds the project to the list of projects. +* Client: `client -a n/CLIENT_NAME p/PROJECT_ID [m/CLIENT_MOBILE] [e/CLIENT_EMAIL]` + * Adds the client to the list of clients and to the project with the specified `PROJECT_ID`. +* Issue: `issue -a p/PROJECT_ID t/TITLE [d/DEADLINE] [u/URGENCY]` + * Adds the issue to the overall list of issues and to the list of issues of the project with the specified `PROJECT_ID`. + Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` - -### Listing all persons : `list` +* `project -a n/Coding101` Adds a project with `PROJECT_NAME` Coding101 to the list of projects. +* `client -a n/Bob p/2 m/12345678` Adds a client with `CLIENT_NAME` Bob and `CLIENT_MOBILE` 12345678 to the + list of clients and to the project with `PROJECT_ID` 2. +* `issue -a t/Design GUI u/0 d/2022-09-12 p/3` Adds an issue with `TITLE` Design GUI, `URGENCY` NONE(0) and `DEADLINE` + 2022-09-12 to the overall list of issues and to the list of issues of the project with `PROJECT_ID` 3. +
-Shows a list of all persons in the address book. +### Edit Command: `-e` -Format: `list` +Edits the specified existing project, client or issue. -### Editing a person : `edit` + -Edits an existing person in the address book. + +
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: +* Project: `project -e p/PROJECT_ID [n/PROJECT_NAME] [r/REPOSITORY] [c/CLIENT_ID] [d/DEADLINE]` + * Edits the project with the specified `PROJECT_ID`. The ID refers to the unique ID generated upon adding a project. +* Client: `client -e c/CLIENT_ID [n/CLIENT_NAME] [m/CLIENT_MOBILE] [e/CLIENT_EMAIL]` + * Edits the client with the specified `CLIENT_ID`. The ID refers to the unique ID generated upon adding a client. +* Issue: `issue -e i/ISSUE_ID [t/TITLE] [d/DEADLINE] [u/URGENCY]` + * Edits the issue with the specified `ISSUE_ID` The ID refers to the unique ID generated upon adding an issue. -* 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, …​ +Note: * 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. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `client -e c/1 n/Amy` Edits the client with `CLIENT_ID` 1 to have the new `CLIENT_NAME` Amy. +* `issue -e i/2 d/2021-12-20 u/1` Edits the issue with `ISSUE_ID` 2 to have the new `DEADLINE` 2021-12-20 and `URGENCY` + LOW(1). +* `project -e p/4 n/ThirdProject d/2022-02-02 r/Jackson/ThirdProject c/2` Edits the project with `PROJECT_ID` 4 to + have the new `PROJECT_NAME` ThirdProject, new `DEADLINE` 2022-02-02, new `REPOSITORY` Jackson/ThirdProject and new + client with `CLIENT_ID` 2. +
+ +### Delete Command: `-d` + +Removes the specified existing project, client or issue. -### Locating persons by name: `find` + -Finds persons whose names contain any of the given keywords. + +
-Format: `find KEYWORD [MORE_KEYWORDS]` +Format: +* Project: `project -d PROJECT_ID` + * Deletes the project with the specified `PROJECT_ID` +* Client: `client -d CLIENT_ID` + * Deletes the client with the specified `CLIENT_ID` +* Issue: `issue -d ISSUE_ID` + * Deletes the issue with the specified `ISSUE_ID + +Note: +* The ID **must exist and be a positive integer** 1, 2, 3, …​ + +Examples: +* `project -d 1` Deletes project with `PROJECT_ID` 1. +* `client -d 6` Deletes client with `CLIENT_ID` 6. +* `issue -d 3` Deletes issue with `ISSUE_ID` 3. +
+ +### List Command: `-l` + +Shows a list of all projects, clients or issues. + +Format: +* Project: `project -l` +* Client: `client -l` +* Issue: `issue -l` +
+ +### Find Command: `-f` + +Finds and lists all the projects, clients or issues matching the search criteria. + + + + +
+ +Format: +* Project: `project -f [n/PROJECT_NAME] [p/PROJECT_ID] [r/REPOSITORY] [l/CLIENT_LABEL] [c/CLIENT_ID]` + * Finds all the projects with the specified `PROJECT_NAME`, `PROJECT_ID`, `REPOSITORY`, `CLIENT_LABEL` and + `CLIENT_ID`. The `CLIENT_LABEL` is the name of the project's client, appearing as a label on the project card. + * Finds all the projects such that the fields under the project contain at least one word from the keywords provided + after each search criteria if the search criteria is project or client name. +* Client: `client -f [n/CLIENT_NAME] [c/CLIENT_ID] [e/CLIENT_EMAIL] [m/CLIENT_MOBILE]` + * Finds all the clients with the specified `CLIENT_NAME`, `CLIENT_ID`, `CLIENT_EMAIL`, and `CLIENT_MOBILE` + * Finds all the clients such that the fields under the client contain at least one word from the keywords provided + after each search criteria if the search criteria is client name. +* Issue: `issue -f [t/TITLE] [s/STATUS] [u/URGENCY] [n/PROJECT_NAME] [p/PROJECT_ID] [i/ISSUE_ID]` + * Finds all the issues with the specified `TITLE`, `STATUS`, `URGENCY`, `PROJECT_NAME`, `PROJECT_ID` and `ISSUE_ID`. + * Finds all the issues such that the fields under the issue contain at least one word from the keywords provided + after each search criteria if the search criteria is title or project name. + +Note: +* At least one of the optional fields must be provided. +* The keywords provided must be valid arguments for their respective search criteria. +* Repeated prefixes (e.g. t/Do t/Find) in a command will return results containing either of the two arguments -* 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: +* `project -f n/DevEnable` Finds and lists all the projects whose `PROJECT_NAME` contains the word DevEnable. +* `client -f n/Amy Bob` Finds and lists all the clients with the `CLIENT_NAME` contains the word Amy or Bob. +* `issue -f p/3 p/5` Finds and lists the issue tied to project with `PROJECT_ID` 3 or 5. +* `client -f n/Amy e/amy@gmail.com m/12345678` Finds and lists all the clients whose `CLIENT_NAME` contains the word + Amy and with `CLIENT_EMAIL` amy@gmail.com and with `CLIENT_MOBILE` 12345678. +* `project -f n/AB4 AB3 r/tp/F13` Finds and lists all the projects whose `PROJECT_NAME` contains the word AB4 or AB3 + and with the `REPOSITORY` tp/F13. +* `issue -f t/enhancement p/DevEnable AB3 u/HIGH u/LOW` Finds and lists all the issues with `TITLE` enhancement and + `URGENCY` HIGH or LOW and tied to project with `PROJECT_NAME` containing DevEnable or AB3. +* `issue -f s/Incomplete` Finds and lists all the issues with the `STATUS` Incomplete. +
+ +### Pin Command: `-p` + +Pins a project, client, or issue to the top of the project list, client list or issue list. + + + + +
+ +Format: +* Project: `project -p PROJECT_ID` + * Pins the project with the specified `PROJECT_ID` +* Client: `client -p CLIENT_ID` + * Pins the client with the specified `CLIENT_ID` +* Issue: `issue -p ISSUE_ID` + * Pins the issue with the specified `ISSUE_ID` + +Note: +* Executing the command on an already pinned project will unpin the project. +* The ID **must be a positive integer** 1, 2, 3, …​ Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `project -p 1` Pins the project with `PROJECT_ID` 1. +* `client -p 4` Pins the client with `CLIENT_ID` 4. +* `issue -p 2` Pins the issue with `ISSUE_ID` 2. +
+ +### Sort Command: `-s` + +Sorts all projects, clients or issues based on a specified key. -### Deleting a person : `delete` + -Deletes the specified person from the address book. + -Format: `delete INDEX` +> IMPORTANT: Unlike other commands, input 0/1 as the argument for each prefix (e.g.p/0) -* 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, …​ +Format: +* Project: `project -s [p/PROJECT_ID] [d/DEADLINE] [i/ISSUE_COUNT] [n/PROJECT_NAME]` + * For `PROJECT_ID`, 0 for ascending and 1 for descending. + * For `DEADLINE`, 0 for chronological and 1 for reverse chronological. + * For `ISSUE_COUNT`, 0 for incomplete issues and 1 for completed issues. + * For `PROJECT_NAME`, 0 for alphabetical and 1 for reverse alphabetical. +* Client: `client -s [c/CLIENT_ID] [n/CLIENT_NAME]` + * For `CLIENT_ID`, 0 for ascending and 1 for descending. + * For `CLIENT_NAME`, 0 for alphabetical and 1 for reverse alphabetical +* Issue: `issue -s [i/ISSUE_ID] [d/DEADLINE] [u/URGENCY]` +* For `ISSUE_ID`, 0 for ascending and 1 for descending. + * For `DEADLINE`, 0 for chronological and 1 for reverse chronological. + * For `URGENCY`, 0 for ascending and 1 for descending. + +Note: +* Exactly one optional value (the prefix/the sorting key) is to be provided +(Command will not accept multiple optional values or repeated prefixes). 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. +* `project -s p/1` Sorts list of projects based on `PROJECT_ID` from highest to lowest. +* `issue -s d/0` Sorts list of issues based on `DEADLINE` from earliest to latest. +* `client -s n/1` Sort list of clients based on `CLIENT_NAME` in reverse alphabetical order. +* `project -s i/0` Sorts list of projects based on `ISSUE_COUNT` from the highest incomplete issue count to the lowest. +* `issue -s u/1` Sorts list of issues based on `URGENCY` from highest to lowest. +
-### Clearing all entries : `clear` +### Default View Command `-v` -Clears all entries from the address book. +Sets list of projects, clients or issues to be the default view when application is re-opened. -Format: `clear` +Format: +* Project: `project -v` + * Sets list of projects as default view +* Client: `client -v` + * Sets list of clients as default view +* Issue: `issue -v` + * Sets list of issues as default view +
+ +### Mark/Unmark Issue Command: `-m` -### Exiting the program : `exit` +Marks specified existing issue as completed or incomplete -Exits the program. + -Format: `exit` + +
+ +Format: +* Mark: `issue -m ISSUE_ID` + * Marks the issue with the specified `ISSUE_ID`, changing its `STATUS` to completed. +* Unmark:`issue -u ISSUE_ID` + * Unmarks the issue with the specified `ISSUE_ID`, changing its `STATUS` to incomplete. + +Note: +* If issue is already completed, it remains completed when marked (the same goes for incomplete issue that is unmarked). +* The ID **must exist and be a positive integer** 1, 2, 3, …​ + +Examples: +* `issue -m 1` Changes the `STATUS` of the issue with `ISSUE_ID` 1 to completed. +* `issue -m 4` Changes the `STATUS` of the issue with `ISSUE_ID` 4 to incomplete. +
### 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. +DevEnable data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +DevEnable data is saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. +If your changes to the data file makes its format invalid, DevEnable will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` - -_Details coming soon ..._ - -------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the data file it creates with the file that contains the data of your previous DevEnable home folder. -------------------------------------------------------------------------------------------------------------------- +
## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Action | Format | +|--------------------------------|:-------------------------------------------------------------------------------------------| +| **Add Project** | `project -a n/PROJECT_NAME [r/REPOSITORY] [c/CLIENT_ID] [d/DEADLINE] ` | +| **Edit Project** | `project -e p/PROJECT_ID [n/PROJECT_NAME] [r/REPOSITORY] [c/CLIENT_ID] [d/DEADLINE]` | +| **Delete Project** | `project -d PROJECT_ID` | +| **List Projects** | `project -l` | +| **Find Projects** | `project -f [n/PROJECT_NAME] [r/REPOSITORY] [p/PROJECT_ID] [c/CLIENT_ID] [l/CLIENT_LABEL]` | +| **Pin Projects** | `project -p PROJECT_ID` | +| **Sort Projects** | `project -s [p/PROJECT_ID] [d/DEADLINE] [i/ISSUE_COUNT] [n/PROJECT_NAME]` | +| **Set Default View (Project)** | `project -v` | +| **Add Client** | `client -a n/CLIENT_NAME p/PROJECT_ID [m/CLIENT_MOBILE] [e/CLIENT_EMAIL]` | +| **Edit Client** | `client -e c/CLIENT_ID [n/CLIENT_NAME] [m/CLIENT_MOBILE] [e/CLIENT_EMAIL]` | +| **Delete Client** | `client -d CLIENT_ID` | +| **List Clients** | `client -l` | +| **Find Clients** | `client -f [n/CLIENT_NAME] [m/CLIENT_MOBILE] [e/CLIENT_EMAIL] [c/CLIENT_ID]` | +| **Pin Clients** | `client -p CLIENT_ID` | +| **Sort Clients** | `client -s [c/CLIENT_ID] [n/CLIENT_NAME]` | +| **Set Default View (Client)** | `client -v` | +| **Add Issue** | `issue -a p/PROJECT_ID t/TITLE [d/DEADLINE] [u/URGENCY]` | +| **Edit Issue** | `issue -e i/ISSUE_ID [t/TITLE] [d/DEADLINE] [u/URGENCY]` | +| **Delete Issue** | `issue -d ISSUE_ID` | +| **List Issues** | `issue -l` | +| **Find Issues** | `issue -f [t/TITLE] [n/PROJECT_NAME] [p/PROJECT_ID] [u/URGENCY] [s/STATUS] [i/ISSUE_ID]` | +| **Pin Issues** | `issue -p ISSUE_ID` | +| **Sort Issues** | `issue -s [i/ISSUE_ID] [d/DEADLINE] [u/URGENCY]` | +| **Mark Issue** | `issue -m ISSUE_ID` | +| **Unmark Issue** | `issue -u ISSUE_ID` | +| **Set Default View (Issue)** | `issue -v` | +| **Clear** | `clear` | +| **Help** | `help` | +| **Exit** | `exit` | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..be6da20f623 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "DevEnable" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2223S1-CS2103-F13-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..a76fa8386b4 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: "DevEnable"; font-size: 32px; } } diff --git a/docs/diagrams/AddSequenceDiagram.puml b/docs/diagrams/AddSequenceDiagram.puml new file mode 100644 index 00000000000..e5288fd8fd4 --- /dev/null +++ b/docs/diagrams/AddSequenceDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml + +box Ui MODEL_COLOR_T1 +participant ":MainWindow" as MainWindow MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ProjectCommandParser" as ProjectCommandParser LOGIC_COLOR +participant ":AddProjectCommand" as AddProjectCommand LOGIC_COLOR +end box + +[-> MainWindow : executeCommand\n(project -a n/Team Project) +activate MainWindow + +MainWindow -> LogicManager : execute\n(project -a n/Team Project) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand\n(project -a n/Team Project) +activate AddressBookParser + +create ProjectCommandParser +AddressBookParser -> ProjectCommandParser +activate ProjectCommandParser +ProjectCommandParser --> AddressBookParser +deactivate ProjectCommandParser +AddressBookParser -> ProjectCommandParser : parse\n(-a, n/Team Project) +activate ProjectCommandParser + +create AddProjectCommand +ProjectCommandParser -> AddProjectCommand +activate AddProjectCommand + +AddProjectCommand --> ProjectCommandParser +deactivate AddProjectCommand + +ProjectCommandParser --> AddressBookParser +deactivate ProjectCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> AddProjectCommand : execute\n(model, ui) +activate AddProjectCommand + +AddProjectCommand --> LogicManager : result +deactivate AddProjectCommand + +AddProjectCommand -[hidden]-> LogicManager : result +destroy AddProjectCommand + +MainWindow <-- LogicManager : result +deactivate LogicManager +MainWindow --> MainWindow : swapProjectListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..5d9406d70f2 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,13 +7,13 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "project -d 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("project -d 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteProject(p) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..57ffa8ec097 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -15,7 +15,7 @@ UniquePersonList -right-> Person Person -up-> "*" Tag Person *--> Name -Person *--> Phone +Person *--> Mobile Person *--> Email Person *--> Address @enduml diff --git a/docs/diagrams/DefaultViewActivityDiagram.puml b/docs/diagrams/DefaultViewActivityDiagram.puml new file mode 100644 index 00000000000..86969b128ae --- /dev/null +++ b/docs/diagrams/DefaultViewActivityDiagram.puml @@ -0,0 +1,12 @@ +@startuml +start +:User executes default view command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +:Set default view to user input; +:User restarts application; +:List of previously set entity is shown by default; +stop +@enduml diff --git a/docs/diagrams/DefaultViewSequenceDiagram.puml b/docs/diagrams/DefaultViewSequenceDiagram.puml new file mode 100644 index 00000000000..0b9901d3eb2 --- /dev/null +++ b/docs/diagrams/DefaultViewSequenceDiagram.puml @@ -0,0 +1,67 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ClientCommandParser" as ClientCommandParser LOGIC_COLOR +participant ":SetClientDefaultViewCommand" as SetClientDefaultViewCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +-> LogicManager : execute("client -v") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("client -v") +activate AddressBookParser + +create ClientCommandParser +AddressBookParser -> ClientCommandParser +activate ClientCommandParser +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser +AddressBookParser -> ClientCommandParser : parse("-v") +activate ClientCommandParser + +create SetClientDefaultViewCommand +ClientCommandParser -> SetClientDefaultViewCommand +activate SetClientDefaultViewCommand + +SetClientDefaultViewCommand --> ClientCommandParser +deactivate SetClientDefaultViewCommand + +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> SetClientDefaultViewCommand : execute() +activate SetClientDefaultViewCommand + +SetClientDefaultViewCommand -> Model : setDefaultView(CLIENT) +activate Model + +Model --> SetClientDefaultViewCommand +deactivate Model + +create CommandResult +SetClientDefaultViewCommand -> CommandResult +activate CommandResult + +CommandResult --> SetClientDefaultViewCommand +deactivate CommandResult + +SetClientDefaultViewCommand --> LogicManager : result +deactivate SetClientDefaultViewCommand + +SetClientDefaultViewCommand -[hidden]-> LogicManager : result +destroy SetClientDefaultViewCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditSequenceDiagram.puml b/docs/diagrams/EditSequenceDiagram.puml new file mode 100644 index 00000000000..4567f31a168 --- /dev/null +++ b/docs/diagrams/EditSequenceDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml + +box Ui MODEL_COLOR_T1 +participant ":MainWindow" as MainWindow MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":IssueCommandParser" as IssueCommandParser LOGIC_COLOR +participant ":EditIssueCommand" as EditIssueCommand LOGIC_COLOR +end box + +[-> MainWindow : executeCommand\n(issue -e i/1 t/To edit \n issue command d/2022-04-09 u/1) +activate MainWindow + +MainWindow -> LogicManager : execute\n(issue -e i/1 t/To edit \n issue command d/2022-04-09 u/1) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand\n(issue -e i/1 t/To edit \n issue command d/2022-04-09 u/1) +activate AddressBookParser + +create IssueCommandParser +AddressBookParser -> IssueCommandParser +activate IssueCommandParser +IssueCommandParser --> AddressBookParser +deactivate IssueCommandParser +AddressBookParser -> IssueCommandParser : parse\n(issue -e i/1 t/To edit \n issue command d/2022-04-09 u/1) +activate IssueCommandParser + +create EditIssueCommand +IssueCommandParser -> EditIssueCommand +activate EditIssueCommand + +EditIssueCommand --> IssueCommandParser +deactivate EditIssueCommand + +IssueCommandParser --> AddressBookParser +deactivate IssueCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> EditIssueCommand : execute\n(model, ui) +activate EditIssueCommand + +EditIssueCommand --> LogicManager : result +deactivate EditIssueCommand + +EditIssueCommand -[hidden]-> LogicManager : result +destroy EditIssueCommand + +MainWindow <-- LogicManager : result +deactivate LogicManager +MainWindow --> MainWindow : swapIssueListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/ListActivityDiagram.puml b/docs/diagrams/ListActivityDiagram.puml new file mode 100644 index 00000000000..8816b74f2fd --- /dev/null +++ b/docs/diagrams/ListActivityDiagram.puml @@ -0,0 +1,11 @@ +@startuml +start +:User executes list command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +:Clear current display nodes; +:Display list of requested entity type; +stop +@enduml diff --git a/docs/diagrams/ListSequenceDiagram.puml b/docs/diagrams/ListSequenceDiagram.puml new file mode 100644 index 00000000000..aa60fd7f669 --- /dev/null +++ b/docs/diagrams/ListSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Ui MODEL_COLOR_T1 +participant ":MainWindow" as MainWindow MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ClientCommandParser" as ClientCommandParser LOGIC_COLOR +participant ":ListClientCommand" as ListClientCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +[-> MainWindow : executeCommand("client -l") +activate MainWindow + +MainWindow -> LogicManager : execute("client -l") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("client -l") +activate AddressBookParser + +create ClientCommandParser +AddressBookParser -> ClientCommandParser +activate ClientCommandParser +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser +AddressBookParser -> ClientCommandParser : parse("-l") +activate ClientCommandParser + +create ListClientCommand +ClientCommandParser -> ListClientCommand +activate ListClientCommand + +ListClientCommand --> ClientCommandParser +deactivate ListClientCommand + +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> ListClientCommand : execute() +activate ListClientCommand + +create CommandResult +ListClientCommand -> CommandResult +activate CommandResult + +CommandResult --> ListClientCommand +deactivate CommandResult + +ListClientCommand --> LogicManager : result +deactivate ListClientCommand + +ListClientCommand -[hidden]-> LogicManager : result +destroy ListClientCommand + +MainWindow <-- LogicManager : result +deactivate LogicManager +MainWindow --> MainWindow : swapClientListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index d4193173e18..c535f828049 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -8,6 +8,7 @@ package Logic { Class AddressBookParser Class XYZCommand +Class XYZCommandParser Class CommandResult Class "{abstract}\nCommand" as Command @@ -29,6 +30,8 @@ HiddenOutside ..> Logic LogicManager .right.|> Logic LogicManager -right->"1" AddressBookParser AddressBookParser ..> XYZCommand : creates > +AddressBookParser ..> XYZCommandParser : creates > +XYZCommandParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > @@ -38,7 +41,8 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddClientCommand, \nFindIssueCommand, etc +'note top of XYZCommandParser: XYZCommandParser = \nClientCommandParser, IssueCommandParser, etc. Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/MarkSequenceDiagram.puml b/docs/diagrams/MarkSequenceDiagram.puml new file mode 100644 index 00000000000..10a9c8cdadb --- /dev/null +++ b/docs/diagrams/MarkSequenceDiagram.puml @@ -0,0 +1,90 @@ +@startuml +!include style.puml + +box Ui MODEL_COLOR_T1 +participant ":MainWindow" as MainWindow MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":IssueCommandParser" as IssueCommandParser LOGIC_COLOR +participant ":MarkIssueCommand" as MarkIssueCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":model" as model MODEL_COLOR +participant "toMarkIssue:Issue" as Issue MODEL_COLOR +end box + +[-> MainWindow : executeCommand("issue -m 1") +activate MainWindow + +MainWindow -> LogicManager : execute("issue -m 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("issue -m 1") +activate AddressBookParser + +create IssueCommandParser +AddressBookParser -> IssueCommandParser +activate IssueCommandParser +IssueCommandParser --> AddressBookParser +deactivate IssueCommandParser + +AddressBookParser -> IssueCommandParser : parse("-m", "1") +activate IssueCommandParser + +IssueCommandParser -> IssueCommandParser : parseMarkIssueCommand("1") +activate IssueCommandParser + +create MarkIssueCommand +IssueCommandParser -> MarkIssueCommand : MarkIssueCommand(newStatus, newIssueId) +activate MarkIssueCommand +MarkIssueCommand --> IssueCommandParser +deactivate MarkIssueCommand + +IssueCommandParser --> IssueCommandParser +deactivate IssueCommandParser + +IssueCommandParser --> AddressBookParser +deactivate IssueCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> MarkIssueCommand : execute(model, ui) +activate MarkIssueCommand + +MarkIssueCommand -> model : getIssueById(1) +activate model +model --> MarkIssueCommand : Issue +deactivate model + +MarkIssueCommand -> Issue : setStatus(newStatus) +activate Issue +Issue --> MarkIssueCommand +deactivate Issue + +create CommandResult +MarkIssueCommand -> CommandResult +activate CommandResult +CommandResult --> MarkIssueCommand +deactivate CommandResult + +MarkIssueCommand --> LogicManager : result +deactivate MarkIssueCommand + +MarkIssueCommand -[hidden]-> LogicManager : result +destroy MarkIssueCommand + +LogicManager --> MainWindow : result +deactivate LogicManager + +MainWindow --> MainWindow : swapIssueListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..0acbb2bd65f 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -12,13 +12,22 @@ Class AddressBook Class ModelManager Class UserPrefs -Class UniquePersonList -Class Person -Class Address -Class Email +Class UniqueEntityList > +Class Client +Class ClientEmail Class Name -Class Phone -Class Tag +Class ClientMobile +Class ClientId +Class ClientProjectList +Class Project +Class ProjectId +Class Repository +Class Deadline +Class Issue +Class IssueId +Class Status +Class Title +Class Urgency } @@ -34,17 +43,29 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +AddressBook *--> "1" UniqueEntityList +UniqueEntityList --> "~* all" Client +UniqueEntityList --> "~* all" Project +UniqueEntityList --> "~* all" Issue -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Client *--> Name +Client *--> ClientMobile +Client *--> ClientEmail +Client *--> ClientId +Client *--> ClientProjectList -ModelManager -->"~* filtered" Person +Project *--> Name +Project *--> ProjectId +Project *--> Repository +Project *--> Deadline + +Issue *--> IssueId +Issue *--> Status +Issue *--> Title +Issue *--> Deadline +Issue *--> Urgency + +ModelManager -->"~* filtered" Client +ModelManager -->"~* filtered" Project +ModelManager -->"~* filtered" Issue @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..543b6f080e1 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -11,7 +11,7 @@ package "Parser classes"{ Class "<>\nParser" as Parser Class AddressBookParser Class XYZCommandParser -Class CliSyntax +Class XYZCliSyntax Class ParserUtil Class ArgumentMultimap Class ArgumentTokenizer @@ -29,10 +29,11 @@ XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer ArgumentTokenizer .left.> ArgumentMultimap -XYZCommandParser ..> CliSyntax -CliSyntax ..> Prefix +XYZCommandParser ..> XYZCliSyntax +XYZCliSyntax ..> Prefix XYZCommandParser ..> ParserUtil ParserUtil .down.> Prefix ArgumentTokenizer .down.> Prefix XYZCommand -up-|> Command +note bottom of XYZCliSyntax: ClientCliSyntax, ProjectCliSyntax, \nIssueCliSyntax @enduml diff --git a/docs/diagrams/ParserPartialInitialisationSequenceDiagram.puml b/docs/diagrams/ParserPartialInitialisationSequenceDiagram.puml new file mode 100644 index 00000000000..530e7275807 --- /dev/null +++ b/docs/diagrams/ParserPartialInitialisationSequenceDiagram.puml @@ -0,0 +1,56 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ProjectCommandParser" as ProjectCommandParser LOGIC_COLOR +participant ":AddProjectCommand" as AddProjectCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":ProjectWithoutModel" as ProjectWithoutModel MODEL_COLOR +participant ":Project" as Project MODEL_COLOR +end box + +-> ProjectCommandParser +activate ProjectCommandParser + +create ProjectWithoutModel +ProjectCommandParser -> ProjectWithoutModel : new ProjectWithoutModel(name, ...) +activate ProjectWithoutModel + +ProjectWithoutModel --> ProjectCommandParser +deactivate ProjectWithoutModel + +create AddProjectCommand +ProjectCommandParser -> AddProjectCommand : new AddProjectCommand(projectWithoutModel) +activate AddProjectCommand + +AddProjectCommand --> ProjectCommandParser +deactivate AddProjectCommand + +activate LogicManager +LogicManager -> AddProjectCommand : execute(model) +activate AddProjectCommand + +AddProjectCommand -> ProjectWithoutModel : apply(model) +activate ProjectWithoutModel + +create Project +ProjectWithoutModel -> Project : new Project(...) +activate Project + +Project --> ProjectWithoutModel +deactivate Project + +ProjectWithoutModel --> AddProjectCommand : new Project object +deactivate ProjectWithoutModel + +AddProjectCommand --> LogicManager : CommandResult object +deactivate AddProjectCommand + + + + + +@enduml diff --git a/docs/diagrams/PinActivityDiagram.puml b/docs/diagrams/PinActivityDiagram.puml new file mode 100644 index 00000000000..843241dd947 --- /dev/null +++ b/docs/diagrams/PinActivityDiagram.puml @@ -0,0 +1,15 @@ +@startuml +start +:User executes pin command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([Entity is not pinned]) + :Set entity status to pinned; +else ([else]) + :Set entity status to unpinned; +endif +:Sort entity list; +stop +@enduml diff --git a/docs/diagrams/PinSequenceDiagram.puml b/docs/diagrams/PinSequenceDiagram.puml new file mode 100644 index 00000000000..cce67be778f --- /dev/null +++ b/docs/diagrams/PinSequenceDiagram.puml @@ -0,0 +1,80 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ClientCommandParser" as ClientCommandParser LOGIC_COLOR +participant ":PinClientCommand" as PinClientCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Client" as Client MODEL_COLOR +end box + +-> LogicManager : execute("client -p 3") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("client -p 3") +activate AddressBookParser + +create ClientCommandParser +AddressBookParser -> ClientCommandParser +activate ClientCommandParser +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser +AddressBookParser -> ClientCommandParser : parse("-p 3") +activate ClientCommandParser + +create PinClientCommand +ClientCommandParser -> PinClientCommand +activate PinClientCommand + +PinClientCommand --> ClientCommandParser +deactivate PinClientCommand + +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> PinClientCommand : execute() +activate PinClientCommand + +PinClientCommand -> Client : togglePin() +activate Client + +Client --> PinClientCommand +deactivate Client + +PinClientCommand -> Model : sortClientsByCurrentCategory() +activate Model + +Model --> PinClientCommand +deactivate Model + +PinClientCommand -> Model : sortClientsByPin() +activate Model + +Model --> PinClientCommand +deactivate Model + +create CommandResult +PinClientCommand -> CommandResult +activate CommandResult + +CommandResult --> PinClientCommand +deactivate CommandResult + +PinClientCommand --> LogicManager : result +deactivate PinClientCommand + +PinClientCommand -[hidden]-> LogicManager : result +destroy PinClientCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 760305e0e58..314170b132d 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -18,8 +18,10 @@ package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag +Class JsonAdaptedClient +Class JsonAdaptedProject +Class JsonAdaptedIssue +Class StorageUtil } } @@ -37,7 +39,14 @@ Storage -right-|> AddressBookStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSerializableAddressBook --> "*" JsonAdaptedProject +JsonSerializableAddressBook --> "*" JsonAdaptedIssue + +StorageUtil .right..> JsonSerializableAddressBook +StorageUtil ..> JsonAdaptedClient +StorageUtil ..> JsonAdaptedIssue +StorageUtil ..> JsonAdaptedProject +JsonAdaptedProject "*" -- "1 " JsonAdaptedClient +JsonAdaptedProject "1" -- "*" JsonAdaptedIssue @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..e4e2a6f879c 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,8 +11,12 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class ProjectListPanel +Class ProjectCard +Class IssueListPanel +Class IssueCard +Class ClientListPanel +Class ClientCard Class StatusBarFooter Class CommandBox } @@ -32,26 +36,35 @@ UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" ProjectListPanel +MainWindow *-down-> "1" IssueListPanel +MainWindow *-down-> "1" ClientListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +ProjectListPanel -down-> "*" ProjectCard +IssueListPanel -down-> "*" IssueCard +ClientListPanel -down-> "*" ClientCard MainWindow -left-|> UiPart - ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart +ProjectListPanel --|> UiPart +ProjectCard --|> UiPart +IssueListPanel --|> UiPart +IssueCard --|> UiPart +ClientListPanel --|> UiPart +ClientCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart -PersonCard ..> Model +ProjectCard ..> Model +IssueCard ..> Model +ClientCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow +ProjectListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/plantuml/UpAndDown.puml b/docs/diagrams/plantuml/UpAndDown.puml index e7a0313ad01..63f4c1065d6 100644 --- a/docs/diagrams/plantuml/UpAndDown.puml +++ b/docs/diagrams/plantuml/UpAndDown.puml @@ -19,7 +19,7 @@ A --> Z B -down-> Z 'shorthand for down C -d-> Z -'arrow lengths take priority +'arrow lengths take urgency D -down> Z A -up-> 1 diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..425cb726ab5 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -41,7 +41,7 @@ skinparam Class { FontColor #FFFFFF BorderThickness 1 BorderColor #FFFFFF - StereotypeFontColor #FFFFFF + StereotypeFontColor #000000 FontName Arial } diff --git a/docs/images/AddIssueCommand1.png b/docs/images/AddIssueCommand1.png new file mode 100644 index 00000000000..0280a8dd97b Binary files /dev/null and b/docs/images/AddIssueCommand1.png differ diff --git a/docs/images/AddIssueCommand2.png b/docs/images/AddIssueCommand2.png new file mode 100644 index 00000000000..efcf8857909 Binary files /dev/null and b/docs/images/AddIssueCommand2.png differ diff --git a/docs/images/AddSequenceDiagram.png b/docs/images/AddSequenceDiagram.png new file mode 100644 index 00000000000..4a4cedeb1aa Binary files /dev/null and b/docs/images/AddSequenceDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..53b6b172fef 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/DefaultViewActivityDiagram.png b/docs/images/DefaultViewActivityDiagram.png new file mode 100644 index 00000000000..9438dea8368 Binary files /dev/null and b/docs/images/DefaultViewActivityDiagram.png differ diff --git a/docs/images/DefaultViewSequenceDiagram.png b/docs/images/DefaultViewSequenceDiagram.png new file mode 100644 index 00000000000..134c026796b Binary files /dev/null and b/docs/images/DefaultViewSequenceDiagram.png differ diff --git a/docs/images/DeleteClientCommand1.png b/docs/images/DeleteClientCommand1.png new file mode 100644 index 00000000000..df718b4bfa0 Binary files /dev/null and b/docs/images/DeleteClientCommand1.png differ diff --git a/docs/images/DeleteClientCommand2.png b/docs/images/DeleteClientCommand2.png new file mode 100644 index 00000000000..f17006af3b9 Binary files /dev/null and b/docs/images/DeleteClientCommand2.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..cb433ee0146 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/DeleteSequenceDiagram.puml b/docs/images/DeleteSequenceDiagram.puml new file mode 100644 index 00000000000..65b4eb88019 --- /dev/null +++ b/docs/images/DeleteSequenceDiagram.puml @@ -0,0 +1,59 @@ +@startuml +box Ui #F97181 +participant ":MainWindow" as MainWindow #9D0012 +end box + +box Logic #C8C8FA +participant ":LogicManager" as LogicManager #3333C4 +participant ":AddressBookParser" as AddressBookParser #3333C4 +participant ":ClientCommandParser" as ClientCommandParser #3333C4 +participant ":DeleteClientCommand" as DeleteClientCommand #3333C4 +end box + +[-> MainWindow : executeCommand\n(client -d 1) +activate MainWindow + +MainWindow -> LogicManager : execute\n(client -d 1) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand\n(client -d 1) +activate AddressBookParser + +create ClientCommandParser +AddressBookParser -> ClientCommandParser +activate ClientCommandParser +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser +AddressBookParser -> ClientCommandParser : parse\n(client -d 1) +activate ClientCommandParser + +create DeleteClientCommand +ClientCommandParser -> DeleteClientCommand +activate DeleteClientCommand + +DeleteClientCommand --> ClientCommandParser +deactivate DeleteClientCommand + +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> DeleteClientCommand : execute\n(model, ui) +activate DeleteClientCommand + +DeleteClientCommand --> LogicManager : result +deactivate DeleteClientCommand + +DeleteClientCommand -[hidden]-> LogicManager : result +destroy DeleteClientCommand + +MainWindow <-- LogicManager : result +deactivate LogicManager +MainWindow --> MainWindow : swapClientListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/images/EditProjectCommand1.png b/docs/images/EditProjectCommand1.png new file mode 100644 index 00000000000..e55df16b81a Binary files /dev/null and b/docs/images/EditProjectCommand1.png differ diff --git a/docs/images/EditProjectCommand2.png b/docs/images/EditProjectCommand2.png new file mode 100644 index 00000000000..86029ea0bfe Binary files /dev/null and b/docs/images/EditProjectCommand2.png differ diff --git a/docs/images/EditSequenceDiagram.png b/docs/images/EditSequenceDiagram.png new file mode 100644 index 00000000000..23a7228130a Binary files /dev/null and b/docs/images/EditSequenceDiagram.png differ diff --git a/docs/images/FindProjectCommand1.png b/docs/images/FindProjectCommand1.png new file mode 100644 index 00000000000..cf8166ff5ba Binary files /dev/null and b/docs/images/FindProjectCommand1.png differ diff --git a/docs/images/FindProjectCommand2.png b/docs/images/FindProjectCommand2.png new file mode 100644 index 00000000000..436aeb96228 Binary files /dev/null and b/docs/images/FindProjectCommand2.png differ diff --git a/docs/images/FindSequenceDiagram.png b/docs/images/FindSequenceDiagram.png new file mode 100644 index 00000000000..2d6ff015b3b Binary files /dev/null and b/docs/images/FindSequenceDiagram.png differ diff --git a/docs/images/FindSequenceDiagram.puml b/docs/images/FindSequenceDiagram.puml new file mode 100644 index 00000000000..069a9c0fe59 --- /dev/null +++ b/docs/images/FindSequenceDiagram.puml @@ -0,0 +1,59 @@ +@startuml +box Ui #F97181 +participant ":MainWindow" as MainWindow #9D0012 +end box + +box Logic #C8C8FA +participant ":LogicManager" as LogicManager #3333C4 +participant ":AddressBookParser" as AddressBookParser #3333C4 +participant ":ClientCommandParser" as ClientCommandParser #3333C4 +participant ":FindClientCommand" as FindClientCommand #3333C4 +end box + +[-> MainWindow : executeCommand\n(client -f n/Harry) +activate MainWindow + +MainWindow -> LogicManager : execute\n(client -f n/Harry) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand\n(client -f n/Harry) +activate AddressBookParser + +create ClientCommandParser +AddressBookParser -> ClientCommandParser +activate ClientCommandParser +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser +AddressBookParser -> ClientCommandParser : parse\n(client -f n/Harry) +activate ClientCommandParser + +create FindClientCommand +ClientCommandParser -> FindClientCommand +activate FindClientCommand + +FindClientCommand --> ClientCommandParser +deactivate FindClientCommand + +ClientCommandParser --> AddressBookParser +deactivate ClientCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindClientCommand : execute\n(model, ui) +activate FindClientCommand + +FindClientCommand --> LogicManager : result +deactivate FindClientCommand + +FindClientCommand -[hidden]-> LogicManager : result +destroy FindClientCommand + +MainWindow <-- LogicManager : result +deactivate LogicManager +MainWindow --> MainWindow : swapClientListDisplay() + +[<-- MainWindow : result +deactivate MainWindow + +@enduml diff --git a/docs/images/ListActivityDiagram.png b/docs/images/ListActivityDiagram.png new file mode 100644 index 00000000000..28dbec35974 Binary files /dev/null and b/docs/images/ListActivityDiagram.png differ diff --git a/docs/images/ListSequenceDiagram.png b/docs/images/ListSequenceDiagram.png new file mode 100644 index 00000000000..250b3fa024e Binary files /dev/null and b/docs/images/ListSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index 9e9ba9f79e5..0207468c9de 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/MarkIssueCommand1.png b/docs/images/MarkIssueCommand1.png new file mode 100644 index 00000000000..24427504a99 Binary files /dev/null and b/docs/images/MarkIssueCommand1.png differ diff --git a/docs/images/MarkIssueCommand2.png b/docs/images/MarkIssueCommand2.png new file mode 100644 index 00000000000..a7682f4f6c4 Binary files /dev/null and b/docs/images/MarkIssueCommand2.png differ diff --git a/docs/images/MarkSequenceDiagram.png b/docs/images/MarkSequenceDiagram.png new file mode 100644 index 00000000000..d3aeef7e7f9 Binary files /dev/null and b/docs/images/MarkSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..d2165a06e72 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index e7b4c8880cd..a1edc5965c1 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/ParserPartialInitialisationSequenceDiagram.png b/docs/images/ParserPartialInitialisationSequenceDiagram.png new file mode 100644 index 00000000000..75fa1073680 Binary files /dev/null and b/docs/images/ParserPartialInitialisationSequenceDiagram.png differ diff --git a/docs/images/PinActivityDiagram.png b/docs/images/PinActivityDiagram.png new file mode 100644 index 00000000000..b032ad8f418 Binary files /dev/null and b/docs/images/PinActivityDiagram.png differ diff --git a/docs/images/PinIssueCommand1.png b/docs/images/PinIssueCommand1.png new file mode 100644 index 00000000000..24427504a99 Binary files /dev/null and b/docs/images/PinIssueCommand1.png differ diff --git a/docs/images/PinIssueCommand2.png b/docs/images/PinIssueCommand2.png new file mode 100644 index 00000000000..509dd7d6901 Binary files /dev/null and b/docs/images/PinIssueCommand2.png differ diff --git a/docs/images/PinSequenceDiagram.png b/docs/images/PinSequenceDiagram.png new file mode 100644 index 00000000000..7791437d166 Binary files /dev/null and b/docs/images/PinSequenceDiagram.png differ diff --git a/docs/images/SortIssueCommand1.png b/docs/images/SortIssueCommand1.png new file mode 100644 index 00000000000..24427504a99 Binary files /dev/null and b/docs/images/SortIssueCommand1.png differ diff --git a/docs/images/SortIssueCommand2.png b/docs/images/SortIssueCommand2.png new file mode 100644 index 00000000000..e6f93dca7ee Binary files /dev/null and b/docs/images/SortIssueCommand2.png differ diff --git a/docs/images/StorageClassDiagramNew.png b/docs/images/StorageClassDiagramNew.png new file mode 100644 index 00000000000..a00386bb3cd Binary files /dev/null and b/docs/images/StorageClassDiagramNew.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..6f561e81cdf 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 785e04dbab4..9b8cfbd6f92 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/aishwarya-hariharan-iyer.png b/docs/images/aishwarya-hariharan-iyer.png new file mode 100644 index 00000000000..ed95b1b9bca Binary files /dev/null and b/docs/images/aishwarya-hariharan-iyer.png differ diff --git a/docs/images/conradlew.png b/docs/images/conradlew.png new file mode 100644 index 00000000000..da281d07f91 Binary files /dev/null and b/docs/images/conradlew.png differ diff --git a/docs/images/crvstalphua.png b/docs/images/crvstalphua.png new file mode 100644 index 00000000000..f96b9d7cfbc Binary files /dev/null and b/docs/images/crvstalphua.png differ diff --git a/docs/images/donovan9617.png b/docs/images/donovan9617.png new file mode 100644 index 00000000000..c2b53a351cd Binary files /dev/null and b/docs/images/donovan9617.png differ diff --git a/docs/images/yongbeom-kim.png b/docs/images/yongbeom-kim.png new file mode 100644 index 00000000000..6955d63c4af Binary files /dev/null and b/docs/images/yongbeom-kim.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..1595e172cb7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: DevEnable --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2223S1-CS2103-F13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S1-CS2103-F13-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2223S1-CS2103-F13-1/tp/branch/master/graph/badge.svg?token=EZIXA3TYQA)](https://codecov.io/gh/AY2223S1-CS2103-F13-1/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**DevEnable is a desktop application for managing your developer projects.** 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 DevEnable, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing DevEnable, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Developed from: [AddressBook Level-3](https://github.com/se-edu/addressbook-level3) diff --git a/docs/team/aishwarya-hariharan-iyer.md b/docs/team/aishwarya-hariharan-iyer.md new file mode 100644 index 00000000000..39010f0334a --- /dev/null +++ b/docs/team/aishwarya-hariharan-iyer.md @@ -0,0 +1,75 @@ +--- +layout: page +title: Aishwarya's Project Portfolio Page +--- + +### Project Overview: DevEnable +DevEnable is a product for developers who have to manage different projects spread across multiple GitHub +repositories. It helps developers organize information about different projects they are working on in one place so +that they may prioritize and have an overview. It removes the hassle of having to navigate to our/organization’s +GitHub repo every time and manually check different pages to see which tasks require immediate attention. + +Given below are my contributions to the project. + +* **Code Contributed**: The link to the tP dashboard can be found [here](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=aishwarya-hariharan-iyer&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-09-16&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* **New features implemented**: A summary of the enhancements you implemented. + +* **New Feature**: `Client` class with `add`, `delete` and `edit` commands (Pull requests [\#83](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/83), [\#128](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/128)) + * What it does: The `Client`, which can be a person or an organization, supervises one or more projects. + Client have attributes such as name, phone number, email and the list of projects they supervise. We can + add a client to the `ClientList`, edit an existing client by specifying the attributes to be modified and + delete clients as well. + * Justification: Each project can be tied to at most one client so that developers can know who is in charge + of their work. Moreover, the feature can later be expanded to have attributes such as importance of the + client for the developer. + * Highlights: As with all other classes in `DevEnable` clients can be found and filtered based on their + attributes, sorted, pinned and much more. + * Credits: The `Client` class is based on the AB3's `Person` class and replaces it in DevEnable. However, + unlike the `Person` class in AB3, the `Client` has mutable fields and do not have `Address` + and `Set`. Initially, the `Client` package PR contained other `Client` attributes but we chose to + keep that for a future iterations in interest of UI/UX considerations. (Pull requests [\#72](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/72), [\#66](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/66)) + +* **New Feature**: `find` command (Pull requests [\#100](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/100), [\#129](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/129), [\#138](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/138)) + * What it does: Finds items on the list that match the user's search criteria on various attributes and + displays the corresponding filtered list. It matches keywords with targets on an exact-word basis such + that at least one word form the target must match exactly with at least one word from the keyword. + * Justification: The developer may want to filter and find items on the list based on various parameters. + * Highlights: The find command takes the intersection of all keywords from different prefixes to increase + the specificity of the search. It takes the union of all such keywords of the same (repeated) prefix so as + increase the flexibility of use in being able to search for multiple keywords at once under the same field. + It also validates the inputs entered for each prefix to prevent users from getting confused between no + items being listed because of an incorrect input (i.e. such an item can never exist) and because of no + such item existing on the list at the time of search. + * Credits: The `find` command builds on the AB3 `find` functionality. + +* **Enhancements to existing features**: + * Wrote parser tests for edit commands of `Client`, `Project` and `Issue` (Pull request [\#248](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/248)) + * Wrote parser tests for the different `find` commands and for the `AddClientCommand` (Pull requests [\#248](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/248), [\#94](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/94)) + +-------------------------------------------------------------------------------------------------------------------- + +* **Documentation**: + +* Contributions to the UG + + Help set up the initial UG with all content planned in first iteration (Pull request [\#23](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/23)) + + Added documentation and images for client command features: `add`, `delete`, `edit` (Pull request [\#130](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/130)) + + Added documentation and images for find command features under `Client`, `Project` and `Issue`(Pull request [\#145](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/145)) + +* Contributions to the DG + * Write-up for Delete Command Feature consisting of Delete Project Command, Delete Issue Command, and Delete + Client Command (Pull request [\#236](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/236)) + * Write-up for Find Command Feature consisting of Find Project Command, Find Issue Command, and Find Client + Command (Pull request [\#244](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/244)) + * UML Sequence Diagram for deleting a client from the app and for finding a client in the app + * Design considerations for Delete Command Feature and for Find Command Feature + * Instructions for Manual Testing of all find commands and all client commands (Pull request [\#240](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/240)) + * User Stories for all find commands and all client commands (Pull request [\#115](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/115)) + +* **Contributions to team-based tasks and beyond**: + * Created some issues for the weekly deliverables + * Reviewed and approved some PRs + * Helped manage weekly meetings by setting Zoom links and taking notes + * Reported bugs and suggestions for other teams in the class [(Link)](https://github.com/Aishwarya-Hariharan-Iyer/ped/issues) + +-------------------------------------------------------------------------------------------------------------------- diff --git a/docs/team/conradlew.md b/docs/team/conradlew.md new file mode 100644 index 00000000000..7573a4f5072 --- /dev/null +++ b/docs/team/conradlew.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Conrad's Project Portfolio Page +--- + +### Project: DevEnable + +DevEnable is a product for developers who have to manage different projects spread +across multiple GitHub repositories. It helps developers organize information about +different projects they are working on in one place so that they may prioritize and +have a convenient overview of what needs to be done. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to pin/unpin entities (projects, clients and issues) + * What it does: Allows the user to pin entities to the top of their respective lists one at a time. Preceding pin commands can be reversed by using the pin command again on the same entity. + * Justification: This feature improves the product significantly because a user can value some entities more than others and the application should provide a way for users to keep the more important entities easily visible. + * Highlights: This enhancement affected the existing sort commands. It required an in-depth analysis of design alternatives. The implementation was challenging as it required changes to the existing sort commands. + +* **New Feature**: Added a list command that allows the user to navigate to the different entity lists commands using the CLI. + +* **New Feature**: Added a default view command that allows the user to choose what entity list they would prefer to see on opening the application. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=conradlew&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-09-16&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* **Project management**: + * Managed releases `v1.3.trial` - `v1.3` (2 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the storage, model and UI to support display of multiple entities (Pull requests [\#64](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/64), [\#65](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/65)) + * Wrote additional tests for implemented features to increase coverage (Pull requests [\#104](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/104), [\#133](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/133)) + +* **Documentation**: + * User Guide: + * Added documentation for the `pin` feature [\#141](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/141). + * Developer Guide: + * Added implementation details of the `list`, `default view` and `pin` features [\#106](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/106). + +* **Contributions to team-based tasks**: + * Set up team repository + * Managed issue tracker + * Created demo video for v1.2 + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#30](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/30), [\#80](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/80), [\#62](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/62) + * Reported bugs and suggestions for other teams in the class [(Link)](https://github.com/ConradLew/ped/issues) diff --git a/docs/team/crvstalphua.md b/docs/team/crvstalphua.md new file mode 100644 index 00000000000..32e4e0e769f --- /dev/null +++ b/docs/team/crvstalphua.md @@ -0,0 +1,67 @@ +--- +layout: page +title: Crystal's Project Portfolio Page +--- + +### Project: DevEnable + +DevEnable is a product for developers who have to manage different projects spread across multiple GitHub repositories. +It helps developers organize information about different projects they are working on in one place, making it easier +for them to prioritize and have an overview of their projects, issues and clients. It helps them avoid the hassle of +having to manually navigate to different Github repositories to check which tasks require immediate attention. + +Given below are my contributions to the project. + +* **Code Contributed**: The link to the tP dashboard can be found [here](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=crvstalphua&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-09-16&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=crvstalphua&tabRepo=AY2223S1-CS2103-F13-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false). + +* **New features implemented**: A summary of the enhancements you implemented. + * **New Feature**: `Issue` class with `add`, `delete` and `edit` commands (Pull requests: [\#80](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/80), [\#96](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/96)) + * What it does: The `Issue` class represents an issue belonging to a project. Each issue consists of a title, + deadline, status, urgency, pin and a project which it belongs to. Title, deadline, status and urgency fields each + have their own classes. The user can create an issue, adding it to the `IssueList`and the list of issues of the + relevant project. The title, deadline, and urgency of an existing issue can also be edited. The user can remove + an issue, deleting it from the `IssueList` and the list of issues of the project. A list of all issues can be + viewed through `issue -l` command as well. + * Justification: It enables developers to easily add or delete issues to or from each of their projects. Each + issue has its own deadline, status and urgency so that the developer can decide how to prioritize and choose + which issues to work on. The developer can also modify fields if there are changes to the issue without having to + delete and add another issue. + * Highlights: Similar to the `Project` and `Client` classes, issues can be sorted based on various keys or filtered + based on specific search terms, as well as pinned. + + * **New Feature**: `mark` and `unmark` commands in `Issue` class (Pull requests: [\#105](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/105)) + * What it does: When an issue is create, the default status is `incomplete`. The `mark` command changes the status + of an issue to `completed` while the `unmark` command changes the status of an issue to `incomplete`. + * Justification: Upon completing an issue, the developer can mark it as completed, making it easier to see + which issues still need to be addressed. If the developer realises that a completed issue requires more work, + they can unmark it, changing its status to incomplete. + * Highlights: Projects are displayed with the count for number of complete and incomplete issues so that the + developer can see which project has more issues that require attention. + +* **Enhancements to existing features**: + * Added tests for add, mark, unmark commands and issue class components (Pull requests: [\#249](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/249), [\#262](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/262), [\#63](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/63), [\#266](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/266)) + * Added parser tests for add issue and delete issue (Pull requests: [\#249](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/249)) + * Refactored all Status to Urgency, Description to Title, and Phone to Mobile (Pull requests: [\#134](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/134)) + +* **Documentation**: + * Contributions to the UG + * Added documentation and images for project command features: `add`, `delete`, `edit`, `list`, `sort`and + `set default view` (Pull requests: [\#134](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/134)) + * Added documentation and images for issue command features: `add`, `delete`, `edit`, `list`, `sort`, `mark`, + `unmark`, and `set default view` (Pull requests: [\#134](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/134)) + * Added documentation and images for client command features: `list` and `sort` (Pull requests: [\#134](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/134)) + * Reformatted entire UG, from arranging by entity types to arranging by command type (Pull requests: [\#245](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/245)) + * Contributions to the DG + * Write-up for Edit Command Feature consisting of Edit Project Command, Edit Issue Command and Edit Client Command (Pull requests: [\#116](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/116)) + * Design considerations for Edit Command Feature (Pull requests: [\#116](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/116)) + * Write-up and UML Diagram for Mark Issue Command Feature and Unmark Issue Command Feature (Pull requests: [\#242](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/242), [\#245](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/245)) + * Design considerations for Mark Issue Command Feature (Pull requests: [\#242](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/242)) + * Edited Model Component and Model Class Diagram (Pull requests: [\#245](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/245)) + +* **Contributions to the team-based tasks**: + * Reviewed some team pull requests (Pull requests: [\#83](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/83), [\#132](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/132)) + * Completed all issues assigned to me each week + * Occasionally helped to manage weekly meetings by taking notes or setting up zoom links + +* **Community**: + * Reported bugs as well as provided suggestions for other teams during PED ([PED_ISSUES](https://github.com/crvstalphua/ped/issues)) diff --git a/docs/team/donovan9617.md b/docs/team/donovan9617.md new file mode 100644 index 00000000000..8bc18fc3bc6 --- /dev/null +++ b/docs/team/donovan9617.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Donovan's Project Portfolio Page +--- + +### Project Overview: DevEnable +DevEnable is a product for developers who have to manage different projects spread across multiple GitHub +repositories. It helps developers organize information about different projects they are working on in one place so +that they may prioritize and have an overview. It removes the hassle of having to navigate to our/organization’s +GitHub repo every time and manually check different pages to see which tasks require immediate attention. + +Given below are my contributions to the project. + +* **Code Contributed**: The link to the tP dashboard can be found [here](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=Donovan9617&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-09-16&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* **New Feature**: `Project` class with `add`, `delete` and `edit` commands [\#62](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/62) [\#70](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/70) [\#93](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/93) + * What it does: The `Project` class represents a project consisting of a name, repository, deadline, client, pin + and a list of issues. The name, repository and deadline fields are classes on their own. The developer may add + a new project to the project list, delete an existing project from the project list, or edit the fields of an + existing project in the project list. The developer may view these projects through the `project -l` command. + * Justification: Objects that instantiate the `Project` class represent projects that the developer wants to + keep track of. They are stored in `projectList` and may be viewed through DevEnable user interface. The + developer may add, delete or edit these projects in order to manage them. + +* **New Feature**: `sort` command [\#108](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/108) [\#126](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/126) [\#132](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/132) + * What it does: Sorts projects, issues, and clients according to various keys and orders as specified. + * Justification: The developer may want to sort the projects, issues, and clients according to various keys such + as urgency, deadline, name, issue count in order to view important entities easily. + +* **New Feature**: Graphical User Interface (GUI) [\#127](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/127) [\#135](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/135) + * What it does: Makes the user interface for DevEnable aesthetic and pretty. + * Justification: Improves the user experience when using DevEnable for project managements. + +* **Documentation**: + * User Guide + * Commands and validation related to adding a project, deleting a project, editing a project and sorting a project [\#143](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/143) + * Developer Guide + * Write-up and UML Diagrams for UI Component, Add Command Feature and Sort Command Feature [\#239](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/239) [\#108](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/108) [\#241](https://github.com/AY2223S1-CS2103-F13-1/tp/pull/241) + +* **Contributions to team-based tasks**: + * Helped to take meeting notes for weekly project meetings online + * Reviewed some team pull requests + * Created demo video for v1.3 + * Fixed bugs arising from integration of project client and issue classes + +* **Community** + * Reported bugs and suggestions for other teams in the class during PED diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..f6dfc360020 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -5,7 +5,7 @@ title: John Doe's Project Portfolio Page ### Project: AddressBook Level 3 -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. +AddressBook - Level 3 is a desktop project book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. Given below are my contributions to the project. diff --git a/docs/team/skeletal_ppp.md b/docs/team/skeletal_ppp.md new file mode 100644 index 00000000000..9632ef8f255 --- /dev/null +++ b/docs/team/skeletal_ppp.md @@ -0,0 +1,39 @@ +#Project Portfolio Page + +### Project Overview: DevEnable +A short overview of your product to provide some context to the reader. The opening 1-2 sentences may be reused by all + team members. If your product overview extends beyond 1-2 sentences, the remainder should be written by yourself. +(to be added soon) + +Given below are my contributions to the project. + +* **Code Contributed**: Give a link to your code on tP Code Dashboard. The link is available in the Project List Page -- linked to the icon under your profile picture. +(to be added soon) + +* **New features implemented**: A summary of the enhancements you implemented. + * **New Feature**: + * What it does: + * Justification + * Highlights: + * Credits: mention here if any code/idea is reused + +* **Enhancements to existing features**: + * E.g. changes to GUI, wrote additional tests, etc. + +* **Documentation**: + * Contributions to the UG + * Which sections did you contribute to the UG? (to be added soon) + * Contributions to the DG + * Which sections did you contribute to the DG? Which UML diagrams did you add/updated? (to be added soon) + +* **Contributions to team-based tasks**: + * (to be added soon) + +* **Community**: + * Review/mentoring contributions + * Links to PRs reviewed, instances of helping team members in other ways + (to be added soon) + * Contributions beyond the project team + * Evidence of helping others (e.g. responses on forums, bugs reported in other team's products) + * Evidence of technical leadership (e.g. sharing useful info on forum) + (to be added soon) diff --git a/docs/team/yongbeom-kim.md b/docs/team/yongbeom-kim.md new file mode 100644 index 00000000000..a7d4e1249ba --- /dev/null +++ b/docs/team/yongbeom-kim.md @@ -0,0 +1,36 @@ +--- +layout: page +title: Yong Beom's Project Portfolio Page +--- + +### Project: DevEnable + +DevEnable is a product for developers who have to manage different projects spread across multiple GitHub repositories. It helps developers organize information about different projects they are working on in one place so that they may prioritize and have an overview. It removes the hassle of having to navigate to our/organization’s GitHub repo every time and manually check different pages to see which tasks require immediate attention. + +Below are my contributions to the project. +- **Code Contributed**: [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=kim&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-09-16&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=false) +- **Features**: + - Fixed various bugs in Edit Commands +- **Refactoring**: + - Refactored Logic and Model architecture to fix storage and other commands. + - Abstracted interfaces and lists to standardize implementation + - Exposed methods in Model to follow Demeter's principle + - Implemented partial initialisation of classes/objects +- **Documentation**: + - Developer Guide: + - Updated description of Logic component + - Updated and edited sequence and class diagrams on Logic component + - Updated sequence diagrams for commands + - Updated User Stories + - Use cases & Glossary + - Updated extensions for use cases + - Added use cases for issue classes, set display commands and system commands + - User Guide: + - Update command syntax in user guide +- **CI/CD**: + - Fixed codecov CI/CD issue + +- **Community**: + - Helped to remind and set up group meetings + - Reviewed team PRs + - Guided team members on technical problems and design decisions diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 880c701042f..5b97908e982 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -5,7 +5,7 @@ title: "Tutorial: Adding a command" Let's walk you through the implementation of a new command — `remark`. -This command allows users of the AddressBook application to add optional remarks to people in their address book and edit it if required. The command should have the following format: +This command allows users of the AddressBook application to add optional remarks to people in their project book and edit it if required. The command should have the following format: `remark INDEX r/REMARK` (e.g., `remark 2 r/Likes baseball`) @@ -28,7 +28,7 @@ package seedu.address.logic.commands; import seedu.address.model.Model; /** - * Changes the remark of an existing person in the address book. + * Changes the remark of an existing person in the project book. */ public class RemarkCommand extends Command { @@ -229,7 +229,7 @@ Now that we have all the information that we need, let’s lay the groundwork fo ### Add a new `Remark` class -Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. +Create a new `Remark` in `seedu.address.model.client`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). Note how `Remark` has no constrains and thus does not require input validation. @@ -295,7 +295,7 @@ While the changes to code may be minimal, the test data will have to be updated
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty project book!
diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..c596f144c06 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -28,7 +28,7 @@ IntelliJ IDEA provides a refactoring tool that can identify *most* parts of a re ### Assisted refactoring -The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. +The `address` field in `Person` is actually an instance of the `seedu.address.model.client.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. * :bulb: To make things simpler, you can unselect the options `Search in comments and strings` and `Search for text occurrences` ![Usages detected](../images/remove/UnsafeDelete.png) diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..d0467c51a86 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -292,10 +292,10 @@ Here are some quick questions you can try to answer based on your execution path 2. Allow `delete` to remove more than one index at a time - 3. Save the address book in the CSV format instead + 3. Save the project book in the CSV format instead 4. Add a new command 5. Add a new field to `Person` - 6. Add a new entity to the address book + 6. Add a new entity to the project book diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..a0f96c286a2 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -66,12 +66,14 @@ public void init() throws Exception { logic = new LogicManager(model, storage); ui = new UiManager(logic); + + logic.setUi(ui); } /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + * Returns a {@code ModelManager} with the data from {@code storage}'s project book and {@code userPrefs}.
+ * The data from the sample project book will be used instead if {@code storage}'s project book is not found, + * or an empty project book will be used instead if errors occur when reading {@code storage}'s project book. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { Optional addressBookOptional; @@ -80,6 +82,8 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Data file not found. Will be starting with a sample AddressBook"); + } else { + logger.info("AddressBook successfully loaded from storage"); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataConversionException e) { @@ -145,6 +149,9 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { UserPrefs initializedPrefs; try { Optional prefsOptional = storage.readUserPrefs(); + if (prefsOptional.isPresent()) { + logger.info("Successfully loaded UserPrefs with values " + prefsOptional.get().toString()); + } initializedPrefs = prefsOptional.orElse(new UserPrefs()); } catch (DataConversionException e) { logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " diff --git a/src/main/java/seedu/address/commons/core/DefaultView.java b/src/main/java/seedu/address/commons/core/DefaultView.java new file mode 100644 index 00000000000..15e7104f5e1 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/DefaultView.java @@ -0,0 +1,10 @@ +package seedu.address.commons.core; + +/** + * Encapsulates the different default view preferences of the user. + */ +public enum DefaultView { + PROJECT, + CLIENT, + ISSUE +} diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index ba33653be67..ab64da3d732 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -1,5 +1,7 @@ package seedu.address.commons.core; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + import java.awt.Point; import java.io.Serializable; import java.util.Objects; @@ -16,6 +18,7 @@ public class GuiSettings implements Serializable { private final double windowWidth; private final double windowHeight; private final Point windowCoordinates; + private DefaultView defaultView; /** * Constructs a {@code GuiSettings} with the default height, width and position. @@ -24,15 +27,18 @@ public GuiSettings() { windowWidth = DEFAULT_WIDTH; windowHeight = DEFAULT_HEIGHT; windowCoordinates = null; // null represent no coordinates + defaultView = DefaultView.PROJECT; } /** * Constructs a {@code GuiSettings} with the specified height, width and position. */ - public GuiSettings(double windowWidth, double windowHeight, int xPosition, int yPosition) { + public GuiSettings(double windowWidth, double windowHeight, int xPosition, int yPosition, DefaultView defaultView) { + requireAllNonNull(windowWidth, windowHeight, xPosition, yPosition, defaultView); this.windowWidth = windowWidth; this.windowHeight = windowHeight; windowCoordinates = new Point(xPosition, yPosition); + this.defaultView = defaultView; } public double getWindowWidth() { @@ -47,6 +53,10 @@ public Point getWindowCoordinates() { return windowCoordinates != null ? new Point(windowCoordinates) : null; } + public DefaultView getDefaultView() { + return this.defaultView; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -60,12 +70,13 @@ public boolean equals(Object other) { return windowWidth == o.windowWidth && windowHeight == o.windowHeight - && Objects.equals(windowCoordinates, o.windowCoordinates); + && Objects.equals(windowCoordinates, o.windowCoordinates) + && defaultView.equals(o.defaultView); } @Override public int hashCode() { - return Objects.hash(windowWidth, windowHeight, windowCoordinates); + return Objects.hash(windowWidth, windowHeight, windowCoordinates, defaultView); } @Override @@ -73,7 +84,12 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Width : " + windowWidth + "\n"); sb.append("Height : " + windowHeight + "\n"); - sb.append("Position : " + windowCoordinates); + sb.append("Position : " + windowCoordinates + "\n"); + sb.append("Default View : " + defaultView + "\n"); return sb.toString(); } + + public void setDefaultView(DefaultView defaultView) { + this.defaultView = defaultView; + } } diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..aec2d5957aa 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -6,8 +6,15 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String FLAG_UNKNOWN_COMMAND = "Unknown flag"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_PROJECT_DISPLAYED_ID = "The project id provided is invalid"; + public static final String MESSAGE_CLIENTS_LISTED_OVERVIEW = "%1$d client(s) listed!"; + public static final String MESSAGE_ISSUES_LISTED_OVERVIEW = "%1$d issue(s) listed!"; + public static final String MESSAGE_PROJECTS_LISTED_OVERVIEW = "%1$d project(s) listed!"; + public static final String MESSAGE_INVALID_ISSUE_DISPLAYED_ID = "The issue id provided is invalid"; + public static final String MESSAGE_INVALID_CLIENT_DISPLAYED_ID = "The client id provided is invalid"; + public static final String MESSAGE_MISSING_ARGUMENTS = "Missing some arguments! \n%1$s"; + public static final String MESSAGE_ISSUE_NOT_FOUND = "This issue id does not exist in the project book"; } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..60a91f3ce38 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -3,12 +3,16 @@ import java.nio.file.Path; import javafx.collections.ObservableList; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; +import seedu.address.ui.Ui; /** * API of the Logic component @@ -30,11 +34,14 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + ObservableList getFilteredProjectList(); + + ObservableList getFilteredIssueList(); + + ObservableList getFilteredClientList(); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' project book file path. */ Path getAddressBookFilePath(); @@ -47,4 +54,10 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + void setUi(Ui ui); + + DefaultView getDefaultView(); + + void setDefaultView(DefaultView defaultView); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..0b36cc74441 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -5,6 +5,7 @@ import java.util.logging.Logger; import javafx.collections.ObservableList; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; @@ -14,8 +15,11 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; import seedu.address.storage.Storage; +import seedu.address.ui.Ui; /** * The main LogicManager of the app. @@ -26,12 +30,16 @@ public class LogicManager implements Logic { private final Model model; private final Storage storage; + + private Ui ui; private final AddressBookParser addressBookParser; /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. */ + public LogicManager(Model model, Storage storage) { + this.ui = null; this.model = model; this.storage = storage; addressBookParser = new AddressBookParser(); @@ -43,7 +51,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); + commandResult = command.execute(model, ui); try { storage.saveAddressBook(model.getAddressBook()); @@ -60,8 +68,18 @@ public ReadOnlyAddressBook getAddressBook() { } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredProjectList() { + return model.getFilteredProjectList(); + } + + @Override + public ObservableList getFilteredIssueList() { + return model.getFilteredIssueList(); + } + + @Override + public ObservableList getFilteredClientList() { + return model.getFilteredClientList(); } @Override @@ -78,4 +96,19 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public void setUi(Ui ui) { + this.ui = ui; + } + + @Override + public DefaultView getDefaultView() { + return model.getDefaultView(); + } + + @Override + public void setDefaultView(DefaultView defaultView) { + model.setDefaultView(defaultView); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 71656d7c5c8..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..923d9c83f19 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -4,18 +4,19 @@ import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.ui.Ui; /** - * Clears the address book. + * Clears the project book. */ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "DevEnable has been cleared!"; @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model, Ui ui) { requireNonNull(model); model.setAddressBook(new AddressBook()); return new CommandResult(MESSAGE_SUCCESS); diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..8b5ca64589d 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -2,12 +2,14 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.ui.Ui; /** * Represents a command with hidden internal logic and the ability to be executed. */ public abstract class Command { + public static final String ID_OVERFLOW = "The project book is full!"; /** * Executes the command and returns the result message. * @@ -15,6 +17,6 @@ public abstract class Command { * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(Model model) throws CommandException; + public abstract CommandResult execute(Model model, Ui ui) throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..5e265167e7c 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -67,5 +67,4 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(feedbackToUser, showHelp, exit); } - } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 02fd256acba..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(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 personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 7e36114902f..00000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,226 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -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.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -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.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - 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()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * 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) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..817e3ffc372 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -1,6 +1,7 @@ package seedu.address.logic.commands; import seedu.address.model.Model; +import seedu.address.ui.Ui; /** * Terminates the program. @@ -12,7 +13,7 @@ public class ExitCommand extends Command { public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model, Ui ui) { return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index d6b19b0a0de..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate 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) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..699eb80a9f7 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -1,6 +1,7 @@ package seedu.address.logic.commands; import seedu.address.model.Model; +import seedu.address.ui.Ui; /** * Format full help instructions for every command for display. @@ -15,7 +16,7 @@ public class HelpCommand extends Command { public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model, Ui ui) { return new CommandResult(SHOWING_HELP_MESSAGE, true, false); } } diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/client/AddClientCommand.java b/src/main/java/seedu/address/logic/commands/client/AddClientCommand.java new file mode 100644 index 00000000000..4fdfd1afd6b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/AddClientCommand.java @@ -0,0 +1,107 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_MOBILE; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_PROJECT_ID; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientWithoutModel; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.ui.Ui; + + +/** + * Adds a client to the project book. + */ +public class AddClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-a"; + + public static final String MESSAGE_ADD_CLIENT_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Adds a client to the project book. \n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + "[" + PREFIX_MOBILE + "MOBILE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + PREFIX_PROJECT_ID + "PROJECT ID \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "John Doe " + + PREFIX_MOBILE + "98765432 " + + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_PROJECT_ID + "1"; + + public static final String MESSAGE_SUCCESS = "New client added: %1$s"; + public static final String MESSAGE_DUPLICATE_CLIENT = "A client with the same name already exists!"; + public static final String MESSAGE_CLIENT_ALREADY_PRESENT = "This project already has a client"; + public static final String MESSAGE_PROJECT_NOT_FOUND = "This project id does not exist in the project book"; + + private final ClientWithoutModel toAddClientWithoutModel; + private final ProjectId projectId; + + /** + * Creates an AddCommand to add the specified {@code Client} + * @param clientWithoutModel + */ + public AddClientCommand(ClientWithoutModel clientWithoutModel, ProjectId pid) { + requireNonNull(clientWithoutModel); + toAddClientWithoutModel = clientWithoutModel; + projectId = pid; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + + if (!model.hasProjectId(projectId.getIdInt())) { + throw new CommandException(MESSAGE_PROJECT_NOT_FOUND); + } + + Client toAddClient = toAddClientWithoutModel.apply(model); + + if (!toAddClient.hasValidId()) { + throw new CommandException(ID_OVERFLOW); + } + + + Project toModifyProject = model.getProjectById(projectId.getIdInt()); + + if (!toModifyProject.getClient().isEmpty()) { + throw new CommandException(MESSAGE_CLIENT_ALREADY_PRESENT); + } + + if (model.hasClient(toAddClient)) { + throw new CommandException(MESSAGE_DUPLICATE_CLIENT); + } else { + toAddClient.addProjects(toModifyProject); + model.addClient(toAddClient); + } + + toModifyProject.setClient(toAddClient); + + ui.showClients(); + model.updateFilteredClientList(Model.PREDICATE_SHOW_ALL_CLIENTS); + + return new CommandResult(String.format(MESSAGE_SUCCESS, toAddClient)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddClientCommand)) { + return false; + } + + return this.toAddClientWithoutModel.equals(((AddClientCommand) other).toAddClientWithoutModel); + } +} diff --git a/src/main/java/seedu/address/logic/commands/client/ClientCommand.java b/src/main/java/seedu/address/logic/commands/client/ClientCommand.java new file mode 100644 index 00000000000..8ade9d34153 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/ClientCommand.java @@ -0,0 +1,10 @@ +package seedu.address.logic.commands.client; + +import seedu.address.logic.commands.Command; + +/** + * Base abstract class for client commands. + */ +public abstract class ClientCommand extends Command { + public static final String COMMAND_WORD = "client"; +} diff --git a/src/main/java/seedu/address/logic/commands/client/DeleteClientCommand.java b/src/main/java/seedu/address/logic/commands/client/DeleteClientCommand.java new file mode 100644 index 00000000000..a11251ca3aa --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/DeleteClientCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.project.Project; +import seedu.address.ui.Ui; + +/** + * A delete client command to delete clients + */ +public class DeleteClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-d"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Deletes the client by its Id. Id must be positive and valid \n" + + "Parameters: CLIENT_ID \n" + + "Example: " + COMMAND_WORD + " " + COMMAND_FLAG + " 1"; + + public static final String MESSAGE_SUCCESS = "Deleted Client: %1$s"; + + private final Index targetIndex; + + public DeleteClientCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredClientList(); + + for (Client c : lastShownList) { + if (c.getClientId().getIdInt() == targetIndex.getOneBased()) { + Client clientToDelete = c; + model.deleteClient(clientToDelete); + for (Project p: clientToDelete.getProjects()) { + p.removeClient(); + } + ui.showClients(); + model.updateFilteredClientList(Model.PREDICATE_SHOW_ALL_CLIENTS); + return new CommandResult(String.format(MESSAGE_SUCCESS, clientToDelete)); + } + } + + throw new CommandException(Messages.MESSAGE_INVALID_CLIENT_DISPLAYED_ID); + + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof DeleteClientCommand + && targetIndex.equals(((DeleteClientCommand) other).targetIndex)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/client/EditClientCommand.java b/src/main/java/seedu/address/logic/commands/client/EditClientCommand.java new file mode 100644 index 00000000000..f987c172f5f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/EditClientCommand.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands.client; + +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_MOBILE; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CLIENTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.Name; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientEmail; +import seedu.address.model.client.ClientId; +import seedu.address.model.client.ClientMobile; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.ui.Ui; + +/** + * Edits the details of an existing client in the project book. + */ +public class EditClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-e"; + + public static final String MESSAGE_SUCCESS = "Client %1$s has been edited"; + + public static final String MESSAGE_DUPLICATE_CLIENT_NAME = "A client with this name already " + + "exists in the project book"; + + public static final String MESSAGE_CLIENT_NOT_FOUND = "Client id %1$d does not exist in the project book"; + + public static final String MESSAGE_CLIENT_ALREADY_HAS_THAT_NAME = "This client already has that name"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Edits a client in the project book. \n" + + "Parameters: " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_MOBILE + "MOBILE] " + + PREFIX_CLIENT_ID + "CLIENT ID \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "John " + + PREFIX_EMAIL + "john@gmail.com " + + PREFIX_MOBILE + "12345678 " + + PREFIX_CLIENT_ID + "1 "; + + private final Name newName; + private final ClientEmail newEmail; + private final ClientMobile newMobile; + private final ClientId clientId; + + /** + * Creates an EditClientCommand to edit the specified {@code Client} + */ + public EditClientCommand(ClientId clientId, Name newName, ClientEmail newEmail, ClientMobile newMobile) { + this.clientId = clientId; + this.newName = newName; + this.newEmail = newEmail; + this.newMobile = newMobile; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + ui.showClients(); + model.updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + + if (!HasIntegerIdentifier.containsId(model.getFilteredClientList(), clientId.getIdInt())) { + throw new CommandException(String.format(MESSAGE_CLIENT_NOT_FOUND, clientId.getIdInt())); + } + + Client toEditClient = model.getClientById(clientId.getIdInt()); + + if (newName != null) { + for (Client c : model.getFilteredClientList()) { + if (c.getClientName().equals(newName)) { + if (toEditClient.getClientName().equals(newName)) { + throw new CommandException(MESSAGE_CLIENT_ALREADY_HAS_THAT_NAME); + } + throw new CommandException(MESSAGE_DUPLICATE_CLIENT_NAME); + } + } + toEditClient.setName(newName); + } + + if (newEmail != null) { + toEditClient.setEmail(newEmail); + } + + if (newMobile != null) { + toEditClient.setMobile(newMobile); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, toEditClient)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/client/FindClientCommand.java b/src/main/java/seedu/address/logic/commands/client/FindClientCommand.java new file mode 100644 index 00000000000..6a2dbc4e2dc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/FindClientCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_MOBILE; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_NAME; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.parser.predicates.ClientContainsKeywordsPredicate; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + + +/** + * Represent a class to find and filter client list. + */ +public class FindClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-f"; + public static final String MESSAGE_SUCCESS = "Filtered list of client(s) shown."; + private static final String MESSAGE_CLIENT_NOT_FOUND = "A client matching requirements not found."; + public static final String MESSAGE_FIND_CLIENT_USAGE = COMMAND_WORD + " " + COMMAND_FLAG + + ": Finds clients by keyword.\n" + + "Parameters: " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_CLIENT_ID + "CLIENT ID] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_MOBILE + "MOBILE] \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "John " + + PREFIX_CLIENT_ID + "1 " + + PREFIX_EMAIL + "john@gmail.com " + + PREFIX_MOBILE + "12345678 "; + + private final ClientContainsKeywordsPredicate predicate; + public FindClientCommand(ClientContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showClients(); + model.updateFilteredClientList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_CLIENTS_LISTED_OVERVIEW, model.getFilteredClientList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindClientCommand // instanceof handles nulls + && predicate.equals(((FindClientCommand) other).predicate)); // state check + } + +} diff --git a/src/main/java/seedu/address/logic/commands/client/ListClientCommand.java b/src/main/java/seedu/address/logic/commands/client/ListClientCommand.java new file mode 100644 index 00000000000..c3b71ad7b12 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/ListClientCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CLIENTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Lists all persons in the project book to the user. + */ +public class ListClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-l"; + + public static final String MESSAGE_SUCCESS = "Listed all clients in the project book"; + + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showClients(); + model.updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/client/PinClientCommand.java b/src/main/java/seedu/address/logic/commands/client/PinClientCommand.java new file mode 100644 index 00000000000..cad43ab956b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/PinClientCommand.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CLIENTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientId; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to pin a client entity. + */ +public class PinClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-p"; + + public static final String MESSAGE_PIN_SUCCESS = "Client pinned: %1$s"; + public static final String MESSAGE_UNPIN_SUCCESS = "Client unpinned: %1$s"; + public static final String MESSAGE_CLIENT_NOT_FOUND = "This client id does not exist."; + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Pins the client identified by the client id \n" + + "Parameters: CLIENT_ID (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " 1"; + + private ClientId toPinClientId; + + /** + * Constructor for a command to pin clients. + * + * @param toPinClientId The ID of the client to be pinned. + */ + public PinClientCommand(ClientId toPinClientId) { + requireNonNull(toPinClientId); + this.toPinClientId = toPinClientId; + } + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + if (!model.hasClientId(this.toPinClientId.getIdInt())) { + throw new CommandException(MESSAGE_CLIENT_NOT_FOUND); + } + Client toPinClient = model.getClientById(this.toPinClientId.getIdInt()); + toPinClient.togglePin(); + model.sortClientsByCurrentCategory(); + model.sortClientsByPin(); + ui.showClients(); + model.updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + return new CommandResult(String.format( + toPinClient.isPinned() ? MESSAGE_PIN_SUCCESS : MESSAGE_UNPIN_SUCCESS, + toPinClient)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PinClientCommand)) { + return false; + } + + return this.toPinClientId.equals(((PinClientCommand) other).toPinClientId); + } +} diff --git a/src/main/java/seedu/address/logic/commands/client/SetClientDefaultViewCommand.java b/src/main/java/seedu/address/logic/commands/client/SetClientDefaultViewCommand.java new file mode 100644 index 00000000000..59e2499e3ce --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/SetClientDefaultViewCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands.client; + +import seedu.address.commons.core.DefaultView; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to set the default view to client. + */ +public class SetClientDefaultViewCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-v"; + + public static final String MESSAGE_SUCCESS = "Default view successfully set to clients."; + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + model.setDefaultView(DefaultView.CLIENT); + return new CommandResult(MESSAGE_SUCCESS, false, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/client/SortClientCommand.java b/src/main/java/seedu/address/logic/commands/client/SortClientCommand.java new file mode 100644 index 00000000000..e9296e7b22c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/client/SortClientCommand.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_NAME; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CLIENTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.Model; +import seedu.address.model.SortCategory; +import seedu.address.model.client.Client; +import seedu.address.ui.Ui; + +/** + * Sort clients in project book. + */ +public class SortClientCommand extends ClientCommand { + + public static final String COMMAND_FLAG = "-s"; + + public static final String MESSAGE_SUCCESS = "Sorted clients"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sort clients in project book. \n" + + "Sort by client id: " + + "[" + PREFIX_CLIENT_ID + "0] (ascending) or " + + "[" + PREFIX_CLIENT_ID + "1] (descending)." + + "Sort by name: " + + "[" + PREFIX_NAME + "0] (alphabetical) or " + + "[" + PREFIX_NAME + "1] (reverse alphabetical). \n" + + "Example: " + + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "0"; + + private final Prefix sortKey; + private final int sortOrder; + + /** + * Specifies a sorting client command which has a key and an order to sort by. + * + * @param order is the element to sort by + * @param key is the variant of the element to sort by + */ + public SortClientCommand(Prefix key, int order) { + this.sortKey = key; + this.sortOrder = order; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + String sortKeyString = ""; + Client.setSortOrder(this.sortOrder); + + if (sortKey.equals(PREFIX_CLIENT_ID)) { + model.sortClientsById(sortOrder); + Client.setSortCategory(SortCategory.ID); + sortKeyString = "client id"; + } + + if (sortKey.equals(PREFIX_NAME)) { + model.sortClientsByName(sortOrder); + Client.setSortCategory(SortCategory.NAME); + sortKeyString = "name."; + } + model.sortClientsByPin(); + + ui.showClients(); + model.updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + return new CommandResult(MESSAGE_SUCCESS + " according to " + sortKeyString); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java index a16bd14f2cd..6071fc473ef 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -1,7 +1,7 @@ package seedu.address.logic.commands.exceptions; /** - * Represents an error which occurs during execution of a {@link Command}. + * Represents an error which occurs during execution of a Command. */ public class CommandException extends Exception { public CommandException(String message) { diff --git a/src/main/java/seedu/address/logic/commands/issue/AddIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/AddIssueCommand.java new file mode 100644 index 00000000000..64cd9cc92d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/AddIssueCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_TITLE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_URGENCY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueWithoutModel; +import seedu.address.model.project.ProjectId; +import seedu.address.ui.Ui; + +/** + * Command to add issue + */ +public class AddIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-a"; + + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Adds an issue to the project book. \n" + + "Parameters: " + + PREFIX_PROJECT_ID + "PROJECT_ID " + + PREFIX_TITLE + "TITLE " + + "[" + PREFIX_DEADLINE + "DEADLINE] " + + "[" + PREFIX_URGENCY + "URGENCY(0, 1, 2, 3)] \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_PROJECT_ID + "1 " + + PREFIX_TITLE + "to create a person class which stores all relevant person data " + + PREFIX_DEADLINE + "2022-12-10 " + + PREFIX_URGENCY + "0 "; + + + public static final String MESSAGE_SUCCESS = "New issue added: %1$s"; + public static final String MESSAGE_DUPLICATE_ISSUE = "This issue already exists in the project book"; + public static final String MESSAGE_PROJECT_NOT_FOUND = "This project id does not exist in the project book"; + + // private final Issue toAdd; + private final IssueWithoutModel toAddWithoutModel; + private final ProjectId projectId; + + /** + * Creates an AddCommand to add the specified {@code Issue} + */ + public AddIssueCommand(IssueWithoutModel issueWithoutModel, ProjectId pid) { + requireNonNull(issueWithoutModel); + toAddWithoutModel = issueWithoutModel; + projectId = pid; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + + if (!projectId.isValid()) { + throw new CommandException(ID_OVERFLOW); + } + + if (!model.hasProjectId(projectId.getIdInt())) { + throw new CommandException(MESSAGE_PROJECT_NOT_FOUND); + } + + Issue toAdd = toAddWithoutModel.apply(model); + + if (!toAdd.hasValidId()) { + throw new CommandException(ID_OVERFLOW); + } + + if (model.hasIssue(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_ISSUE); + } + + ui.showIssues(); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + + + model.addIssue(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddIssueCommand)) { + return false; + } + + return this.toAddWithoutModel.equals(((AddIssueCommand) other).toAddWithoutModel); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/issue/DeleteIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/DeleteIssueCommand.java new file mode 100644 index 00000000000..618b6c0f002 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/DeleteIssueCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.issue.Issue; +import seedu.address.ui.Ui; + +/** + * A delete issue command to delete issues + */ +public class DeleteIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-d"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Deletes the issue by its id. Id must be positive and valid \n" + + "Parameters: ISSUE_ID \n" + + "Example: " + COMMAND_WORD + " " + COMMAND_FLAG + " 1"; + + public static final String MESSAGE_SUCCESS = "Deleted Issue: %1$s"; + + private final Index targetIndex; + + public DeleteIssueCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredIssueList(); + + for (Issue i : lastShownList) { + if (i.getIssueIdInInt() == targetIndex.getOneBased()) { + i.deleteProjectIssue(i); + model.deleteIssue(i); + ui.showIssues(); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(String.format(MESSAGE_SUCCESS, i)); + } + } + + + + throw new CommandException(Messages.MESSAGE_INVALID_ISSUE_DISPLAYED_ID); + + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof DeleteIssueCommand + && targetIndex.equals(((DeleteIssueCommand) other).targetIndex)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/issue/EditIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/EditIssueCommand.java new file mode 100644 index 00000000000..ce93025f0c8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/EditIssueCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands.issue; + +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_ISSUE_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_TITLE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_URGENCY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Deadline; +import seedu.address.model.Model; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Title; +import seedu.address.model.issue.Urgency; +import seedu.address.ui.Ui; + +/** + * Edit issue command to edit issues + */ +public class EditIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-e"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Edits an issue in the project book. \n" + + "Parameters: " + + PREFIX_ISSUE_ID + "ISSUE_ID " + + "[" + PREFIX_TITLE + "TITLE] " + + "[" + PREFIX_DEADLINE + "DEADLINE] " + + "[" + PREFIX_URGENCY + "URGENCY(0, 1, 2, 3)] " + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_ISSUE_ID + "1 " + + PREFIX_TITLE + "To Edit Class " + + PREFIX_DEADLINE + "2022-03-05 " + + PREFIX_URGENCY + "1 "; + + public static final String MESSAGE_ISSUE_NOT_FOUND = "Issue id %1$d does not exist in the project book"; + + public static final String MESSAGE_SUCCESS = "Issue %1$s has been edited"; + private final Title newTitle; + private final Urgency newUrgency; + private final Deadline newDeadline; + private final IssueId issueId; + + + /** + * Creates an EditIssueCommand to edit the specified {@code Issue} + */ + public EditIssueCommand(Title newTitle, Deadline newDeadline, Urgency newUrgency, IssueId issueId) { + // NULL values passed into constructor here represent absent optional inputs + this.newTitle = newTitle; + this.newDeadline = newDeadline; + this.newUrgency = newUrgency; + this.issueId = issueId; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + ui.showIssues(); + if (!HasIntegerIdentifier.containsId(model.getFilteredIssueList(), issueId.getIdInt())) { + throw new CommandException(String.format(MESSAGE_ISSUE_NOT_FOUND, issueId.getIdInt())); + } + Issue toEditIssue = model.getIssueById(issueId.getIdInt()); + + if (newTitle != null) { + toEditIssue.setTitle(newTitle); + } + + if (newDeadline != null) { + toEditIssue.setDeadline(newDeadline); + } + + if (newUrgency != null) { + toEditIssue.setUrgency(newUrgency); + } + + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(String.format(MESSAGE_SUCCESS, toEditIssue)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/issue/FindIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/FindIssueCommand.java new file mode 100644 index 00000000000..b0e43af6881 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/FindIssueCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_ISSUE_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_PROJECT_NAME; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_STATUS; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_TITLE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_URGENCY; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.parser.predicates.IssueContainsKeywordsPredicate; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Represents a class to find and filter issue list. + */ +public class FindIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-f"; + public static final String MESSAGE_SUCCESS = "Filtered list of issues shown."; + private static final String MESSAGE_ISSUE_NOT_FOUND = "An issue matching requirements not found."; + + public static final String MESSAGE_FIND_ISSUE_USAGE = COMMAND_WORD + " " + COMMAND_FLAG + + ": Finds issues by keyword.\n" + + "Parameters: " + + "[" + PREFIX_TITLE + "TITLE] " + + "[" + PREFIX_STATUS + "STATUS] " + + "[" + PREFIX_URGENCY + "URGENCY] " + + "[" + PREFIX_PROJECT_NAME + "PROJECT NAME] " + + "[" + PREFIX_PROJECT_ID + "PROJECT ID] " + + "[" + PREFIX_ISSUE_ID + "ISSUE ID] " + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_TITLE + "This is an issue " + + PREFIX_STATUS + "COMPLETED " + + PREFIX_URGENCY + "HIGH " + + PREFIX_PROJECT_NAME + "DevEnable " + + PREFIX_PROJECT_ID + "1 " + + PREFIX_ISSUE_ID + "3 "; + + private final IssueContainsKeywordsPredicate predicate; + + public FindIssueCommand(IssueContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showIssues(); + model.updateFilteredIssueList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_ISSUES_LISTED_OVERVIEW, model.getFilteredIssueList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindIssueCommand // instanceof handles nulls + && predicate.equals(((FindIssueCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/IssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/IssueCommand.java new file mode 100644 index 00000000000..b5792230c23 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/IssueCommand.java @@ -0,0 +1,11 @@ +package seedu.address.logic.commands.issue; + +import seedu.address.logic.commands.Command; + +/** + * Abstract base command for issues + */ +public abstract class IssueCommand extends Command { + public static final String COMMAND_WORD = "issue"; + +} diff --git a/src/main/java/seedu/address/logic/commands/issue/ListIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/ListIssueCommand.java new file mode 100644 index 00000000000..5ae4676c955 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/ListIssueCommand.java @@ -0,0 +1,27 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Lists all persons in the project book to the user. + */ +public class ListIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-l"; + + public static final String MESSAGE_SUCCESS = "Listed all issues in the project book"; + + // TODO: implement + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showIssues(); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/MarkIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/MarkIssueCommand.java new file mode 100644 index 00000000000..436e5f1dadc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/MarkIssueCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands.issue; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_ISSUE_NOT_FOUND; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Status; +import seedu.address.ui.Ui; + +/** + * Command to mark an issue as complete + */ +public class MarkIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-m"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Marks the issue identified by the issue id as complete \n" + + "Parameters: ISSUE_ID (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " 1"; + + + public static final String MESSAGE_SUCCESS = "Issue marked completed: %1$s"; + + private final Status newStatus; + private final IssueId issueId; + + /** + * Creates a MarkIssueCommand to mark the specified {@code Issue} as complete + */ + public MarkIssueCommand(Status newStatus, IssueId issueId) { + requireAllNonNull(newStatus, issueId); + this.newStatus = newStatus; + this.issueId = issueId; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + ui.showIssues(); + if (!model.hasIssueId(this.issueId.getIdInt())) { + throw new CommandException(MESSAGE_ISSUE_NOT_FOUND); + } + Issue toMarkIssue = model.getIssueById(issueId.getIdInt()); + toMarkIssue.setStatus(newStatus); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(String.format(MESSAGE_SUCCESS, toMarkIssue)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/PinIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/PinIssueCommand.java new file mode 100644 index 00000000000..4acd0b69a1d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/PinIssueCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to pin a issue entity. + */ +public class PinIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-p"; + + public static final String MESSAGE_PIN_SUCCESS = "Issue pinned: %1$s"; + public static final String MESSAGE_UNPIN_SUCCESS = "Issue unpinned: %1$s"; + public static final String MESSAGE_ISSUE_NOT_FOUND = "This issue id does not exist."; + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Pins the issue identified by the issue id \n" + + "Parameters: ISSUE_ID (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " 1"; + + private IssueId toPinIssueId; + + /** + * Constructor for a command to pin an issue to the list. + * @param toPinIssueId The ID of the issue to be pinned. + */ + public PinIssueCommand(IssueId toPinIssueId) { + requireNonNull(toPinIssueId); + this.toPinIssueId = toPinIssueId; + } + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + if (!model.hasIssueId(this.toPinIssueId.getIdInt())) { + throw new CommandException(MESSAGE_ISSUE_NOT_FOUND); + } + Issue toPinIssue = model.getIssueById(this.toPinIssueId.getIdInt()); + toPinIssue.togglePin(); + model.sortIssuesByCurrentCategory(); + model.sortIssuesByPin(); + ui.showIssues(); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(String.format( + toPinIssue.isPinned() ? MESSAGE_PIN_SUCCESS : MESSAGE_UNPIN_SUCCESS, + toPinIssue)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PinIssueCommand)) { + return false; + } + + return this.toPinIssueId.equals(((PinIssueCommand) other).toPinIssueId); + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/SetIssueDefaultViewCommand.java b/src/main/java/seedu/address/logic/commands/issue/SetIssueDefaultViewCommand.java new file mode 100644 index 00000000000..5a59a1f34f2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/SetIssueDefaultViewCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands.issue; + +import seedu.address.commons.core.DefaultView; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to set the default view to issue. + */ +public class SetIssueDefaultViewCommand extends IssueCommand { + public static final String COMMAND_FLAG = "-v"; + + public static final String MESSAGE_SUCCESS = "Default view successfully set to issues."; + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + model.setDefaultView(DefaultView.ISSUE); + return new CommandResult(MESSAGE_SUCCESS, false, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/SortIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/SortIssueCommand.java new file mode 100644 index 00000000000..ca20f7dd0be --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/SortIssueCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_ISSUE_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_URGENCY; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_DEADLINE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.Model; +import seedu.address.model.SortCategory; +import seedu.address.model.issue.Issue; +import seedu.address.ui.Ui; + +/** + * Sort issues in project book. + */ +public class SortIssueCommand extends IssueCommand { + + public static final String COMMAND_FLAG = "-s"; + + public static final String MESSAGE_SUCCESS = "Sorted issues"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_FLAG + + ": Sort issues in project book. \n" + + "Sort by issue id: " + + "[" + PREFIX_ISSUE_ID + "0] (ascending) or " + + "[" + PREFIX_ISSUE_ID + "1] (descending). " + + "Sort by deadline: " + + "[" + PREFIX_DEADLINE + "0] (chronological) or " + + "[" + PREFIX_DEADLINE + "1] (reverse chronological). " + + "Sort by urgency: " + + "[" + PREFIX_URGENCY + "0] (ascending) or " + + "[" + PREFIX_URGENCY + "1] (descending). " + + "Example: " + + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_URGENCY + "0"; + + private final Prefix sortKey; + private final int sortOrder; + + /** + * Specifies a sorting issue command which has a key and an order to sort by. + * + * @param order is the element to sort by + * @param key is the variant of the element to sort by + */ + public SortIssueCommand(Prefix key, int order) { + this.sortKey = key; + this.sortOrder = order; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + String sortKeyString = ""; + Issue.setSortOrder(this.sortOrder); + + if (sortKey.equals(PREFIX_ISSUE_ID)) { + model.sortIssuesById(sortOrder); + Issue.setSortCategory(SortCategory.ID); + sortKeyString = "issue id."; + } + + if (sortKey.equals(PREFIX_DEADLINE)) { + model.sortIssuesByDeadline(sortOrder); + Issue.setSortCategory(SortCategory.DEADLINE); + sortKeyString = "deadline."; + } + + if (sortKey.equals(PREFIX_URGENCY)) { + model.sortIssuesByUrgency(sortOrder); + Issue.setSortCategory(SortCategory.URGENCY); + sortKeyString = "urgency."; + } + + model.sortIssuesByPin(); + + ui.showIssues(); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(MESSAGE_SUCCESS + " according to " + sortKeyString); + } +} diff --git a/src/main/java/seedu/address/logic/commands/issue/UnmarkIssueCommand.java b/src/main/java/seedu/address/logic/commands/issue/UnmarkIssueCommand.java new file mode 100644 index 00000000000..f7fc0990069 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/issue/UnmarkIssueCommand.java @@ -0,0 +1,56 @@ +package seedu.address.logic.commands.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_ISSUE_NOT_FOUND; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ISSUES; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Status; +import seedu.address.ui.Ui; + +/** + * Command to mark an issue as incomplete + */ +public class UnmarkIssueCommand extends IssueCommand { + public static final String COMMAND_FLAG = "-u"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Marks the issue identified by the issue id as incomplete \n" + + "Parameters: ISSUE_ID (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " 1"; + + + public static final String MESSAGE_SUCCESS = "Issue marked incomplete: %1$s"; + + private final Status newStatus; + private final IssueId issueId; + + /** + * Creates an UnmarkIssueCommand to mark the specified {@code Issue} as incomplete + */ + public UnmarkIssueCommand(Status newStatus, IssueId issueId) { + requireAllNonNull(newStatus, issueId); + this.newStatus = newStatus; + this.issueId = issueId; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + ui.showIssues(); + if (!model.hasIssueId(this.issueId.getIdInt())) { + throw new CommandException(MESSAGE_ISSUE_NOT_FOUND); + } + Issue toMarkIssue = model.getIssueById(issueId.getIdInt()); + toMarkIssue.setStatus(newStatus); + model.updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + return new CommandResult(String.format(MESSAGE_SUCCESS, toMarkIssue)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/AddProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/AddProjectCommand.java new file mode 100644 index 00000000000..38160d7492a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/AddProjectCommand.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_REPOSITORY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectWithoutModel; +import seedu.address.ui.Ui; + +/** + * Adds a project to the project book. + */ +public class AddProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-a"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Adds a project to the project book. \n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + "[" + PREFIX_CLIENT_ID + "CLIENT_ID] " + + "[" + PREFIX_REPOSITORY + "REPOSITORY] " + + "[" + PREFIX_DEADLINE + "DEADLINE] \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "John " + + PREFIX_CLIENT_ID + "1 " + + PREFIX_REPOSITORY + "JohnDoe/tp " + + PREFIX_DEADLINE + "2022-03-05 "; + + public static final String MESSAGE_SUCCESS = "New project added: %1$s"; + public static final String MESSAGE_DUPLICATE_PROJECT = "This project already exists in the project book"; + public static final String MESSAGE_CLIENT_NOT_FOUND = "This client id does not exist in the project book"; + + private final ProjectWithoutModel toAddProjectWithoutModel; + + /** + * Creates an AddProjectCommand to add the specified {@code Project} + */ + public AddProjectCommand(ProjectWithoutModel projectWithoutModel) { + requireNonNull(projectWithoutModel); + toAddProjectWithoutModel = projectWithoutModel; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + + Project toAddProject = toAddProjectWithoutModel.apply(model); + + if (!toAddProject.hasValidId()) { + throw new CommandException(ID_OVERFLOW); + } + + if (model.hasProject(toAddProject)) { + throw new CommandException(MESSAGE_DUPLICATE_PROJECT); + } + + Client projectClient = toAddProject.getClient(); + + if (projectClient == null) { + throw new CommandException(MESSAGE_CLIENT_NOT_FOUND); + } + + if (!projectClient.isEmpty()) { + projectClient.addProjects(toAddProject); + model.setClient(projectClient, projectClient); + } + + model.addProject(toAddProject); + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAddProject)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof AddProjectCommand)) { + return false; + } + + return this.toAddProjectWithoutModel.equals(((AddProjectCommand) other).toAddProjectWithoutModel); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/DeleteProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/DeleteProjectCommand.java new file mode 100644 index 00000000000..9c12d2d0f91 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/DeleteProjectCommand.java @@ -0,0 +1,81 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; +import seedu.address.ui.Ui; + +/** + * Deletes a project in the project book. + */ +public class DeleteProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-d"; + + public static final String MESSAGE_SUCCESS = "Deleted Project"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Deletes the project by its id. Id must be positive and valid \n" + + "Parameters: PROJECT ID \n" + + "Example: " + COMMAND_WORD + " " + COMMAND_FLAG + " 1"; + + public static final String MESSAGE_DELETE_PROJECT_SUCCESS = "Deleted Project: %1$s"; + + public final Index targetIndex; + + public DeleteProjectCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredProjectList(); + + for (Project p : lastShownList) { + if (p.getProjectIdInInt() == targetIndex.getOneBased()) { + List listOfIssuesToDelete = p.getIssueList(); + for (Issue i : listOfIssuesToDelete) { + model.deleteIssue(i); + } + + Client projectClient = p.getClient(); + if (!projectClient.isEmpty()) { + Client clientInList = model.getClientById(projectClient.getClientIdInInt()); + if (!clientInList.isEmpty()) { + clientInList.removeProject(p); + if (clientInList.getProjectListSize() == 0) { + model.deleteClient(clientInList); + } + } + } + + model.deleteProject(p); + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + return new CommandResult(String.format(MESSAGE_DELETE_PROJECT_SUCCESS, p)); + } + } + + throw new CommandException(Messages.MESSAGE_INVALID_PROJECT_DISPLAYED_ID); + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteProjectCommand // instanceof handles nulls + && targetIndex.equals(((DeleteProjectCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/EditProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/EditProjectCommand.java new file mode 100644 index 00000000000..966a090f1a8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/EditProjectCommand.java @@ -0,0 +1,125 @@ +package seedu.address.logic.commands.project; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_REPOSITORY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Deadline; +import seedu.address.model.Model; +import seedu.address.model.Name; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientId; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.list.NotFoundException; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.model.project.Repository; +import seedu.address.ui.Ui; + +/** + * Edit project command to edit projects + */ +public class EditProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-e"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Edits a project in the project book. \n" + + "Parameters: " + + PREFIX_PROJECT_ID + "PROJECT_ID " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_CLIENT_ID + "CLIENT_ID] " + + "[" + PREFIX_REPOSITORY + "REPOSITORY] " + + "[" + PREFIX_DEADLINE + "DEADLINE] \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_PROJECT_ID + "1 " + + PREFIX_NAME + "John " + + PREFIX_CLIENT_ID + "1 " + + PREFIX_REPOSITORY + "JohnDoe/tp " + + PREFIX_DEADLINE + "2022-03-05 "; + + public static final String MESSAGE_SUCCESS = "Project %1$s has been edited"; + public static final String MESSAGE_INVALID_CLIENT = "This client id does not exist in the project book"; + public static final String MESSAGE_PROJECT_NOT_FOUND = "Project id %1$d does not exist in the project book"; + public static final String MESSAGE_DUPLICATE_PROJECT_NAME = "A project with this name already " + + "exists in the project book"; + public static final String MESSAGE_PROJECT_ALREADY_HAS_THAT_NAME = "This project already has that name"; + + private final ProjectId projectToEditId; + private final Name newName; + private final ClientId newClientId; + private final Repository newRepository; + private final Deadline newDeadline; + + /** + * Creates an EditProjectCommand to edit the specified {@code Project} + */ + public EditProjectCommand( + ProjectId projectToEditId, Name newName, ClientId newClientId, + Repository newRepository, Deadline newDeadline) { + // NULL values passed into constructor here represent absent optional inputs + this.projectToEditId = projectToEditId; + this.newName = newName; + this.newClientId = newClientId; + this.newRepository = newRepository; + this.newDeadline = newDeadline; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + + if (!HasIntegerIdentifier.containsId(model.getFilteredProjectList(), projectToEditId.getIdInt())) { + throw new CommandException(String.format(MESSAGE_PROJECT_NOT_FOUND, projectToEditId.getIdInt())); + } + + Project toEditProject = model.getProjectById(projectToEditId.getIdInt()); + + if (newName != null) { + for (Project p : model.getFilteredProjectList()) { + if (p.getProjectName().equals(newName)) { + if (toEditProject.getProjectName().equals(newName)) { + throw new CommandException(MESSAGE_PROJECT_ALREADY_HAS_THAT_NAME); + } + throw new CommandException(MESSAGE_DUPLICATE_PROJECT_NAME); + } + } + } + + if (newClientId != null) { + try { + Client newClient = model.getClientById(newClientId.getIdInt()); + Client oldClient = toEditProject.getClient(); + toEditProject.setClient(newClient); + oldClient.removeProject(toEditProject); + newClient.addProjects(toEditProject); + } catch (NotFoundException e) { + throw new CommandException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditProjectCommand.MESSAGE_INVALID_CLIENT)); + } + } + + if (newName != null) { + toEditProject.setName(newName); + } + + if (newRepository != null) { + toEditProject.setRepository(newRepository); + } + + if (newDeadline != null) { + toEditProject.setDeadline(newDeadline); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, toEditProject)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/FindProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/FindProjectCommand.java new file mode 100644 index 00000000000..5ae1cfa8ac4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/FindProjectCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_LABEL; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_REPOSITORY; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.parser.predicates.ProjectContainsKeywordsPredicate; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Represents a class to find and filter project list. + */ +public class FindProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-f"; + public static final String MESSAGE_SUCCESS = "Filtered list of projects shown."; + private static final String MESSAGE_PROJECT_NOT_FOUND = "A project matching requirements not found."; + + public static final String MESSAGE_FIND_PROJECT_USAGE = COMMAND_WORD + " " + COMMAND_FLAG + + ": Finds projects by keyword.\n" + + "Parameters: " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PROJECT_ID + "PROJECT ID] " + + "[" + PREFIX_REPOSITORY + "REPOSITORY] " + + "[" + PREFIX_CLIENT_LABEL + "CLIENT LABEL] " + + "[" + PREFIX_CLIENT_ID + "CLIENT ID] \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_NAME + "DevEnable " + + PREFIX_PROJECT_ID + "2 " + + PREFIX_REPOSITORY + "tp/devenable " + + PREFIX_CLIENT_LABEL + "Amy " + + PREFIX_CLIENT_ID + "1 "; + + private static ProjectContainsKeywordsPredicate predicate; + + public FindProjectCommand(ProjectContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showProjects(); + model.updateFilteredProjectList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PROJECTS_LISTED_OVERVIEW, model.getFilteredProjectList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindProjectCommand // instanceof handles nulls + && predicate.equals(((FindProjectCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/ListProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/ListProjectCommand.java new file mode 100644 index 00000000000..3a591dfbe2a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/ListProjectCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Lists all projects in the project book to the user. + */ +public class ListProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-l"; + + public static final String MESSAGE_SUCCESS = "Listed all projects in the project book"; + + @Override + public CommandResult execute(Model model, Ui ui) { + requireNonNull(model); + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/PinProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/PinProjectCommand.java new file mode 100644 index 00000000000..64ffb12702d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/PinProjectCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to pin a project entity. + */ +public class PinProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-p"; + + public static final String MESSAGE_PIN_SUCCESS = "Project pinned: %1$s"; + public static final String MESSAGE_UNPIN_SUCCESS = "Project unpinned: %1$s"; + public static final String MESSAGE_PROJECT_NOT_FOUND = "This project id does not exist."; + public static final String MESSAGE_USAGE = COMMAND_WORD + + " " + COMMAND_FLAG + + ": Pins the project identified by the project id \n" + + "Parameters: PROJECT_ID (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " " + + COMMAND_FLAG + " 1"; + + private ProjectId toPinProjectId; + + /** + * Constructor for a command to pin a project to the list. + * @param toPinProjectId The ID of the project to be pinned. + */ + public PinProjectCommand(ProjectId toPinProjectId) { + requireNonNull(toPinProjectId); + this.toPinProjectId = toPinProjectId; + } + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + if (!model.hasProjectId(this.toPinProjectId.getIdInt())) { + throw new CommandException(MESSAGE_PROJECT_NOT_FOUND); + } + Project toPinProject = model.getProjectById(this.toPinProjectId.getIdInt()); + toPinProject.togglePin(); + model.sortProjectsByCurrentCategory(); + model.sortProjectsByPin(); + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + return new CommandResult(String.format( + toPinProject.isPinned() ? MESSAGE_PIN_SUCCESS : MESSAGE_UNPIN_SUCCESS, + toPinProject)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PinProjectCommand)) { + return false; + } + + return this.toPinProjectId.equals(((PinProjectCommand) other).toPinProjectId); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/ProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/ProjectCommand.java new file mode 100644 index 00000000000..523368d0138 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/ProjectCommand.java @@ -0,0 +1,11 @@ +package seedu.address.logic.commands.project; + +import seedu.address.logic.commands.Command; + +/** + * Base abstract class for all project commands + */ +public abstract class ProjectCommand extends Command { + public static final String COMMAND_WORD = "project"; + +} diff --git a/src/main/java/seedu/address/logic/commands/project/SetProjectDefaultViewCommand.java b/src/main/java/seedu/address/logic/commands/project/SetProjectDefaultViewCommand.java new file mode 100644 index 00000000000..d8693b16591 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/SetProjectDefaultViewCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands.project; + +import seedu.address.commons.core.DefaultView; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.ui.Ui; + +/** + * Encapsulates a command to set the default view to project. + */ +public class SetProjectDefaultViewCommand extends ProjectCommand { + public static final String COMMAND_FLAG = "-v"; + + public static final String MESSAGE_SUCCESS = "Default view successfully set to projects."; + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param ui + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + model.setDefaultView(DefaultView.PROJECT); + return new CommandResult(MESSAGE_SUCCESS, false, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/project/SortProjectCommand.java b/src/main/java/seedu/address/logic/commands/project/SortProjectCommand.java new file mode 100644 index 00000000000..8b92553346a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/project/SortProjectCommand.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_ISSUE_COUNT; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PROJECTS; + +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.Model; +import seedu.address.model.SortCategory; +import seedu.address.model.project.Project; +import seedu.address.ui.Ui; + +/** + * Sort projects in project book. + */ +public class SortProjectCommand extends ProjectCommand { + + public static final String COMMAND_FLAG = "-s"; + + public static final String MESSAGE_SUCCESS = "Sorted projects"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " " + COMMAND_FLAG + + ": Sort projects in project book. \n" + + "Sort by project id: " + + "[" + PREFIX_PROJECT_ID + "0] (ascending) or " + + "[" + PREFIX_PROJECT_ID + "1] (descending). " + + "Sort by deadline: " + + "[" + PREFIX_DEADLINE + "0] (chronological) or " + + "[" + PREFIX_DEADLINE + "1] (reverse chronological). " + + "Sort by issue count: " + + "[" + PREFIX_ISSUE_COUNT + "0] (incomplete) or " + + "[" + PREFIX_ISSUE_COUNT + "1] (completed). " + + "Sort by name: " + + "[" + PREFIX_NAME + "0] (alphabetical) or " + + "[" + PREFIX_NAME + "1] (reverse alphabetical). \n" + + "Example: " + + COMMAND_WORD + " " + + COMMAND_FLAG + " " + + PREFIX_DEADLINE + "0"; + + private final Prefix sortKey; + private final int sortOrder; + + /** + * Specifies a sorting project command which has a key and an order to sort by. + * + * @param order is the element to sort by + * @param key is the variant of the element to sort by + */ + public SortProjectCommand(Prefix key, int order) { + this.sortKey = key; + this.sortOrder = order; + } + + @Override + public CommandResult execute(Model model, Ui ui) throws CommandException { + requireNonNull(model); + String sortKeyString = ""; + Project.setSortOrder(this.sortOrder); + + if (sortKey.equals(PREFIX_DEADLINE)) { + model.sortProjectsByDeadline(sortOrder); + Project.setSortCategory(SortCategory.DEADLINE); + sortKeyString = "deadline."; + } + + if (sortKey.equals(PREFIX_ISSUE_COUNT)) { + model.sortProjectsByIssueCount(sortOrder); + Project.setSortCategory(SortCategory.ISSUE_COUNT); + sortKeyString = "issue count."; + } + + if (sortKey.equals(PREFIX_NAME)) { + model.sortProjectsByName(sortOrder); + Project.setSortCategory(SortCategory.NAME); + sortKeyString = "names."; + } + + if (sortKey.equals(PREFIX_PROJECT_ID)) { + model.sortProjectsById(sortOrder); + Project.setSortCategory(SortCategory.ID); + sortKeyString = "project id."; + } + model.sortProjectsByPin(); + + model.sortProjectsByPin(); + + ui.showProjects(); + model.updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + return new CommandResult(MESSAGE_SUCCESS + " according to " + sortKeyString); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e8..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public 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) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..100e86843bd 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -6,17 +6,17 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.client.ClientCommand; +import seedu.address.logic.commands.issue.IssueCommand; +import seedu.address.logic.commands.project.ProjectCommand; import seedu.address.logic.parser.exceptions.ParseException; + + /** * Parses user input. */ @@ -25,7 +25,8 @@ public class AddressBookParser { /** * Used for initial separation of command word and args. */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + private static final Pattern BASIC_COMMAND_FORMAT = + Pattern.compile("(?\\w+)(?(\\s+-\\w+)?)(?.*)"); /** * Parses user input into command for execution. @@ -41,27 +42,20 @@ public Command parseCommand(String userInput) throws ParseException { } final String commandWord = matcher.group("commandWord"); + final String flag = matcher.group("flag").trim(); final String arguments = matcher.group("arguments"); switch (commandWord) { + case ClientCommand.COMMAND_WORD: + return new ClientCommandParser().parse(flag, arguments); - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case IssueCommand.COMMAND_WORD: + return new IssueCommandParser().parse(flag, arguments); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case ProjectCommand.COMMAND_WORD: + return new ProjectCommandParser().parse(flag, arguments); case ClearCommand.COMMAND_WORD: return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - case ExitCommand.COMMAND_WORD: return new ExitCommand(); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 954c8e18f8e..96cf5eadcef 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * Stores mapping of prefixes to their respective arguments. @@ -39,6 +40,15 @@ public Optional getValue(Prefix prefix) { return values.isEmpty() ? Optional.empty() : Optional.of(values.get(values.size() - 1)); } + /** + * Returns the first word of the last value of {@code prefix}. + */ + public Optional getFirstWordValue(Prefix prefix) { + List values = getAllValues(prefix); + return values.isEmpty() + ? Optional.empty() : Optional.of(ParserUtil.getFirstWord(values.get(values.size() - 1))); + } + /** * Returns all values of {@code prefix}. * If the prefix does not exist or has no values, this will return an empty list. @@ -51,6 +61,19 @@ public List getAllValues(Prefix prefix) { return new ArrayList<>(argMultimap.get(prefix)); } + /** + * Returns all values of {@code prefix}, but only their first word. + * If the prefix does not exist or has no values, this will return an empty list. + * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. + */ + public List getAllFirstWordValues(Prefix prefix) { + if (!argMultimap.containsKey(prefix)) { + return new ArrayList<>(); + } + return argMultimap.get(prefix).stream().map((s) -> s.split(" ", 2)[0]) + .collect(Collectors.toList()); + } + /** * Returns the preamble (text before the first valid prefix). Trims any leading/trailing spaces. */ diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf119..00000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -public class CliSyntax { - - /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/ClientCliSyntax.java b/src/main/java/seedu/address/logic/parser/ClientCliSyntax.java new file mode 100644 index 00000000000..51825bc2fe1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClientCliSyntax.java @@ -0,0 +1,16 @@ +package seedu.address.logic.parser; + +/** + * Represents Prefix definitions for client commands. + */ +public class ClientCliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_MOBILE = new Prefix("m/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_PROJECT_ID = new Prefix("p/"); + public static final Prefix PREFIX_CLIENT_ID = new Prefix("c/"); + + +} diff --git a/src/main/java/seedu/address/logic/parser/ClientCommandParser.java b/src/main/java/seedu/address/logic/parser/ClientCommandParser.java new file mode 100644 index 00000000000..30c13ed18a8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClientCommandParser.java @@ -0,0 +1,332 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.FLAG_UNKNOWN_COMMAND; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_MISSING_ARGUMENTS; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_MOBILE; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ClientCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.ParserUtil.parseEmail; +import static seedu.address.logic.parser.ParserUtil.parseEmailValidity; +import static seedu.address.logic.parser.ParserUtil.parseIndexValidity; +import static seedu.address.logic.parser.ParserUtil.parseMobile; +import static seedu.address.logic.parser.ParserUtil.parseMobileValidity; +import static seedu.address.logic.parser.ParserUtil.parseName; +import static seedu.address.logic.parser.ParserUtil.parseNameValidity; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; + +import java.util.ArrayList; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.client.AddClientCommand; +import seedu.address.logic.commands.client.ClientCommand; +import seedu.address.logic.commands.client.DeleteClientCommand; +import seedu.address.logic.commands.client.EditClientCommand; +import seedu.address.logic.commands.client.FindClientCommand; +import seedu.address.logic.commands.client.ListClientCommand; +import seedu.address.logic.commands.client.PinClientCommand; +import seedu.address.logic.commands.client.SetClientDefaultViewCommand; +import seedu.address.logic.commands.client.SortClientCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.predicates.ClientContainsKeywordsPredicate; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.ClientEmail; +import seedu.address.model.client.ClientId; +import seedu.address.model.client.ClientMobile; +import seedu.address.model.client.ClientWithoutModel; +import seedu.address.model.project.ProjectId; + +/** + * Parser to parse any commands related to Client + */ +public class ClientCommandParser implements Parser { + /** + * Method to parse any commands that have to do with client (start with 'client) + * + * @param flag the flag used in the command + * @param arguments arguments used in the command + * @return a ClientCommand + * @throws ParseException + */ + @Override + public ClientCommand parse(String flag, String arguments) throws ParseException { + // Strip is just there for good measure. + switch (flag.strip()) { + case AddClientCommand.COMMAND_FLAG: + return parseAddClientCommand(arguments); + case EditClientCommand.COMMAND_FLAG: + return parseEditClientCommand(arguments); + case DeleteClientCommand.COMMAND_FLAG: + return parseDeleteClientCommand(arguments); + case ListClientCommand.COMMAND_FLAG: + return parseListClientCommand(arguments); + case SetClientDefaultViewCommand.COMMAND_FLAG: + return parseSetClientDefaultViewCommand(arguments); + case SortClientCommand.COMMAND_FLAG: + return parseSortClientCommand(arguments); + case FindClientCommand.COMMAND_FLAG: + return parseFindClientCommand(arguments); + case PinClientCommand.COMMAND_FLAG: + return parsePinClientCommand(arguments); + default: + throw new ParseException(FLAG_UNKNOWN_COMMAND); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Verifies only one valid user input argument + * Length of a valid command for sort key for issue by name e.g.n/1 + * + * @param arguments user input for key for sort + * @return true if there is only one valid input + */ + private boolean hasOneArgumentOfLengthThree(String arguments) { + return arguments.trim().length() == 3; + } + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * + * @param arguments string of arguments + * @return an AddClientCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private AddClientCommand parseAddClientCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_NAME, PREFIX_MOBILE, + PREFIX_EMAIL, PREFIX_PROJECT_ID); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PROJECT_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddClientCommand.MESSAGE_ADD_CLIENT_USAGE)); + } + + Name name = parseName(argMultimap.getValue(PREFIX_NAME).get()); + + + ClientMobile mobile = ClientMobile.EmptyClientMobile.EMPTY_MOBILE; + if (arePrefixesPresent(argMultimap, PREFIX_MOBILE)) { + mobile = ParserUtil.parseMobile(argMultimap.getValue(PREFIX_MOBILE).get()); + } + + ClientEmail email = ClientEmail.EmptyEmail.EMPTY_EMAIL; + if (arePrefixesPresent(argMultimap, PREFIX_EMAIL)) { + email = parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } + + ClientWithoutModel clientWithoutModel = new ClientWithoutModel(name, mobile, email, + new ArrayList<>(), new Pin(false)); + + // + ProjectId projectId = + ParserUtil.parseProjectId(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + + return new AddClientCommand(clientWithoutModel, projectId); + } + + /** + * Parse a string of arguments for an edit client command + * From original AB3 code + * + * @param arguments a string of arguments + * @return an EditClientCommand object + * @throws ParseException + */ + private EditClientCommand parseEditClientCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_CLIENT_ID, PREFIX_NAME, + PREFIX_EMAIL, PREFIX_MOBILE); + + if (!arePrefixesPresent(argMultimap, PREFIX_CLIENT_ID) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditClientCommand.MESSAGE_USAGE)); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + parseIndexValidity(argMultimap.getValue(PREFIX_CLIENT_ID).get()); + } + + Name newName = null; + ClientEmail newEmail = null; + ClientMobile newMobile = null; + ClientId newClientId = ParserUtil.parseClientId(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + + if (!anyPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_EMAIL, PREFIX_MOBILE)) { + throw new ParseException(String.format(MESSAGE_MISSING_ARGUMENTS, + EditClientCommand.MESSAGE_USAGE)); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_NAME)) { + parseNameValidity(argMultimap.getValue(PREFIX_NAME).get()); + newName = parseName(argMultimap.getValue(PREFIX_NAME).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_MOBILE)) { + parseMobileValidity(argMultimap.getValue(PREFIX_MOBILE).get()); + newMobile = parseMobile(argMultimap.getValue(PREFIX_MOBILE).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_EMAIL)) { + parseEmailValidity(argMultimap.getValue(PREFIX_EMAIL).get()); + newEmail = parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } + + return new EditClientCommand(newClientId, newName, newEmail, newMobile); + } + + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * From original AB3 code + * + * @param arguments string of arguments + * @return return statement + * @throws ParseException if the user input does not conform the expected format + */ + private DeleteClientCommand parseDeleteClientCommand(String arguments) throws ParseException { + try { + Index index = ParserUtil.parseIndex(arguments); + return new DeleteClientCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteClientCommand.MESSAGE_USAGE), pe); + } + } + + private ListClientCommand parseListClientCommand(String args) throws ParseException { + return new ListClientCommand(); + } + + private ClientCommand parseSetClientDefaultViewCommand(String arguments) { + return new SetClientDefaultViewCommand(); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param arguments string of arguments + * @return an FindClientCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private FindClientCommand parseFindClientCommand(String arguments) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_NAME, PREFIX_EMAIL, PREFIX_MOBILE, PREFIX_CLIENT_ID); + + if (noPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_EMAIL, PREFIX_MOBILE, PREFIX_CLIENT_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindClientCommand.MESSAGE_FIND_CLIENT_USAGE)); + } + + //check for validity of arguments + + if (anyPrefixesPresent(argMultimap, PREFIX_NAME)) { + parseNameValidity(argMultimap.getValue(PREFIX_NAME).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_MOBILE)) { + parseMobileValidity(argMultimap.getValue(PREFIX_MOBILE).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_EMAIL)) { + parseEmailValidity(argMultimap.getValue(PREFIX_EMAIL).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + parseIndexValidity(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + } + + ClientContainsKeywordsPredicate predicate = + new ClientContainsKeywordsPredicate(argMultimap.getAllValues(PREFIX_NAME), + argMultimap.getAllValues(PREFIX_EMAIL), + argMultimap.getAllValues(PREFIX_MOBILE), + argMultimap.getAllFirstWordValues(PREFIX_CLIENT_ID)); + + return new FindClientCommand(predicate); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param flag String representing command flag + * @param arguments String representing arguments + * @return an FindClientCommand object + * @throws ParseException if the user input does not conform the expected format + */ + public FindClientCommand parseFindClientCommands(String flag, String arguments) throws ParseException { + return parseFindClientCommand(arguments); + } + + private SortClientCommand parseSortClientCommand(String arguments) throws ParseException { + + Prefix sortPrefix = null; + int key = -1; + + if (!hasOneArgumentOfLengthThree(arguments)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortClientCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(arguments, PREFIX_CLIENT_ID, PREFIX_NAME); + + if (!anyPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_CLIENT_ID)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortClientCommand.MESSAGE_USAGE)); + } + + if (arePrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + sortPrefix = PREFIX_CLIENT_ID; + key = ParserUtil.parseClientNameSort(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_NAME)) { + sortPrefix = PREFIX_NAME; + key = ParserUtil.parseClientIdSort(argMultimap.getValue(PREFIX_NAME).get()); + } + + return new SortClientCommand(sortPrefix, key); + } + + private PinClientCommand parsePinClientCommand(String arguments) throws ParseException { + try { + ClientId pinnedClientId = ParserUtil.parseClientId(arguments); + return new PinClientCommand(pinnedClientId); + } catch (ParseException e) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PinClientCommand.MESSAGE_USAGE), e); + } + + } + + /** + * Returns true if there are no prefixes present in the given {@code ArgumentMultimap}. + */ + private static boolean noPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } + + /** + * Returns true if any of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean anyPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 522b93081cc..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea..00000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new EditCommand object - */ -public class EditCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); - } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 4fb71f23103..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/IssueCliSyntax.java b/src/main/java/seedu/address/logic/parser/IssueCliSyntax.java new file mode 100644 index 00000000000..21ccfce58a9 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/IssueCliSyntax.java @@ -0,0 +1,17 @@ +package seedu.address.logic.parser; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class IssueCliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_TITLE = new Prefix("t/"); + public static final Prefix PREFIX_DEADLINE = new Prefix("d/"); + public static final Prefix PREFIX_PROJECT_NAME = new Prefix("n/"); + public static final Prefix PREFIX_URGENCY = new Prefix("u/"); + public static final Prefix PREFIX_PROJECT_ID = new Prefix("p/"); + public static final Prefix PREFIX_ISSUE_ID = new Prefix("i/"); + public static final Prefix PREFIX_STATUS = new Prefix("s/"); + +} diff --git a/src/main/java/seedu/address/logic/parser/IssueCommandParser.java b/src/main/java/seedu/address/logic/parser/IssueCommandParser.java new file mode 100644 index 00000000000..821c983f508 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/IssueCommandParser.java @@ -0,0 +1,350 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.FLAG_UNKNOWN_COMMAND; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_MISSING_ARGUMENTS; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_ISSUE_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_PROJECT_NAME; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_STATUS; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_TITLE; +import static seedu.address.logic.parser.IssueCliSyntax.PREFIX_URGENCY; +import static seedu.address.logic.parser.ParserUtil.parseIndexValidity; +import static seedu.address.logic.parser.ParserUtil.parseNameValidity; +import static seedu.address.logic.parser.ParserUtil.parseStatusValidity; +import static seedu.address.logic.parser.ParserUtil.parseTitleValidity; +import static seedu.address.logic.parser.ParserUtil.parseUrgencyValidity; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.issue.AddIssueCommand; +import seedu.address.logic.commands.issue.DeleteIssueCommand; +import seedu.address.logic.commands.issue.EditIssueCommand; +import seedu.address.logic.commands.issue.FindIssueCommand; +import seedu.address.logic.commands.issue.IssueCommand; +import seedu.address.logic.commands.issue.ListIssueCommand; +import seedu.address.logic.commands.issue.MarkIssueCommand; +import seedu.address.logic.commands.issue.PinIssueCommand; +import seedu.address.logic.commands.issue.SetIssueDefaultViewCommand; +import seedu.address.logic.commands.issue.SortIssueCommand; +import seedu.address.logic.commands.issue.UnmarkIssueCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.predicates.IssueContainsKeywordsPredicate; +import seedu.address.model.Deadline; +import seedu.address.model.Pin; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.IssueWithoutModel; +import seedu.address.model.issue.Status; +import seedu.address.model.issue.Title; +import seedu.address.model.issue.Urgency; +import seedu.address.model.project.ProjectId; + +/** + * Parser to parse any commands related to issues + */ +public class IssueCommandParser implements Parser { + /** + * Method to parse any commands that have to do with issues (start with 'issue') + * + * @param flag flag used in command + * @param arguments arguments used in command + * @return a IssueCommand + * @throws ParseException + */ + @Override + public IssueCommand parse(String flag, String arguments) throws ParseException { + switch (flag.strip()) { + case AddIssueCommand.COMMAND_FLAG: + return parseAddIssueCommand(arguments); + case EditIssueCommand.COMMAND_FLAG: + return parseEditIssueCommand(arguments); + case DeleteIssueCommand.COMMAND_FLAG: + return parseDeleteIssueCommand(arguments); + case SortIssueCommand.COMMAND_FLAG: + return parseSortIssueCommand(arguments); + case ListIssueCommand.COMMAND_FLAG: + return parseListIssueCommand(arguments); + case MarkIssueCommand.COMMAND_FLAG: + return parseMarkIssueCommand(arguments); + case UnmarkIssueCommand.COMMAND_FLAG: + return parseUnmarkIssueCommand(arguments); + case SetIssueDefaultViewCommand.COMMAND_FLAG: + return parseSetIssueDefaultViewCommand(arguments); + case FindIssueCommand.COMMAND_FLAG: + return parseFindIssueCommand(arguments); + case PinIssueCommand.COMMAND_FLAG: + return parsePinIssueCommand(arguments); + default: + throw new ParseException(FLAG_UNKNOWN_COMMAND); + } + } + + + /** + * 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()); + } + + /** + * Returns true if any of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean anyPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + + /** + * Verifies only one valid user input argument + * Length of a valid command for sort key for issue by deadline and urgency e.g.d/1 + * + * @param arguments user input for key for sort + * @return true if there is only one valid input + */ + private boolean hasOneArgumentOfLengthThree(String arguments) { + return arguments.trim().length() == 3; + } + + private AddIssueCommand parseAddIssueCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_TITLE, PREFIX_DEADLINE, + PREFIX_URGENCY, PREFIX_PROJECT_ID); + + if (!arePrefixesPresent(argMultimap, PREFIX_TITLE, PREFIX_PROJECT_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddIssueCommand.MESSAGE_USAGE)); + } + + Title title = ParserUtil.parseTitle(argMultimap.getValue(PREFIX_TITLE).get()); + + Deadline deadline = Deadline.EmptyDeadline.EMPTY_DEADLINE; + if (arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + deadline = ParserUtil.parseDeadline(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + Urgency urgency = Urgency.NONE; + if (arePrefixesPresent(argMultimap, PREFIX_URGENCY)) { + urgency = ParserUtil.parseUrgency(argMultimap.getValue(PREFIX_URGENCY).get()); + } + + Status status = Status.EmptyStatus.EMPTY_STATUS; + ProjectId projectid = ParserUtil.parseProjectId(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + + IssueWithoutModel issueWithoutModel = new IssueWithoutModel(title, deadline, + urgency, status, projectid, new Pin(false)); + return new AddIssueCommand(issueWithoutModel, projectid); + } + + private EditIssueCommand parseEditIssueCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_ISSUE_ID, PREFIX_TITLE, + PREFIX_DEADLINE, PREFIX_URGENCY); + + if (!arePrefixesPresent(argMultimap, PREFIX_ISSUE_ID) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditIssueCommand.MESSAGE_USAGE)); + } + + Title newTitle = null; + Deadline newDeadline = null; + Urgency newUrgency = null; + IssueId newIssueId = ParserUtil.parseIssueId(argMultimap.getFirstWordValue(PREFIX_ISSUE_ID).get()); + + if (!anyPrefixesPresent(argMultimap, PREFIX_TITLE, PREFIX_DEADLINE, PREFIX_URGENCY)) { + throw new ParseException(String.format(MESSAGE_MISSING_ARGUMENTS, + EditIssueCommand.MESSAGE_USAGE)); + } + + if (arePrefixesPresent(argMultimap, PREFIX_TITLE)) { + newTitle = ParserUtil.parseTitle(argMultimap.getValue(PREFIX_TITLE).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + newDeadline = ParserUtil.parseDeadline(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_URGENCY)) { + newUrgency = ParserUtil.parseUrgency(argMultimap.getValue(PREFIX_URGENCY).get()); + } + + return new EditIssueCommand(newTitle, newDeadline, newUrgency, newIssueId); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param arguments String representing arguments + * @return an FindIssueCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private FindIssueCommand parseFindIssueCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_PROJECT_NAME, PREFIX_TITLE, + PREFIX_STATUS, PREFIX_URGENCY, PREFIX_PROJECT_ID, PREFIX_ISSUE_ID); + + if (noPrefixesPresent(argMultimap, PREFIX_PROJECT_NAME, PREFIX_URGENCY, PREFIX_STATUS, + PREFIX_TITLE, PREFIX_PROJECT_ID, PREFIX_ISSUE_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindIssueCommand.MESSAGE_FIND_ISSUE_USAGE)); + } + + String trimmedArgs = arguments.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindIssueCommand.MESSAGE_FIND_ISSUE_USAGE)); + } + + //check for validity of arguments + + if (anyPrefixesPresent(argMultimap, PREFIX_TITLE)) { + parseTitleValidity(argMultimap.getValue(PREFIX_TITLE).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_PROJECT_NAME)) { + parseNameValidity(argMultimap.getValue(PREFIX_PROJECT_NAME).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_PROJECT_ID)) { + parseIndexValidity(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_ISSUE_ID)) { + parseIndexValidity(argMultimap.getFirstWordValue(PREFIX_ISSUE_ID).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_URGENCY)) { + parseUrgencyValidity(argMultimap.getValue(PREFIX_URGENCY).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_STATUS)) { + parseStatusValidity(argMultimap.getValue(PREFIX_STATUS).get()); + } + + IssueContainsKeywordsPredicate predicate = + new IssueContainsKeywordsPredicate(argMultimap.getAllValues(PREFIX_TITLE), + argMultimap.getAllValues(PREFIX_STATUS), + argMultimap.getAllValues(PREFIX_URGENCY), + argMultimap.getAllValues(PREFIX_PROJECT_NAME), + argMultimap.getAllFirstWordValues(PREFIX_PROJECT_ID), + argMultimap.getAllFirstWordValues(PREFIX_ISSUE_ID)); + + + return new FindIssueCommand(predicate); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param flag String representing command flag + * @param arguments String representing arguments + * @return an FindIssueCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private FindIssueCommand parseFindIssueCommand(String flag, String arguments) throws ParseException { + return parseFindIssueCommand(arguments); + } + + private DeleteIssueCommand parseDeleteIssueCommand(String arguments) throws ParseException { + try { + Index index = ParserUtil.parseIndex(arguments); + return new DeleteIssueCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteIssueCommand.MESSAGE_USAGE), pe); + } + } + + private SortIssueCommand parseSortIssueCommand(String arguments) throws ParseException { + + Prefix sortPrefix = null; + int key = -1; + + if (!hasOneArgumentOfLengthThree(arguments)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortIssueCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(arguments, PREFIX_ISSUE_ID, PREFIX_URGENCY, + PREFIX_DEADLINE); + + if (!anyPrefixesPresent(argMultimap, PREFIX_ISSUE_ID, PREFIX_URGENCY, PREFIX_DEADLINE)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortIssueCommand.MESSAGE_USAGE)); + } + + if (arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + sortPrefix = PREFIX_DEADLINE; + key = ParserUtil.parseDeadlineSortForIssue(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_URGENCY)) { + sortPrefix = PREFIX_URGENCY; + key = ParserUtil.parseUrgencySort(argMultimap.getValue(PREFIX_URGENCY).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_ISSUE_ID)) { + sortPrefix = PREFIX_ISSUE_ID; + key = ParserUtil.parseIssueIdSort(argMultimap.getFirstWordValue(PREFIX_ISSUE_ID).get()); + } + + return new SortIssueCommand(sortPrefix, key); + } + + private PinIssueCommand parsePinIssueCommand(String arguments) throws ParseException { + try { + IssueId pinnedIssueId = ParserUtil.parseIssueId(arguments); + return new PinIssueCommand(pinnedIssueId); + } catch (ParseException e) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PinIssueCommand.MESSAGE_USAGE), e); + } + + } + + private ListIssueCommand parseListIssueCommand(String arguments) { + return new ListIssueCommand(); + } + + private MarkIssueCommand parseMarkIssueCommand(String arguments) throws ParseException { + try { + Status newStatus = new Status(true); + IssueId newIssueId = ParserUtil.parseIssueId(arguments); + return new MarkIssueCommand(newStatus, newIssueId); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkIssueCommand.MESSAGE_USAGE), pe); + } + } + + private UnmarkIssueCommand parseUnmarkIssueCommand(String arguments) throws ParseException { + try { + Status newStatus = new Status(false); + IssueId newIssueId = ParserUtil.parseIssueId(arguments); + return new UnmarkIssueCommand(newStatus, newIssueId); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkIssueCommand.MESSAGE_USAGE), pe); + } + } + + private IssueCommand parseSetIssueDefaultViewCommand(String arguments) { + return new SetIssueDefaultViewCommand(); + + } + + /** + * Returns true if there are no prefixes present in the given {@code ArgumentMultimap}. + */ + private static boolean noPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d6551ad8e3f..41e4d0b4f74 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -12,5 +12,5 @@ public interface Parser { * Parses {@code userInput} into a command and returns it. * @throws ParseException if {@code userInput} does not conform the expected format */ - T parse(String userInput) throws ParseException; + T parse(String flag, String arguments)throws ParseException; } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..33937406b75 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,18 +2,27 @@ import static java.util.Objects.requireNonNull; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; -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.model.Deadline; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientEmail; +import seedu.address.model.client.ClientId; +import seedu.address.model.client.ClientMobile; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Status; +import seedu.address.model.issue.Title; +import seedu.address.model.issue.Urgency; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.model.project.Repository; + /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -25,6 +34,7 @@ public class ParserUtil { /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -35,6 +45,19 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses {@code oneBasedIndex} to check its validity. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static void parseIndexValidity(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -51,74 +74,432 @@ public static Name parseName(String name) throws ParseException { } /** - * Parses a {@code String phone} into a {@code Phone}. + * Parses a {@code String name} to check if it is valid. + * Leading and trailing whitespaces will be trimmed. + * It is an overloaded method that doesn't return a Name object. + * @throws ParseException if the given {@code name} is invalid. + */ + public static void parseNameValidity(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code phone} is invalid. + * @param clientId in string format from user input + * @return Client of the client id + * @throws ParseException if the fiven {@code client} is invalid. + */ + public static ClientId parseClientId(String clientId) throws ParseException, NumberFormatException { + requireNonNull(clientId); + String trimmedClientId = clientId.trim(); + if (!ClientId.isValidClientId(trimmedClientId)) { + throw new ParseException(ClientId.MESSAGE_CONSTRAINTS); + } + int clientIdInt = Integer.parseInt(trimmedClientId); + return new ClientId(clientIdInt); + } + + /** + * Parses a {@code String repository} into a {@code Repository}. + * Leading a trailing whitespaces will be trimmed. + * + * @param repository string argument input + * @return parsed Repository object + * @throws ParseException if the given {@code repository} is invalid. + */ + public static Repository parseRepository(String repository) throws ParseException { + requireNonNull(repository); + String trimmedRepository = repository.trim(); + if (!Repository.isValidRepository(trimmedRepository)) { + throw new ParseException(Repository.MESSAGE_CONSTRAINTS); + } + return new Repository(trimmedRepository); + } + + /** + * Parses a {@code String repository} and checks its validity. + * Leading a trailing whitespaces will be trimmed. + * It is an overloaded method that does not return a Repository object + * @param repository string argument input + * @throws ParseException if the given {@code repository} is invalid. + */ + public static void parseRepositoryValidity(String repository) throws ParseException { + requireNonNull(repository); + String trimmedRepository = repository.trim(); + if (!Repository.isValidRepository(trimmedRepository)) { + throw new ParseException(Repository.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String deadline} into a {@code Deadline}. + * Leading a trailing whitespaces will be trimmed. + * + * @param deadline string argument input + * @return parsed Deadline object + * @throws ParseException if the given {@code deadline} is invalid. */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); + public static Deadline parseDeadline(String deadline) throws ParseException { + requireNonNull(deadline); + String trimmedDeadline = deadline.trim(); + if (!Deadline.isValidDeadline(trimmedDeadline)) { + throw new ParseException(Deadline.MESSAGE_CONSTRAINTS); } - return new Phone(trimmedPhone); + return new Deadline(trimmedDeadline); } + /** - * Parses a {@code String address} into an {@code Address}. + * Parses a {@code String mobile} into a {@code ClientMobile}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code address} is invalid. + * @throws ParseException if the given {@code mobile} is invalid. + */ + public static ClientMobile parseMobile(String mobile) throws ParseException { + requireNonNull(mobile); + String trimmedMobile = mobile.trim(); + if (!ClientMobile.isValidClientMobile(trimmedMobile)) { + throw new ParseException(ClientMobile.MESSAGE_CONSTRAINTS); + } + return new ClientMobile(trimmedMobile); + } + + /** + * Parses a {@code String mobile} to check if it is valid. + * Leading and trailing whitespaces will be trimmed. + * It is an overloaded method that doesn't return a ClientMobile object. + * @throws ParseException if the given {@code mobile} is invalid. */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); + public static void parseMobileValidity(String mobile) throws ParseException { + requireNonNull(mobile); + String trimmedMobile = mobile.trim(); + if (!ClientMobile.isValidClientMobile(trimmedMobile)) { + throw new ParseException(ClientMobile.MESSAGE_CONSTRAINTS); } - return new Address(trimmedAddress); } /** - * Parses a {@code String email} into an {@code Email}. + * Parses a {@code String email} into an {@code ClientEmail}. * Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code email} is invalid. */ - public static Email parseEmail(String email) throws ParseException { + public static ClientEmail parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!ClientEmail.isValidEmail(trimmedEmail)) { + throw new ParseException(ClientEmail.MESSAGE_CONSTRAINTS); + } + return new ClientEmail(trimmedEmail); + } + + /** + * Parses a {@code String email} to check if it is valid. + * Leading and trailing whitespaces will be trimmed. + * It is an overloaded method that doesn't return a ClientEmail object. + * @throws ParseException if the given {@code email} is invalid. + */ + public static void parseEmailValidity(String email) throws ParseException { requireNonNull(email); String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); + if (!ClientEmail.isValidEmail(trimmedEmail)) { + throw new ParseException(ClientEmail.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String title} into a {@code Title}. + * Leading and trailing whitespaces will be trimmed. + * It is an overloaded method that doesn't return a Title object. + * @throws ParseException if the given {@code title} is invalid. + */ + public static void parseTitleValidity(String title) throws ParseException { + requireNonNull(title); + String trimmedTitle = title.trim(); + if (!Title.isValidTitle(trimmedTitle)) { + throw new ParseException(Title.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String title} into a {@code Title}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code title} is invalid. + */ + public static Title parseTitle(String title) throws ParseException { + requireNonNull(title); + String trimmedTitle = title.trim(); + if (!Title.isValidTitle(trimmedTitle)) { + throw new ParseException(seedu.address.model.issue.Title.MESSAGE_CONSTRAINTS); + } + return new Title(trimmedTitle); + } + + /** + * Parses a {@code String urgency} into a {@code Urgency}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code urgency} is invalid. + */ + public static Urgency parseUrgency(String urgency) throws ParseException { + requireNonNull(urgency); + String trimmedUrgency = urgency.trim(); + if (!Urgency.isValidUrgency(trimmedUrgency)) { + throw new ParseException(Urgency.MESSAGE_CONSTRAINTS); + } + switch (trimmedUrgency) { + case ("0"): + return Urgency.NONE; + case ("1"): + return Urgency.LOW; + case ("2"): + return Urgency.MEDIUM; + case ("3"): + return Urgency.HIGH; + default: + throw new ParseException(Urgency.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String status} to check if it is valid. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code status} is invalid. + */ + public static void parseStatusValidity(String status) throws ParseException { + requireNonNull(status); + String trimmedStatus = status.trim(); + boolean isValidStringCounterpart = + trimmedStatus.equalsIgnoreCase("completed") + || trimmedStatus.equalsIgnoreCase("incomplete"); + if (!isValidStringCounterpart) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String urgency} into a {@code Urgency}. + * Leading and trailing whitespaces will be trimmed. + * It is an overloaded method that doesn't return a Urgency object. + * @throws ParseException if the given {@code urgency} is invalid. + */ + + public static void parseUrgencyValidity(String urgency) throws ParseException { + requireNonNull(urgency); + String trimmedUrgency = urgency.trim(); + boolean isValidStringCounterpart = + trimmedUrgency.equalsIgnoreCase("high") + || trimmedUrgency.equalsIgnoreCase("low") + || trimmedUrgency.equalsIgnoreCase("medium") + || trimmedUrgency.equalsIgnoreCase("none"); + if (!isValidStringCounterpart) { + throw new ParseException(Urgency.MESSAGE_STRING_CONSTRAINTS); } - return new Email(trimmedEmail); } /** - * Parses a {@code String tag} into a {@code Tag}. + * Parses a {@code String projectId} into a {@code Project}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code tag} is invalid. + * @return project id. + * @throws ParseException if the given {@code projectId} is invalid. + */ + public static ProjectId parseProjectId(String projectId) throws ParseException { + requireNonNull(projectId); + String trimmedId = projectId.trim(); + if (!ProjectId.isValidProjectId(trimmedId)) { + throw new ParseException(ProjectId.MESSAGE_CONSTRAINTS); + } + int trimmedIdInt = Integer.parseInt(trimmedId); + return new ProjectId(trimmedIdInt); + } + + /** + * Parses a {@code String projectId} into a stub default {@code Project}. + * + * @param projectId is the id of the project + * @return A stub default project + */ + public static Project parseProjectStub(String projectId) throws ParseException { + requireNonNull(projectId); + String trimmedId = projectId.trim(); + if (!ProjectId.isValidProjectId(trimmedId)) { + throw new ParseException(ProjectId.MESSAGE_INVALID); + } + int trimmedIdInt = Integer.parseInt(trimmedId); + return new Project(new Name("default"), new Repository("default/default"), + new Deadline("2022-03-05"), new Client(new Name("default")), + new ArrayList<>(), new ProjectId(trimmedIdInt), new Pin(false)); + } + + + /** + * Parses a {@code String issueId} into a {@code IssueId}. + * + * @param issueId is the id of the issue + * @return An issueId object + */ + public static IssueId parseIssueId(String issueId) throws ParseException { + requireNonNull(issueId); + String trimmedId = issueId.trim(); + if (!IssueId.isValidIssueId(trimmedId)) { + throw new ParseException(IssueId.MESSAGE_INVALID); + } + int trimmedIdInt = Integer.parseInt(trimmedId); + return new IssueId(trimmedIdInt); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by deadline. + * @return Integer of 0 or 1 which specifies the chronology of sort + */ + public static Integer parseDeadlineSortForProject(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Project.isValidDeadlineSortKey(trimmedKey)) { + throw new ParseException(Project.MESSAGE_INVALID_DEADLINE_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by issue count. + * @return Integer of 0 or 1 which specifies the numeric order of sorting. + */ + public static Integer parseIssueCountSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Project.isValidIssueCountSortKey(trimmedKey)) { + throw new ParseException(Project.MESSAGE_INVALID_ISSUE_COUNT_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by name. + * @return Integer of 0 or 1 which specifies the alphabetical order of sorting. + */ + public static Integer parseProjectNameSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Project.isValidNameSortKey(trimmedKey)) { + throw new ParseException(Project.MESSAGE_INVALID_NAME_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user to sort by urgency. + * @return Integer of 0 or 1 which specifies the urgency order of sorting. + */ + public static Integer parseUrgencySort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Issue.isValidUrgencySortKey(trimmedKey)) { + throw new ParseException(Issue.MESSAGE_INVALID_URGENCY_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by deadline. + * @return Integer of 0 or 1 which specifies the chronology of sort + */ + public static Integer parseDeadlineSortForIssue(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Issue.isValidDeadlineSortKey(trimmedKey)) { + throw new ParseException(Issue.MESSAGE_INVALID_DEADLINE_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by name. + * @return Integer of 0 or 1 which specifies the alphabetical order of sorting. + */ + public static Integer parseClientNameSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Client.isValidNameSortKey(trimmedKey)) { + throw new ParseException(Client.MESSAGE_INVALID_NAME_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by project id. + * @return Integer of 0 or 1 which specifies the order of sorting. + */ + public static Integer parseProjectIdSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Project.isValidProjectIdSortKey(trimmedKey)) { + throw new ParseException(Project.MESSAGE_INVALID_PROJECT_ID_SORT_KEY); + } + return Integer.parseInt(trimmedKey); + } + + /** + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by issue id. + * @return Integer of 0 or 1 which specifies the order of sorting. */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + public static Integer parseIssueIdSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Issue.isValidIssueIdSortKey(trimmedKey)) { + throw new ParseException(Issue.MESSAGE_INVALID_ISSUE_ID_SORT_KEY); } - return new Tag(trimmedTag); + return Integer.parseInt(trimmedKey); } /** - * Parses {@code Collection tags} into a {@code Set}. + * Parses a {@code String key} into an {@code Integer}. + * + * @param key is the value entered by the user for sort by client id. + * @return Integer of 0 or 1 which specifies the order of sorting. */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + public static Integer parseClientIdSort(String key) throws ParseException { + requireNonNull(key); + String trimmedKey = key.trim(); + if (!Client.isValidClientIdSortKey(trimmedKey)) { + throw new ParseException(Client.MESSAGE_INVALID_CLIENT_ID_SORT_KEY); } - return tagSet; + return Integer.parseInt(trimmedKey); + } + + /** + * Parse a string to get only the first word of said string. + * @param str string to parse + * @return the first word of the string + */ + public static String getFirstWord(String str) { + requireNonNull(str); + + return str.split(" ", 2)[0]; } } diff --git a/src/main/java/seedu/address/logic/parser/ProjectCliSyntax.java b/src/main/java/seedu/address/logic/parser/ProjectCliSyntax.java new file mode 100644 index 00000000000..7fe06f2667d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ProjectCliSyntax.java @@ -0,0 +1,17 @@ +package seedu.address.logic.parser; + +/** + * Contains Project Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class ProjectCliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_CLIENT_ID = new Prefix("c/"); + public static final Prefix PREFIX_CLIENT_LABEL = new Prefix("l/"); + public static final Prefix PREFIX_REPOSITORY = new Prefix("r/"); + public static final Prefix PREFIX_DEADLINE = new Prefix("d/"); + public static final Prefix PREFIX_PROJECT_ID = new Prefix("p/"); + public static final Prefix PREFIX_ISSUE_COUNT = new Prefix("i/"); + +} diff --git a/src/main/java/seedu/address/logic/parser/ProjectCommandParser.java b/src/main/java/seedu/address/logic/parser/ProjectCommandParser.java new file mode 100644 index 00000000000..04e96b434e4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ProjectCommandParser.java @@ -0,0 +1,331 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.FLAG_UNKNOWN_COMMAND; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_MISSING_ARGUMENTS; +import static seedu.address.logic.parser.ParserUtil.parseIndexValidity; +import static seedu.address.logic.parser.ParserUtil.parseNameValidity; +import static seedu.address.logic.parser.ParserUtil.parseRepositoryValidity; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_CLIENT_LABEL; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_DEADLINE; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_ISSUE_COUNT; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_PROJECT_ID; +import static seedu.address.logic.parser.ProjectCliSyntax.PREFIX_REPOSITORY; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.project.AddProjectCommand; +import seedu.address.logic.commands.project.DeleteProjectCommand; +import seedu.address.logic.commands.project.EditProjectCommand; +import seedu.address.logic.commands.project.FindProjectCommand; +import seedu.address.logic.commands.project.ListProjectCommand; +import seedu.address.logic.commands.project.PinProjectCommand; +import seedu.address.logic.commands.project.ProjectCommand; +import seedu.address.logic.commands.project.SetProjectDefaultViewCommand; +import seedu.address.logic.commands.project.SortProjectCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.predicates.ProjectContainsKeywordsPredicate; +import seedu.address.model.Deadline; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.ClientId; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.ProjectId; +import seedu.address.model.project.ProjectWithoutModel; +import seedu.address.model.project.Repository; + + +/** + * A parser to parse any commands related to project + */ +public class ProjectCommandParser implements Parser { + /** + * Parse any commands that have to do with Projects + * + * @param flag flag used in command + * @param arguments arguments used in command + * @return a ProjectCommand + * @throws ParseException + */ + @Override + public ProjectCommand parse(String flag, String arguments) throws ParseException { + switch (flag.strip()) { + case AddProjectCommand.COMMAND_FLAG: + return parseAddProjectCommand(arguments); + case EditProjectCommand.COMMAND_FLAG: + return parseEditProjectCommand(arguments); + case DeleteProjectCommand.COMMAND_FLAG: + return parseDeleteProjectCommand(arguments); + case SortProjectCommand.COMMAND_FLAG: + return parseSortProjectCommand(arguments); + case ListProjectCommand.COMMAND_FLAG: + return parseListProjectCommand(arguments); + case SetProjectDefaultViewCommand.COMMAND_FLAG: + return parseSetProjectDefaultViewCommand(arguments); + case FindProjectCommand.COMMAND_FLAG: + return parseFindProjectCommand(arguments); + case PinProjectCommand.COMMAND_FLAG: + return parsePinProjectCommand(arguments); + + default: + throw new ParseException(FLAG_UNKNOWN_COMMAND); + } + } + + private ProjectCommand parsePinProjectCommand(String arguments) throws ParseException { + try { + ProjectId pinnedProjectId = ParserUtil.parseProjectId(arguments); + return new PinProjectCommand(pinnedProjectId); + } catch (ParseException e) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PinProjectCommand.MESSAGE_USAGE), e); + } + } + + + /** + * 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()); + } + + /** + * Returns true if any of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean anyPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Verifies only one valid user input argument + * Length of a valid command for sort key for project by deadline, issue count and name e.g.d/1 + * + * @param arguments user input for key for sort + * @return true if there is only one valid input + */ + private boolean hasOneArgumentOfLengthThree(String arguments) { + return arguments.trim().length() == 3; + } + + private AddProjectCommand parseAddProjectCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_NAME, PREFIX_CLIENT_ID, + PREFIX_REPOSITORY, PREFIX_DEADLINE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddProjectCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + ClientId clientId; + Repository repository; + Deadline deadline; + + if (!arePrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + clientId = ClientId.EmptyClientId.EMPTY_CLIENT_ID; + } else { + clientId = ParserUtil.parseClientId(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_REPOSITORY)) { + repository = Repository.EmptyRepository.EMPTY_REPOSITORY; + } else { + repository = ParserUtil.parseRepository(argMultimap.getValue(PREFIX_REPOSITORY).get()); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + deadline = Deadline.EmptyDeadline.EMPTY_DEADLINE; + } else { + deadline = ParserUtil.parseDeadline(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + List issueList = new ArrayList<>(); + + ProjectWithoutModel projectWithoutModel = + new ProjectWithoutModel(name, repository, deadline, clientId, issueList, new Pin(false)); + + return new AddProjectCommand(projectWithoutModel); + } + + private EditProjectCommand parseEditProjectCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_PROJECT_ID, PREFIX_NAME, PREFIX_CLIENT_ID, + PREFIX_REPOSITORY, PREFIX_DEADLINE); + + if (!arePrefixesPresent(argMultimap, PREFIX_PROJECT_ID) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditProjectCommand.MESSAGE_USAGE)); + } + + Name newName = null; + ClientId newClientId = null; + Repository newRepository = null; + Deadline newDeadline = null; + ProjectId newProjectId = ParserUtil.parseProjectId(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + + if (!anyPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_CLIENT_ID, PREFIX_REPOSITORY, PREFIX_DEADLINE)) { + throw new ParseException(String.format(MESSAGE_MISSING_ARGUMENTS, + EditProjectCommand.MESSAGE_USAGE)); + } + + if (arePrefixesPresent(argMultimap, PREFIX_NAME)) { + newName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + newClientId = ParserUtil.parseClientId(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_REPOSITORY)) { + newRepository = ParserUtil.parseRepository(argMultimap.getValue(PREFIX_REPOSITORY).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + newDeadline = ParserUtil.parseDeadline(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + return new EditProjectCommand(newProjectId, newName, newClientId, newRepository, newDeadline); + } + + private DeleteProjectCommand parseDeleteProjectCommand(String arguments) throws ParseException { + try { + Index index = ParserUtil.parseIndex(arguments); + return new DeleteProjectCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteProjectCommand.MESSAGE_USAGE), pe); + } + } + + private SortProjectCommand parseSortProjectCommand(String arguments) throws ParseException { + + Prefix sortPrefix = null; + int key = -1; + + if (!hasOneArgumentOfLengthThree(arguments)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortProjectCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_DEADLINE, PREFIX_ISSUE_COUNT, PREFIX_NAME, + PREFIX_PROJECT_ID); + + if (!anyPrefixesPresent(argMultimap, PREFIX_DEADLINE, PREFIX_ISSUE_COUNT, PREFIX_NAME, PREFIX_PROJECT_ID)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SortProjectCommand.MESSAGE_USAGE)); + } + + if (arePrefixesPresent(argMultimap, PREFIX_PROJECT_ID)) { + sortPrefix = PREFIX_PROJECT_ID; + key = ParserUtil.parseProjectIdSort(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_DEADLINE)) { + sortPrefix = PREFIX_DEADLINE; + key = ParserUtil.parseDeadlineSortForProject(argMultimap.getValue(PREFIX_DEADLINE).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_ISSUE_COUNT)) { + sortPrefix = PREFIX_ISSUE_COUNT; + key = ParserUtil.parseIssueCountSort(argMultimap.getValue(PREFIX_ISSUE_COUNT).get()); + } + + if (arePrefixesPresent(argMultimap, PREFIX_NAME)) { + sortPrefix = PREFIX_NAME; + key = ParserUtil.parseProjectNameSort(argMultimap.getValue(PREFIX_NAME).get()); + } + + return new SortProjectCommand(sortPrefix, key); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param arguments String representing arguments + * @return an FindProjectCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private FindProjectCommand parseFindProjectCommand(String arguments) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(arguments, PREFIX_NAME, PREFIX_REPOSITORY, PREFIX_CLIENT_ID, + PREFIX_CLIENT_LABEL, PREFIX_PROJECT_ID); + + if (noPrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_REPOSITORY, PREFIX_CLIENT_ID, + PREFIX_CLIENT_LABEL, PREFIX_PROJECT_ID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindProjectCommand.MESSAGE_FIND_PROJECT_USAGE)); + } + + //check for validity of arguments + + if (anyPrefixesPresent(argMultimap, PREFIX_NAME)) { + parseNameValidity(argMultimap.getValue(PREFIX_NAME).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_REPOSITORY)) { + parseRepositoryValidity(argMultimap.getValue(PREFIX_REPOSITORY).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_CLIENT_LABEL)) { + parseNameValidity(argMultimap.getValue(PREFIX_CLIENT_LABEL).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_CLIENT_ID)) { + parseIndexValidity(argMultimap.getFirstWordValue(PREFIX_CLIENT_ID).get()); + } + + if (anyPrefixesPresent(argMultimap, PREFIX_PROJECT_ID)) { + parseIndexValidity(argMultimap.getFirstWordValue(PREFIX_PROJECT_ID).get()); + } + + ProjectContainsKeywordsPredicate predicate = + new ProjectContainsKeywordsPredicate(argMultimap.getAllValues(PREFIX_NAME), + argMultimap.getAllValues(PREFIX_REPOSITORY), argMultimap.getAllValues(PREFIX_CLIENT_LABEL), + argMultimap.getAllFirstWordValues(PREFIX_CLIENT_ID), + argMultimap.getAllFirstWordValues(PREFIX_PROJECT_ID)); + + + return new FindProjectCommand(predicate); + } + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns FindCommand object for execution. + * + * @param flag String representing command flag + * @param arguments String representing arguments + * @return an FindProjectCommand object + * @throws ParseException if the user input does not conform the expected format + */ + private FindProjectCommand parseFindIssueCommand(String flag, String arguments) throws ParseException { + return parseFindProjectCommand(arguments); + } + + private ListProjectCommand parseListProjectCommand(String arguments) { + return new ListProjectCommand(); + } + + private ProjectCommand parseSetProjectDefaultViewCommand(String arguments) { + return new SetProjectDefaultViewCommand(); + } + + /** + * Returns true if there are no prefixes present in the given {@code ArgumentMultimap}. + */ + private static boolean noPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/predicates/ClientContainsKeywordsPredicate.java b/src/main/java/seedu/address/logic/parser/predicates/ClientContainsKeywordsPredicate.java new file mode 100644 index 00000000000..023e1d564c8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/predicates/ClientContainsKeywordsPredicate.java @@ -0,0 +1,147 @@ +package seedu.address.logic.parser.predicates; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.client.Client; + +/** + * Tests that a Client object matches any of the keywords given. + */ +public class ClientContainsKeywordsPredicate implements Predicate { + + private final List nameKeywords; + private final List emailKeywords; + private final List mobileKeywords; + private final List clientIdKeywords; + + /** + * Constructs a ClientContainsKeywordsPredicate object with the user inputs. + * @param nameKeywords List of Strings representing keywords to search for in name + * @param emailKeywords List of Strings representing keywords to search for in email + * @param mobileKeywords List of Strings representing keywords to search for in mobile + * @param clientIdKeywords List of Strings representing keywords to search for in client id + */ + public ClientContainsKeywordsPredicate(List nameKeywords, List emailKeywords, + List mobileKeywords, List clientIdKeywords) { + this.nameKeywords = nameKeywords; + this.emailKeywords = emailKeywords; + this.mobileKeywords = mobileKeywords; + this.clientIdKeywords = clientIdKeywords; + } + + /** + * Checks if given name matches with any word in the name present. + * @param namePresent String representing name present + * @param nameGiven String representing name given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testName(String namePresent, String nameGiven) { + return Arrays.stream(namePresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(nameGiven, words)); + } + + /** + * Checks if the client's name matches the name keyword being search for. + * @param client Client whose name is being used to search the keyword in + * @return boolean true if the name fulfills the search criteria and false otherwise + */ + public boolean testName(Client client) { + if (nameKeywords.isEmpty()) { + return true; + } else { + return nameKeywords.stream().anyMatch(name -> testName(name, client.getClientName().toString())); + } + } + + /** + * Checks if the client id matches the id keyword being search for. + * @param client Client whose id is being used to search the keyword in + * @return true if the client id fulfills the search criteria and false otherwise + */ + public boolean testClientId(Client client) { + if (clientIdKeywords.isEmpty()) { + return true; + } else { + return clientIdKeywords.stream().anyMatch( + c -> testClientId(c, client.getClientId().toString())); + } + } + + /** + * Checks if given id matches with any word in the id present. + * @param idPresent String representing id present + * @param idGiven String representing id given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testClientId(String idPresent, String idGiven) { + return Arrays.stream(idPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(idGiven, words)); + } + + /** + * Checks if given mobile matches with any word in the mobile present. + * @param mobilePresent String representing mobile present + * @param mobileGiven String representing mobile given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testMobile(String mobilePresent, String mobileGiven) { + return Arrays.stream(mobilePresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(mobileGiven, words)); + } + + /** + * Checks if the client's mobile matches the mobile keyword being search for. + * @param client Client whose mobile is being used to search the keyword in + * @return boolean true if the mobile fulfills the search criteria and false otherwise + */ + public boolean testMobile(Client client) { + if (mobileKeywords.isEmpty()) { + return true; + } else { + return mobileKeywords.stream().anyMatch(mobile -> testMobile(mobile, client.getClientMobile().toString())); + } + } + + /** + * Checks if given email matches with any word in the email present. + * @param emailPresent String representing email present + * @param emailGiven String representing email given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testEmail(String emailPresent, String emailGiven) { + return Arrays.stream(emailPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(emailGiven, words)); + } + + /** + * Checks if the client's email matches the email keyword being search for. + * @param client Client whose email is being used to search the keyword in + * @return boolean true if the email fulfills the search criteria and false otherwise + */ + public boolean testEmail(Client client) { + if (emailKeywords.isEmpty()) { + return true; + } else { + return emailKeywords.stream().anyMatch(email -> testEmail(email, client.getClientEmail().toString())); + } + } + + @Override + public boolean test(Client client) { + return testName(client) && testEmail(client) && testMobile(client) && testClientId(client); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ClientContainsKeywordsPredicate // instanceof handles nulls + && nameKeywords.equals(((ClientContainsKeywordsPredicate) other).nameKeywords) //state checks + && mobileKeywords.equals(((ClientContainsKeywordsPredicate) other).mobileKeywords) + && emailKeywords.equals(((ClientContainsKeywordsPredicate) other).emailKeywords) + && clientIdKeywords.equals(((ClientContainsKeywordsPredicate) other).clientIdKeywords)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/predicates/IssueContainsKeywordsPredicate.java b/src/main/java/seedu/address/logic/parser/predicates/IssueContainsKeywordsPredicate.java new file mode 100644 index 00000000000..246dbe4b672 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/predicates/IssueContainsKeywordsPredicate.java @@ -0,0 +1,214 @@ +package seedu.address.logic.parser.predicates; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.issue.Issue; + + +/** + * Tests that an Issue object matches any of the keywords given. + */ +public class IssueContainsKeywordsPredicate implements Predicate { + + private final List titleKeywords; + private final List statusKeywords; + private final List urgencyKeywords; + private final List projectNameKeywords; + private final List projectIdKeywords; + private final List issueIdKeywords; + + /** + * Constructs an IssueContainsKeywordsPredicate object with the user inputs. + * @param titleKeywords List of Strings representing keywords to search for in title + * @param statusKeywords List of Strings representing keywords to search for in status + * @param urgencyKeywords List of Strings representing keywords to search for in urgency + * @param projectNameKeywords List of Strings representing keywords to search for in project name + * @param projectIdKeywords List of Strings representing keywords to search for in project id + * @param issueIdKeywords List of Strings representing keywords to search for in issue id + */ + public IssueContainsKeywordsPredicate(List titleKeywords, List statusKeywords, + List urgencyKeywords, List projectNameKeywords, + List projectIdKeywords, List issueIdKeywords) { + this.titleKeywords = titleKeywords; + this.statusKeywords = statusKeywords; + this.urgencyKeywords = urgencyKeywords; + this.projectNameKeywords = projectNameKeywords; + this.projectIdKeywords = projectIdKeywords; + this.issueIdKeywords = issueIdKeywords; + } + + /** + * Checks if given title matches with any word in the title present. + * @param tiPresent String representing title present + * @param tiGiven String representing title given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testTitle(String tiPresent, String tiGiven) { + return Arrays.stream(tiPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(tiGiven, words)); + } + + /** + * Checks if the issue's title matches the title keyword being search for. + * @param issue Issue whose title is being used to search the keyword in + * @return true if the title fulfills the search criteria and false otherwise + */ + public boolean testTitle(Issue issue) { + if (titleKeywords.isEmpty()) { + return true; + } else { + return titleKeywords.stream().anyMatch( + ti -> testTitle(ti, issue.getTitle().toString())); + } + } + + + /** + * Checks if given urgency matches with any word in the urgency present. + * @param urPresent String representing urgency present + * @param urGiven String representing urgency given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testUrgency(String urPresent, String urGiven) { + return Arrays.stream(urPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(urGiven, words)); + } + + /** + * Checks if the issue's urgency matches the urgency keyword being search for. + * @param issue Issue whose urgency is being used to search the keyword in + * @return true if the urgency fulfills the search criteria and false otherwise + */ + public boolean testUrgency(Issue issue) { + if (urgencyKeywords.isEmpty()) { + return true; + } else { + return urgencyKeywords.stream().anyMatch( + ur -> testUrgency(ur, issue.getUrgency().toString())); + } + } + + /** + * Checks if given status matches with any word in the status present. + * @param stPresent String representing status present + * @param stGiven String representing status given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testStatus(String stPresent, String stGiven) { + return Arrays.stream(stPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(stGiven, words)); + } + + /** + * Checks if the issue's status matches the status keyword being search for. + * @param issue Issue whose status is being used to search the keyword in + * @return true if the status fulfills the search criteria and false otherwise + */ + public boolean testStatus(Issue issue) { + if (statusKeywords.isEmpty()) { + return true; + } else { + return statusKeywords.stream().anyMatch( + st -> testStatus(st, issue.getStatus().getCompletionStatus())); + } + } + + /** + * Checks if the issue's project's id matches the project id keyword being search for. + * @param issue Issue whose project id is being used to search the keyword in + * @return true if the project id fulfills the search criteria and false otherwise + */ + public boolean testProjectId(Issue issue) { + if (projectIdKeywords.isEmpty()) { + return true; + } else { + return projectIdKeywords.stream().anyMatch( + pr -> testProjectId(pr, issue.getProject().getProjectId().toString())); + } + } + + /** + * Checks if given project id matches with any word in the project id present. + * @param projIdPresent String representing project id present + * @param projIdGiven String representing project id given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testProjectId(String projIdPresent, String projIdGiven) { + return Arrays.stream(projIdPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(projIdGiven, words)); + } + + + /** + * Checks if the issue's id matches the id keyword being search for. + * @param issue Issue whose id is being used to search the keyword in + * @return true if the id fulfills the search criteria and false otherwise + */ + public boolean testIssueId(Issue issue) { + if (issueIdKeywords.isEmpty()) { + return true; + } else { + return issueIdKeywords.stream().anyMatch( + i -> testIssueId(i, issue.getIssueId().toString())); + } + } + + /** + * Checks if given id matches with any word in the id present. + * @param idPresent String representing id present + * @param idGiven String representing id given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testIssueId(String idPresent, String idGiven) { + return Arrays.stream(idPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(idGiven, words)); + } + + + /** + * Checks if given project name matches with any word in the project name present. + * @param projPresent String representing project name present + * @param projGiven String representing project name given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testProjectName(String projPresent, String projGiven) { + return Arrays.stream(projPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(projGiven, words)); + } + + /** + * Checks if the issue's project's name matches the project name keyword being search for. + * @param issue Issue whose project name is being used to search the keyword in + * @return true if the project name fulfills the search criteria and false otherwise + */ + public boolean testProjectName(Issue issue) { + if (projectNameKeywords.isEmpty()) { + return true; + } else { + return projectNameKeywords.stream().anyMatch( + pr -> testProjectName(pr, issue.getProject().getProjectName().toString())); + } + } + + @Override + public boolean test(Issue issue) { + return testTitle(issue) && testUrgency(issue) && testStatus(issue) + && testProjectName(issue) && testProjectId(issue) && testIssueId(issue); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof IssueContainsKeywordsPredicate // instanceof handles nulls + && titleKeywords.equals(((IssueContainsKeywordsPredicate) other).titleKeywords) + && statusKeywords.equals(((IssueContainsKeywordsPredicate) other).statusKeywords) //state checks + && urgencyKeywords.equals(((IssueContainsKeywordsPredicate) other).urgencyKeywords) + && projectNameKeywords.equals(((IssueContainsKeywordsPredicate) other).projectNameKeywords)) + && projectIdKeywords.equals(((IssueContainsKeywordsPredicate) other).projectIdKeywords) + && issueIdKeywords.equals(((IssueContainsKeywordsPredicate) other).issueIdKeywords); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/predicates/ProjectContainsKeywordsPredicate.java b/src/main/java/seedu/address/logic/parser/predicates/ProjectContainsKeywordsPredicate.java new file mode 100644 index 00000000000..c544a92b925 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/predicates/ProjectContainsKeywordsPredicate.java @@ -0,0 +1,179 @@ +package seedu.address.logic.parser.predicates; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.project.Project; + +/** + * Tests that a Project object matches any of the keywords given. + */ +public class ProjectContainsKeywordsPredicate implements Predicate { + + private final List nameKeywords; + private final List repositoryKeywords; + private final List clientNameKeywords; + private final List clientIdKeywords; + private final List projectIdKeywords; + + /** + * Constructs a ProjectContainsKeywordsPredicate object with the user inputs. + * @param nameKeywords List of Strings representing keywords to search for in name + * @param repositoryKeywords List of Strings representing keywords to search for in repository + * @param clientNameKeywords List of Strings representing keywords to search for in project's client's name + * @param clientIdKeywords List of Strings representing keywords to search for in project's client's Id + * @param projectIdKeywords List of Strings representing keywords to search for in project Id + */ + public ProjectContainsKeywordsPredicate(List nameKeywords, List repositoryKeywords, + List clientNameKeywords, List clientIdKeywords, + List projectIdKeywords) { + this.nameKeywords = nameKeywords; + this.repositoryKeywords = repositoryKeywords; + this.clientNameKeywords = clientNameKeywords; + this.clientIdKeywords = clientIdKeywords; + this.projectIdKeywords = projectIdKeywords; + } + + /** + * Checks if given name matches with any word in the name present. + * @param namePresent String representing name present + * @param nameGiven String representing name given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testName(String namePresent, String nameGiven) { + return Arrays.stream(namePresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(nameGiven, words)); + } + + /** + * Checks if the project's name matches the name keyword being search for. + * @param project Project whose name is being used to search the keyword in + * @return boolean true if the name fulfills the search criteria and false otherwise + */ + public boolean testName(Project project) { + if (nameKeywords.isEmpty()) { + return true; + } else { + return nameKeywords.stream().anyMatch(name -> testName(name, project.getProjectName().toString())); + } + } + + /** + * Checks if given client name matches with any word in the name present. + * @param namePresent String representing client name present + * @param nameGiven String representing client name given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testClientName(String namePresent, String nameGiven) { + return Arrays.stream(namePresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(nameGiven, words)); + } + + /** + * Checks if the project's client's name matches the name keyword being search for. + * @param project Project whose client's name is being used to search the keyword in + * @return boolean true if the name fulfills the search criteria and false otherwise + */ + public boolean testClientName(Project project) { + if (clientNameKeywords.isEmpty()) { + return true; + } else { + return clientNameKeywords.stream().anyMatch(name -> testClientName(name, + project.getClient().getClientName().toString())); + } + } + + /** + * Checks if given client id matches with any word in the client id present. + * @param idPresent String representing client id present + * @param idGiven String representing client id given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testClientId(String idPresent, String idGiven) { + return Arrays.stream(idPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(idGiven, words)); + } + + /** + * Checks if the project's client's id matches the id keyword being search for. + * @param project Project whose client's id is being used to search the keyword in + * @return boolean true if the id fulfills the search criteria and false otherwise + */ + public boolean testClientId(Project project) { + if (clientIdKeywords.isEmpty()) { + return true; + } else { + return clientIdKeywords.stream().anyMatch(id -> testClientId(id, + project.getClient().getClientId().toString())); + } + } + + /** + * Checks if the project's id matches the id keyword being search for. + * @param project Project whose id is being used to search the keyword in + * @return boolean true if the id fulfills the search criteria and false otherwise + */ + public boolean testProjectId(Project project) { + if (projectIdKeywords.isEmpty()) { + return true; + } else { + return projectIdKeywords.stream().anyMatch(id -> testProjectId(id, + project.getProjectId().toString())); + } + } + + /** + * Checks if given id matches with any word in the id present. + * @param idPresent String representing id present + * @param idGiven String representing id given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testProjectId(String idPresent, String idGiven) { + return Arrays.stream(idPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(idGiven, words)); + } + + /** + * Checks if given repository matches with any word in the repository present. + * @param repoPresent String representing repository present + * @param repoGiven String representing repository given (keyword to search for) + * @return boolean true if at least one word matches with the keyword and false otherwise + */ + public boolean testRepository(String repoPresent, String repoGiven) { + return Arrays.stream(repoPresent.trim().split("\\s+")) + .anyMatch(words -> StringUtil.containsWordIgnoreCase(repoGiven, words)); + } + + /** + * Checks if the project's repository matches the name keyword being search for. + * @param project Project whose repository is being used to search the keyword in + * @return boolean true if the repository fulfills the search criteria and false otherwise + */ + public boolean testRepository(Project project) { + if (repositoryKeywords.isEmpty()) { + return true; + } else { + return repositoryKeywords.stream().anyMatch(name -> testRepository(name, + project.getRepository().toString())); + } + } + + @Override + public boolean test(Project project) { + return testName(project) && testRepository(project) && testClientId(project) + && testClientName(project) && testProjectId(project); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ProjectContainsKeywordsPredicate // instanceof handles nulls + && nameKeywords.equals(((ProjectContainsKeywordsPredicate) other).nameKeywords) //state checks + && repositoryKeywords.equals(((ProjectContainsKeywordsPredicate) other).repositoryKeywords) + && clientNameKeywords.equals(((ProjectContainsKeywordsPredicate) other).clientNameKeywords) + && clientIdKeywords.equals(((ProjectContainsKeywordsPredicate) other).clientIdKeywords) + && projectIdKeywords.equals(((ProjectContainsKeywordsPredicate) other).projectIdKeywords)); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..e429853ec57 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,35 +2,42 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.client.Client; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.issue.Issue; +import seedu.address.model.list.UniqueEntityList; +import seedu.address.model.project.Project; + /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed + * + * This is a SINGLETON CLASS. + * Static methods are present to retrieve, and create a new instance of the class. + * When creating a new instance, the previous instance is destroyed. */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniqueEntityList clients; + private final UniqueEntityList projects; + private final UniqueEntityList issues; - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. + /** + * Creates an empty addressbook */ - { - persons = new UniquePersonList(); + public AddressBook() { + clients = new UniqueEntityList<>(); + projects = new UniqueEntityList<>(); + issues = new UniqueEntityList<>(); } - public AddressBook() {} - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook. */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -40,11 +47,27 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the project list with {@code projects}. + * {@code projects} must not contain duplicate projects. + */ + public void setProjects(List projects) { + this.projects.setList(projects); + } + + /** + * Replaces the contents of the issue list with {@code issues}. + * {@code issues} must not contain duplicate issues. + */ + public void setIssues(List issues) { + this.issues.setList(issues); + } + + /** + * Replaces the contents of the client list with {@code clients}. + * {@code clients} must not contain duplicate clients. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + public void setClients(List clients) { + this.clients.setList(clients); } /** @@ -52,69 +75,513 @@ public void setPersons(List persons) { */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); + setIssues(newData.getIssueList()); + setProjects(newData.getProjectList()); + setClients(newData.getClientList()); + } + + //// client-level operations + + /** + * Sorts client list by ClientId + * + */ + public void sortClientListById() { + clients.sortById(); + } + + /** + * Returns true if a project with the same identity as {@code project} exists in the project book. + */ + public boolean hasProject(Project project) { + requireNonNull(project); + return projects.containsByName(project); + } + + /** + * Returns true if an issue with the same identity as {@code issue} exists in the project book. + */ + public boolean hasIssue(Issue issue) { + requireNonNull(issue); + return issues.containsByName(issue); + } + + /** + * Returns true if a client with the same identity as {@code client} exists in the project book. + */ + public boolean hasClient(Client client) { + requireNonNull(client); + return clients.containsByName(client); + } + + /** + * Returns true if a project with the same identity as {@code project} exists in the project book. + */ + public boolean hasProjectId(int id) { + return projects.containsId(id); + } - setPersons(newData.getPersonList()); + /** + * Returns true if an issue with the same identity as {@code issue} exists in the project book. + */ + public boolean hasIssueId(int id) { + return issues.containsId(id); } - //// person-level operations + /** + * Returns true if a client with the same identity as {@code client} exists in the project book. + */ + public boolean hasClientId(int id) { + return clients.containsId(id); + } + + /** + * Adds a project to the project book. + * The project must not already exist in the project book. + */ + public void addProject(Project p) { + projects.add(p); + } /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Adds an issue to the project book. + * The issue must not already exist in the project book. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public void addIssue(Issue i) { + issues.add(i); } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a client to the project book. + * The client must not already exist in the project book. */ - public void addPerson(Person p) { - persons.add(p); + public void addClient(Client c) { + clients.add(c); } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces the given project {@code target} in the list with {@code editedProject}. + * {@code target} must exist in the project book. + * The project identity of {@code editedProject} must not be the same as another + * existing project in the project book. */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void setProject(Project target, Project editedProject) { + requireNonNull(editedProject); - persons.setPerson(target, editedPerson); + projects.setItem(target, editedProject); + } + + /** + * Replaces the given issue {@code target} in the list with {@code editedIssue}. + * {@code target} must exist in the project book. + * The issue identity of {@code editedIssue} must not be the same as another + * existing issue in the project book. + */ + public void setIssue(Issue target, Issue editedIssue) { + requireNonNull(editedIssue); + + issues.setItem(target, editedIssue); + } + + /** + * Replaces the given issue {@code target} in the list with {@code editedClient}. + * {@code target} must exist in the project book. + * The issue identity of {@code editedClient} must not be the same as another + * existing client in the project book. + */ + public void setClient(Client target, Client editedClient) { + requireNonNull(editedClient); + + clients.setItem(target, editedClient); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the project book. + */ + public void removeProject(Project key) { + projects.remove(key); } /** * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. + * {@code key} must exist in the project book. + */ + public void removeIssue(Issue key) { + issues.remove(key); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the project book. + */ + public void removeClient(Client key) { + clients.remove(key); + } + + /** + * Sort projects by id in ascending or descending order based on key value of 0 or 1 respectively. + * + * @param order zero for ascending order and one for descending order + */ + public void sortProjectsById(int order) { + ObservableList sortedProjectsById; + if (order == 0) { + //sort in ascending order + sortedProjectsById = + getModifiableProjectList().sorted(Comparator.comparing(Project::getProjectIdInInt)); + } else { + //sort in descending order + sortedProjectsById = getModifiableProjectList().sorted((p1, p2) -> + p2.getProjectIdInInt() - p1.getProjectIdInInt()); + } + setProjects(sortedProjectsById); + } + + /** + * Sort projects in chronological order or reverse chronological order based on key value of 0 or 1 respectively. + * + * @param order zero for chronological order and one for reverse chronological order + */ + public void sortProjectsByDeadline(int order) { + ObservableList sortedProjectsByDeadline; + if (order == 0) { + //sort according to chronological deadlines + sortedProjectsByDeadline = + getModifiableProjectList().sorted(Comparator.comparing(p -> p.getDeadline().getLocalDate())); + } else { + //sort according to reverse chronological deadlines + sortedProjectsByDeadline = getModifiableProjectList().sorted((p1, p2) -> + p2.getDeadline().getLocalDate().compareTo(p1.getDeadline().getLocalDate())); + } + setProjects(sortedProjectsByDeadline); + } + + /** + * Sort projects according to incomplete issue count or completed issue count based on key of 0 or 1 respectively. + * + * @param order zero for sorting by incomplete issue count and one for sorting by completed issue count + */ + public void sortProjectsByIssueCount(int order) { + ObservableList sortedProjectsByIssueCount; + if (order == 0) { + //sort according to incomplete issue count + sortedProjectsByIssueCount = + getModifiableProjectList().sorted((p1, p2) -> { + return p2.getIncompleteIssueCount() - p1.getIncompleteIssueCount(); + }); + } else { + //sort according to completed issue count + sortedProjectsByIssueCount = + getModifiableProjectList().sorted((p1, p2) -> { + return p2.getCompletedIssueCount() - p1.getCompletedIssueCount(); + }); + } + setProjects(sortedProjectsByIssueCount); + } + + /** + * Sort projects in alphabetical or reverse alphabetical order of names based on key value of 0 or 1 respectively. + * + * @param order zero for alphabetical order and one for reverse alphabetical order + */ + public void sortProjectsByName(int order) { + ObservableList sortedProjectsByName; + if (order == 0) { + //sort according to alphabetical order + sortedProjectsByName = + getModifiableProjectList().sorted(Comparator.comparing(p -> p.getProjectName().toString())); + } else { + //sort according to reverse alphabetical order + sortedProjectsByName = getModifiableProjectList().sorted((p1, p2) -> + p2.getProjectName().toString().compareTo(p1.getProjectName().toString())); + } + setProjects(sortedProjectsByName); + } + + /** + * Sort issues by id in ascending or descending order based on key value of 0 or 1 respectively. + * + * @param order zero for ascending order and one for descending order + */ + public void sortIssuesById(int order) { + ObservableList sortedIssuesById; + if (order == 0) { + //sort in ascending order + sortedIssuesById = + getModifiableIssueList().sorted(Comparator.comparing(Issue::getIssueIdInInt)); + } else { + //sort in descending order + sortedIssuesById = getModifiableIssueList().sorted((i1, i2) -> + i2.getIssueIdInInt() - i1.getIssueIdInInt()); + } + setIssues(sortedIssuesById); + } + + /** + * Sort issues in chronological order or reverse chronological order based on key value of 0 or 1 respectively. + * + * @param order zero for chronological order and one for reverse chronological order + */ + public void sortIssuesByDeadline(int order) { + ObservableList sortedIssuesByDeadline; + if (order == 0) { + //sort according to chronological deadlines + sortedIssuesByDeadline = + getModifiableIssueList().sorted(Comparator.comparing(i -> i.getDeadline().getLocalDate())); + } else { + //sort according to reverse chronological deadlines + sortedIssuesByDeadline = getModifiableIssueList().sorted((i1, i2) -> + i2.getDeadline().getLocalDate().compareTo(i1.getDeadline().getLocalDate())); + } + setIssues(sortedIssuesByDeadline); + } + + /** + * Sort issues according to lowest or highest urgency based on key value of 0 or 1 respectively. + * + * @param order zero for lowest urgency and one for highest urgency + */ + public void sortIssuesByUrgency(int order) { + ObservableList sortedIssuesByUrgency; + if (order == 0) { + //sort according to the lowest urgency + sortedIssuesByUrgency = + getModifiableIssueList().sorted(Comparator.comparing(Issue::getUrgency)); + } else { + //sort according to the highest urgency + sortedIssuesByUrgency = getModifiableIssueList().sorted((i1, i2) -> + i2.getUrgency().compareTo(i1.getUrgency())); + } + setIssues(sortedIssuesByUrgency); + } + + /** + * Sort clients by id in ascending or descending order based on key value of 0 or 1 respectively. + * + * @param order zero for ascending order and one for descending order + */ + public void sortClientsById(int order) { + ObservableList sortedClientsById; + if (order == 0) { + //sort in ascending order + sortedClientsById = + getModifiableClientList().sorted(Comparator.comparing(Client::getClientIdInInt)); + } else { + //sort in descending order + sortedClientsById = getModifiableClientList().sorted((c1, c2) -> + c2.getClientIdInInt() - c1.getClientIdInInt()); + } + setClients(sortedClientsById); + } + + /** + * Sort clients in alphabetical or reverse alphabetical order of names based on key value of 0 or 1 respectively. + * + * @param order zero for alphabetical and one for reverse alphabetical */ - public void removePerson(Person key) { - persons.remove(key); + public void sortClientsByName(int order) { + ObservableList sortedClientsByName; + if (order == 0) { + //sort according to alphabetical order + sortedClientsByName = + getModifiableClientList().sorted(Comparator.comparing(c -> c.getClientName().toString())); + } else { + //sort according to reverse alphabetical order + sortedClientsByName = getModifiableClientList().sorted((c1, c2) -> + c2.getClientName().toString().compareTo(c1.getClientName().toString())); + } + setClients(sortedClientsByName); + } + + /** + * Sorts client list with pinned clients at the front of the list. + */ + public void sortClientsByPin() { + ObservableList sortedClientsByPin; + sortedClientsByPin = getModifiableClientList().sorted(Comparator.comparing(c -> !c.isPinned())); + setClients(sortedClientsByPin); + } + + /** + * Sorts project list with pinned projects at the front of the list. + */ + public void sortProjectsByPin() { + ObservableList sortedProjectsByPin; + sortedProjectsByPin = getModifiableProjectList().sorted(Comparator.comparing(p -> !p.isPinned())); + setProjects(sortedProjectsByPin); + } + + /** + * Sorts issue list with pinned issues at the front of the list. + */ + public void sortIssuesByPin() { + ObservableList sortedIssuesByPin; + sortedIssuesByPin = getModifiableIssueList().sorted(Comparator.comparing(i -> !i.isPinned())); + setIssues(sortedIssuesByPin); } //// util methods @Override public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later + return projects.asUnmodifiableObservableList().size() + " projects\n" + + issues.asUnmodifiableObservableList().size() + " issues\n" + + clients.asUnmodifiableObservableList().size() + " clients\n"; + } + + @Override + public ObservableList getProjectList() { + return projects.asUnmodifiableObservableList(); } @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + public ObservableList getModifiableProjectList() { + return projects.asModifiableObservableList(); } + @Override + public ObservableList getIssueList() { + return issues.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getModifiableIssueList() { + return issues.asModifiableObservableList(); + } + + @Override + public ObservableList getModifiableClientList() { + return clients.asModifiableObservableList(); + } + + @Override + public ObservableList getClientList() { + return clients.asUnmodifiableObservableList(); + } + + @Override + public Project getProjectById(int id) { + return HasIntegerIdentifier.getElementById(projects, id); + } + + @Override + public Issue getIssueById(int id) { + return HasIntegerIdentifier.getElementById(issues, id); + } + + @Override + public Client getClientById(int id) { + return HasIntegerIdentifier.getElementById(clients, id); + } + + @Override + public Client getClient(Client client) { + for (Client c : getModifiableClientList()) { + if (c.hasSameName(client)) { + return c; + } + } + return null; + } + + @Override + public int generateClientId() { + return HasIntegerIdentifier.generateNextId(clients); + } + + @Override + public int generateIssueId() { + return HasIntegerIdentifier.generateNextId(issues); + } + + @Override + public int generateProjectId() { + return HasIntegerIdentifier.generateNextId(projects); + } + + @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); + && clients.equals(((AddressBook) other).clients)); } @Override public int hashCode() { - return persons.hashCode(); + return clients.hashCode(); + } + + /** + * Sorts all entity lists according to the default order - by id in ascending order, and by pin. + */ + public void sortAllLists() { + sortClientsById(0); + sortProjectsById(0); + sortIssuesById(0); + sortClientsByPin(); + sortProjectsByPin(); + sortIssuesByPin(); + } + + /** + * Sorts project list by the last known sort category and order. + */ + public void sortProjectsByCurrentCategory() { + switch(Project.getSortCategory()) { + case ID: + sortProjectsById(Project.getSortOrder()); + break; + case NAME: + sortProjectsByName(Project.getSortOrder()); + break; + case DEADLINE: + sortProjectsByDeadline(Project.getSortOrder()); + break; + case ISSUE_COUNT: + sortProjectsByIssueCount(Project.getSortOrder()); + break; + default: + assert false : "Invalid sort category for projects"; + break; + } + } + + /** + * Sorts client list by the last known sort category and order. + */ + public void sortClientsByCurrentCategory() { + switch(Client.getSortCategory()) { + case ID: + sortClientsById(Client.getSortOrder()); + break; + case NAME: + sortClientsByName(Client.getSortOrder()); + break; + default: + assert false : "Invalid sort category for clients"; + break; + } + } + + /** + * Sorts issue list by the last known sort category and order. + */ + public void sortIssuesByCurrentCategory() { + switch(Issue.getSortCategory()) { + case ID: + sortIssuesById(Issue.getSortOrder()); + break; + case DEADLINE: + sortIssuesByDeadline(Issue.getSortOrder()); + break; + case URGENCY: + sortIssuesByUrgency(Issue.getSortOrder()); + break; + default: + assert false : "Invalid sort category for issues"; + break; + } } } diff --git a/src/main/java/seedu/address/model/Deadline.java b/src/main/java/seedu/address/model/Deadline.java new file mode 100644 index 00000000000..2e4747ad1c3 --- /dev/null +++ b/src/main/java/seedu/address/model/Deadline.java @@ -0,0 +1,106 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Represents a deadline. + */ +public class Deadline { + + /** + * Represents an empty deadline. + */ + public static class EmptyDeadline extends Deadline { + public static final Deadline EMPTY_DEADLINE = new EmptyDeadline(); + + private EmptyDeadline() { + super("1900-01-01"); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String uiRepresentation() { + return "No Deadline Set"; + } + + @Override + public String toString() { + return ""; + } + } + + public static final String MESSAGE_CONSTRAINTS = + "Deadlines should be entered in yyyy-mm-dd format and must exist \n" + + "Year (yyyy): Year from 0001 onwards \n" + + "Month (mm): Month in the range of 1 to 12 \n" + + "Day (dd): Day in the range of 1 to 31"; + /* + * The date must be entered in yyyy-mm-dd format + */ + public static final String VALIDATION_REGEX = "^(\\d{4})-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$"; + + private LocalDate deadline; + + /** + * Constructs a Deadline. + * + * @param deadline A valid deadline. + */ + public Deadline(String deadline) { + requireNonNull(deadline); + checkArgument(isValidDeadline(deadline), MESSAGE_CONSTRAINTS); + this.deadline = LocalDate.parse(deadline); + } + + public boolean isEmpty() { + return false; + } + + /** + * Returns true if a given string is a valid deadline. + */ + public static boolean isValidDeadline(String deadline) { + if (deadline.startsWith("0000") || !deadline.matches(VALIDATION_REGEX)) { + return false; + } + try { + LocalDate.parse(deadline); + } catch (DateTimeParseException e) { + return false; + } + return true; + } + + public String getFormattedDeadline() { + return deadline.format(DateTimeFormatter.ofPattern("MMM d yyyy")); + } + + public String uiRepresentation() { + return "Due by: " + this.getFormattedDeadline(); + } + + public LocalDate getLocalDate() { + return this.deadline; + } + + @Override + public String toString() { + return deadline.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Deadline // instanceof handles nulls + && deadline.equals(((Deadline) other).deadline)); // state check + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..f597c4e2511 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -4,15 +4,20 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; /** * The API of the Model component. */ public interface Model { /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_CLIENTS = unused -> true; + Predicate PREDICATE_SHOW_ALL_PROJECTS = unused -> true; + Predicate PREDICATE_SHOW_ALL_ISSUES = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -35,53 +40,127 @@ public interface Model { void setGuiSettings(GuiSettings guiSettings); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' project book file path. */ Path getAddressBookFilePath(); /** - * Sets the user prefs' address book file path. + * Sets the user prefs' project book file path. */ void setAddressBookFilePath(Path addressBookFilePath); + DefaultView getDefaultView(); + /** - * Replaces address book data with the data in {@code addressBook}. + * Replaces project book data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + boolean hasProject(Project project); + + boolean hasIssue(Issue issue); + boolean hasClient(Client client); + + /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if an entity with the given id is present in the project book. */ - boolean hasPerson(Person person); + boolean hasProjectId(int id); + + boolean hasIssueId(int id); + + boolean hasClientId(int id); /** - * Deletes the given person. - * The person must exist in the address book. + * Get a entities by id */ - void deletePerson(Person target); + Project getProjectById(int id); + Issue getIssueById(int id); + Client getClientById(int id); + + Client getClient(Client client); + + void deleteProject(Project target); + + void deleteIssue(Issue target); + void deleteClient(Client target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given client. + * {@code client} must not already exist in the project book. */ - void addPerson(Person person); + void addProject(Project project); + void addClient(Client client); + + void addIssue(Issue issue); /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces the given project {@code target} with {@code editedProject}. + * {@code target} must exist in the project book. + * The client identity of {@code editedProject} must not be the same as another existing project + * in the project book. */ - void setPerson(Person target, Person editedPerson); + void setProject(Project target, Project editedProject); + + void setIssue(Issue target, Issue editedIssue); + + void setClient(Client target, Client editedClient); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. + * Generate the next entity ids + * @return id */ - void updateFilteredPersonList(Predicate predicate); + int generateClientId(); + + int generateIssueId(); + + int generateProjectId(); + + ObservableList getFilteredProjectList(); + + ObservableList getFilteredIssueList(); + + ObservableList getFilteredClientList(); + + void updateFilteredProjectList(Predicate predicate); + + void updateFilteredIssueList(Predicate predicate); + + void updateFilteredClientList(Predicate predicate); + + void sortProjectsById(int sortProjectsById); + + void sortProjectsByDeadline(int sortProjectsByDeadlineKey); + + void sortProjectsByIssueCount(int sortProjectsByIssueCountKey); + + void sortProjectsByName(int sortProjectsByNameKey); + + void sortIssuesById(int sortIssuesById); + + void sortIssuesByDeadline(int sortIssuesByDeadline); + + void sortIssuesByUrgency(int sortIssuesByUrgency); + + void sortClientsById(int sortClientsById); + + void sortClientsByName(int sortClientsByName); + + void setDefaultView(DefaultView defaultView); + + void sortClientsByPin(); + + void sortProjectsByPin(); + + void sortIssuesByPin(); + + void sortProjectsByCurrentCategory(); + + void sortClientsByCurrentCategory(); + + void sortIssuesByCurrentCategory(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 86c1df298d7..6a7af54ef03 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -9,19 +9,24 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; /** - * Represents the in-memory model of the address book data. + * Represents the in-memory model of the project book data. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final AddressBook addressBook; + private AddressBook addressBook; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private final FilteredList filteredProjects; + private final FilteredList filteredIssues; + private final FilteredList filteredClients; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -29,11 +34,13 @@ public class ModelManager implements Model { public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine("Initializing with project book: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredProjects = new FilteredList<>(this.addressBook.getProjectList()); + filteredIssues = new FilteredList<>(this.addressBook.getIssueList()); + filteredClients = new FilteredList<>(this.addressBook.getClientList()); } public ModelManager() { @@ -75,6 +82,16 @@ public void setAddressBookFilePath(Path addressBookFilePath) { userPrefs.setAddressBookFilePath(addressBookFilePath); } + @Override + public DefaultView getDefaultView() { + return userPrefs.getDefaultView(); + } + + @Override + public void setDefaultView(DefaultView defaultView) { + userPrefs.setDefaultView(defaultView); + } + //=========== AddressBook ================================================================================ @Override @@ -82,50 +99,172 @@ public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); } + @Override public ReadOnlyAddressBook getAddressBook() { return addressBook; } @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); + public boolean hasProject(Project project) { + requireNonNull(project); + return addressBook.hasProject(project); + } + + @Override + public boolean hasIssue(Issue issue) { + requireNonNull(issue); + return addressBook.hasIssue(issue); + } + + @Override + public boolean hasClient(Client client) { + requireNonNull(client); + return addressBook.hasClient(client); + } + + @Override + public boolean hasProjectId(int id) { + return addressBook.hasProjectId(id); + } + + @Override + public boolean hasIssueId(int id) { + return addressBook.hasIssueId(id); + } + + @Override + public boolean hasClientId(int id) { + return addressBook.hasClientId(id); + } + + @Override + public Project getProjectById(int id) { + return addressBook.getProjectById(id); + } + + @Override + public Issue getIssueById(int id) { + return addressBook.getIssueById(id); + } + + @Override + public Client getClientById(int id) { + return addressBook.getClientById(id); } @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); + public Client getClient(Client client) { + return addressBook.getClient(client); + } + + @Override + public void deleteProject(Project target) { + addressBook.removeProject(target); + } + + @Override + public void deleteIssue(Issue target) { + addressBook.removeIssue(target); + } + + @Override + public void deleteClient(Client target) { + addressBook.removeClient(target); + } + + @Override + public void addProject(Project project) { + addressBook.addProject(project); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + } + + @Override + public void addIssue(Issue issue) { + addressBook.addIssue(issue); + updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + } + + @Override + public void addClient(Client client) { + addressBook.addClient(client); + updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + } + + @Override + public void setProject(Project target, Project editedProject) { + requireAllNonNull(target, editedProject); + + addressBook.setProject(target, editedProject); + } + + @Override + public void setIssue(Issue target, Issue editedIssue) { + requireAllNonNull(target, editedIssue); + + addressBook.setIssue(target, editedIssue); + } + + @Override + public void setClient(Client target, Client editedClient) { + requireAllNonNull(target, editedClient); + + addressBook.setClient(target, editedClient); } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public int generateClientId() { + return addressBook.generateClientId(); } @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public int generateIssueId() { + return addressBook.generateIssueId(); + } - addressBook.setPerson(target, editedPerson); + @Override + public int generateProjectId() { + return addressBook.generateProjectId(); } - //=========== Filtered Person List Accessors ============================================================= + //=========== Filtered List Accessors ============================================================= /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * Returns an unmodifiable view of the list of projects backed by the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; + public ObservableList getFilteredProjectList() { + return filteredProjects; + } + + @Override + public ObservableList getFilteredIssueList() { + return filteredIssues; + } + + @Override + public ObservableList getFilteredClientList() { + return filteredClients; + } + + + @Override + public void updateFilteredProjectList(Predicate predicate) { + requireNonNull(predicate); + filteredProjects.setPredicate(predicate); } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void updateFilteredIssueList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredIssues.setPredicate(predicate); + } + + @Override + public void updateFilteredClientList(Predicate predicate) { + requireNonNull(predicate); + filteredClients.setPredicate(predicate); } @Override @@ -144,7 +283,95 @@ public boolean equals(Object obj) { ModelManager other = (ModelManager) obj; return addressBook.equals(other.addressBook) && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); + && filteredProjects.equals(other.filteredProjects) + && filteredIssues.equals(other.filteredIssues) + && filteredClients.equals(other.filteredClients); + } + + @Override + public void sortProjectsByDeadline(int key) { + addressBook.sortProjectsByDeadline(key); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); } + @Override + public void sortProjectsByIssueCount(int key) { + addressBook.sortProjectsByIssueCount(key); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + } + + @Override + public void sortProjectsByName(int key) { + addressBook.sortProjectsByName(key); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + } + + @Override + public void sortProjectsById(int key) { + addressBook.sortProjectsById(key); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + } + + @Override + public void sortIssuesById(int key) { + addressBook.sortIssuesById(key); + updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + } + + @Override + public void sortIssuesByDeadline(int key) { + addressBook.sortIssuesByDeadline(key); + updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + } + + @Override + public void sortIssuesByUrgency(int key) { + addressBook.sortIssuesByUrgency(key); + updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + } + + @Override + public void sortClientsById(int key) { + addressBook.sortClientsById(key); + updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + } + + @Override + public void sortClientsByName(int key) { + addressBook.sortClientsByName(key); + updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + } + + @Override + public void sortClientsByPin() { + addressBook.sortClientsByPin(); + updateFilteredClientList(PREDICATE_SHOW_ALL_CLIENTS); + } + + @Override + public void sortProjectsByPin() { + addressBook.sortProjectsByPin(); + updateFilteredProjectList(PREDICATE_SHOW_ALL_PROJECTS); + } + + @Override + public void sortIssuesByPin() { + addressBook.sortIssuesByPin(); + updateFilteredIssueList(PREDICATE_SHOW_ALL_ISSUES); + } + + @Override + public void sortProjectsByCurrentCategory() { + addressBook.sortProjectsByCurrentCategory(); + } + + @Override + public void sortClientsByCurrentCategory() { + addressBook.sortClientsByCurrentCategory(); + } + + @Override + public void sortIssuesByCurrentCategory() { + addressBook.sortIssuesByCurrentCategory(); + } } diff --git a/src/main/java/seedu/address/model/Name.java b/src/main/java/seedu/address/model/Name.java new file mode 100644 index 00000000000..e046c25954a --- /dev/null +++ b/src/main/java/seedu/address/model/Name.java @@ -0,0 +1,120 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the name of the client. This class is modelled after the Name class in the Person package of AB3 + */ +public class Name { + + public static final String MESSAGE_CONSTRAINTS = + "Names should not be blank and can contain only contain alphanumeric characters and spaces."; + + + /** + * The name can contain only letters and spaces. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + private String fullName; + + /** + * Constructs a Name. + * + * @param name A valid name. + */ + public Name(String name) { + requireNonNull(name); + checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); + fullName = name; + } + + /** + * Represents an Empty Name. + */ + public static class EmptyName extends Name { + public static final Name EMPTY_NAME = new EmptyName(); + public EmptyName() { + super("empty"); + } + + /** + * Checks if this Client Name is empty. + * @return true if the Client Name is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + } + + + /** + * Returns true if a given string is a valid name. A name is valid only if it contains only letters and spaces and + * had a maximum of four words, each of length less than 10 characters. + * @param test String representing name to be tested + * @return boolean true if a given string is a valid name + */ + public static boolean isValidName(String test) { + return !test.isEmpty() && test.matches(VALIDATION_REGEX); + } + + /** + * Checks if this Client Name is empty. + * @return true if the Client Name is empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Returns the full name of the client. + * @return String representing full name + */ + public String getFullNameRepresentation() { + return this.fullName; + } + + /** + * Returns the UI string representation of the name. + */ + public String uiRepresentation(boolean isPinned, String id) { + return this.fullName + " " + id + + (isPinned ? " \uD83D\uDCCC" : ""); + } + + /** + * Returns the String representation of the Client Name. + * @return String representing the Client Name + */ + @Override + public String toString() { + return this.fullName; + } + + /** + * Checks if an object equals this. + * @param other Object to be checked + * @return boolean true if this is equal to other and false otherwise + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + + } else if (other instanceof Name) { + Name otherName = (Name) other; + return this.fullName.equals(otherName.fullName); + + } else { + return false; + } + } + +} diff --git a/src/main/java/seedu/address/model/Pin.java b/src/main/java/seedu/address/model/Pin.java new file mode 100644 index 00000000000..74b229ea39f --- /dev/null +++ b/src/main/java/seedu/address/model/Pin.java @@ -0,0 +1,46 @@ +package seedu.address.model; + +/** + * Represents whether an entity is pinned. + */ +public class Pin { + + public static final String MESSAGE_CONSTRAINTS = "Pin can only take truth values"; + private boolean isPinned; + + public Pin(boolean isPinned) { + this.isPinned = isPinned; + } + + public boolean isPinned() { + return this.isPinned; + } + + public void togglePinned() { + this.isPinned = !this.isPinned; + } + + /** + * Returns true if a given string is a valid pin. + */ + public static boolean isValidPin(String pinString) { + if (pinString.toUpperCase().equals("FALSE") || pinString.toUpperCase().equals("TRUE")) { + return true; + } else { + return false; + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Pin)) { + return false; + } + + return this.isPinned == ((Pin) other).isPinned; + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..a7d252c93b6 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,17 +1,82 @@ package seedu.address.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; /** - * Unmodifiable view of an address book + * Unmodifiable view of an project book */ public interface ReadOnlyAddressBook { /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. + * Returns an unmodifiable view of the projects list. + * This list will not contain any duplicate projects. */ - ObservableList getPersonList(); + ObservableList getProjectList(); + /** + * Returns a modifiable view of the projects list. + * This list will not contain any duplicate projects. + */ + ObservableList getModifiableProjectList(); + + /** + * Returns an unmodifiable view of the issues list. + * This list will not contain any duplicate issues. + */ + ObservableList getIssueList(); + + ObservableList getModifiableIssueList(); + + ObservableList getModifiableClientList(); + + /** + * Returns an unmodifiable view of the clients list. + * This list will not contain any duplicate clients. + */ + ObservableList getClientList(); + + /** + * Get a project object via its id + * @param id id to retrieve + * @return project object + */ + Project getProjectById(int id); + + /** + * Get a issue object via its id + * @param id id to retrieve + * @return issue object + */ + Issue getIssueById(int id); + + /** + * Get a client object via its id + * @param id id to retrieve + * @return client object + */ + Client getClientById(int id); + + + Client getClient(Client client); + + /** + * Generate the next client id + * @return id + */ + int generateClientId(); + + /** + * Generate the next issue id + * @return id + */ + int generateIssueId(); + + /** + * Generate the next project id + * @return id + */ + int generateProjectId(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c73..0ac50f160e8 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -2,6 +2,7 @@ import java.nio.file.Path; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; /** @@ -13,4 +14,5 @@ public interface ReadOnlyUserPrefs { Path getAddressBookFilePath(); + void setDefaultView(DefaultView defaultView); } diff --git a/src/main/java/seedu/address/model/SortCategory.java b/src/main/java/seedu/address/model/SortCategory.java new file mode 100644 index 00000000000..3e117dcf911 --- /dev/null +++ b/src/main/java/seedu/address/model/SortCategory.java @@ -0,0 +1,12 @@ +package seedu.address.model; + +/** + * Encapsulates the different sorting categories possible for an entity. + */ +public enum SortCategory { + ID, + NAME, + DEADLINE, + ISSUE_COUNT, + Low, URGENCY +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..d924503c343 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -6,6 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; /** @@ -51,11 +52,20 @@ public Path getAddressBookFilePath() { return addressBookFilePath; } + @Override + public void setDefaultView(DefaultView defaultView) { + this.guiSettings.setDefaultView(defaultView); + } + public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); this.addressBookFilePath = addressBookFilePath; } + public DefaultView getDefaultView() { + return this.guiSettings.getDefaultView(); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/client/Client.java b/src/main/java/seedu/address/model/client/Client.java new file mode 100644 index 00000000000..6b9bd775063 --- /dev/null +++ b/src/main/java/seedu/address/model/client/Client.java @@ -0,0 +1,405 @@ +package seedu.address.model.client; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.SortCategory; +import seedu.address.model.interfaces.ComparableByName; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.project.Project; + +/** + * Represents a Client associated with a project. This is modelled after the AB3 Person. + */ +public class Client implements ComparableByName, HasIntegerIdentifier { + + public static final String MESSAGE_INVALID_NAME_SORT_KEY = + "Enter either a 0 to sort by alphabetical order or a 1 to sort by reverse alphabetical order"; + + public static final String MESSAGE_INVALID_CLIENT_ID_SORT_KEY = + "Enter either a 0 to sort in ascending order or a 1 to sort in descending order"; + + private static SortCategory sortCategory = SortCategory.ID; + private static int sortOrder = 0; + + //@@author Aishwarya-Hariharan-Iyer + + //Represents the Client's name + private Name name; + + //Represents the Client's ClientEmail + private ClientEmail email; + + //Represents the Client's ClientMobile + private ClientMobile mobile; + + //Represents a Collection of projects that the client is responsible for + private List projects; + + //Represents the Client's id + private ClientId clientId; + + //@@author + + //Represents whether the client is pinned + private Pin pin; + + //@@author Aishwarya-Hariharan-Iyer + /** + * Constructs a client with inputs given by the user. + * @param name String representing name of the client + * @param mobile String representing mobile number of the client + * @param email String representing email address of the client + */ + public Client(Name name, ClientMobile mobile, ClientEmail email, List projects, + ClientId clientId, Pin pin) { + requireAllNonNull(name, mobile, email, clientId, pin); + this.name = name; + this.mobile = mobile; + this.email = email; + this.projects = projects; + this.clientId = clientId; + this.pin = pin; + } + + /** + * Constructs a client with inputs given by the user. + * @param name String representing name of the client + */ + public Client(Name name) { + requireAllNonNull(name); + this.name = name; + this.mobile = ClientMobile.EmptyClientMobile.EMPTY_MOBILE; + this.email = ClientEmail.EmptyEmail.EMPTY_EMAIL; + this.projects = new ArrayList<>(); + this.clientId = ClientId.EmptyClientId.EMPTY_CLIENT_ID; + this.pin = new Pin(false); + } + + /** + * Sets the client name to be as user input. + * @param name Name representing the new name of client + */ + public void setName(Name name) { + this.name = name; + } + + /** + * Sets the client email to be as user input. + * @param email ClientEmail representing the new email of client + */ + public void setEmail(ClientEmail email) { + this.email = email; + } + + /** + * Sets the client mobile to be as user input. + * @param mobile ClientMobile representing the new mobile of client + */ + public void setMobile(ClientMobile mobile) { + this.mobile = mobile; + } + + /** + * Returns the id of the client. + * @return ClientId the id of the client + */ + public ClientId getClientId() { + return this.clientId; + } + //@@author + + /** + * Returns the id of the client as an integer. + * @return int representing the client id. + */ + public int getClientIdInInt() { + return getClientId().getIdInt(); + } + + /** + * Checks if this Client is empty. + * @return boolean true if the Client is empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Returns the size of the project list. + * @return int representing the project list size. + */ + public int getProjectListSize() { + return this.projects.size(); + } + + /** + * Returns the id of the client as an integer. + * @return int representing the client id. + */ + @Override + public int getId() { + return this.getClientId().getIdInt(); + } + + /** + * Checks if this client has a valid id. + * @return boolean true if the client id is valid + */ + public boolean hasValidId() { + return this.clientId.isValid(); + } + + /** + * Represents an Empty Client. + */ + public static class EmptyClient extends Client { + public static final Client EMPTY_CLIENT = new EmptyClient(); + + /** + * Constructs an empty client. + */ + private EmptyClient() { + super(Name.EmptyName.EMPTY_NAME); + } + + /** + * Checks if this Client is empty. + * @return boolean true if the Client is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + /** + * Returns the String representation of the empty client. + * @return String representing the empty client. + */ + @Override + public String toString() { + return ""; + } + + /** + * Returns the UI representation of the client. + * @return String representing the UI state of the client. + */ + @Override + public String uiRepresentation() { + return "No Client Set"; + } + + } + + + /** + * Checks if input is a valid client id sort key. + * + * 0 for ascending and 1 for descending order + * + * @param num input param to validate + * @return boolean true if input is a 0 or 1 + */ + public static boolean isValidClientIdSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid name sort key. + * + * 0 for alphabetical order and 1 for reverse alphabetical order + * + * @param num input param to validate + * @return boolean true if input is a 0 or 1 + */ + public static boolean isValidNameSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + //@@author Aishwarya-Hariharan-Iyer + /** + * Returns the client name as is represented in the Name object. + * @return String representing client's name. + */ + public seedu.address.model.Name getClientName() { + return this.name; + } + + /** + * Returns the client email as is represented in the ClientEmail object. + * @return String representing client's email. + */ + public ClientEmail getClientEmail() { + return this.email; + } + + /** + * Returns the client mobile as is represented in the ClientMobile object. + * @return String representing client's mobile. + */ + public ClientMobile getClientMobile() { + return this.mobile; + } + + /** + * Returns the list of projects under the client. + * @return String representing client's mobile. + */ + public List getProjects() { + return this.projects; + } + //@@author + + /** + * Add A project to the client's project list. + */ + public void addProjects(Project project) { + projects.add(project); + } + + /** + * Returns the string for display in the UI + * + * @return String for display in the UI + */ + public String uiRepresentation() { + return this.name.toString() + clientMobileRepresentation(); + } + + public String clientMobileRepresentation() { + return this.mobile.isEmpty() ? this.mobile.toString() : " (" + this.mobile.toString() + ")"; + } + + //@@author Aishwarya-Hariharan-Iyer + /** + * Removes a project from the project list of the client. + * @param p Project to be removed + */ + public void removeProject(Project p) { + this.projects.remove(p); + } + //@@author + + /** + * Toggles the pin status of the client. + */ + public void togglePin() { + this.pin.togglePinned(); + } + + /** + * Checks if the client is pinned. + * @return boolean true if the client is pinned + */ + public boolean isPinned() { + return this.pin.isPinned(); + } + + public static SortCategory getSortCategory() { + return sortCategory; + } + + public static int getSortOrder() { + return sortOrder; + } + + public static void setSortCategory(SortCategory newSortCategory) { + sortCategory = newSortCategory; + } + + public static void setSortOrder(int newSortOrder) { + sortOrder = newSortOrder; + } + + + /** + * Returns true if both clients have the same name. + * This defines a weaker notion of equality between two clients. + * @return boolean true if the clients have the same name + */ + @Override + public boolean hasSameName(Client otherClient) { + if (otherClient == this) { + return true; + } + + return otherClient != null + && otherClient.getClientName().equals(getClientName()); + } + + /** + * Returns true if client is valid and exists. + * @retun boolean true if the client is valid + */ + public static boolean isValidClient(Client client) { + if (client == EmptyClient.EMPTY_CLIENT) { + return false; + } + return true; + } + + /** + * Returns true if all other attributes besides project list are the same. + * @param otherClient Client to be compared with + * @return Boolean value representing whether the basic client details are equal + */ + public boolean hasSameDetails(Client otherClient) { + return this.getClientId().equals(otherClient.getClientId()) + && this.getClientName().equals(otherClient.getClientName()) + && this.getClientEmail().equals(otherClient.getClientEmail()) + && this.getClientMobile().equals(otherClient.getClientMobile()) + && this.getPin().equals(otherClient.getPin()); + } + + /** + * Returns the String representation of the client name. + * @return String representing the client + */ + @Override + public String toString() { + return this.name.getFullNameRepresentation(); + } + + //@@author Aishwarya-Hariharan-Iyer + /** + * Checks if an object equals this. + * @param other Object to be checked + * @return boolean true if this is equal to other and false otherwise + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Client) { + Client otherClient = (Client) other; + boolean hasSameId = this.getClientId().equals(otherClient.getClientId()); + boolean hasSameName = this.getClientName().equals(otherClient.getClientName()); + boolean hasSameEmail = this.getClientEmail().equals(otherClient.getClientEmail()); + boolean hasSameMobile = this.getClientMobile().equals(otherClient.getClientMobile()); + boolean hasSamePin = this.getPin().equals(otherClient.getPin()); + return hasSameId && hasSameEmail && hasSameMobile && hasSameName && hasSamePin; + } else { + return false; + } + } + //@@author + + private Pin getPin() { + return this.pin; + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/client/ClientEmail.java similarity index 57% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/seedu/address/model/client/ClientEmail.java index f866e7133de..52e9416e327 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/client/ClientEmail.java @@ -1,15 +1,14 @@ -package seedu.address.model.person; +package seedu.address.model.client; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} + * Represents a Person's email in the project book. */ -public class Email { - +public class ClientEmail { 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" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " @@ -21,6 +20,7 @@ public class Email { + " - end with a domain label at least 2 characters long\n" + " - have each domain label start and end with alphanumeric characters\n" + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; + // alphanumeric and special characters private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" @@ -31,17 +31,46 @@ public class Email { private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - public final String value; + private String email; /** - * Constructs an {@code Email}. + * Constructs a Client Email. * * @param email A valid email address. */ - public Email(String email) { + public ClientEmail(String email) { requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; + checkArgument(isValidClientEmail(email), MESSAGE_CONSTRAINTS); + this.email = email; + } + + /** + * Represents an Empty ClientEmail. + */ + public static class EmptyEmail extends ClientEmail { + public static final ClientEmail EMPTY_EMAIL = new EmptyEmail(); + public EmptyEmail() { + super("cs2103@nus.edu"); + } + + @Override + public String toString() { + return ""; + } + + @Override + public String uiRepresentation() { + return "No email set"; + } + + } + + /** + * Checks if this Client Email is empty. + * @return true if the Client Email is empty. + */ + public boolean isEmpty() { + return false; } /** @@ -51,21 +80,42 @@ public static boolean isValidEmail(String test) { return test.matches(VALIDATION_REGEX); } - @Override - public String toString() { - return value; - } - @Override public boolean equals(Object other) { return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check + || (other instanceof ClientEmail // instanceof handles nulls + && this.email.equals(((ClientEmail) other).email)); // state check } @Override public int hashCode() { - return value.hashCode(); + return this.email.hashCode(); + } + + /** + * Checks if the client email is valid. + * @param test String representing the email to be tested + * @return boolean true if this is a valid client email + */ + public static boolean isValidClientEmail(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns the email of the client. + * @return String representing email + */ + public String uiRepresentation() { + return "Email: " + this.email; + } + + /** + * Returns the String representation of the Client Email. + * @return String representing the Client Email + */ + @Override + public String toString() { + return this.email; } } diff --git a/src/main/java/seedu/address/model/client/ClientId.java b/src/main/java/seedu/address/model/client/ClientId.java new file mode 100644 index 00000000000..a1bcfe99ed2 --- /dev/null +++ b/src/main/java/seedu/address/model/client/ClientId.java @@ -0,0 +1,100 @@ +package seedu.address.model.client; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Client's id. + */ +public class ClientId { + + public static final String MESSAGE_CONSTRAINTS = "Client ID must exist in Client list"; + public static final String MESSAGE_INVALID = "Client ID must be an integer"; + private int clientId; + + /** + * Construct's an client's id. + * + * @param id A valid client id. + */ + public ClientId(int id) { + requireNonNull(id); + + this.clientId = id; + } + + public int getIdInt() { + return this.clientId; + } + + /** + * Checks if this ClientID is empty. + * @return false since the ClientID is not empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Checks if this ClientID is valid. + */ + public boolean isValid() { + return clientId > 0; + } + + /** + * Represents an Empty ClientID. + */ + public static class EmptyClientId extends ClientId { + public static final ClientId EMPTY_CLIENT_ID = new EmptyClientId(); + public static final int ID = Integer.MAX_VALUE; + public EmptyClientId() { + super(ID); + } + + /** + * Checks if this ClientID is empty. + * @return true since the ClientID is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + } + + /** + * Checks whether the client ID string is valid. + * @param clientId + * @return Boolean value representing the validity of the string. + */ + public static boolean isValidClientId(String clientId) { + try { + int pid = Integer.parseInt(clientId); + return pid > 0; + } catch (NumberFormatException e) { + return false; + } + } + + + public String uiRepresentation() { + return "(#" + toString() + ")"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ClientId // instanceof handles nulls + && this.clientId == ((ClientId) other).clientId); + } + + @Override + public String toString() { + return String.valueOf(this.clientId); + } + +} diff --git a/src/main/java/seedu/address/model/client/ClientMobile.java b/src/main/java/seedu/address/model/client/ClientMobile.java new file mode 100644 index 00000000000..5cdb80489da --- /dev/null +++ b/src/main/java/seedu/address/model/client/ClientMobile.java @@ -0,0 +1,104 @@ +package seedu.address.model.client; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the mobile of the client. This class is modelled after the Phone class in the Person package of AB3 + */ +public class ClientMobile { + + public static final String MESSAGE_CONSTRAINTS = + "Mobile numbers should only contain numbers, and it should be at least 3 digits long"; + public static final String VALIDATION_REGEX = "\\d{3,}"; + + private String mobile; + + /** + * Constructs a Client Mobile. + * + * @param mobile A valid mobile number. + */ + public ClientMobile(String mobile) { + requireNonNull(mobile); + checkArgument(isValidClientMobile(mobile), MESSAGE_CONSTRAINTS); + this.mobile = mobile; + } + + /** + * Represents an Empty Client Mobile. + */ + public static class EmptyClientMobile extends ClientMobile { + public static final ClientMobile EMPTY_MOBILE = new EmptyClientMobile(); + public EmptyClientMobile() { + super("90000000"); + } + + /** + * Checks if this Client Email is empty. + * @return true if the Client Email is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String uiRepresentation() { + return "No contact number set"; + } + + @Override + public String toString() { + return ""; + } + } + + /** + * Returns true if a given string is a valid mobile number. + */ + public static boolean isValidClientMobile(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Checks if this Client Email is empty. + * @return true if the Client Email is empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Returns the mobile of the client. + * @return String representing mobile + */ + public String uiRepresentation() { + return "Contact Number: " + this.mobile; + } + + /** + * Returns the String representation of the Client Mobile. + * @return String representing the Client Mobile + */ + @Override + public String toString() { + return this.mobile; + } + /** + * Checks if an object equals this. + * @param other Object to be checked + * @return boolean true if this is equal to other and false otherwise + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ClientMobile) { + ClientMobile otherMobile = (ClientMobile) other; + return this.mobile.equals(otherMobile.mobile); + } else { + return false; + } + } +} diff --git a/src/main/java/seedu/address/model/client/ClientProjectList.java b/src/main/java/seedu/address/model/client/ClientProjectList.java new file mode 100644 index 00000000000..f5c38269a28 --- /dev/null +++ b/src/main/java/seedu/address/model/client/ClientProjectList.java @@ -0,0 +1,71 @@ +package seedu.address.model.client; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; + +import seedu.address.model.project.Project; + + +/** + * Represents the list of projects the client is associated with. + */ +public class ClientProjectList { + + private ArrayList projects; + + /** + * Constructs an empty project list. + */ + public ClientProjectList() { + this.projects = new ArrayList(); + } + + /** + * Adds a project to the list. + * @param project Project to be added. + */ + public void addProject(Project project) { + requireNonNull(project); + this.projects.add(project); + } + + /** + * Removes a project from the list. + * @param project Project to be removed. + */ + public void removeProject(Project project) { + this.projects.remove(project); + } + + /** + * Returns a list of all projects. + * @return String representing all projects in the list + */ + public String listAllProjects() { + if (projects.size() > 0) { + String list = this.projects.toArray().toString(); + return list; + } else { + return "none"; + } + } + + /** + * Returns the number of projects in the list + * @return int representing number of projects in the list + */ + public int getListLength() { + return this.projects.size(); + } + + /** + * Checks if list is empty. + * @return true if list is empty + */ + public boolean isEmpty() { + return projects.size() == 0; + } + + +} diff --git a/src/main/java/seedu/address/model/client/ClientWithoutModel.java b/src/main/java/seedu/address/model/client/ClientWithoutModel.java new file mode 100644 index 00000000000..2a6677db681 --- /dev/null +++ b/src/main/java/seedu/address/model/client/ClientWithoutModel.java @@ -0,0 +1,75 @@ +package seedu.address.model.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import seedu.address.model.Model; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.exceptions.ClientNotFoundException; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; + +// @@author Dernbu +/** + * This class represents a partial initialisation of client (without access to model). + * The reason why this exists is to separate Parser classes, and to ensure they are not dependent on + * Addressbook/Model classes. + *

+ * This is inspired from a functional programming appraoch, where we store intermediate data by nesting functions. + */ +public class ClientWithoutModel implements Function { + + private final Name name; + private final ClientMobile mobile; + private final ClientEmail email; + private final List projectIdList; + private final Pin pin; + + /** + * Partially initialise a client without access to a Model object. + * @param name name of cline + * @param mobile mobile number of client + * @param email email of client + * @param projectIdList the list of project ids the client is involved in. + */ + + public ClientWithoutModel(Name name, ClientMobile mobile, ClientEmail email, + List projectIdList, Pin pin) { + this.name = name; + this.mobile = mobile; + this.email = email; + this.projectIdList = projectIdList; + this.pin = pin; + } + + @Override + public Client apply(Model model) { + ArrayList projectList = new ArrayList<>(); + for (ProjectId pid : projectIdList) { + projectList.add(model.getProjectById(pid.getIdInt())); + } + + Client client = new Client(name, mobile, email, projectList, new ClientId(model.generateClientId()), pin); + if (client.isEmpty()) { + throw new ClientNotFoundException(); + } + + return client; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ClientWithoutModel)) { + return false; + } + ClientWithoutModel c = (ClientWithoutModel) o; + return c == this || ( + this.name.equals(c.name) + && this.mobile.equals(c.mobile) + && this.email.equals(c.email) + && this.projectIdList.equals(c.projectIdList) + && this.pin.equals(c.pin)); + } +} diff --git a/src/main/java/seedu/address/model/client/Type.java b/src/main/java/seedu/address/model/client/Type.java new file mode 100644 index 00000000000..950cfb72644 --- /dev/null +++ b/src/main/java/seedu/address/model/client/Type.java @@ -0,0 +1,9 @@ +package seedu.address.model.client; + +/** + * Represents the types of clients for whom the project is developed as Employer, Organization (external) or + * individual. + */ +public enum Type { + INDIVIDUAL, ORGANIZATION, EMPLOYER, UNSPECIFIED +} diff --git a/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java b/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java new file mode 100644 index 00000000000..fdb3671d5ac --- /dev/null +++ b/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.client.exceptions; + +/** + * Signals that the operation is unable to find the specified client. + */ +public class ClientNotFoundException extends RuntimeException{} diff --git a/src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java b/src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java new file mode 100644 index 00000000000..7ba474b1f52 --- /dev/null +++ b/src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java @@ -0,0 +1,16 @@ +package seedu.address.model.client.exceptions; + +/** + * Signals that the operation will result in duplicate clients. Clients are considered as duplicate if they have the + * same identity. + */ + +public class DuplicateClientException extends RuntimeException { + + /** + * Alerts the user with respect to creating duplicate clients. + */ + public DuplicateClientException() { + super("Operation would result in duplicate clients"); + } +} diff --git a/src/main/java/seedu/address/model/interfaces/ComparableByName.java b/src/main/java/seedu/address/model/interfaces/ComparableByName.java new file mode 100644 index 00000000000..ed3d4991dfe --- /dev/null +++ b/src/main/java/seedu/address/model/interfaces/ComparableByName.java @@ -0,0 +1,16 @@ +package seedu.address.model.interfaces; + +/** + * Interface for an object that is comparable by a string representation (name) + * @param type of object + */ +public interface ComparableByName { + + /** + * This defines a lower level of equality - comparison by name/title + * for relevant objects that can be compared by such. + * @param other other object + * @return true if the objects have the same name. + */ + boolean hasSameName(T other); +} diff --git a/src/main/java/seedu/address/model/interfaces/HasIntegerIdentifier.java b/src/main/java/seedu/address/model/interfaces/HasIntegerIdentifier.java new file mode 100644 index 00000000000..e0c4e064da1 --- /dev/null +++ b/src/main/java/seedu/address/model/interfaces/HasIntegerIdentifier.java @@ -0,0 +1,59 @@ +package seedu.address.model.interfaces; + +import seedu.address.model.list.NotFoundException; + +// @@author Dernbu +/** + * Interface for any class whose objects can be identified by an integer ID. + * + */ +public interface HasIntegerIdentifier { + + /** + * Method to get the integer identifier of an object + * @return an integer + */ + int getId(); + + /** + * Generate the next object ID. + * @return the max ID in list + 1 + */ + public static int generateNextId(Iterable i) { + int maxId = 0; + for (T t: i) { + maxId = t.getId() > maxId ? t.getId() : maxId; + } + return maxId + 1; + } + + /** + * Check if a list contains an object with the given id + * @return true if list contains object. + */ + public static boolean containsId(Iterable i, int id) { + for (T t: i) { + if (t.getId() == id) { + return true; + } + } + return false; + } + + /** + * Get an element from a list of objects by ID + * @param iterable iterable to queyr + * @param id id of item to retrieve + * @param object type + * @return + */ + public static T getElementById(Iterable iterable, int id) { + for (T item: iterable) { + if (item.getId() == id) { + return item; + } + } + throw new NotFoundException(); + } + +} diff --git a/src/main/java/seedu/address/model/issue/Issue.java b/src/main/java/seedu/address/model/issue/Issue.java new file mode 100644 index 00000000000..a398700eaf2 --- /dev/null +++ b/src/main/java/seedu/address/model/issue/Issue.java @@ -0,0 +1,293 @@ +package seedu.address.model.issue; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import seedu.address.model.Deadline; +import seedu.address.model.Pin; +import seedu.address.model.SortCategory; +import seedu.address.model.interfaces.ComparableByName; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.project.Project; + +/** + * Represents an Issue. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Issue implements ComparableByName, HasIntegerIdentifier { + + public static final String MESSAGE_INVALID_DEADLINE_SORT_KEY = + "Enter either a 0 to sort by chronological order or a 1 to sort by reverse chronological order"; + + public static final String MESSAGE_INVALID_URGENCY_SORT_KEY = + "Enter either a 0 to sort by lowest urgency or a 1 to sort by highest urgency"; + + public static final String MESSAGE_INVALID_ISSUE_ID_SORT_KEY = + "Enter either a 0 to sort in ascending order or a 1 to sort in descending order"; + + private static SortCategory sortCategory = SortCategory.ID; + private static int sortOrder = 0; + + // Components of an issue + private Title title; + private Deadline deadline; + private Urgency urgency; + private Status status; + private Project project; + private IssueId issueId; + private Pin pin; + + /** + * Title field and project field must be present and not null, but all other fields are optional. + */ + public Issue(Title title, Deadline deadline, Urgency urgency, + Status status, Project project, IssueId issueId, Pin pin) { + requireAllNonNull(title, deadline, urgency, status, project, issueId, pin); + this.title = title; + this.deadline = deadline; + this.urgency = urgency; + this.status = status; + this.project = project; + this.issueId = issueId; + this.project.getIssueList().add(this); + this.pin = pin; + } + + /** + * Title field and project field must be present and not null. + */ + public Issue(Title title, Project project) { + requireAllNonNull(title, project); + this.title = title; + this.project = project; + //todo: set other fields to emptyOptionals post-merge + } + + /** + * Represents an Empty Issue. + */ + public static class EmptyIssue extends Issue { + public static final Issue EMPTY_ISSUE = new EmptyIssue(); + private EmptyIssue() { + super(Title.EmptyTitle.EMPTY_TITLE, Project.EmptyProject.EMPTY_PROJECT); + } + + /** + * Checks if this Project is empty. + * @return true if the Project is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + + } + + public IssueId getIssueId() { + return this.issueId; + } + + public int getIssueIdInInt() { + return getIssueId().getIdInt(); + } + + public Title getTitle() { + return this.title; + } + + public Deadline getDeadline() { + return this.deadline; + } + + public Urgency getUrgency() { + return this.urgency; + } + + public Project getProject() { + return this.project; + } + + public void deleteProjectIssue(Issue i) { + getProject().removeIssue(i); + } + + public Status getStatus() { + return this.status; + } + + /** + * Returns true if both issues have the same title. + * This defines a weaker notion of equality between two issues. + */ + @Override + public boolean hasSameName(Issue otherIssue) { + return otherIssue.title == this.title; + } + + + + public void setTitle(Title title) { + this.title = title; + } + + public void setDeadline(Deadline deadline) { + this.deadline = deadline; + } + + public void setUrgency(Urgency urgency) { + this.urgency = urgency; + } + + public void setStatus(Status status) { + this.status = status; + } + + /** + * Checks if this Issue is empty. + * @return true if the Issue is empty. + */ + public boolean isEmpty() { + return false; + } + + public void togglePin() { + this.pin.togglePinned(); + } + + public boolean isPinned() { + return this.pin.isPinned(); + } + + public static SortCategory getSortCategory() { + return sortCategory; + } + + public static int getSortOrder() { + return sortOrder; + } + + public static void setSortCategory(SortCategory newSortCategory) { + sortCategory = newSortCategory; + } + + public static void setSortOrder(int newSortOrder) { + sortOrder = newSortOrder; + } + + public String uiRepresentation() { + return this.title + " " + this.issueId.uiRepresentation(); + } + + /** + * Check if the project has a valid id. + */ + public boolean hasValidId() { + return this.issueId.isValid(); + } + @Override + public String toString() { + return this.title.toString(); + } + + /** + * Checks if input is a valid deadline sort key. + * + * 0 for chronological order and 1 for reverse chronological order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidDeadlineSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid urgency sort key. + * + * 0 for lowest urgency order and 1 for highest urgency order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidUrgencySortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid issue id sort key. + * + * 0 for ascending and 1 for descending order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidIssueIdSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Returns true if both issues have the same title. + * This defines a weaker notion of equality between two issues. + */ + public boolean isSameIssue(Issue otherIssue) { + if (otherIssue == this) { + return true; + } + + return otherIssue != null + && otherIssue.getTitle().equals(getTitle()); + } + + /** + * Returns true if both projects have the same identity and data fields. + * This defines a stronger notion of equality between two projects. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Issue)) { + return false; + } + + Issue otherIssue = (Issue) other; + return otherIssue.getTitle().equals(getTitle()) + && otherIssue.getProject().equals(getProject()) + && otherIssue.getDeadline().equals(getDeadline()) + && otherIssue.getStatus().equals(getStatus()) + && otherIssue.getUrgency().equals(getUrgency()) + && otherIssue.getIssueId().equals(getIssueId()) + && otherIssue.getPin().equals(getPin()); + } + + private Pin getPin() { + return this.pin; + } + + @Override + public int getId() { + return this.issueId.getIdInt(); + } + +} diff --git a/src/main/java/seedu/address/model/issue/IssueId.java b/src/main/java/seedu/address/model/issue/IssueId.java new file mode 100644 index 00000000000..d21fe4369e3 --- /dev/null +++ b/src/main/java/seedu/address/model/issue/IssueId.java @@ -0,0 +1,91 @@ +package seedu.address.model.issue; + +import static java.util.Objects.requireNonNull; + +/** + * Represents an Issue's id. + */ +public class IssueId { + + public static final String MESSAGE_CONSTRAINTS = "Issue ID must be a valid integer"; + public static final String MESSAGE_INVALID = "Issue ID must be a valid integer. " + + "No existing issue with this issue ID"; + private int issueId; + + /** + * Construct's an issue's id. + * + * @param id A valid issue id. + */ + public IssueId(int id) { + requireNonNull(id); + + this.issueId = id; + } + + /** + * Checks if this IssueID is valid. + */ + public boolean isValid() { + return issueId > 0; + } + + /** + * Represents an empty issue id. + */ + public static class EmptyIssueId extends IssueId { + public static final IssueId EMPTY_ISSUE_ID = new EmptyIssueId(); + + private EmptyIssueId() { + super(Integer.MAX_VALUE); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + } + + public int getIdInt() { + return this.issueId; + } + + /** + * Checks if the issue ID string is valid. + * @param issueId + * @return Boolean value denoting whether the id string is valid. + */ + public static boolean isValidIssueId(String issueId) { + try { + int pid = Integer.parseInt(issueId); + return pid > 0; + } catch (NumberFormatException e) { + return false; + } + } + + public boolean isEmpty() { + return false; + } + + public String uiRepresentation() { + return "(#" + toString() + ")"; + } + @Override + public String toString() { + return String.valueOf(this.issueId); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof IssueId // instanceof handles nulls + && this.issueId == ((IssueId) other).issueId); + } + +} diff --git a/src/main/java/seedu/address/model/issue/IssueWithoutModel.java b/src/main/java/seedu/address/model/issue/IssueWithoutModel.java new file mode 100644 index 00000000000..e2cc75a2c0b --- /dev/null +++ b/src/main/java/seedu/address/model/issue/IssueWithoutModel.java @@ -0,0 +1,72 @@ +package seedu.address.model.issue; + +import java.util.function.Function; + +import seedu.address.model.Deadline; +import seedu.address.model.Model; +import seedu.address.model.Pin; +import seedu.address.model.project.ProjectId; + +// @@author Dernbu +/** + * This class represents a partial initialisation of issue (without access to model). + * The reason why this exists is to separate Parser classes, and to ensure they are not dependent on + * Addressbook/Model classes. + *

+ * This is inspired from a functional programming appraoch, where we store intermediate data by nesting functions. + */ +public class IssueWithoutModel implements Function { + + private final Title title; + private final Deadline deadline; + private final Status status; + private final Urgency urgency; + private final ProjectId projectId; + private final Pin pin; + + /** + * Partially initialise an issue without access to a Model object. + * @param title title of issue + * @param deadline deadline of isseu + * @param urgency urgency of issue + * @param status status of issue + * @param projectId projectId of project that issue is attached to. + */ + public IssueWithoutModel(Title title, Deadline deadline, + Urgency urgency, Status status, ProjectId projectId, Pin pin) { + this.title = title; + this.deadline = deadline; + this.urgency = urgency; + this.status = status; + this.projectId = projectId; + this.pin = pin; + + } + + @Override + public Issue apply(Model model) { + return new Issue(title, + deadline, + urgency, + status, + model.getProjectById(projectId.getIdInt()), + new IssueId(model.generateIssueId()), + pin + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IssueWithoutModel)) { + return false; + } + IssueWithoutModel i = (IssueWithoutModel) o; + return i == this || ( + this.title.equals(i.title) + && this.deadline.equals(i.deadline) + && this.urgency.equals(i.urgency) + && this.status.equals(i.status) + && this.projectId.equals(i.projectId) + && this.pin.equals(i.pin)); + } +} diff --git a/src/main/java/seedu/address/model/issue/Status.java b/src/main/java/seedu/address/model/issue/Status.java new file mode 100644 index 00000000000..20ec7b7305a --- /dev/null +++ b/src/main/java/seedu/address/model/issue/Status.java @@ -0,0 +1,107 @@ +package seedu.address.model.issue; + +import static java.lang.Boolean.parseBoolean; + +/** + * Represents the various completion statuses of an issue. + */ +public class Status { + + public static final String MESSAGE_CONSTRAINTS = "Status should be completed or incomplete."; + /** + * Represents and empty Issue status. + */ + public static class EmptyStatus extends Status { + public static final Status EMPTY_STATUS = new EmptyStatus(); + + private EmptyStatus() { + super(false); + } + + @Override + public boolean isEmpty() { + return true; + } + } + + private boolean isCompleted; + + /** + * Constructs an issue completion Status. + * + * @param isCompleted true/false to indicate if issue is completed. + */ + public Status(boolean isCompleted) { + this.isCompleted = isCompleted; + } + + /** + * Returns true if a given string is a valid status. + */ + public static boolean isValidStatus(String status) { + if (status.toUpperCase().equals("FALSE")) { + return !parseBoolean(status); + } else { + return parseBoolean(status); + } + } + + public boolean getStatus() { + return this.isCompleted; + } + + public void setStatus(boolean isCompleted) { + this.isCompleted = isCompleted; + } + + public boolean isEmpty() { + return false; + } + + /** + * Returns the ui representation of the status + */ + public String uiRepresentation() { + if (this.isCompleted == true) { + return "Status: Completed"; + } else { + return "Status: Incomplete"; + } + } + + /** + * Returns the String representation of only the completion status. + * @return String representing whether the issue is completed. + */ + public String getCompletionStatus() { + if (this.isCompleted) { + return "Completed"; + } else { + return "Incomplete"; + } + } + + @Override + public String toString() { + return String.valueOf(this.isCompleted); + } + + /** + * Returns true if both projects have the same identity and data fields. + * This defines a stronger notion of equality between two projects. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Status)) { + return false; + } + + Status otherStatus = (Status) other; + return otherStatus.isCompleted == this.isCompleted; + + } +} diff --git a/src/main/java/seedu/address/model/issue/Title.java b/src/main/java/seedu/address/model/issue/Title.java new file mode 100644 index 00000000000..d43ae6e6efa --- /dev/null +++ b/src/main/java/seedu/address/model/issue/Title.java @@ -0,0 +1,84 @@ +package seedu.address.model.issue; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents an Issue's title. + */ +public class Title { + + /** + * Represents an empty title. + */ + public static class EmptyTitle extends Title { + + public static final Title EMPTY_TITLE = new EmptyTitle(); + + private EmptyTitle() { + super("empty"); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + } + public static final String MESSAGE_CONSTRAINTS = + "Titles can take any values, and it should not be blank"; + + /* + * The first character of issue title must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s].*"; + + private String title; + + /** + * Constructs a issue title. + * + * @param title A valid issue title. + */ + public Title(String title) { + requireNonNull(title); + checkArgument(isValidTitle(title), MESSAGE_CONSTRAINTS); + this.title = title; + } + + /** + * Returns true if a given string is a valid title. + */ + public static boolean isValidTitle(String title) { + return title.matches(VALIDATION_REGEX); + } + + public boolean isEmpty() { + return false; + } + + /** + * Returns the UI string representation of the issue title. + */ + public String uiRepresentation(boolean isPinned, String issueId) { + return this.title + " " + issueId + + (isPinned ? " \uD83D\uDCCC" : ""); + } + + @Override + public String toString() { + return this.title; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Title // instanceof handles nulls + && title.equals(((Title) other).title)); + } +} diff --git a/src/main/java/seedu/address/model/issue/Urgency.java b/src/main/java/seedu/address/model/issue/Urgency.java new file mode 100644 index 00000000000..28857364f10 --- /dev/null +++ b/src/main/java/seedu/address/model/issue/Urgency.java @@ -0,0 +1,62 @@ +package seedu.address.model.issue; + +import java.util.ArrayList; + +/** + * Represents the various urgency levels of an issue. + */ +public enum Urgency { + NONE, LOW, MEDIUM, HIGH; + + public static final String MESSAGE_CONSTRAINTS = "Urgency should be an integer 0 (NONE), 1 (LOW), 2 (MEDIUM) or " + + "3 (HIGH)"; + public static final String MESSAGE_STRING_CONSTRAINTS = "Urgency should be High, Low, Medium or " + + "None."; + + /** + * Checks if the urgency integer string is valid. + * @param urgencyValue + * @return Boolean denoting whether the urgency integer string is valid. + */ + public static boolean isValidUrgency(String urgencyValue) { + try { + ArrayList priorities = new ArrayList(); + priorities.add(0); + priorities.add(1); + priorities.add(2); + priorities.add(3); + for (Integer i: priorities) { + if (Integer.valueOf(i).equals(Integer.valueOf(urgencyValue))) { + return true; + } + } + return false; + } catch (NumberFormatException e) { + return false; + } + + } + + /** + * Checks if urgency string is valid. + * @param urgency + * @return Boolean denoting whether the urgency string is valid. + */ + public static boolean isValidUrgencyString(String urgency) { + try { + Urgency tempUrgency = valueOf(urgency); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public String uiRepresentation() { + return "Urgency: " + super.toString(); + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/src/main/java/seedu/address/model/issue/exceptions/DuplicateIssueException.java b/src/main/java/seedu/address/model/issue/exceptions/DuplicateIssueException.java new file mode 100644 index 00000000000..098098fde6c --- /dev/null +++ b/src/main/java/seedu/address/model/issue/exceptions/DuplicateIssueException.java @@ -0,0 +1,11 @@ +package seedu.address.model.issue.exceptions; + +/** + * Signals that the operation will result in duplicate Issues (Issues are considered duplicates if they have the same + * identity). + */ +public class DuplicateIssueException extends RuntimeException { + public DuplicateIssueException() { + super("Operation would result in duplicate issues"); + } +} diff --git a/src/main/java/seedu/address/model/issue/exceptions/IssueNotFoundException.java b/src/main/java/seedu/address/model/issue/exceptions/IssueNotFoundException.java new file mode 100644 index 00000000000..c0b7f7b30a6 --- /dev/null +++ b/src/main/java/seedu/address/model/issue/exceptions/IssueNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.issue.exceptions; + +/** + * Signals that the operation is unable to find the specified project. + */ +public class IssueNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/list/DuplicateException.java b/src/main/java/seedu/address/model/list/DuplicateException.java new file mode 100644 index 00000000000..87799ec21ad --- /dev/null +++ b/src/main/java/seedu/address/model/list/DuplicateException.java @@ -0,0 +1,11 @@ +package seedu.address.model.list; + +/** + * Signals that the operation will result in duplicate Projects + * (Projects are considered duplicates if they have the same identity). + */ +public class DuplicateException extends RuntimeException { + public DuplicateException() { + super("Operation would result in duplicate items"); + } +} diff --git a/src/main/java/seedu/address/model/list/NotFoundException.java b/src/main/java/seedu/address/model/list/NotFoundException.java new file mode 100644 index 00000000000..04ead84ea14 --- /dev/null +++ b/src/main/java/seedu/address/model/list/NotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.list; + +/** + * Signals that the operation is unable to find the specified object. + */ +public class NotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/list/UniqueEntityList.java b/src/main/java/seedu/address/model/list/UniqueEntityList.java new file mode 100644 index 00000000000..99161852b6c --- /dev/null +++ b/src/main/java/seedu/address/model/list/UniqueEntityList.java @@ -0,0 +1,167 @@ +package seedu.address.model.list; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.interfaces.ComparableByName; +import seedu.address.model.interfaces.HasIntegerIdentifier; + +/** + * A class for a list of unique entities. + * Notably, entities need to extend {@link ComparableByName} and {@link HasIntegerIdentifier}. + * @param Type of entity in list + */ +public class UniqueEntityList & HasIntegerIdentifier> implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent object (by comparing name) as the given argument. + */ + public boolean containsByName(T toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::hasSameName); + } + + /** + * Returns true if the list contains the given ID. + */ + public boolean containsId(int id) { + return internalList.stream().anyMatch((item) -> id == item.getId()); + } + + /** + * Adds an object T to the list. + * The object must not already exist in the list. + */ + public void add(T toAdd) { + requireNonNull(toAdd); + if (containsByName(toAdd)) { + throw new DuplicateException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the object {@code t} in the list with {@code editedT}. + * {@code t} must exist in the list. + * The text identity of {@code editedT} must not be the same as another existing object in the list. + */ + public void setItem(T t, T editedT) { + requireAllNonNull(t, editedT); + + int index = internalList.indexOf(t); + if (index == -1) { + throw new NotFoundException(); + } + + if (!t.hasSameName(editedT) && containsByName(editedT)) { + throw new DuplicateException(); + } + + internalList.set(index, editedT); + } + + /** + * Removes the equivalent object from the list. + * The object must exist in the list. + */ + public void remove(T toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new NotFoundException(); + } + } + + public void setList(UniqueEntityList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code newList}. + * {@code newList} must not contain duplicate objects. + */ + public void setList(List newList) { + requireAllNonNull(newList); + if (!itemsAreUnique(newList)) { + throw new DuplicateException(); + } + + internalList.setAll(newList); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + /** + * Returns the backing list as a modifiable {@code ObservableList}. + */ + public ObservableList asModifiableObservableList() { + return internalList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueEntityList // instanceof handles nulls + && internalList.equals(((UniqueEntityList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code tList} contains only unique objects. + */ + private boolean itemsAreUnique(List tList) { + for (int i = 0; i < tList.size() - 1; i++) { + for (int j = i + 1; j < tList.size(); j++) { + if (tList.get(i).hasSameName(tList.get(j))) { + return false; + } + } + } + return true; + } + + + + /** + * Get an element by its id + * @param id id to retrieve with + * @return an element if it is found. + * @throws NotFoundException if element is not found. + */ + public T getElementById(int id) { + for (T t: this) { + if (t.getId() == id) { + return t; + } + } + throw new NotFoundException(); + } + + public void sortById() { + internalList.sort(Comparator.comparingInt(HasIntegerIdentifier::getId)); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java deleted file mode 100644 index 79244d71cf7..00000000000 --- a/src/main/java/seedu/address/model/person/Name.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's name in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} - */ -public class Name { - - public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; - - public final String fullName; - - /** - * Constructs a {@code Name}. - * - * @param name A valid name. - */ - public Name(String name) { - requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); - fullName = name; - } - - /** - * Returns true if a given string is a valid name. - */ - public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); - } - - - @Override - public String toString() { - return fullName; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check - } - - @Override - public int hashCode() { - return fullName.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427c..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 8ff1d83fe89..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,123 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append("; Phone: ") - .append(getPhone()) - .append("; Email: ") - .append(getEmail()) - .append("; Address: ") - .append(getAddress()); - - Set tags = getTags(); - if (!tags.isEmpty()) { - builder.append("; Tags: "); - tags.forEach(builder::append); - } - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382f..00000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -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 final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f59442..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/project/Project.java b/src/main/java/seedu/address/model/project/Project.java new file mode 100644 index 00000000000..9cb07c79b42 --- /dev/null +++ b/src/main/java/seedu/address/model/project/Project.java @@ -0,0 +1,361 @@ +package seedu.address.model.project; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.model.Deadline; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.SortCategory; +import seedu.address.model.client.Client; +import seedu.address.model.interfaces.ComparableByName; +import seedu.address.model.interfaces.HasIntegerIdentifier; +import seedu.address.model.issue.Issue; + +/** + * Represents a Project. + */ +public class Project implements ComparableByName, HasIntegerIdentifier { + + public static final String MESSAGE_INVALID_DEADLINE_SORT_KEY = + "Enter either a 0 to sort by chronological order or a 1 to sort by reverse chronological order"; + + public static final String MESSAGE_INVALID_ISSUE_COUNT_SORT_KEY = + "Enter either a 0 to sort by incomplete issue count or a 1 to sort by completed issue count"; + + public static final String MESSAGE_INVALID_NAME_SORT_KEY = + "Enter either a 0 to sort by alphabetical order or a 1 to sort by reverse alphabetical order"; + + public static final String MESSAGE_INVALID_PROJECT_ID_SORT_KEY = + "Enter either a 0 to sort in ascending order or a 1 to sort in descending order"; + + private static SortCategory sortCategory = SortCategory.ID; + private static int sortOrder = 0; + + // Components of a project + private Name name; + private Repository repository; + private Deadline deadline; + private Client client; + private ProjectId projectId; + private List issueList; + private Pin pin; + + /** + * Name field must be present and not null and other fields may be optional. + */ + public Project(Name name, Repository repository, Deadline deadline, + Client client, List issueList, ProjectId projectId, Pin pin) { + requireAllNonNull(name); + this.name = name; + this.repository = repository; + this.deadline = deadline; + this.client = client; + this.issueList = issueList; + this.projectId = projectId; + this.pin = pin; + } + + /** + * Name field must be present and not null . + */ + public Project(Name name) { + requireAllNonNull(name); + this.name = name; + this.repository = Repository.EmptyRepository.EMPTY_REPOSITORY; + this.deadline = Deadline.EmptyDeadline.EMPTY_DEADLINE; + this.client = Client.EmptyClient.EMPTY_CLIENT; + this.issueList = new ArrayList<>(); + this.projectId = ProjectId.EmptyProjectId.EMPTY_PROJECT_ID; + this.pin = new Pin(false); + } + + public void setClient(Client toAddClient) { + this.client = toAddClient; + } + + @Override + public int getId() { + return this.projectId.getIdInt(); + } + + /** + * Check if the project has a valid id. + */ + public boolean hasValidId() { + return this.projectId.isValid(); + } + + public void addIssue(Issue toAddIssue) { + this.issueList.add(toAddIssue); + } + + /** + * Represents an Empty Project. + */ + public static class EmptyProject extends Project { + public static final Project EMPTY_PROJECT = new EmptyProject(); + + private EmptyProject() { + super(Name.EmptyName.EMPTY_NAME); + } + + /** + * Checks if this Project is empty. + * + * @return true if the Project is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + + } + + /** + * Returns the UI string representation of the project. + */ + public String uiRepresentation() { + return "Project: " + this.toString(); + } + + /** + * Returns the UI string representation of the issue list. + */ + public String issueListUiRepresentation() { + return issueList.size() + + (issueList.size() == 1 ? " issue (" : " issues (") + + getCompletedIssueCount() + " complete, " + + getIncompleteIssueCount() + " incomplete)"; + } + + public ProjectId getProjectId() { + return projectId; + } + + public int getProjectIdInInt() { + return getProjectId().getIdInt(); + } + + public Name getProjectName() { + return name; + } + + public Repository getRepository() { + return repository; + } + + public Deadline getDeadline() { + return deadline; + } + + public Client getClient() { + return client; + } + + public List getIssueList() { + return issueList; + } + + public void setName(Name name) { + this.name = name; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setDeadline(Deadline deadline) { + this.deadline = deadline; + } + + public void removeClient() { + this.client = Client.EmptyClient.EMPTY_CLIENT; + } + + public void removeIssue(Issue i) { + getIssueList().remove(i); + } + + public void togglePin() { + this.pin.togglePinned(); + } + + public boolean isPinned() { + return this.pin.isPinned(); + } + + public static SortCategory getSortCategory() { + return sortCategory; + } + + public static int getSortOrder() { + return sortOrder; + } + + public static void setSortCategory(SortCategory newSortCategory) { + sortCategory = newSortCategory; + } + + public static void setSortOrder(int newSortOrder) { + sortOrder = newSortOrder; + } + + /** + * Checks if input is a valid deadline sort key. + * + * 0 for chronological order and 1 for reverse chronological order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidDeadlineSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid issue count sort key. + * + * 0 for incomplete issue count order and 1 for completed issue count order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidIssueCountSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid name sort key. + * + * 0 for alphabetical order and 1 for reverse alphabetical order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidNameSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if input is a valid project id sort key. + * + * 0 for ascending and 1 for descending order + * + * @param num input param to validate + * @return true if input is a 0 or 1 + */ + public static boolean isValidProjectIdSortKey(String num) { + try { + int number = Integer.parseInt(num); + return number == 0 || number == 1; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if this Project is empty. + * + * @return true if the Project is empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Counts number of completed issues in Project Issue List + * @return number of completed issues + */ + public int getCompletedIssueCount() { + int count = 0; + for (Issue i: issueList) { + if (i.getStatus().getStatus() == true) { + count += 1; + } + } + return count; + } + + /** + * Counts number of incomplete issues in Project Issue List + * @return number of incomplete issues; + */ + public int getIncompleteIssueCount() { + int count = 0; + for (Issue i: issueList) { + if (i.getStatus().getStatus() == false) { + count += 1; + } + } + return count; + } + /** + * Returns true if both projects have the same identity and data fields. + * This defines a stronger notion of equality between two projects. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Project)) { + return false; + } + + Project otherProject = (Project) other; + return otherProject.getProjectName().equals(getProjectName()) + && otherProject.getRepository().equals(getRepository()) + && otherProject.getDeadline().equals(getDeadline()) + && otherProject.getClient().equals(getClient()) + && otherProject.getIssueList().equals(getIssueList()) + && otherProject.getProjectId().equals(getProjectId()) + && otherProject.getPin().equals(getPin()); + } + + private Pin getPin() { + return this.pin; + } + + /** + * Returns true if both projects have the same name. + * This defines a weaker notion of equality between two projects. + */ + @Override + public boolean hasSameName(Project otherProject) { + if (otherProject == this) { + return true; + } + + return otherProject != null + && otherProject.getProjectName().equals(getProjectName()); + } + + @Override + public String toString() { + return this.getProjectName().toString(); + } +} diff --git a/src/main/java/seedu/address/model/project/ProjectId.java b/src/main/java/seedu/address/model/project/ProjectId.java new file mode 100644 index 00000000000..95da66f18b2 --- /dev/null +++ b/src/main/java/seedu/address/model/project/ProjectId.java @@ -0,0 +1,101 @@ +package seedu.address.model.project; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Project's id. + */ +public class ProjectId { + + public static final String MESSAGE_CONSTRAINTS = "Project ID must be a positive integer, less than " + + Integer.MAX_VALUE + "."; + public static final String MESSAGE_INVALID = "Project ID must be an integer. " + + "No existing project with this project ID"; + private int projectId; + + /** + * Construct's an project's id. + * + * @param id A valid project id. + */ + public ProjectId(int id) { + requireNonNull(id); + + this.projectId = id; + } + + /** + * Checks if this ProjectID is valid. + */ + public boolean isValid() { + return projectId > 0; + } + + /** + * Checks if this ProjectID is empty. + * @return false since the ProjectID is not empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Represents an Empty ProjectID. + */ + public static class EmptyProjectId extends ProjectId { + public static final ProjectId EMPTY_PROJECT_ID = new EmptyProjectId(); + public EmptyProjectId() { + super(Integer.MAX_VALUE); + } + + + /** + * Checks if this ProjectID is empty. + * @return true since the ProjectID is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + } + + /** + * Checks whether the project ID string is valid. + * @param projectId + * @return Boolean value representing the validity of the project ID string. + */ + public static boolean isValidProjectId(String projectId) { + try { + Integer pid = Integer.parseInt(projectId); + return pid > 0; + } catch (NumberFormatException e) { + return false; + } + } + + public int getIdInt() { + return this.projectId; + } + + public String uiRepresentation() { + return "(#" + this + ")"; + } + + @Override + public String toString() { + return String.valueOf(this.projectId); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ProjectId // instanceof handles nulls + && projectId == (((ProjectId) other).projectId)); // state check + } +} + diff --git a/src/main/java/seedu/address/model/project/ProjectWithoutModel.java b/src/main/java/seedu/address/model/project/ProjectWithoutModel.java new file mode 100644 index 00000000000..fd9abf5fd0e --- /dev/null +++ b/src/main/java/seedu/address/model/project/ProjectWithoutModel.java @@ -0,0 +1,83 @@ +package seedu.address.model.project; + +import java.util.List; +import java.util.function.Function; + +import seedu.address.model.Deadline; +import seedu.address.model.Model; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientId; +import seedu.address.model.issue.Issue; +import seedu.address.model.list.NotFoundException; + +// @@author Dernbu +/** + * This class represents a partial initialisation of project (without access to model). + * The reason why this exists is to separate Parser classes, and to ensure they are not dependent on + * Addressbook/Model classes. + *

+ * This is inspired from a functional programming appraoch, where we store intermediate data by nesting functions. + */ +public class ProjectWithoutModel implements Function { + + private final Name name; + private final Repository repository; + private final ClientId clientId; + private final Deadline deadline; + private final List issueList; + private final Pin pin; + + + /** + * Partially initialise a Project object without access to a Model class. + * @param name project name + * @param repository project repository + * @param deadline project deadline + * @param clientId client associated with project + * @param issueList list of issues in project + */ + public ProjectWithoutModel(Name name, Repository repository, Deadline deadline, + ClientId clientId, List issueList, Pin pin) { + this.name = name; + this.repository = repository; + this.deadline = deadline; + this.clientId = clientId; + this.issueList = issueList; + this.pin = pin; + } + + @Override + public Project apply(Model model) { + Client client = Client.EmptyClient.EMPTY_CLIENT; + + if (!clientId.isEmpty()) { + try { + client = model.getClientById(clientId.getIdInt()); + } catch (NotFoundException e) { + client = null; + } + } + + System.out.println(model.generateProjectId()); + return new Project(name, repository, deadline, + client, issueList, new ProjectId(model.generateProjectId()), pin); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ProjectWithoutModel)) { + return false; + } + ProjectWithoutModel p = (ProjectWithoutModel) o; + return p == this || ( + this.name.equals(p.name) + && this.repository.equals(p.repository) + && this.deadline.equals(p.deadline) + && this.clientId.equals(p.clientId) + && this.issueList.equals(p.issueList) + && this.pin.equals(p.pin)); + } + +} diff --git a/src/main/java/seedu/address/model/project/Repository.java b/src/main/java/seedu/address/model/project/Repository.java new file mode 100644 index 00000000000..a955b827f7b --- /dev/null +++ b/src/main/java/seedu/address/model/project/Repository.java @@ -0,0 +1,96 @@ +package seedu.address.model.project; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Project's repository. + */ +public class Repository { + + /** + * Represents an empty Project repository. + */ + public static class EmptyRepository extends Repository { + public static final Repository EMPTY_REPOSITORY = new EmptyRepository(); + + private EmptyRepository() { + super("conrad/tp"); + } + + /** + * Checks if this Repository is empty. + * @return true since the Repository is empty. + */ + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return ""; + } + + @Override + public String uiRepresentation() { + return "No Repository Set"; + } + } + + public static final String MESSAGE_CONSTRAINTS = + "Repository should be entered in format \n" + + "Username: Maximum of 39 characters with 1 or more alphanumeric characters \n" + + "Repository: Maximum of 39 characters with 1 or more alphanumeric characters \n" + + "Dashes are allowed"; + + /* + * Username and repository name should have a maximum of 39 characters with 1 or more alphanumeric characters. + * Dashes are allowed. + */ + public static final String VALIDATION_REGEX = "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,38})/[a-zA-Z0-9]([a-zA-Z0-9-]{0,38})"; + + private String projectRepository; + + /** + * Construct's a project Repository. + * + * @param repository A valid repository name. + */ + public Repository(String repository) { + requireNonNull(repository); + checkArgument(isValidRepository(repository), MESSAGE_CONSTRAINTS); + this.projectRepository = repository; + } + + /** + * Checks if this Repository is empty. + * @return false since the Repository is not empty. + */ + public boolean isEmpty() { + return false; + } + + /** + * Returns true if a given string is a valid repository. + */ + public static boolean isValidRepository(String repository) { + return repository.matches(VALIDATION_REGEX); + } + + public String uiRepresentation() { + return "https://github.com/" + projectRepository; + } + + @Override + public String toString() { + return projectRepository; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Repository // instanceof handles nulls + && projectRepository.equals(((Repository) other).projectRepository)); // state check + } +} diff --git a/src/main/java/seedu/address/model/project/exceptions/DuplicateProjectException.java b/src/main/java/seedu/address/model/project/exceptions/DuplicateProjectException.java new file mode 100644 index 00000000000..b68d9ae7d41 --- /dev/null +++ b/src/main/java/seedu/address/model/project/exceptions/DuplicateProjectException.java @@ -0,0 +1,11 @@ +package seedu.address.model.project.exceptions; + +/** + * Signals that the operation will result in duplicate Projects + * (Projects are considered duplicates if they have the same identity). + */ +public class DuplicateProjectException extends RuntimeException { + public DuplicateProjectException() { + super("Operation would result in duplicate projects"); + } +} diff --git a/src/main/java/seedu/address/model/project/exceptions/ProjectNotFoundException.java b/src/main/java/seedu/address/model/project/exceptions/ProjectNotFoundException.java new file mode 100644 index 00000000000..90b698a2f27 --- /dev/null +++ b/src/main/java/seedu/address/model/project/exceptions/ProjectNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.project.exceptions; + +/** + * Signals that the operation is unable to find the specified project. + */ +public class ProjectNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index b0ea7e7dad7..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..bc381d17c85 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,60 +1,155 @@ package seedu.address.model.util; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; import seedu.address.model.AddressBook; +import seedu.address.model.Deadline; +import seedu.address.model.Name; +import seedu.address.model.Pin; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientEmail; +import seedu.address.model.client.ClientId; +import seedu.address.model.client.ClientMobile; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Status; +import seedu.address.model.issue.Title; +import seedu.address.model.issue.Urgency; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.model.project.Repository; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + + public static Project[] getSampleProjects() { + return new Project[] { + new Project(new Name("Individual Project"), new Repository("johndoe/ip"), new Deadline("2022-03-03"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(1), new Pin(false)), + new Project(new Name("Team Project"), new Repository("johndoe/tp"), new Deadline("2023-01-02"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(2), new Pin(false)), + new Project(new Name("Group Project"), new Repository("johndoe/gp"), new Deadline("2023-11-29"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(3), new Pin(false)), + new Project(new Name("Personal Project"), new Repository("johndoe/pp"), new Deadline("2022-04-11"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(4), new Pin(false)), + new Project(new Name("Random Project"), new Repository("johndoe/rp"), new Deadline("2022-05-27"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(5), new Pin(false)), + new Project(new Name("Final Year Project"), new Repository("johndoe/fyp"), new Deadline("2023-02-27"), + Client.EmptyClient.EMPTY_CLIENT, new ArrayList<>(), new ProjectId(6), new Pin(false)), + }; + } + + public static Client[] getSampleClients(Project[] sampleProjects) { + Client[] sampleClients = new Client[] { + new Client(new Name("Alex Yeoh"), + new ClientMobile("87438807"), + new ClientEmail("alexyeoh@example.com"), + new ArrayList<>(List.of(sampleProjects[0], sampleProjects[1])), + new ClientId(1), + new Pin(false)), + new Client(new Name("Bernice Yu"), + new ClientMobile("99272758"), + new ClientEmail("berniceyu@example.com"), + new ArrayList<>(List.of(sampleProjects[2])), + new ClientId(2), + new Pin(false)), + new Client(new Name("Charlotte Oliveiro"), + new ClientMobile("93210283"), + new ClientEmail("charlotte@example.com"), + new ArrayList<>(List.of(sampleProjects[3])), + new ClientId(3), + new Pin(false)), + new Client(new Name("David Li"), + new ClientMobile("91031282"), + new ClientEmail("lidavid@example.com"), + new ArrayList<>(List.of(sampleProjects[4])), + new ClientId(4), + new Pin(false)), + new Client(new Name("Irfan Ibrahim"), + new ClientMobile("92492021"), + new ClientEmail("irfan@example.com"), + new ArrayList<>(List.of(sampleProjects[5])), + new ClientId(5), + new Pin(false)), + }; + sampleProjects[0].setClient(sampleClients[0]); + sampleProjects[1].setClient(sampleClients[0]); + sampleProjects[2].setClient(sampleClients[1]); + sampleProjects[3].setClient(sampleClients[2]); + sampleProjects[4].setClient(sampleClients[3]); + sampleProjects[5].setClient(sampleClients[4]); + return sampleClients; + } + + public static Issue[] getSampleIssues(Project[] sampleProjects) { + Issue[] sampleIssues = new Issue[] { + new Issue(new Title("Refactor UI Classes"), + new Deadline("2022-01-01"), + Urgency.LOW, + new Status(false), + sampleProjects[0], + new IssueId(1), + new Pin(false)), + new Issue(new Title("Fix loading screen bug"), + new Deadline("2022-10-04"), + Urgency.MEDIUM, + new Status(true), + sampleProjects[1], + new IssueId(2), + new Pin(false)), + new Issue(new Title("Implement filter command"), + new Deadline("2022-09-03"), + Urgency.LOW, + new Status(false), + sampleProjects[1], + new IssueId(3), + new Pin(false)), + new Issue(new Title("Update collision logic"), + new Deadline("2023-07-14"), + Urgency.HIGH, + new Status(true), + sampleProjects[2], + new IssueId(4), + new Pin(false)), + new Issue(new Title("Create Developer Guide"), + new Deadline("2022-02-04"), + Urgency.HIGH, + new Status(true), + sampleProjects[3], + new IssueId(5), + new Pin(false)), + new Issue(new Title("Change default behaviour"), + new Deadline("2022-02-19"), + Urgency.MEDIUM, + new Status(false), + sampleProjects[4], + new IssueId(6), + new Pin(false)), }; + return sampleIssues; } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); + Project[] sampleProjects = getSampleProjects(); + Client[] sampleClients = getSampleClients(sampleProjects); + Issue[] sampleIssues = getSampleIssues(sampleProjects); + + for (Project sampleProject : sampleProjects) { + sampleAb.addProject(sampleProject); + } + for (Client sampleClient : sampleClients) { + sampleAb.addClient(sampleClient); } - return sampleAb; - } - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); + for (Issue sampleIssue : sampleIssues) { + sampleAb.addIssue(sampleIssue); + } + return sampleAb; } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedClient.java b/src/main/java/seedu/address/storage/JsonAdaptedClient.java new file mode 100644 index 00000000000..bb1675b02b5 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedClient.java @@ -0,0 +1,74 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.Client; +import seedu.address.model.client.ClientEmail; +import seedu.address.model.client.ClientId; +import seedu.address.model.client.ClientMobile; +import seedu.address.model.project.Project; + + +/** + * Jackson-friendly version of {@link Client}. + */ +class JsonAdaptedClient { + + private final String name; + private final String mobile; + private final String email; + private final String clientId; + private final String pin; + + /** + * Constructs a {@code JsonAdaptedClient} with the given client details. + */ + @JsonCreator + public JsonAdaptedClient(@JsonProperty("name") String name, @JsonProperty("mobile") String mobile, + @JsonProperty("email") String email, + @JsonProperty("clientId") String clientId, + @JsonProperty("pin") String pin) { + this.name = name; + this.mobile = mobile; + this.email = email; + this.clientId = clientId; + this.pin = pin; + } + + /** + * Converts a given {@code Client} into this class for Jackson use. + */ + public JsonAdaptedClient(Client source) { + name = source.getClientName().toString(); + mobile = source.getClientMobile().toString(); + email = source.getClientEmail().toString(); + clientId = source.getClientId().toString(); + pin = String.valueOf(source.isPinned()); + } + + /** + * Converts this Jackson-friendly adapted client object into the model's {@code Client} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted client. + */ + public Client toModelType() throws IllegalValueException { + if (name.isEmpty()) { + return Client.EmptyClient.EMPTY_CLIENT; + } + final List clientProjects = new ArrayList<>(); + final Name modelName = StorageUtil.readNameFromStorage(name, Client.class.getSimpleName()); + final ClientMobile modelMobile = StorageUtil.readMobileFromStorage(mobile); + final ClientEmail modelEmail = StorageUtil.readEmailFromStorage(email); + final Pin modelPin = StorageUtil.readPinFromStorage(pin, Client.class.getSimpleName()); + final ClientId modelClientId = StorageUtil.readClientIdFromStorage(clientId); + return new Client(modelName, modelMobile, modelEmail, clientProjects, modelClientId, modelPin); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedIssue.java b/src/main/java/seedu/address/storage/JsonAdaptedIssue.java new file mode 100644 index 00000000000..588f0be0bac --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedIssue.java @@ -0,0 +1,81 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.AddressBook; +import seedu.address.model.Deadline; +import seedu.address.model.Pin; +import seedu.address.model.issue.Issue; +import seedu.address.model.issue.IssueId; +import seedu.address.model.issue.Status; +import seedu.address.model.issue.Title; +import seedu.address.model.issue.Urgency; +import seedu.address.model.project.Project; + +/** + * Jackson-friendly version of {@link Issue}. + */ +class JsonAdaptedIssue { + + private final String title; + private final String urgency; + private final String deadline; + private final String status; + private final String issueId; + private final String project; + private final String pin; + + /** + * Constructs a {@code JsonAdaptedIssue} with the given issue details. + */ + @JsonCreator + public JsonAdaptedIssue(@JsonProperty("title") String title, + @JsonProperty("urgency") String urgency, + @JsonProperty("deadline") String deadline, + @JsonProperty("status") String status, + @JsonProperty("issueId") String issueId, + @JsonProperty("project") String project, + @JsonProperty("pin") String pin) { + this.title = title; + this.urgency = urgency; + this.deadline = deadline; + this.status = status; + this.project = project; + this.pin = pin; + this.issueId = issueId; + } + + /** + * Converts a given {@code Issue} into this class for Jackson use. + */ + public JsonAdaptedIssue(Issue source) { + title = source.getTitle().toString(); + urgency = source.getUrgency().toString(); + deadline = source.getDeadline().toString(); + status = source.getStatus().toString(); + issueId = source.getIssueId().toString(); + project = source.getProject().getProjectId().toString(); + pin = String.valueOf(source.isPinned()); + } + + /** + * Converts this Jackson-friendly adapted issue object into the model's {@code Issue} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted issue. + */ + public Issue toModelType(AddressBook addressBook) throws IllegalValueException { + + final Title modelTitle = StorageUtil.readTitleFromStorage(title); + final Urgency modelUrgency = StorageUtil.readUrgencyFromStorage(urgency); + final Deadline modelDeadline = StorageUtil.readDeadlineFromStorage(deadline, Issue.class.getSimpleName()); + final Status modelStatus = StorageUtil.readStatusFromStorage(status); + final Project modelProject = StorageUtil.readProjectFromStorage(project, addressBook); + final Pin modelPin = StorageUtil.readPinFromStorage(pin, Issue.class.getSimpleName()); + final IssueId modelIssueId = StorageUtil.readIssueIdFromStorage(issueId); + return new Issue(modelTitle, modelDeadline, modelUrgency, modelStatus, + modelProject, modelIssueId, modelPin); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2ea..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = new ArrayList<>(); - - /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. - */ - @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedProject.java b/src/main/java/seedu/address/storage/JsonAdaptedProject.java new file mode 100644 index 00000000000..9e58b4b10ae --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedProject.java @@ -0,0 +1,79 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.Deadline; +import seedu.address.model.Name; +import seedu.address.model.Pin; +import seedu.address.model.client.Client; +import seedu.address.model.issue.Issue; +import seedu.address.model.project.Project; +import seedu.address.model.project.ProjectId; +import seedu.address.model.project.Repository; + +/** + * Jackson-friendly version of {@link Project}. + */ +class JsonAdaptedProject { + + private final String name; + private final String repository; + private final String deadline; + private final String projectId; + private final String pin; + + + private final JsonAdaptedClient client; + + /** + * Constructs a {@code JsonAdaptedProject} with the given project details. + */ + @JsonCreator + public JsonAdaptedProject(@JsonProperty("name") String name, @JsonProperty("repository") String repository, + @JsonProperty("deadline") String deadline, + @JsonProperty("client") JsonAdaptedClient client, + @JsonProperty("projectId") String projectId, + @JsonProperty("pin") String pin) { + this.name = name; + this.repository = repository; + this.deadline = deadline; + this.client = client; + this.projectId = projectId; + this.pin = pin; + } + + /** + * Converts a given {@code Project} into this class for Jackson use. + */ + public JsonAdaptedProject(Project source) { + name = source.getProjectName().toString(); + repository = source.getRepository().toString(); + deadline = source.getDeadline().toString(); + client = new JsonAdaptedClient(source.getClient()); + projectId = source.getProjectId().toString(); + pin = String.valueOf(source.isPinned()); + } + + /** + * Converts this Jackson-friendly adapted project object into the model's {@code Project} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted project. + */ + public Project toModelType() throws IllegalValueException { + final Name modelName = StorageUtil.readNameFromStorage(name, Project.class.getSimpleName()); + final Repository modelRepository = StorageUtil.readRepositoryFromStorage(repository); + final Deadline modelDeadline = StorageUtil.readDeadlineFromStorage(deadline, Project.class.getSimpleName()); + final Client modelClient = StorageUtil.readClientFromStorage(client); + final Pin modelPin = StorageUtil.readPinFromStorage(pin, Project.class.getSimpleName()); + final ProjectId modelProjectId = StorageUtil.readProjectIdFromStorage(projectId); + final List modelIssues = new ArrayList<>(); + return new Project(modelName, modelRepository, modelDeadline, + modelClient, modelIssues, modelProjectId, modelPin); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..793f7b9fe83 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,7 +11,6 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; /** * An Immutable AddressBook that is serializable to JSON format. @@ -19,16 +18,17 @@ @JsonRootName(value = "addressbook") class JsonSerializableAddressBook { - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); + private final List projects = new ArrayList<>(); + private final List issues = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook}. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); + public JsonSerializableAddressBook(@JsonProperty("projects") List projects, + @JsonProperty("issues") List issues) { + this.projects.addAll(projects); + this.issues.addAll(issues); } /** @@ -37,23 +37,20 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List= 0 : "Project ID should be positive"; + + return modelProjectId; + } + + /** + * Parses title string from storage. + */ + public static Title readTitleFromStorage(String title) throws IllegalValueException { + if (title == null) { + throw new IllegalValueException(String.format(MISSING_ISSUE_FIELD_MESSAGE_FORMAT, + Title.class.getSimpleName())); + } + if (!Title.isValidTitle(title)) { + throw new IllegalValueException(Title.MESSAGE_CONSTRAINTS); + } + return new Title(title); + } + + /** + * Parses urgency string from storage. + */ + public static Urgency readUrgencyFromStorage(String urgency) throws IllegalValueException { + if (urgency == null) { + throw new IllegalValueException(String.format(MISSING_ISSUE_FIELD_MESSAGE_FORMAT, + Urgency.class.getSimpleName())); + } + + if (!Urgency.isValidUrgencyString(urgency)) { + throw new IllegalValueException(Urgency.MESSAGE_CONSTRAINTS); + } + + return Urgency.valueOf(urgency); + } + + /** + * Parses status string from storage. + */ + public static Status readStatusFromStorage(String status) throws IllegalValueException { + if (status == null) { + throw new IllegalValueException(String.format(MISSING_ISSUE_FIELD_MESSAGE_FORMAT, + Status.class.getSimpleName())); + } + if (!Status.isValidStatus(status)) { + throw new IllegalValueException(Status.MESSAGE_CONSTRAINTS); + } + return new Status(Boolean.valueOf(status)); + } + + /** + * Parses project object from storage. + */ + public static Project readProjectFromStorage(String project, + AddressBook addressBook) throws IllegalValueException { + if (project == null) { + throw new IllegalValueException(String.format(MISSING_ISSUE_FIELD_MESSAGE_FORMAT, + Project.class.getSimpleName())); + } + try { + return HasIntegerIdentifier.getElementById( + addressBook.getProjectList(), Integer.parseInt(project)); + } catch (NotFoundException | NumberFormatException e) { + throw new IllegalValueException(ProjectId.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses issue id string from storage. + */ + public static IssueId readIssueIdFromStorage(String issueId) throws IllegalValueException { + if (issueId == null) { + throw new IllegalValueException(String.format(MISSING_ISSUE_FIELD_MESSAGE_FORMAT, + IssueId.class.getSimpleName())); + } + if (!IssueId.isValidIssueId(issueId)) { + throw new IllegalValueException(IssueId.MESSAGE_CONSTRAINTS); + } + final IssueId modelIssueId = new IssueId(Integer.parseInt(issueId)); + + assert modelIssueId.getIdInt() >= 0 : "Issue ID should be positive"; + return modelIssueId; + } + + /** + * Parses client mobile string from storage. + */ + public static ClientMobile readMobileFromStorage(String mobile) throws IllegalValueException { + if (mobile == null) { + throw new IllegalValueException(String.format(MISSING_CLIENT_FIELD_MESSAGE_FORMAT, + ClientMobile.class.getSimpleName())); + } + + if (mobile.isEmpty()) { + return ClientMobile.EmptyClientMobile.EMPTY_MOBILE; + } else { + if (!ClientMobile.isValidClientMobile(mobile)) { + throw new IllegalValueException(ClientMobile.MESSAGE_CONSTRAINTS); + } + return new ClientMobile(mobile); + } + } + + /** + * Parses client email string from storage. + */ + public static ClientEmail readEmailFromStorage(String email) throws IllegalValueException { + if (email == null) { + throw new IllegalValueException(String.format(MISSING_CLIENT_FIELD_MESSAGE_FORMAT, + ClientEmail.class.getSimpleName())); + } + + if (email.isEmpty()) { + return ClientEmail.EmptyEmail.EMPTY_EMAIL; + } else { + if (!ClientEmail.isValidEmail(email)) { + throw new IllegalValueException(ClientEmail.MESSAGE_CONSTRAINTS); + } + return new ClientEmail(email); + } + } + + /** + * Parses client id string from storage. + */ + public static ClientId readClientIdFromStorage(String clientId) throws IllegalValueException { + if (clientId == null) { + throw new IllegalValueException(String.format(MISSING_CLIENT_FIELD_MESSAGE_FORMAT, + ClientId.class.getSimpleName())); + } + + if (!ClientId.isValidClientId(clientId)) { + throw new IllegalValueException(ClientId.MESSAGE_CONSTRAINTS); + } + + final ClientId modelClientId = new ClientId(Integer.parseInt(clientId)); + + assert modelClientId.getIdInt() >= 0 : "Client ID should be positive"; + return modelClientId; + } + + /** + * Parses issue list from storage. + */ + public static void readIssueListFromStorage(List issues, + AddressBook addressBook) throws IllegalValueException { + for (JsonAdaptedIssue jsonAdaptedIssue : issues) { + Issue issue = jsonAdaptedIssue.toModelType(addressBook); + if (addressBook.hasIssue(issue) || addressBook.hasIssueId(issue.getId())) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ISSUE); + } + addressBook.addIssue(issue); + } + } + + /** + * Parses project list from storage. + */ + public static void readProjectListFromStorage(List projects, + AddressBook addressBook) throws IllegalValueException { + for (JsonAdaptedProject jsonAdaptedProject : projects) { + Project project = jsonAdaptedProject.toModelType(); + if (addressBook.hasProject(project) || addressBook.hasProjectId(project.getId())) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PROJECT); + } + addressBook.addProject(project); + Client projectClient = project.getClient(); + if (projectClient.isEmpty()) { + continue; + } + + if (!addressBook.hasClient(projectClient) && !addressBook.hasClientId(projectClient.getId())) { + projectClient.addProjects(project); + addressBook.addClient(projectClient); + continue; + } + + if (addressBook.hasClientId(projectClient.getId())) { + Client existingClient = addressBook.getClientById(projectClient.getId()); + if (!existingClient.hasSameDetails(projectClient)) { + throw new IllegalValueException(MESSAGE_INVALID_CLIENT); + } + project.setClient(existingClient); + existingClient.addProjects(project); + } else { + throw new IllegalValueException(MESSAGE_INVALID_CLIENT); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/ClientCard.java similarity index 50% rename from src/main/java/seedu/address/ui/PersonCard.java rename to src/main/java/seedu/address/ui/ClientCard.java index 7fc927bc5d9..c02ba1dc941 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/ClientCard.java @@ -7,14 +7,14 @@ import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; -import seedu.address.model.person.Person; +import seedu.address.model.client.Client; /** - * An UI component that displays information of a {@code Person}. + * An UI component that displays information of a {@code Client}. */ -public class PersonCard extends UiPart { +public class ClientCard extends UiPart { - private static final String FXML = "PersonListCard.fxml"; + private static final String FXML = "ClientListCard.fxml"; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -24,7 +24,7 @@ public class PersonCard extends UiPart { * @see The issue on AddressBook level 4 */ - public final Person person; + public final Client client; @FXML private HBox cardPane; @@ -33,28 +33,25 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML - private Label phone; - @FXML - private Label address; + private Label mobile; @FXML private Label email; @FXML - private FlowPane tags; + private FlowPane projects; /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. + * Creates a {@code ClientCard} with the given {@code Client} and index to display. */ - public PersonCard(Person person, int displayedIndex) { + public ClientCard(Client client) { super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + this.client = client; + String clientIdString = client.getClientId().uiRepresentation(); + name.setText(client.getClientName().uiRepresentation(client.isPinned(), clientIdString)); + mobile.setText(client.getClientMobile().uiRepresentation()); + email.setText(client.getClientEmail().uiRepresentation()); + client.getProjects().stream() + .sorted(Comparator.comparing(project -> project.getProjectName().toString())) + .forEach(project -> projects.getChildren().add(new Label(project.getProjectName().toString()))); } @Override @@ -65,13 +62,13 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof PersonCard)) { + if (!(other instanceof ClientCard)) { return false; } // state check - PersonCard card = (PersonCard) other; + ClientCard card = (ClientCard) other; return id.getText().equals(card.id.getText()) - && person.equals(card.person); + && client.equals(card.client); } } diff --git a/src/main/java/seedu/address/ui/ClientListPanel.java b/src/main/java/seedu/address/ui/ClientListPanel.java new file mode 100644 index 00000000000..369c6cbd5eb --- /dev/null +++ b/src/main/java/seedu/address/ui/ClientListPanel.java @@ -0,0 +1,54 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.client.Client; + +/** + * Panel containing the list of clients. + */ +public class ClientListPanel extends UiPart { + private static final String FXML = "ClientListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ClientListPanel.class); + + @FXML + private ListView clientListView; + + @FXML + private Label clientListViewName; + + /** + * Creates a {@code ClientListPanel} with the given {@code ObservableList}. + */ + public ClientListPanel(ObservableList clientList) { + super(FXML); + clientListView.setItems(clientList); + clientListView.setCellFactory(listView -> new ClientListViewCell()); + clientListViewName.setText("Clients List"); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Client} using a {@code ClientCard}. + */ + class ClientListViewCell extends ListCell { + @Override + protected void updateItem(Client client, boolean empty) { + super.updateItem(client, empty); + + if (empty || client == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ClientCard(client).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..e7d2b3a9c36 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2223s1-cs2103-f13-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/IssueCard.java b/src/main/java/seedu/address/ui/IssueCard.java new file mode 100644 index 00000000000..8c015432b28 --- /dev/null +++ b/src/main/java/seedu/address/ui/IssueCard.java @@ -0,0 +1,74 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.issue.Issue; + +/** + * An UI component that displays information of a {@code Issue}. + */ +public class IssueCard extends UiPart { + + private static final String FXML = "IssueListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Issue issue; + + @FXML + private HBox cardPane; + @FXML + private Label title; + @FXML + private Label id; + @FXML + private Label urgency; + @FXML + private Label deadline; + @FXML + private Label project; + @FXML + private Label status; + + /** + * Creates a {@code IssueCard} with the given {@code Issue} and index to display. + */ + public IssueCard(Issue issue) { + super(FXML); + this.issue = issue; + String issueIdString = issue.getIssueId().uiRepresentation(); + title.setText(issue.getTitle().uiRepresentation(issue.isPinned(), issueIdString)); + deadline.setText(issue.getDeadline().uiRepresentation()); + urgency.setText(issue.getUrgency().uiRepresentation()); + project.setText(issue.getProject().uiRepresentation()); + status.setText(issue.getStatus().uiRepresentation()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof IssueCard)) { + return false; + } + + // state check + IssueCard card = (IssueCard) other; + return id.getText().equals(card.id.getText()) + && issue.equals(card.issue); + } +} + + diff --git a/src/main/java/seedu/address/ui/IssueListPanel.java b/src/main/java/seedu/address/ui/IssueListPanel.java new file mode 100644 index 00000000000..f607bce8e3a --- /dev/null +++ b/src/main/java/seedu/address/ui/IssueListPanel.java @@ -0,0 +1,54 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.issue.Issue; + +/** + * Panel containing the list of issues. + */ +public class IssueListPanel extends UiPart { + private static final String FXML = "IssueListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(IssueListPanel.class); + + @FXML + private ListView issueListView; + + @FXML + private Label issueListViewName; + + /** + * Creates a {@code IssueListPanel} with the given {@code ObservableList}. + */ + public IssueListPanel(ObservableList issueList) { + super(FXML); + issueListView.setItems(issueList); + issueListView.setCellFactory(listView -> new IssueListViewCell()); + issueListViewName.setText("Issues List"); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Issue} using a {@code IssueCard}. + */ + class IssueListViewCell extends ListCell { + @Override + protected void updateItem(Issue issue, boolean empty) { + super.updateItem(issue, empty); + + if (empty || issue == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new IssueCard(issue).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..6387c3c01c5 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -10,6 +10,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import seedu.address.commons.core.DefaultView; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.Logic; @@ -31,7 +32,9 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ProjectListPanel projectListPanel; + private IssueListPanel issueListPanel; + private ClientListPanel clientListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -42,7 +45,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane listPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -50,6 +53,9 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane listNamePlaceholder; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -106,23 +112,60 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { }); } + void setDefaultView() { + switch (logic.getDefaultView()) { + case PROJECT: + projectListPanel = new ProjectListPanel(logic.getFilteredProjectList()); + listPanelPlaceholder.getChildren().add(projectListPanel.getRoot()); + break; + case CLIENT: + clientListPanel = new ClientListPanel(logic.getFilteredClientList()); + listPanelPlaceholder.getChildren().add(clientListPanel.getRoot()); + break; + case ISSUE: + issueListPanel = new IssueListPanel(logic.getFilteredIssueList()); + listPanelPlaceholder.getChildren().add(issueListPanel.getRoot()); + break; + default: + assert false : "Code should not reach here"; + break; + } + } + /** * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + setDefaultView(); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } + void swapProjectListDisplay() { + projectListPanel = new ProjectListPanel(logic.getFilteredProjectList()); + listPanelPlaceholder.getChildren().clear(); + listPanelPlaceholder.getChildren().add(projectListPanel.getRoot()); + } + + void swapIssueListDisplay() { + issueListPanel = new IssueListPanel(logic.getFilteredIssueList()); + listPanelPlaceholder.getChildren().clear(); + listPanelPlaceholder.getChildren().add(issueListPanel.getRoot()); + } + + void swapClientListDisplay() { + clientListPanel = new ClientListPanel(logic.getFilteredClientList()); + listPanelPlaceholder.getChildren().clear(); + listPanelPlaceholder.getChildren().add(clientListPanel.getRoot()); + } + /** * Sets the default size based on {@code guiSettings}. */ @@ -157,14 +200,26 @@ void show() { @FXML private void handleExit() { GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); + (int) primaryStage.getX(), (int) primaryStage.getY(), logic.getDefaultView()); logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + private void handleDefaultView(DefaultView defaultView) { + logic.setDefaultView(defaultView); + } + + public ClientListPanel getClientListPanel() { + return clientListPanel; + } + + public ProjectListPanel getProjectListPanel() { + return projectListPanel; + } + + public IssueListPanel getIssueListPanel() { + return issueListPanel; } /** diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/ProjectCard.java b/src/main/java/seedu/address/ui/ProjectCard.java new file mode 100644 index 00000000000..d73dfb53d08 --- /dev/null +++ b/src/main/java/seedu/address/ui/ProjectCard.java @@ -0,0 +1,91 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.project.Project; + +/** + * An UI component that displays information of a {@code Project}. + */ +public class ProjectCard extends UiPart { + + private static final String FXML = "ProjectListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Project project; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Hyperlink repository; + @FXML + private Label deadline; + @FXML + private Label client; + @FXML + private Label issueCount; + + /** + * Creates a {@code ProjectCard} with the given {@code Project} and index to display. + */ + public ProjectCard(Project project) { + super(FXML); + this.project = project; + String projectIdString = project.getProjectId().uiRepresentation(); + name.setText(project.getProjectName().uiRepresentation(project.isPinned(), projectIdString)); + repository.setText(project.getRepository().uiRepresentation()); + deadline.setText(project.getDeadline().uiRepresentation()); + client.setText(project.getClient().uiRepresentation()); + issueCount.setText(project.issueListUiRepresentation()); + + repository.setOnMouseClicked(e -> copyRepoUrl(project.getRepository().isEmpty() ? "" + : project.getRepository().uiRepresentation())); + + } + + /** + * Copies the URL to the user guide to the clipboard. + */ + @FXML + private void copyRepoUrl(String repoUrl) { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent url = new ClipboardContent(); + url.putString(repoUrl); + clipboard.setContent(url); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ProjectCard)) { + return false; + } + + // state check + ProjectCard card = (ProjectCard) other; + return id.getText().equals(card.id.getText()) + && project.equals(card.project); + } +} + diff --git a/src/main/java/seedu/address/ui/ProjectListPanel.java b/src/main/java/seedu/address/ui/ProjectListPanel.java new file mode 100644 index 00000000000..171d2a84ef0 --- /dev/null +++ b/src/main/java/seedu/address/ui/ProjectListPanel.java @@ -0,0 +1,55 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.project.Project; + +/** + * Panel containing the list of projects. + */ +public class ProjectListPanel extends UiPart { + private static final String FXML = "ProjectListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ProjectListPanel.class); + + @FXML + private ListView projectListView; + + @FXML + private Label projectListViewName; + + /** + * Creates a {@code ProjectListPanel} with the given {@code ObservableList}. + */ + public ProjectListPanel(ObservableList projectList) { + super(FXML); + projectListView.setItems(projectList); + projectListView.setCellFactory(listView -> new ProjectListViewCell()); + projectListViewName.setText("Projects List"); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Project} using a {@code ProjectCard}. + */ + class ProjectListViewCell extends ListCell { + @Override + protected void updateItem(Project project, boolean empty) { + super.updateItem(project, empty); + + if (empty || project == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ProjectCard(project).getRoot()); + } + } + } + +} + diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index b577f829423..9a2e6a7c9cf 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,8 +1,5 @@ package seedu.address.ui; -import java.nio.file.Path; -import java.nio.file.Paths; - import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; @@ -20,9 +17,9 @@ public class StatusBarFooter extends UiPart { /** * Creates a {@code StatusBarFooter} with the given {@code Path}. */ - public StatusBarFooter(Path saveLocation) { + public StatusBarFooter() { super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + saveLocationStatus.setText(""); } } diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/address/ui/Ui.java index 17aa0b494fe..1400836f907 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/address/ui/Ui.java @@ -10,4 +10,9 @@ public interface Ui { /** Starts the UI (and the App). */ void start(Stage primaryStage); + void showProjects(); + + void showIssues(); + + void showClients(); } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..f4872fcf36f 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/devenable.png"; private Logic logic; private MainWindow mainWindow; @@ -65,7 +65,7 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.getDialogPane().getStylesheets().add("view/DevEnable.css"); alert.initOwner(owner); alert.setTitle(title); alert.setHeaderText(headerText); @@ -85,4 +85,19 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { System.exit(1); } + @Override + public void showProjects() { + mainWindow.swapProjectListDisplay(); + } + + @Override + public void showIssues() { + mainWindow.swapIssueListDisplay(); + } + + @Override + public void showClients() { + mainWindow.swapClientListDisplay(); + } + } diff --git a/src/main/resources/images/devenable.png b/src/main/resources/images/devenable.png new file mode 100644 index 00000000000..4d101f40c7f Binary files /dev/null and b/src/main/resources/images/devenable.png differ diff --git a/src/main/resources/view/ClientListCard.fxml b/src/main/resources/view/ClientListCard.fxml new file mode 100644 index 00000000000..6499dc33318 --- /dev/null +++ b/src/main/resources/view/ClientListCard.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ClientListPanel.fxml b/src/main/resources/view/ClientListPanel.fxml new file mode 100644 index 00000000000..a723d6fda6f --- /dev/null +++ b/src/main/resources/view/ClientListPanel.fxml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..9e0394f4e11 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -3,7 +3,7 @@ - + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css deleted file mode 100644 index 36e6b001cd8..00000000000 --- a/src/main/resources/view/DarkTheme.css +++ /dev/null @@ -1,352 +0,0 @@ -.background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ -} - -.label { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; - -fx-opacity: 0.9; -} - -.label-bright { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.text-field { - -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; -} - -.tab-pane { - -fx-padding: 0 0 0 1; -} - -.tab-pane .tab-header-area { - -fx-padding: 0 0 0 0; - -fx-min-height: 0; - -fx-max-height: 0; -} - -.table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; - -fx-table-cell-border-color: transparent; - -fx-table-header-border-color: transparent; - -fx-padding: 5; -} - -.table-view .column-header-background { - -fx-background-color: transparent; -} - -.table-view .column-header, .table-view .filler { - -fx-size: 35; - -fx-border-width: 0 0 1 0; - -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; - -fx-border-insets: 0 10 1 0; -} - -.table-view .column-header .label { - -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-alignment: center-left; - -fx-opacity: 1; -} - -.table-view:focused .table-row-cell:filled:focused:selected { - -fx-background-color: -fx-focus-color; -} - -.split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: transparent transparent transparent #4d4d4d; -} - -.split-pane { - -fx-border-radius: 1; - -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-view { - -fx-background-insets: 0; - -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-cell { - -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; -} - -.list-cell:filled:even { - -fx-background-color: #3c3e3f; -} - -.list-cell:filled:odd { - -fx-background-color: #515658; -} - -.list-cell:filled:selected { - -fx-background-color: #424d5f; -} - -.list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; -} - -.list-cell .label { - -fx-text-fill: white; -} - -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; - -fx-text-fill: #010504; -} - -.cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; - -fx-text-fill: #010504; -} - -.stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; -} - -.status-bar { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -.result-display .label { - -fx-text-fill: black !important; -} - -.status-bar .label { - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-padding: 4px; - -fx-pref-height: 30px; -} - -.status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); - -fx-border-width: 1px; -} - -.status-bar-with-border .label { - -fx-text-fill: white; -} - -.grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); - -fx-border-width: 1px; -} - -.grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.context-menu { - -fx-background-color: derive(#1d1d1d, 50%); -} - -.context-menu .label { - -fx-text-fill: white; -} - -.menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 0.9; -} - -.menu .left-container { - -fx-background-color: black; -} - -/* - * Metro style Push Button - * Author: Pedro Duque Vieira - * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ - */ -.button { - -fx-padding: 5 22 5 22; - -fx-border-color: #e2e2e2; - -fx-border-width: 2; - -fx-background-radius: 0; - -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; - -fx-font-size: 11pt; - -fx-text-fill: #d8d8d8; - -fx-background-insets: 0 0 0 0, 0, 1, 2; -} - -.button:hover { - -fx-background-color: #3a3a3a; -} - -.button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; -} - -.button:focused { - -fx-border-color: white, white; - -fx-border-width: 1, 1; - -fx-border-style: solid, segments(1, 1); - -fx-border-radius: 0, 0; - -fx-border-insets: 1 1 1 1, 0; -} - -.button:disabled, .button:default:disabled { - -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; - -fx-text-fill: white; -} - -.button:default { - -fx-background-color: -fx-focus-color; - -fx-text-fill: #ffffff; -} - -.button:default:hover { - -fx-background-color: derive(-fx-focus-color, 30%); -} - -.dialog-pane { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.label.content { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-text-fill: white; -} - -.dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); -} - -.dialog-pane:header *.header-panel *.label { - -fx-font-size: 18px; - -fx-font-style: italic; - -fx-fill: white; - -fx-text-fill: white; -} - -.scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); - -fx-background-insets: 3; -} - -.scroll-bar .increment-button, .scroll-bar .decrement-button { - -fx-background-color: transparent; - -fx-padding: 0 0 0 0; -} - -.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { - -fx-shape: " "; -} - -.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { - -fx-padding: 1 8 1 8; -} - -.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { - -fx-padding: 8 1 8 1; -} - -#cardPane { - -fx-background-color: transparent; - -fx-border-width: 0; -} - -#commandTypeLabel { - -fx-font-size: 11px; - -fx-text-fill: #F70D1A; -} - -#commandTextField { - -fx-background-color: transparent #383838 transparent #383838; - -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; - -fx-border-insets: 0; - -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -#filterField, #personListPanel, #personWebpage { - -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); -} - -#resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; -} - -#tags { - -fx-hgap: 7; - -fx-vgap: 3; -} - -#tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; - -fx-border-radius: 2; - -fx-background-radius: 2; - -fx-font-size: 11; -} diff --git a/src/main/resources/view/DevEnable.css b/src/main/resources/view/DevEnable.css new file mode 100644 index 00000000000..9b74ee4ffd0 --- /dev/null +++ b/src/main/resources/view/DevEnable.css @@ -0,0 +1,134 @@ +.label > TextArea { + -fx-font-size: 13pt; +} + +.menu-bar { + -fx-alignment: center-right; + -fx-background-color: #FFB8B3; +} + +.main-body { + -fx-background-color: #FFD7C0; + -fx-padding: 16; +} + +.command-placeholder, +.list-panel-placeholder, +.result-placeholder { + -fx-border-color: black; + -fx-border-radius: 5; + -fx-border-width: 1; + -fx-padding: 10; +} + +.command-placeholder, +.command-box > TextField, +.list-panel-placeholder, +.list-panel, +.result-placeholder, +.result-display > TextArea { + -fx-focus-color: transparent; + -fx-text-box-border: transparent; + -fx-background-color: transparent; +} + +.result-display > TextArea * { + -fx-padding: 0; + -fx-background-color: transparent; +} + +.command-placeholder { + -fx-background-color: #E0E0E0 +} + +.list-panel-placeholder, +.result-placeholder { + -fx-background-color: #FFE3B3; +} + +.command-box > TextField { + -fx-font-family: Sans-serif; + -fx-prompt-text-fill: #363636; +} + +.result-display > TextArea { + -fx-font-family: Sans-serif; + -fx-prompt-text-fill: #363636; + -fx-focus-traversable: false; +} + +.list-cell { + -fx-background-color: #FFE3B3; +} + +.list-cell .label { + -fx-text-fill: black; +} + +#cardPane { + -fx-border-color: #3e7b91; + -fx-border-radius: 5pt; + -fx-background-radius: 5pt; +} + +.list-cell:filled:even #cardPane { + -fx-background-color: #ffedcf; +} + +.list-cell:filled:odd #cardPane { + -fx-background-color: #edcd98; +} + +.list-cell:filled:selected #cardPane { + -fx-background-color: #f0b856; + +} + +.cell_big_label, +.list-panel-name { + -fx-font-weight: bold; +} + +.cell_big_label, +.cell_small_label, +.list-panel-name { + -fx-font-family: Sans-serif; +} + +.scroll-bar { + -fx-background-color: #E8B2A3; +} + +.thumb { + -fx-background-color: #FFFFFF; +} + +.status-bar { + -fx-background-color: #FFD7C0; +} + +#copyRepoButton { + -fx-background-color: #B3E5FC +} + +#copyRepoButton:hover { + -fx-background-color: #81D4FA +} + +#repository { + -fx-padding: 0; +} + +#projects { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#projects .label { + -fx-text-fill: white; + -fx-background-color: #9b4f97; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..6ac12a149d5 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -5,7 +5,7 @@ .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: transparent; } .tag-selector { diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..5c6b07ac098 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,19 +1,19 @@ #copyButton, #helpMessage { - -fx-text-fill: white; + -fx-text-fill: black; } #copyButton { - -fx-background-color: dimgray; + -fx-background-color: #FFB8B3; } #copyButton:hover { - -fx-background-color: gray; + -fx-background-color: #FFDDDD; } #copyButton:armed { - -fx-background-color: darkgray; + -fx-background-color: #FFB8B3; } #helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #FFD7C0; } diff --git a/src/main/resources/view/IssueListCard.fxml b/src/main/resources/view/IssueListCard.fxml new file mode 100644 index 00000000000..1e923d4e68d --- /dev/null +++ b/src/main/resources/view/IssueListCard.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/IssueListPanel.fxml b/src/main/resources/view/IssueListPanel.fxml new file mode 100644 index 00000000000..b5bd0d3b9e7 --- /dev/null +++ b/src/main/resources/view/IssueListPanel.fxml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..5cfb046e9bc 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,58 +1,51 @@ - - - + + title="DevEnable" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> - + - + + -

- - - - - - - + + - - - - - + + + + - - - - - - + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml deleted file mode 100644 index f08ea32ad55..00000000000 --- a/src/main/resources/view/PersonListCard.fxml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml deleted file mode 100644 index 8836d323cc5..00000000000 --- a/src/main/resources/view/PersonListPanel.fxml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/view/ProjectListCard.fxml b/src/main/resources/view/ProjectListCard.fxml new file mode 100644 index 00000000000..22ce4065210 --- /dev/null +++ b/src/main/resources/view/ProjectListCard.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ProjectListPanel.fxml b/src/main/resources/view/ProjectListPanel.fxml new file mode 100644 index 00000000000..3841a5082ef --- /dev/null +++ b/src/main/resources/view/ProjectListPanel.fxml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..f6ea88c4c26 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -3,7 +3,7 @@ - -