diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000000..b37c79c8d82 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,17 @@ +coverage: + range: 60..80 + round: down + precision: 2 + status: + project: + default: + target: 70% + threshold: 1% + patch: + default: + target: 50% + threshold: 10% +ignore: + - "**/java/seedu/address/ui/**" # ignore test coverage on UI + - "**/java/seedu/address/Main" + - "**/java/seedu/address/MainApp" diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..c7664483bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Gradle build files /.gradle/ /build/ +/bin/ src/main/resources/docs/ # IDEA files @@ -15,9 +16,16 @@ src/main/resources/docs/ /*.log.* hs_err_pid[0-9]*.log +# IDE config files +.project +.classpath +.settings/ + # Test sandbox files src/test/data/sandbox/ -# MacOS custom attributes files created by Finder +# Attribute files created by Finder and File Explorer .DS_Store +Desktop.ini + docs/_site/ diff --git a/README.md b/README.md index 13f5c77403f..9f927d5a80b 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) +## Jobby + +[![CI Status](https://github.com/AY2324S1-CS2103T-W08-3/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-W08-3/tp/actions) + +Jobby is a desktop application that helps you manage your job applications by easily recording organization and recruiter contacts, plus note down information about your applications. It is optimized for CLI use, so you can operate the application with only your keyboard. ![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. +* The project simulates an ongoing software project for a desktop application used for managing contact details regarding organizations and recruiters and job application status info. * 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. + * It is an extension built on top of **[Address Book Level 3 by se-education.org](https://se-education.org/addressbook-level3)**. +* For detailed documentation of this project, see the **[Jobby Product Website](https://ay2324s1-cs2103t-w08-3.github.io/tp/)**. diff --git a/build.gradle b/build.gradle index a2951cc709e..402951482fa 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 = 'jobby.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..b44b17c506a 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,50 @@ 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 +### Wern + + + +[[homepage](https://wern.cc/)] +[[github](https://github.com/wxwern)] +[[portfolio](team/wxwern.md)] - +* Role: Member -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Chun Jie -* Role: Project Advisor + -### Jane Doe +[[github](https://github.com/CJ-Lee01)] +[[portfolio](team/cj-lee01.md)] - +* Role: Member -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Bryan Lee -* Role: Team Lead -* Responsibilities: UI + -### Johnny Doe +[[github](http://github.com/mcnabry)] +[[portfolio](team/mcnabry.md)] - +* Role: Member -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +### Shi Yu -* Role: Developer -* Responsibilities: Data + -### Jean Doe +[[github](https://github.com/tanshiyu1999)] [[portfolio](team/tanshiyu1999.md)] - +* Role: Member -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Juanpa Abola -* Role: Developer -* Responsibilities: Dev Ops + Threading + -### James Doe +[[github](https://github.com/wamps-jp)] [[portfolio](team/wamps-jp.md)] - +* Role: Member -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] -* Role: Developer -* Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..7b6b2946686 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,11 +9,19 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* UI rendering via: [JavaFX](https://openjfx.io/) +* Testing suite via: [JUnit5](https://github.com/junit-team/junit5) +* JSON data saving and loading via: [Jackson](https://github.com/FasterXML/jackson) + +* Jobby base UI adapted from: [AddressBook Level-3](https://se-education.org/addressbook-level3/) +* Autocompletion base UI adapted from: [@floralvikings's AutoCompleteTextBox.java](https://gist.github.com/floralvikings/10290131) + +* New user tutorial structure inspired from: [AY2324S1-CS2103T-T17-03](https://ay2324s1-cs2103t-t17-3.github.io/tp/UserGuide.html) -------------------------------------------------------------------------------------------------------------------- ## **Setting up, getting started** +{: .reset-page-break-defaults} Refer to the guide [_Setting up and getting started_](SettingUp.md). @@ -21,9 +29,10 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). ## **Design** -
+
:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +
### Architecture @@ -36,7 +45,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`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)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -68,24 +77,24 @@ 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 Reference** : [`Ui.java`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `ContactListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/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, as it displays `Contact` object residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API Reference** : [`Logic.java`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -100,33 +109,88 @@ The sequence diagram below illustrates the interactions within the `Logic` compo How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to delete a person). -1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +* Executing a command: + + 1. When `Logic` is called upon to execute a command, it is passed to `AppParser` which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. + + 2. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. + + 3. The command can communicate with the `Model` when it is executed (e.g. to delete a contact). -Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: + 4. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. + +* Autocompleting a command: + + 1. When `Logic` is called upon to autocomplete a command, it is passed to `AppParser` which in turn creates an autocompletion generator capable of generate autocompletion results for this command. + + 2. This results in an `AutocompleteGenerator` which is executed by the `LogicManager`. + + 3. The `AutocompleteGenerator` can communicate with the `Model` to obtain the current application state (e.g. to obtain the list of all contact ids) when supplying autocompletion results. + + 4. This results in a `Stream` representing the possible completions, which is returned back from `Logic`. + +Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command for both execution and autocompletion: + +#### Parser classes 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. + +1. When called upon to parse a user command, the `AppParser` class looks up the corresponding **Command Parser** (e.g., `AddCommandParser` if it detects an "add" command). + +2. There are two cases here: + + 1. If there exists a `Parser` for the corresponding command, it will use the other classes shown above to parse the user command and create a `Command` object (e.g., `AddCommand`). + + 2. Otherwise, it will create a `Command` object corresponding to the command name (e.g., `AddCommand`) with no arguments. + +3. Finally, `AppParser` returns back the `Command` object. + +How arguments from a raw command input may be obtained by parsers: + +* When arguments are needed for a command, `ArgumentTokenizer` is used to prepare and tokenize the raw input string, which can then convert it to an `ArgumentMultimap` for easy access. + +* An `ArgumentMultimap` represents the command data (which has the format `name preamble text --flag1 value 1 --flag2 value 2`) in their distinct fields: **preamble**, **flags** and their mapped **values**. Note that as a multimap, multiple values can be mapped to the same flag. + +* With that, all parsers can use resulting `ArgumentMultimap` (obtained from using the raw input on `ArgumentTokenizer`) to access the required arguments to create and execute a `Command`. + +Design Notes: + +* All **Command Parser** classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +#### Autocomplete classes + + + +How autocompletion works: + +1. When called upon to generate autocompletions for a partially typed command, `Logic` passes the request to `AppParser` class. + +2. There are two cases after this happens: + + 1. If a command name is specified and complete (i.e., user added a space after the command name), `AppParser` will look up the corresponding `AutocompleteSupplier` for the command, and create an `AutocompleteGenerator` with it. + + 2. Otherwise, `AppParser` will create an `AutocompleteGenerator` with a `Supplier>` that returns all possible command names. + +3. `AppParser` then returns the `AutocompleteGenerator` to the requester so as they can generate autocompletion results. + +For full details of the autocomplete design and implementation, refer to the [Command Autocompletion Internals](#command-autocompletion-internals) section. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API Reference** : [`Model.java`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/model/Model.java) The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the address book data i.e., all `Contact` objects (which are contained in a `UniqueContactList` object). +* stores the currently 'selected' `Contact` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Contact` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Contact` needing their own `Tag` objects.
@@ -135,7 +199,7 @@ The `Model` component, ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API Reference** : [`Storage.java`](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/storage/Storage.java) @@ -154,90 +218,405 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Command Autocompletion Internals -#### Proposed Implementation +#### Overview -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: +Jobby's Command Autocompletion is designed to provide users with intelligent command suggestions and offer autocompletion by analyzing the existing partial command input and the current application state. -* `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. +Just like programming IDEs, a user may type a prefix subsequence of a long command part, and simply press **TAB** to finish the command using the suggested match. For instance, type `sort -tt` and press **TAB** to finish the command as `sort --title`. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +#### The Autocomplete Package -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +The full autocomplete package (and some of its dependencies) can be summarized by the following class 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. + -![UndoRedoState0](images/UndoRedoState0.png) +The implementation consists of two notable classes that will be used by high-level users: -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. +- **`AutocompleteSupplier`**: + - This class is responsible for generating possible flags and values to be used for suggestions. + - It takes an `AutocompleteItemSet`, an optional `FlagValueSupplier` mapped to each flag, and can have corresponding `AutocompleteConstraint`s applied to flags. + - It helps determine what flags can be added to an existing command phrase based on constraints and existing flags. + - All commands with customizable input should prepare an instance of this class to be used for autocompletion. -![UndoRedoState1](images/UndoRedoState1.png) +- **`AutocompleteGenerator`**: + - This component takes in an `AutocompleteSupplier` or a `Supplier>` and generates autocomplete results based on a partial command input and the current application model. + - Users can invoke `AutocompleteGenerator#generateCompletions(command, model)` to get autocomplete suggestions. + - It does the hard work of taking the possible values provided by either supplier, performing subsequence fuzzy match, and then "predict" what the user is typing. -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`. +The process used to generate autocomplete suggestions with these two classes is mentioned in the [high-level Logic component previously discussed](#logic-component). -![UndoRedoState2](images/UndoRedoState2.png) +In summary, the `AutocompleteGenerator` class is used to generate completions, and any command that has the ability to supply autocomplete results should have their respective `AutocompleteSupplier`s defined and available to be fed into the generator. -
: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`. +#### Autocompletion Constraints -
+Autocompletion constraints are defined via the `AutocompleteConstraint` functional interface. It provides a way to specify rules for autocomplete suggestions. -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. +```java +@FunctionalInterface +public interface AutocompleteConstraint { + boolean isAllowed(T input, Set existingItems); +} +``` -![UndoRedoState3](images/UndoRedoState3.png) +**API Reference:** [AutocompleteConstraint.java](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java) -
: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. +This interface can be thought of as a lambda function that takes in the *input item* and *existing items*, then returns a boolean value indicating whether the input should be allowed. -
+In our autocomplete implementation, we use constraints to define rules for what flags can be added to an existing set of flags. We hence use the type `AutocompleteConstraint`. -The following sequence diagram shows how the undo operation works: +##### Built-in Constraints -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +The interface offers static factory methods for quick creation of many common constraints. For example: -
: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. +- `#oneAmongAllOf(items...)`: Enforces that at most one of the provided items must be present in the command. +- `#onceForEachOf(items...)`: Enforces that each of the provided items can only appear once in the command. +- `#where(item)#isPrerequisiteFor(dependents...)`: Defines dependencies between items, indicating that certain items are prerequisites before its dependents may appear. -
+##### Custom Constraints -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. +It is possible to declare your own constraints. -
: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. +Hence, to create a constraint that **all** flags cannot be used more than once, we can simply declare it just like so: -
+```java +AutocompleteConstraint cannotBeUsedMoreThanOnce = (input, existingItems) -> + !existingFlags.contains(input); +``` + +#### Autocomplete Item Sets + +An autocomplete item set - represented by the `AutocompleteItemSet` class - is a custom set of items with an additional perk: it retains knowledge of which items have what rules and constraints. + +Hence, in our autocomplete implementation, we use `AutocompleteItemSet` to easily store and determine which flags can be added to an existing set of flags given the known constraints. + +**API Reference:** [AutocompleteItemSet.java](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java) + +##### Built-in Item Set Factories + +This dataset can be constructed manually with flags and constraints, but it also offers static factory methods for quick creation of flag sets with common constraints. For example: + +- `#oneAmongAllOf(items...)`: Creates a set where at most one out of all the provided items may appear. +- `#onceForEachOf(items...)`: Ensures that each of the provided items can appear only once. +- `#anyNumberOf(items...)`: Creates a set with the rule that items in the set may appear any number of times. + +##### Helper Chainable Operations + +Some helper operations are provided in a chainable fashion to simplify workflows. For example: + +- `#concat(sets...)`: Combines sets together to create complex combinations of items and their rules. +- `#addDependents(items...)`: Establishes dependencies between items. This way, an item may require another different item to exist in order to be used. +- `#addConstraints(constraints...)`: Adds more custom constraints as desired. + +##### Usage Example + +Suppose we have a set of flags, some supporting repeated usage (`FLAG_REP_1`, `FLAG_REP_2`), and some that may only be used once (`FLAG_ONCE_1`, `FLAG_ONCE_2`). + +We can create such a set, with all the constraints automatically combined, like so: + +```java +AutocompleteItemSet set = AutocompleteItemSet.concat( + AutocompleteItemSet.anyNumberOf(FLAG_REP_1, FLAG_REP_2), + AutocompleteItemSet.onceForEachOf(FLAG_ONCE_1, FLAG_ONCE_2) +); +``` + +##### Computing Usable Items + +Finally, we need a way to compute what items are usable given existing set of items that are present. `AutocompleteItemSet` exposes one final method that is exactly what we need: + +- `#getElementsAfterConsuming(items...)`: Gets the remaining set of elements after "consuming" the given ones. + +#### Flag Value Suppliers + +In some cases, Jobby should be capable of provide suggestions for flags with preset or known values, such as "`--status pending`", or "`--oid alex_yeoh_inc`". This is where flag value suppliers come in. + +The `FlagValueSupplier` functional interface is a simple one that behaves like a lambda function with one task: Given a **partial command** for a flag and the app's **model**, generate all possible values a flag may have. + +```java +@FunctionalInterface +public interface FlagValueSupplier extends + BiFunction> { + + Stream apply(PartitionedCommand partialCommand, Model model); +} +``` + +**API Reference:** [FlagValueSupplier.java](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java) + +With the provided details, it is possible to specify arbitrary suppliers with any data. You can supply a preset list of completions, or even retrieve values from the model itself. + +Accessing the partial command is useful if you'd like to change the results based on the heuristically detected type, such as fields that accept either an `INDEX` or an `ID`. + +**Note to developers:** Custom `FlagValueSupplier`s need not actually do any prefix or subsequence matching - that is done automatically at the `AutocompleteGenerator` class later. + +#### Partitioning Command Strings + +The `PartitionedCommand` class is a simple class for quick access for a command string's constituent parts, specifically for the purposes of autocomplete. This is done simply by initializing it with a partial command string. + +**API Reference:** [PartitionedCommand.java](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java) + +For example, given the partial command "`add --org --name Alice --oid ama`", you will be able to extract the partitions in the following forms: + +| Command Name | Middle Text | Autocompletable Text | +|:------------:|:--------------------------:|:--------------------:| +| `add` | `--org --name Alice --oid` | `ama` | + +| Leading Text | Trailing Text | +|:------------------------------:|:-------------:| +| `add --org --name Alice --oid` | `ama` | + +There are also helper methods to detect flag strings and other properties of the command, which can be found in the API reference. + +#### The Autocomplete Supplier + +The `AutocompleteSupplier` leverages the capabilities of `AutocompleteItemSet` and `FlagValueSupplier` together to form a full supplier for a single command. + +**API Reference:** [AutocompleteSupplier.java](https://github.com/AY2324S1-CS2103T-W08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java) + +Internally, it must be initialized with an `AutocompleteItemSet` to determine what flags can be added to a command at any point in time, inclusive of all known restrictions. It is most easily done via the factory method `#from`. + +Additionally, one may optionally assign `FlagValueSupplier`s into `Flag`s by inputting `Map`. This allows the supplier to provide suggestions for flags with preset or known values. + +You may configure both `AutocompleteItemSet` and `Map` in the same constructor call, or use factory and chaining methods to create such a set - refer to publicly exposed API calls for more details. + +##### Usage Example + +Recall the example from earlier where we created an `AutocompleteItemSet`? A way to create a full `AutocompleteSupplier` from that is as follows: + +```java +AutocompleteSupplier supplier = AutocompleteSupplier.from(set); +``` + +We can add more details on an existing supplier by using a configurator. Suppose we have a `FlagValueSupplier` for a status flag. This is how we can add it to the supplier: + +```java +supplier.configureValueMap(map -> map.put(FLAG_STATUS, statusFlagValueSupplier)); +``` + +##### Obtaining Results + +The supplier exposes methods to obtain the possible flags and values: + +- `#getOtherPossibleFlagsAsideFromFlagsPresent(Flags...)`: Gets the remaining set of flags that can be added to the command, given the flags that are already present. + +- `#getValidValuesForFlag(Flag, PartitionedCommand, Model)`: Gets the possible values for a flag, given the partial command and the model. + +This is used by `AutocompleteGenerator` to generate suggestions later. + +#### The Autocomplete Generator + +The `AutocompleteGenerator` is the final stage of the autocompletion generation process. + +It supports generating results based on those supplied by an `AutocompleteSupplier`, or any arbitrary `Supplier>`, and generates autocomplete suggestions. + +Once initialized, users can simply call the `#generateCompletions(command, model)` method to receive suggestions from their partial command input. It's that easy! + +**API Reference:** [AutocompleteGenerator.java](https://github.com/ay2324s1-cs2103t-w08-3/tp/tree/master/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java) + +##### Execution Flow + +Internally, whenever requested, the `AutocompleteGenerator`: +1. obtains a command's parts with `PartitionedCommand`, +2. uses the `AutocompleteSupplier` provided when initialized to obtain the results based on the available parts, +3. automatically performs fuzzy (subsequence) matching to filter results, +4. ranks them based on their relevance, +5. and finally returns a stream of autocompleted commands. + +#### Design Considerations + +When designing the Autocomplete feature, important considerations include the ability to flexibly define and craft new constraints based on heuristically determined rules. + +By abstracting away all operations into simple components like sets and constraints, the current carefully crafted design allows Jobby's Command Autocompletion to provide context-aware suggestions to users, while adhering to simple constraints defined on a per command basis. -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. +Most notably, it also allows for advanced rulesets to be specified in a human-readable fashion. +Take a look at [AddCommand#AUTOCOMPLETE_SUPPLIER](https://github.com/AY2324S1-CS2103T-W08-3/tp/blob/c484696fe4c12d514ad3fb6a71ff2dfea089fe32/src/main/java/seedu/address/logic/commands/AddCommand.java#L47). -![UndoRedoState4](images/UndoRedoState4.png) +##### Alternatives Considered -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. +###### Alternative 1: Using Hardcoded Rules in Java -![UndoRedoState5](images/UndoRedoState5.png) +One obvious alternative is to simply compute the possible autocompletion results for each command in standard Java. We may achieve this by manually checking against a command string for each command type, and using existing tokenization classes like `ArgumentTokenizer` and `ArgumentMultimap`. -The following activity diagram summarizes what happens when a user executes a new command: +While this would incur less overhead in initial development time, more explicit coding is required - it is neither quick to write nor scalable to tons of commands. This is especially important as autocomplete was developed in parallel with other new features being added to Jobby, which would require constant changes to the autocomplete rules. - +###### Alternative 2: Using a Graph-based Approach + +A graph based approach, e.g., having a tree structure to define constraints and dependencies, may be more efficient than the current solution (which has to check against _all_ known rules every single time). + +However, it will consume even more development time to implement and model the rules as a graph. Since the current implementation involves one set with a list of constraints, multiple sets can be combined by simple concatenation of both the items and the constraints. + +### Adding Organization + +#### Implementation + +The add Organization mechanism is facilitated by `AddOrganization`. It extends `AddContact`. + +These operations are parsed in the `AddCommandParser` class, where the user inputs e.g. `add --org --name Google` will be handled and saved into the JSON database and displayed in the GUI. + +Given below is an example usage scenario and how the `AddOrganization` mechanism behaves at each step. + +Step 1. The user inputs an add organization command. The `AddCommandParser` will check for `--org` flag, and parse the input as an `Organization`. + +Step 2. This triggers the `AddOrganizationCommand`, where a new `Organization` object will be created. And it will be pased down into `JsonAdaptedContact` and `ModelManager` to be converted into JSON data and be displayed into the GUI respectively. + +Step 3. When the user want decide to add more information regarding the Organization, he can use the `Edit` command, which will be handled by the `EditCommandParser`. And the added field will be passed down into into `JsonAdaptedContact` and `ModelManager` to be converted into JSON data and be displayed into the GUI respectively. #### Design considerations: -**Aspect: How undo & redo executes:** +**Aspect: How Add Organization executes:** + +* **Alternative 1 (current choice):** Adds the Organization with a JSON's key 'type': "Organization" + * Pros: Easy to implement and flexible to implement more types. + * Cons: NIL + +### `Recruiter`-`Organization` link + +#### Overview + +There are two types of contacts in Jobby - `Recruiter` and `Organization`. + +Each recruiter can only be linked to zero or one organization while an organization can be linked to multiple recruiters. This association can be represented via a **parent-child** relationship where the parent (`Organization`) is linked to multiple children (`Recruiter`). + +#### Implementing the parent-child relationship + +For the `Contact` class: + * In order to incorporate this relationship into the existing model, the `Contact` class was modified to accept another `Contact` as its parent, accessible through `Contact#getParent()`. + +
+ +For the `Recruiter` class: + * Since the `Contact` class now accepts another `Contact` as its parent, the `Recruiter` can pass in an existing `Organization` to set it as its parent. + + * The parent `Organization` can be retrieved via `Recruiter#getOrganization()` which returns an Optional that contains the `Organization` or an empty Optional if the `Recruiter` is not linked to any. + +
+ +For the `Organization` class: + * The organization does not maintain a direct list of recruiters linked to it. + + * Instead, it is retrieved via `Contact#getChildren(Model model)` where each contact in the model is checked to see whether its parent matches the organization. + +
+ +Given below is an example usage scenario and how a recruiter can be linked to an existing organization at each step. + +**Step 1.** The user launches the application. Assume that the `AddressBook` contains a single unlinked organization that has the id _alex_yeoh_ and no recruiters. + +**Step 2.** The user executes `add --rec --name Ryan --oid alex_yeoh`. As the `--rec` flag is used, the `AddCommandParser` returns a `AddRecruiterCommand`. It also parses _alex_yeoh_ as the id of the organization the recruiter will be linked to and passes it into the `AddRecruiterCommand`. + +**Step 3.** During its execution, the `AddRecruiterCommand` will attempt to retrieve a `Contact` that has the id _alex_yeoh_ and pass it into the new `Recruiter` that will be added to the `AddressBook`. This step can be summarized with the activity diagram below: + + + +**Step 4.** Once done, the UI will add a new `ContactCard` to the bottom of the contacts list, displaying the details of the newly created `Recruiter`. The link will be displayed as a label within the `ContactCard`: _from organization (alex_yeoh)_ + +#### Editing and deleting the linked contacts + +Now that the basic implementation has been discussed, the next concern is about editing and deleting the linked contacts. + +As each field in the `Contact` is `final`, editing it would require creating a new `editedContact` and replacing the old one via `AddressBook#setContact(target, editedContact)`. + +When **editing** the `Organization`: + * As each recruiter maintains an immutable link to the object of its parent organization, editing the organization would require replacing every linked recruiter with a new recruiter that has its parent set to the edited organization. + +
+ +When **editing** the `Recruiter`: + * Since the `Organization` class does not maintain a direct link to its children and dynamically retrieves them, editing its linked recruiter does not require any edits to itself. + + * Changing the organization the recruiter is linked to would require the user to supply a value to the `--oid` flag when executing the `edit` command. + + * If the value matches the id of an organization within the `AddressBook`, the organization retrieved via `AddressBook#getContactById(Id id)` would be used in creating the new edited recruiter. + +
+ +The same principle applies when deleting the linked contacts without recursion. Deleting the parent organization requires replacing every recruiter linked to it, setting their parent to null while deleting its linked recruiter requires no additional replacement. + +#### Storing the `Recruiter`-`Organization` link + +Since only the recruiter stores a direct link to its parent organization, it is sufficient to store this link in the `JsonAdaptedContact` of a recruiter. + +As the id field can uniquely identify the organization, an additional oid field is added to the `JsonAdaptedContact` which records the id of the parent organization. + +Since the organization has to be added to the `AddressBook` before any recruiters can be linked to it, the data is sorted which places any organization at the front of the list, followed by the recruiters. This is performed before writing and after reading from the json data file. + +#### Design Considerations -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +**Aspect: How `Recruiter` and `Organization` are being linked** -* **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 1 (current choice):** `Recruiter` maintains a direct link to `Organization` while `Organization` dynamically retrieves a list of its linked `Recruiter` contacts. + * Pros: Adheres to AB3's immutability of contacts. + * Cons: Expensive to always comb through the `AddressBook` to retrieve all linked `Recruiter` contacts. + * **Alternative 2:** `Organization` maintains a list of linked `Recruiters` that can be changed via setter methods. + * Pros: Computationally less expensive and easier to deal with. + * Cons: Since AB3's design was implemented with immutability in mind, making part of `Organization` mutable might cause unwanted bugs or mistakes in other parts of the application. Additionally, overhauling the classes to be mutable would incur huge cost in development time. -_{more aspects and alternatives to be added}_ +### Apply feature +The apply feature makes use of existing structures to function, notably the `Parser`, `Model` and `Storage` -### \[Proposed\] Data archiving +The following sequence diagram shows how job applications are added to Jobby. -_{Explain here how the data archiving feature will be implemented}_ + +#### Design Considerations +**Aspect: How to store applications** + +* **Actual: Applications are stored as a JSON array belonging to their respective organizations** + * Pros: Easy to implement + * Cons: Need to initialise a list of job applications from every organization every time on startup. + +* **Alternative 1: Applications are stored as a JSON array separate from the contacts** + * Pros: Applications can be loaded immediately into Jobby without waiting for organizations to be initialised. + * Cons: Can have complications on other features, such as identifying which applications belong to which organizations. + +**Aspect: How to show applications** + +* **Actual: Applications are shown on a separate list** + * Pros: Easy to implement, less command needed to switch view from split view. + * Cons: Requires syncing the list with organizations, since there is no guarantee that the applications in the UI list are the same as all the ones in organizations. + +* **Alternative 1: Use a command to switch list view** + * Pros: More compact, does not require larger screen size. + * Cons: More difficult to implement, requires a command that directly changes the UI. + +**Aspect: What should the command syntax be** + +* **Actual: Use a separate command for adding applications** + * Pros: Easier to type out the command, does not require a lot of typing. + * Cons: More implementation effort, to implement a new command with new parser. +* **Alternative 1: Reuse add command** + * Pros: Easier to implement, can make use of existing structures surrounding the add command. + * Cons: Overloading the add command too much. + +### Sort feature +The apply feature makes use of existing structures to function, notably the `Parser`, `Model` and `Storage` + +The following sequence diagram shows how Jobby sorts contacts or job applications (in this example, a job application). + + + +#### Design Considerations +**Aspect: Sorting by multiple flags** + +* **Actual: Sort does not support sorting by multiple flags.** + * Pros: Easy to implement. + * Cons: Limited utility. + +* **Alternative 1: Sort supports sorting by multiple flags.** + * Pros: Sorting can be more complex, allowing for multi-level sorting, or simultaneous sorting of both contacts and job applications. + * Cons: More complex implementation. Would require a rework of the way the lists of contacts and job applications are stored. + +**Aspect: Sorting order** + +* **Actual: Each field has a default sorting order, which can be specified as ascending or descending.** + * Pros: Easy to implement, clear to user what the sorting order will be. + * Cons: Little flexibility when sorting, especially when it comes to dates (sorting chronologically does not take current date into account). + +* **Alternative 1: Allow the user to specify sorting conditions (such as "after certain date").** + * Pros: More utility for sorting command, allowing the user to better arrange data. + * Cons: More difficult to implement. -------------------------------------------------------------------------------------------------------------------- @@ -251,81 +630,327 @@ _{Explain here how the data archiving feature will be implemented}_ -------------------------------------------------------------------------------------------------------------------- -## **Appendix: Requirements** +## **Appendix A: Requirements** + +This section documents the requirements of Jobby. This consists of: +* The [scope](#product-scope) of the product +* The [user stories](#user-stories) and [use cases](#use-cases) that are relevant to Jobby. +* [Non-functional requirements](#non-functional-requirements) ### Product scope **Target user profile**: -* has a need to manage a significant number of contacts +* students looking to apply for jobs +* have a need to manage a significant number of organization and recruiter contacts and application statuses * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +* is reasonably comfortable and familiar with using CLI apps + +**Value proposition**: -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +Allows for comprehensive tracking of job applications and the information of companies and recruiters the user may be interested in, and manage them 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(n) ... | I want to ... | So that I can ... | +|----------|----------------|--------------------------------------------------------------|----------------------------------------------------------------------------| +| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the app | +| `* * *` | user | adding a job application | keep track which organization I am applying to | +| `* * *` | user | delete a job application | remove job applications that I no longer need to track | +| `* * *` | user | add a new contact | keep track of organizations and recruiters I'm interested in | +| `* * *` | user | delete contacts | remove organizations and recruiters that I no longer need | +| `* * *` | user | edit my job application via index | be up to date with changes in the job application | +| `* *` | user | edit my contacts via index and id | be up to date with changes in organization and recruiter details | +| `* *` | user | find contacts by saved details | locate a contact without going through the entire list | +| `* *` | user | find job applications by details | locate a job application without going through the entire list | +| `* *` | user | link recruiters and job application to organizations | see where the recruiter comes from and where I am applying to | +| `* *` | user | sort job applications by deadlines | be able to which job application is most urgent | +| `* *` | user | sort job applications by last updated time | be able to see which job applications have gone cold | +| `* *` | user | find organizations which have no job applications | get a summary of the organizations that I should apply to | +| `* *` | user | tag contacts | organize my contact list for more efficient access of different categories | +| `* *` | efficient user | type shorter arguments and known values with auto-completion | type my command even more quickly | +| `*` | user | import and export contacts | share my list of contacts with my peers | ### 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 `Jobby` and the **Actor** is the `user`, unless specified otherwise) + -**Use case: Delete a person** +**Use case: UC00 - Inputting commands with autocomplete** **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 inputs a command partially. +2. Jobby shows a list of possible completions that matches the partial command. +3. User selects a completion from the list. +4. Jobby updates the user input with the selected completion. +5. User repeats from step 1 until the command is complete. + + Use case ends. + +**Extensions** + +* 2a1. Jobby does not have any suggestions to list for the partial command. + + Use case resumes at step 5. + +* 3a. User dismisses the list of suggestions. + + Use case resumes at step 5. + +* 1a. User requests to undo the last completion. + * 1a1. Jobby undoes the last completion, if any. + + Use case resumes at step 2. + + +**Use case: UC01 - Add an application** + +**MSS** + +1. User requests to add an application. +2. Jobby adds the application into the specified organization. +3. Jobby shows that the application has been added. Use case ends. **Extensions** -* 2a. The list is empty. +* 1a. The given application does not match to any Organization. + * 1a1. Jobby shows an error message. + Use case ends. + + +**Use case: UC02 - Delete an application** + +**MSS** + +1. User requests to delete an application +2. Jobby deletes the application +3. Jobby shows that the application has been deleted + + Use case ends. + +**Extensions** +* 1a. The given application does not exist + * 1a1. Jobby shows an error message. + Use case ends. + + +**Use case: UC03 - Edit an application** + +**MSS** + +1. User requests to edit an application +2. Jobby edits the applications. +3. Jobby shows that the application has been edited + + Use case ends. + +**Extensions** + +* 1a. No details are given for which aspect of the application to edit. + * 1a1. Jobby shows an error message. + Use case ends. +* 1b. The application does not exist. + * 1b1. Jobby shows an error message. + Use case ends. + + +**Use case: UC04 - Add a contact** + +**MSS** + +1. User requests to add a contact. +2. Jobby adds the contact. +3. Jobby shows that the contact has been added. + + Use case ends. + +**Extensions** + +* 1a. The given contact does not have a required field. + * 1a1. Jobby shows an error message. + Use case ends. + + +**Use case: UC05 - Edit a contact** + +**MSS** + +1. User requests to edit a contact +2. Jobby edits the contact + Use case ends. + +**Extensions** + +* 1a. The given request does not match with any contact. + * 1a1. Jobby shows an error message. + + Use case ends. + + +**Use case: UC06 - Delete a contact** + +**MSS** + +1. User requests to list organizations +2. Jobby shows a list of organizations +3. User requests to delete a specific organization in the list +4. Jobby deletes the organization + + Use case ends. + +**Extensions** + +* 2a. The list is empty. Use case ends. * 3a. The given index is invalid. + * 3a1. Jobby shows an error message. + + Use case resumes at step 2. - * 3a1. AddressBook shows an error message. - +* 3b. The given ID does not match to any organization. + * 3b1. Jobby shows an error message. + Use case resumes at step 2. -*{More to be added}* +* 4a. The user has specified to delete recursively. + * 4a1. Jobby deletes all recruiter contacts associated with the recruiter (WIP) + + Use case ends. + + +**Use case: UC07 - List contacts** + +**MSS** + +1. User requests to list contacts +2. Jobby shows a list of contacts + + Use case ends. + +**Extensions** + +* 1a. User requests to list organizations. + * 1a1. Jobby shows a list of organizations. + + Use case ends. + +* 1b. User requests to list recruiters. + * 1b1. Jobby shows a list of recruiters. + + Use case ends. + +* 1c. User requests to list organizations that have no applications. + * 1b1. Jobby shows a list of organizations that have no applications. + + Use case ends. + + +**Use case: UC08 - Find contacts** + +**MSS** + +1. User requests to find contacts or applications +2. Jobby shows a list of contacts or applications found + + Use case ends. + +**Extensions** + +* 1a. User requests to find organizations. + * 1a1. Jobby shows a list of organizations that matches the search. + + Use case ends. + +* 1b. User requests to list recruiters. + * 1b1. Jobby shows a list of recruiters that matches the search. + + Use case ends. + +* 1c. User requests to list. + * 1c1. Jobby shows a list of application that matches the search. + + Use case ends. + +* 1d. No match found. + * 1d1. Jobby shows 0 matched result. + + Use case ends. + + +**Use case: UC09 - Sort data** + +**MSS** + +1. User requests to sort contacts. +2. Jobby shows a sorted list of contacts. + + Use case ends. + +**Extensions** + +* 1a. User requests to sort job applications. + * 1a1. Jobby shows a sorted list of job applications. + + Use case ends. + +* 1b. User requests to reset sorting order. + * 1b1. Jobby resets the sorting order. + * 1b2. Jobby shows an unsorted list of contacts and an unsorted list of job applications. + + Use case ends. + +* 1c. User requests to sort by multiple fields. + * 1b1. Jobby shows an error message. + + Use case ends. + + +**Use case: UC10 - Remind about deadlines** + +**MSS** + +1. User requests to remind about upcoming deadlines. +2. Jobby shows a list of job applications, sorted by most urgent deadline. + + Use case ends. + +**Extensions** + +* 1a. User requests to remind about non-urgent deadlines. + * 1a1. Jobby shows list of job applications, sorted by least urgent deadline. + + 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. +2. Should be able to hold up to 500 contacts (recruiters and organizations) and 1000 job applications 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. A user with familiarity with common Unix/Linux shell command syntax should find the syntax of Jobby to match their habits and easy to pick up. +5. The command syntax should not conflict with something that a user could plausibly use as legitimate data input. +6. This application does not automatically sync with a user's job application, e.g. Does not sync to the user's LinkedIn account to track job applications. ### 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, macOS, Linux, Unix +* **Commands**: A set of keywords that defines the operations the user wishes to execute. +* **Arguments**: A set of keywords that defines the type of data the user wishes to pass into the command line. -------------------------------------------------------------------------------------------------------------------- -## **Appendix: Instructions for manual testing** +## **Appendix B: Instructions for manual testing** Given below are instructions to test the app manually. @@ -334,44 +959,296 @@ testers are expected to do more *exploratory* testing.
+ ### Launch and shutdown 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. 1. Saving window preferences - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + + 1. Re-launch the app by double-clicking the jar file.
+ Expected: The most recent window size and location is retained. + +### Resetting to default data for Jobby + +1. Go to the folder where jobby.jar is located at +2. Delete the data directory. +3. Launch jobby.jar + + +### Adding an organization +1. Adding an organization + 1. Prerequisites: None + + 2. Test case: `add --org --name Woogle --id woogle-1`
+ Expected: Organization named Woogle is added to the list. + + 3. Test case: `add --org --tag GoodPay`
+ Expected: No organization is added. Error details shown in the status message. + +### Adding a recruiter +1. Adding a recruiter not linked to any organization + 1. Prerequisites: None + + 2. Test case: `add --rec --name Joe`
+ Expected: Recruiter named Joe is added to the list. + + 3. Test case: `add --rec --name`
+ Expected: No recruiter is added. Error details shown in the status message. - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +2. Adding a recruiter linked to an organization -1. _{ more test cases …​ }_ + 1. Prerequisites: Added an organization with the id `woogle-1` -### Deleting a person + 2. Test case: `add --rec --name Joe --oid woogle-1`
+ Expected: Recruiter named Joe is added to the list with association to organization with id woogle-1. -1. Deleting a person while all persons are being shown +### Adding a job application - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +1. Adding an application associated with an organization - 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: The list has an organization at index 1. - 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: `apply 1 --title SWE`
+ Expected: Job applications associated with the first organization is added to the list. The stage is at resume and the status is pending with a deadline of 14 days from the current date. + + 3. Test case: `apply 1 --status pending`
+ Expected: No job application is added. Error details shown in the status bar. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +### Editing organization and recruiter details -1. _{ more test cases …​ }_ +1. Editing organization and recruiter details + 1. Prerequisite: The list has contact at index 1. + + 2. Test case: `edit 1 --name Foogle`
+ Expected: Shows that the name of the organization or recruiter has been changed to Foogle. + 3. Test case `edit 1`
+ Expected: Does not edit the organization or recruiter details. Error details shown in status bar. + +### Editing job application details +1. Editing job application details + 1. Prerequisite: The list of job applications has at least 1 application. + 2. Test case: `edit --application 1 --status offered`
+ Expected: Edits the status of the application to offered. + 3. Test case: `edit --application 1 --by None-None-2022`
+ Expected: Does not edit the status of the application. Error details shown in the status bar. + +### Deleting recruiters and organizations + +1. Deleting an organization. + 1. Prerequisite: Have an organization at index 1 with job applications and recruiters associated to it. + 2. Test case: `delete 1`
+ Expected: Deletes the organization along with the job applications linked to it. Delinks the recruiters from the organization. + 3. Test case: `delete 1 --recursive`
+ Expected: Deletes the organization along with both the job applications and the recruiters linked to it. + +2. Deleting a recruiter + 1. Prerequisite: Have a recruiter at index 1 of the list. + 2. Test case: `delete 1`
+ Expected: The first recruiter is deleted from the list. Details of the deleted contact is shown in the status message. + 3. Test case: `delete 0`
+ Expected: No recruiter is deleted. Error details shown in the status message. ### Saving data 1. Dealing with missing/corrupted data files + 1. Prerequisite: None. + 2. Test case: Delete half of a contact in the json data file.
+ Expected: An empty list of contacts and applications are displayed on startup. + 3. Test case: Delete the data file in Jobby's home folder.
+ Expected: The sample list of contacts and applications is displayed on startup. + +2. Modifying the list of contacts and job applications + 1. Prerequisite: Having existing contacts and applications when editing or deleting data. + 2. Test case: Adding a new contact/job application and closing the application.
+ Expected: The new contact/job application is displayed when the application starts up again. + 3. Test case: Editing an existing contact/job application
+ Expected: The edits are saved and is correctly displayed when the application starts up again. + 4. Test case: Deletes an existing contact/job application
+ Expected: The contact/job application is not displayed when the application starts up again. + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix C: Planned Enhancements** + +This section documents the enhancements that will be added to Jobby in the future. + +### Do checks to ensure that old data is not the same as new data when editing data. + +Currently, Jobby sometimes allow editing of data such that the old data to be replaced with has the same contents as the new data. + +For example, `edit --application 1 --title SWE` on a job application with title "SWE" works, even though nothing is effectively changed. + +This can be done with a simple fix. The execute method for the editing of job applications is as follows: + +```java + public CommandResult execute(Model model) throws CommandException { + if (!editApplicationDescriptor.isAnyFieldEdited()) { + throw new CommandException(MESSAGE_NOT_EDITED); + } + List lastShownList = model.getDisplayedApplicationList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICATION_DISPLAYED_INDEX); + } + + JobApplication jobApplication = lastShownList.get(targetIndex.getZeroBased()); + JobApplication newApplication = createApplication(jobApplication, editApplicationDescriptor); + + try { + model.replaceApplication(targetIndex, newApplication); + } catch (IllegalValueException e) { + throw new CommandException(e.getMessage()); + } + + return new CommandResult(String.format(MESSAGE_EDIT_APPLICATION_SUCCESS, newApplication)); + } +``` + +We can easily add a new line to check if the all the contents of the new application is the same as the old one, and throw a CommandException if it is. + +This can also be easily done for editing contacts. + +### Make commands only take in arguments that are applicable to them and reject other extra arguments. + +Currently, some commands with multiple modes share parameters between modes. + +Example: The edit command has a contact mode and job application mode. In contact mode, `--name` is used to edit the name of the contact. However, there is no such thing for job application mode. + +Yet, `edit --application 1 --name John --title SWE` works as long as a valid field that is used for job application is used, and ignores the extra arguments. + +This can be done with a simple fix. Every command alreadly has a list of flags that are accepted. + +At the command parsing level, add additional checks against the list of flags provided by the command to ensure that every flag present in the command is applicable to the command used. + + + +### Better Formatting for Contacts + +Currently, the contacts are not nicely formatted and exposes some internal but non-critical implementation details. + + + +This is due to there not being a proper string conversion for the fields in the Contact class, especially when the fields which used to be compulsory are now optional. + +In this case, it would be easy to address this problem, by using +```java +optionalField.map(OptionalFieldClass::toString).orElse("None") +``` + +The Contact class can use the `Contact.getClass().getSimpleName()` method to get the type of the contact. Alternatively, it can use the `getType` method and use it for the class name, since the `getType` method matches the class name. + +### Disallow values in fields where values are not required + +Currently, Jobby accepts values for fields which do not require values. For example, `delete X --recursive` works as the command parser does not check if there is a value associated to the flag, but only checks if the flag exists. + +In this case, it is simple to add a checker similar to [validating that only the allowed flags are present](#make-commands-only-take-in-arguments-that-are-applicable-to-them-and-reject-other-extra-arguments) + + +### Add confirmation to run destructive commands + +Currently, Jobby does not warn users if they run a destructive command that cannot be undone, such as "clear", "delete" and "edit". + +Users may then destroy their data by accident. + +#### Proposed implementation + +One way to implement warnings is to have the user input 2 commands for destructive commands: +* The actual command +* The confirmation command + +Therefore, Jobby needs to be able to save the previous command to be able to execute it. One way is to store the command inside `LogicManager` and execute the command if the user enters a confirmation. + + + + +Step 1. The user executes a destructive command, such as `clear` to clear the data. + +Step 2. The parser will check if the command is a valid command as usual, and creates the command to be executed later. + +Step 3. The `LogicManager` checks that it is a destructive command (e.g. have commands implement a `isDestructive` method) + +Step 4. If it is destructive, it will generate a warning message to the user, otherwise it will execute the command normally. + +Step 5. The user confirms to continue with the command, which the `LogicManager` will execute the stored command. Otherwise, the `LogicManager` will not execute the command and removes the command. + +An alternative implementation from the above diagram is to allow the `AppParser` to store the destructive command in the `AppParser` instead, and when parsing the confirmation command it will give the destructive command. + + + + +### Add the find job applications feature + +Currently, Jobby does not implement the find function for applications. + +The biggest reason is due to the complexity: The list of job applications is dependent on the list of contacts. If a job application is in the list of job applications, then the organization associated with it should also be in the list of contacts. The converse is also true. This ensures that there is no confusion when using Jobby - it would be weird to have a job application associated to a company that does not exist in the list, and even weirder to see an organization in the list but the applications made to it are not shown! + +However, the find feature for applications may need to change that behavior, since the find feature will look through every job application shown and not shown in the list. Currently, the method to keep the behavior consistent would be: +1. Get a list of job applications that contains the keyword. +2. Filter out the list of contacts by checking if it is associated to any of the job applications from step 1. + +This can be made possible by making `JobApplication` searchable, by providing a method to check if the keyword matches any of its fields, such as title and description. +This will allow a list of job applications that have a match to be generated, and therefore now the `Model` can filter the contact list based on whether the contact is associated to the job application. + +## **Appendix D: Effort** + +This section documents the effort taken to evolve AB3 into Jobby. + +### Renaming `Contact` to `Person` + +To suit the application we were creating, we renamed the original `Person` class to `Contact`. + +This involved: +* Refactoring method names and parameters. (e.g `UniquePersonList` to `UniqueContactList`) +* Refactoring existing javadocs. +* Refactoring the User Guide and Developer Guide. (including existing diagrams) +* Renaming the `person` package to `contact`. + +### Creating three new types of data: `Organization`, `Recruiter` and `JobApplication` + +The `Contact` class was insufficient in representing the two new entities that we wanted to create for our application and we wanted to include a third to represent job applications. + +This involved: +* Making `Contact` an abstract class. +* Create classes for the new fields (e.g. `Url`, `Id`) +* Extending existing testing infrastructure. (e.g. `OrganizationBuilder` and `RecruiterBuilder`) +* Creating and improving new test cases for these classes. +* Adding new documentation on these classes. + +This was challenging as it a huge amount of time was spent modifying AB3's existing design and overhauling the test cases. + +### Different command modes + +As we created new types of data, each with their own set of requirements, AB3's existing command syntax was inadaequate. We had to extend commands such as `add`, `edit` and `delete` to encompass this change. + +For example, `add` can be used to add organizations (`add --org`) or add recruiters (`add --rec`). + +### Linking the `Organization` and `Recruiter` classes + +As multiple recruiters could be associated with a single organization, we wanted to represent this relationship in Jobby. + +Implementing this feature was challenging as AB3's existing design (especially its immutability and execution process) was not suited to easily incorporate this feature. + +Additionally, a huge amount of time was spent creating test cases and modifying previous test cases to include this new feature. + +### Autocompletion + +To improve user experience, we wanted to incorporate support for command autocompletion, which allows users to know which parameters and values could be used for a certain command. This reduced the reliance on the User Guide and would help new users familiarize themselves with Jobby. + +Additionally, the addition of autocompletion allows us to use full-length flags (e.g., `--description`), yet allowing the user to simply type `-dc ` to obtain the full flag, removing the need to memorize multiple 1-, 2-letter abbreviations. + +This involved: +* Creating multiple classes to better organize and provide autocompletion, +* Modifying the parser, tokenizer, flag syntax, etc., to aid in autocompletion, and +* Modifying JavaFX elements and intercepting the appropriate keystroke events to incorporate autocompletion. + +The end-to-end process of parsing the user text and determining what are plausible inputs is not trivial - we need to correctly decide which parameters and values are suggestable values based on command rules, such as "only one of `--name` allowed". - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +Additionally, despite the complex implementation, the autocompletion package has a comprehensive ~80% test coverage, with expected behaviors clearly documented within the tests themselves to guard against unexpected changes. -1. _{ more test cases …​ }_ +For more details, refer to the [Command Autocompletion Internals](#command-autocompletion-internals). diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..03bac73848d 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -10,7 +10,7 @@ title: Documentation guide * To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html). * Note these points when adapting the documentation to a different project/product: * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar. - * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format). + * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `Jobby` that comes into play when converting documentation pages to PDF format). * If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 57437026c7b..dc3e7632f82 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,195 +3,851 @@ 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. +## Introduction +
+ + Welcome to the **Jobby** User Guide! + + **Jobby** is a **desktop app designed for Computer Science students looking for a systematic way to manage their job applications and networking contacts, optimized for use via a Command Line Interface (CLI)** while still having the benefits of a Graphical User Interface (GUI). Jobby can help you manage tracking your job applications and contacts in a more streamlined fashion. If you can type fast, Jobby can get your contact management tasks done faster than traditional GUI apps. + + It is assumed that you know how the job application process is like, and that you know how to look for job applications. + +### Purpose of this guide + + The purpose of this guide is to get you familiar with the features of **Jobby** - from the very basics, to the most advanced features the application has to offer. These features range from the simple task of adding contacts into the application for tracking to how our application can assist you in tracking every step of your application process. We will go through every feature **Jobby** has to offer within this guide. You can check out the Table of Contents to navigate to a feature you might be interested in using. + +### How to use this guide + + _(For users who just want to see the table of contents, click [here](#table-of-contents).)_ + + To begin using this guide, ensure you have [installed Jobby](#installation). + Once this is done, go to the [tutorial](#jobby-tutorial-for-new-users) section to get started on the basic features of Jobby. + + When you become more familiar with the basic features, you can move on to understand [how Jobby's commands are structured and how to use autocomplete to your advantage](#using-jobby). + + Afterwards, please feel free to go through the [features section](#features) to find out what features Jobby has installed. We recommend going through it in the order of this guide but any order works as well! You may check out our table of contents to jump to any section of your choice. + + _The different symbols and formats used are explained in [this section](#navigating-the-guide)._ + + _The summary of the commands can be found [here](#command-summary)._ + +
+ +-------------------------------------------------------------------------------------------------------------------- + +## Table of contents +
* Table of Contents {:toc} +
+ +## Installation + +1. Ensure you have _Java 11_ or above installed in your computer. + * Download Java [here](https://www.oracle.com/java/technologies/downloads/) + * How to check your Java version on [Windows](https://www.howtogeek.com/717330/how-to-check-your-java-version-on-windows-10/), [macOS](https://www.wikihow.com/Check-Java-Version-on-a-Mac), and [Linux](https://stackoverflow.com/questions/47627270/how-to-check-java-version-at-linux-redhat6). + +1. Download the latest _jobby.jar_ from [here](https://github.com/AY2324S1-CS2103T-W08-3/tp/releases). + +1. Copy the file to the folder you want to use as the _home folder_ for your Jobby Application. + +1. Open a command terminal, navigate into the folder you put the jar file in, and enter `java -jar jobby.jar` in the terminal to run the application.
+ A window similar to the below should appear in a few seconds. Notice that the app contains some sample data.
+ * If you do not know how to navigate to your folder in the terminal, check out these links for [Windows](https://www.howtogeek.com/659411/how-to-change-directories-in-command-prompt-on-windows-10/), [macOS](https://appletoolbox.com/navigate-folders-using-the-mac-terminal/), and [Linux](https://askubuntu.com/questions/232442/how-do-i-navigate-between-directories-in-terminal). + ![Ui](images/Ui.png) + +1. Type a command in the command box and press Enter to execute it - e.g., typing `help` and pressing Enter will open the help window.
+ +1. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- -## Quick start -1. Ensure you have Java `11` or above installed in your Computer. +## Navigating the Guide -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +### Code blocks for entering commands -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +   denotes a command or a part of a command that can be entered in Jobby. For example, `add --org --name Woogle`{:.language-sh} is a command. `add` is also part of a command. -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +### Small information pills - * `list` : Lists all contacts. +| Component | Description | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :trophy: Learning outcomes | The learning outcome of the section. | +| Beginner
Intermediate
Expert | The difficulty level of the section, with Beginner for new users, Intermediate for users who have completed the tutorial, and Expert for users who have completed and used the features in the User Guide. | +| :information_source: An info pill | Contains some additional information, such as assumptions and useful information. | +| :warning: Some warning | Contains a short warning regarding the use of a feature. | +| :warning: A danger pill | Contains a short danger message regarding the use of a feature. | +| Organization
Recruiter
Job Application | The different objects of interest in Jobby: Organizations, Recruiters and Job Applications. | +| Organization | Contains information on which objects of interest in Jobby the feature can be used on. | - * `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. +### Large information sections - * `delete 3` : Deletes the 3rd contact shown in the current list. +
+:bulb: This is a blue box. It can be used for additional tips or more useful information. +
- * `clear` : Deletes all contacts. +
+:warning: This is a warning box. It can be used to give more details on the warnings and limitations of features. +
- * `exit` : Exits the app. +
+ +-------------------------------------------------------------------------------------------------------------------- +## Navigating the Interface + +Jobby comes equipped with a user interface that provides visual feedback to you. Below is a quick overview of the various components of our interface. + +![ui overview](images/ug-images/labelled-gui.png) + +| Component | Description | +|---------------------|------------------------------------------------------------------------------------------------------------| +| Command Box | You will enter your commands along with its input here. | +| Result Display | Displays the results of your commands.
Any error messages will also be displayed here. | +| Contact Details | Contains information related to the [contact](#glossary) like name, phone number, email etc. | +| Application Details | Contains information related to the internship application details like status, deadline etc. | -1. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +## Jobby Tutorial (for new users) -## Features +
+ + +Hello and welcome to Jobby! We are delighted that you've chosen our platform to track your internship application process! + +Before we begin, please ensure that you have viewed the following sections of the guide: +* [Installation](#installation) to help you get Jobby up and running. +* [Navigating the Interface](#navigating-the-interface) to get you familiarised with Jobby's User Interface. + +Following this tutorial will guide you through the basic workflow and functionalities of Jobby. +Here, you will learn how to add your first **Organization**, **Recruiter** and **Application**! + +Click on any of the hyperlinks below to jump to respective sections of the guide! +* [The Beginning](#the-beginning) +* [Adding your first Organization](#adding-your-first-organization) +* [Adding your first Recruiter](#adding-your-first-recruiter) +* [Adding your first Application](#adding-your-first-application) + +### The Beginning + +When you first launch Jobby, you will notice that it comes preloaded with sample data for you to play with. You may use +the sample data to familiarise yourself with the various features that Jobby provides straightaway! + +If you wish to continue following our guide to learn the basic operations of Jobby, you can easily remove the sample data by: +1. Type `clear` into the command box +2. press **ENTER** + +Solid! Now it's time to get started with Jobby! + +
+ +### Adding your first Organization + +Let's say you are interested to apply to **Google** as your internship destination, and you found their email **google@gmail.com**. +(This is not their real email, of course) + +You can use [`add --org`{:.language-sh}](#adding-organizations---add---org) to add the Google organization in Jobby: +1. Type `add --org --name Google --id google_id --email google@gmail.com`{:.language-sh} into the command box +2. Press **ENTER** + +![Adding Organization](images/ug-images/org-added.png) + +You have successfully added **Google**, with the email **google@gmail.com** into Jobby! + +### Adding your first Recruiter + +In a job fair, you managed to meet a **Google** internship recruiter, **Josh Mao**, and he provided you with his number +**91219121**. + +Here is how you can use [`add --rec`{:.language-sh}](#adding-recruiters---add---rec) to record the recruiter in Jobby: +1. Type `add --rec --name Josh Mao --oid google_id --phone 91219121`{:.language-sh} into the command box +2. Press **ENTER** + +![Adding Recruiter](images/ug-images/rec-added.png) + +You have successfully added **Josh Mao** - a **recruiter** from Google with the phone number **91219121**. + +### Adding your first Application + +After preparing your resume, you are ready to apply to **Google** as an intern for their **Software Engineer** role! And you know that the application deadline is on the **22-11-2023**. + +Here is how you can use [`apply`](#applying-to-organizations---apply) to track your application in Jobby: +1. Type `apply google_id --title Software Engineer --by 22-11-2023`{:.language-sh} into the command box +2. Press **ENTER** + +![Adding Application](images/ug-images/app-added.png) + +You have successfully added your job application to **Google**! + +**Congratulations!** You have run through the basics of Jobby. We hope that this tutorial has given you an understanding of a basic workflow in Jobby. +However, there are still many features that we have yet to introduce. Please refer to the [Using Jobby](#using-jobby) section to understand how to interpret +command structures and formats, or visit the [Features](#features) section to see the full capabilities of Jobby! + +-------------------------------------------------------------------------------------------------------------------- + +## Using Jobby + +This section explains how you can understand and interact with Jobby via commands. + +If you're looking for the list of available commands, check out the [Features](#features) section instead. + +### Understanding the Command Structure + +:trophy: How to understand and write Jobby commands Beginner + +In Jobby, we write commands in the command box at the top of Jobby's window. + +Commands are made up of a few parts: The **command**, **parameter names** and **input values**. + +A command like "`edit google --name Google SG --id google-sg`{:.language-sh}" would refer to: +* the **command** `edit`, +* with a **command value** `google`, +* with a **parameter** `--name`{:.language-sh}, + * which has the **parameter value** `Google SG`, +* with a **parameter** `--id`{:.language-sh}, + * which has the **parameter value** `google-sg`. + +Parameters may be in any order, whose names are of the form `-a` or `--abc123`{:.language-sh}, and must be surrounded by whitespace. + +Any extra parameters and values to commands that don't accept them will either be ignored or throw an error. + +
+ +**:bulb: Additional information:**
+ +* Parameter names are restricted to the `-`/`--` prefix, contain only letters and numbers, and must begin with a letter. + +* Any parameter names not following the required format will be treated as data input, so an input like *"-5 degrees"* will work. + +* Expert Although Jobby's syntax resembles the usual Unix syntax, you should not quote your text, and you should not leave a trailing `=`. + +
+ +### Reading Command Formats + +:trophy: How to interpret Jobby-formatted command explanations Beginner + +Throughout this guide and within Jobby itself, you will find symbols and placeholders used to describe a command format. They are: + +* **Words in `UPPER_CASE`** + + * The parts where you should be typing your parameter values. + + * e.g., `--name NAME`{:.language-sh} means inputting names along the lines of `--name Alice`{:.language-sh}. + +* **Terms separated by `/` or `|`** + + * Exactly one of the given options. + + * These may be included in the parameter names or value description. + + * e.g., `--a / --b`{:.language-sh} means either `--a`{:.language-sh} or `--b`{:.language-sh} but not `--a --b`{:.language-sh}. + +* **Terms surrounded by `[` and `]`** + + * An optional parameter or option that may be omitted. + + * e.g., `[--id ID]`{:.language-sh} means you may omit setting an ID for the command. + +* **Terms ending with `...`** + + * The parameter is multivalued. + + * e.g., `[--tag TAG]...`{:.language-sh} means `--tag`{:.language-sh} and its value can be repeated from 0 to any number of times. + +* **Terms surrounded by `<` and `>`** + + * A high level description of the parameter or option. + + * e.g., if you see something like `< add some text here >`, it means you should replace it with your own text. + +Parameters may have certain value format restrictions - Jobby will let you know if you do not meet a requirement when you input your command. Optionally, you may also refer to their details in [Appendix A](#appendix-a-acceptable-values-for-parameters) later. + + +### Autocompleting Commands + +:trophy: How to use Jobby's command autocompletion Beginner + +Command autocompletion allows you to type commands in Jobby at unimaginable speeds. + +As you type your command, you may see a list of suggested completions pop up. +Just press **TAB** or **SPACE** to select the first suggestion to fill in that text! + +![Autocomplete Screenshot](images/autocomplete.png) + +To temporarily hide all suggestions, press **ESC**. This temporarily disables autocompletion for the next keystroke. + +If suggestions were hidden or aren't shown when they should, press **TAB** to prompt Jobby to bring it back.
-**:information_source: Notes about the command format:**
+**:bulb: Additional tips:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* If you rather choose from the list instead of typing out the prefix, it is possible to use the **UP** and **DOWN** + arrow keys to navigate through the menu, then press **ENTER** to select them. -* 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`. +* Accidentally triggered autocomplete when you didn't intend to? Don't worry, just press **BACKSPACE** to immediately + revert to your previously typed text. -* 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. +* Expert Autocomplete checks for fuzzy matches - it sorts by the best *subsequence* prefix match first. -* 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. + * For example, you can type `-nm` to get the autocompletion result of `--name`{:.language-sh}. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. + * This allows you to quickly choose between parameter names with similar prefixes, e.g., by typing + `-dsp` to select `--description`{:.language-sh} instead of `--descending`{:.language-sh}. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-### Viewing help : `help` +
-Shows a message explaning how to access the help page. +**:warning: Limitations:**
+ +* Autocomplete is not autocorrect. It will not attempt to correct mistyped details. + +* Autocomplete suggests plausible values you may want to add onto your partially typed command. It does not verify that the command will run. + +
+ +------------------------------------------------------------------------------------------------- + +## Features -![help message](images/helpMessage.png) +### Adding contacts - `add` +
Organization Recruiter

-Format: `help` +The `add` command allows you to create contacts to track details about the organizations and recruiters related to your job application process. To learn more about creating each type of contact, check out the sections below. +#### Adding organizations - `add --org`{:.language-sh} -### Adding a person: `add` +:trophy: How to add organization contacts into Jobby Beginner -Adds a person to the address book. +##### Format +```sh +add --org --name NAME [--id ID] [--phone NUMBER] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]... +``` +Adds an organization contact with the details given to the command. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* If an `ID` is not specified, one will be automatically generated. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). -
:bulb: **Tip:** -A person can have any number of tags (including 0) +##### Valid examples + +| Command | Reason | +|------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `add --org --name J&J`{:.language-sh} | Adds an organization **J&J**. | +| `add --org --name Google --id g-sg --phone 98765432 `{:.language-sh} | Adds an organization **Google** with other flags. | +| `add --org --name Examinations NUS --phone 65166269 --email examinations@nus.edu.sg --url https://luminus.nus.edu.sg/`{:.language-sh} | Adds an organization **Examination NUS** with other flags. | + + +##### Invalid examples + +| Command | Reason | +|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| `add --org --name`{:.language-sh} | `--name`{:.language-sh} field used but not specified. | +| `add --org --name Google --phone 1231*&&@`{:.language-sh} | Optional field (in this case `--phone`{:.language-sh}) was not following the [accepted parameters](#appendix-a-acceptable-values-for-parameters). | + + +#### Adding recruiters - `add --rec`{:.language-sh} + +:trophy: How to add recruiter contacts into Jobby Beginner + +##### Format +```sh +add --rec --name NAME [-id ID] [--oid ORG_ID] [--phone NUMBER] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]... +``` +Adds a recruiter contact with the details given to the command. + +* If an `ID` is not specified, one will be automatically generated. +* To link a Recruiter to an Organization in the contacts list, make sure you include `--oid`{:.language-sh} and pass in the `ID` of the Organization you want to link to. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). + +##### Sample demonstration +* If you execute the command: `add --rec --name Ryan Koh --oid job_seeker_plus`{:.language-sh}, you should see a new Recruiter being added to the bottom of the contacts list. + +* The newly added contact will have a special label _from organization (job\_seeker\_plus)_ to indicate that the Recruiter is associated to the Organization with that particular `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` +##### Valid examples -### Listing all persons : `list` +| Command | Reason | +|-------|----------| +| `add --rec --name John Doe`{:.language-sh} | Adds a recruiter that is not associated to any organization. | +| `add --rec --name John Doe --tag friendly --tag woogle`{:.language-sh} | Adds a recruiter with two tags - friendly and woogle. | +| `add --rec --name John Doe --oid job_seeker_plus`{:.language-sh} | Adds a recruiter that is associated to an organization (if it exists in the address book) with the id **job_seeker_plus**. | +| `add --rec --name John Doe --id johndoe_123 --oid job_seeker_plus --number 912832192 --email johndoe@nus.edu.sg --url example.com --address 21 Kent Ridge Rd --tag network`{:.language-sh} | Adds a recruiter with all the possible fields. | -Shows a list of all persons in the address book. +##### Invalid examples -Format: `list` +| Command | Reason | +|---------|--------| +| `add --rec`{:.language-sh} | Missing a name. | +| `add --rec --name John Doe --phone`{:.language-sh} | Optional fields (in this case `--phone`{:.language-sh}) were used but not specified. | +| `add --rec --name John Doe --oid bogus-org`{:.language-sh} | Given that no organization with the id "bogus-org" exists in the address book. | -### Editing a person : `edit` -Edits an existing person in the address book. +### Editing contacts - `edit` +
Organization Recruiter
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +:trophy: How to edit organization or recruiter info in Jobby Intermediate
+:information_source: Assumes that you have read the `add` command documentation for contacts.
-* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +##### Format +```sh +edit INDEX/ID [--name NAME] [--id ID] [--phone PHONE] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]... +``` +Edits the given contact according to the parameters given. +* You can supply more than one parameter to change multiple details of a contact in one command. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). -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. +##### Valid examples -### Locating persons by name: `find` +| Command | Reason | +|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| `edit google --phone 91292951`{:.language-sh} | Changes phone number of organization with **ID** google to **91292951**. | +| `edit 1 --name Jane Street`{:.language-sh} | Changes name of contact at index 1 to **Jane Street**. | +| `edit 1 --name Google --phone 91241412 --email google@gmail.sg`{:.language-sh} | Changes the name, phone number and email of the contact at index 1 to `Google`, `91241412` and `google@gmail.sg` respectively. | -Finds persons whose names contain any of the given keywords. +##### Invalid examples -Format: `find KEYWORD [MORE_KEYWORDS]` +| Command | Reason | +|-----------------------------------------|-------------------------------------------------------------------------------------| +| `edit google --phone 8124!@#$`{:.language-sh} | `--phone`{:.language-sh} has an [invalid parameter](#appendix-a-acceptable-values-for-parameters) | -* 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` +### Deleting contacts - `delete` +
Organization Recruiter
-Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +:trophy: How to delete contacts and job applications in Jobby Intermediate -### Deleting a person : `delete` +:warning: The deletion of data is permanent and there is no way to undo it. -Deletes the specified person from the address book. +##### Format +```sh +delete INDEX/ID [--recursive] +``` +Deletes the contact at the given `INDEX` or `ID`. +* `--recursive`{:.language-sh} flag deletes the associated recruiter contacts and internship applications if the contact to delete is an organization. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). -Format: `delete INDEX` +##### Valid examples -* 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, …​ +| Command | Reason | +|------------------------|----------------------------------------------------------------------------------------| +| `delete 1` | Deletes the contact at index 1. | +| `delete josh` | Deletes the contact with the **ID** of **josh**. | +| `delete 1 --recursive`{:.language-sh} | Deletes a contact and all its associated recruiter contacts and applications. | -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. +##### Invalid examples -### Clearing all entries : `clear` +| Command | Reason | +|------------------------|---------------------------------------------------------------------| +| `delete 0` | Invalid index, as index starts from 1. | -Clears all entries from the address book. +### Applying to organizations - `apply` +
Job Application
-Format: `clear` +:trophy: How to record your job applications associated with an organization in Jobby Intermediate
+:information_source: You need to have organizations stored in Jobby to use this command. -### Exiting the program : `exit` +##### Format +```sh +apply INDEX/ID --title TITLE [--description DESCRIPTION] [--by DEADLINE: DD-MM-YYYY] [--stage APPLICATION STAGE: resume | online assessment | interview] [--status STATUS: pending | offered | accepted | turned down] +``` +Applies to the given organization by creating a job application associated with it. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). -Exits the program. +##### Valid examples + +| Command | Reason | +|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| `apply 1 --title SWE`{:.language-sh} | Applies to the **organization** at index 1, for the title of **SWE**. | +| `apply google --title Unit Tester --by 12-12-2023`{:.language-sh} | Applies to the **organization** with ID of *google** for title of **Unit Tester** by **12-12-2023**. | + +##### Invalid examples + +| Command | Reason | +|---------------------------------------|-----------------------------------------------------| +| `apply 0 --title SWE`{:.language-sh} | Invalid index as index starts at 1. | +| `apply 1 --title`{:.language-sh} | Invalid as `--title`{:.language-sh} is declared but not specified. | +| `apply 1 --title SWE --by 31-31-2023`{:.language-sh} | Invalid date for deadline. | + +### Editing job applications - `edit --application`{:.language-sh} +
Job Application
+ +:trophy: Able to edit job applications associated with an organization in Jobby Intermediate
+:information_source: Assumes that you have read the `apply` command documentation.
+ +##### Format +```sh +edit --application INDEX [--title TITLE] [--description DESCRIPTION] [--by DEADLINE] [--status STATUS] [--stage STAGE] +``` + +Edits the given job application according to the parameters given. +* You can supply more than one parameter to change multiple details of an application in one command. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). + +##### Valid examples + +| Command | Reason | +|--------------------------------------------|-----------------------------------------------------------------| +| `edit --application 1 --title SRE`{:.language-sh} | Changes the title of the job application at index 1 to **SRE**. | +| `edit --application 1 --status pending`{:.language-sh} | Changes the status of job application at index 1 to **pending**. | + +##### Invalid examples + +| Command | Reason | +|----------------------------------------------|---------------------------------------| +| `edit --application 0 --title SRE`{:.language-sh} | Invalid index. | +| `edit --application 1`{:.language-sh} | None of the fields to edit are given. | +| `edit --application 1 --by 31-31-2023`{:.language-sh} | The date is invalid. | + +### Deleting job applications - `delete --application`{:.language-sh} +
Job Application
+ +:trophy: Able to delete job applications in Jobby Intermediate + +:warning: The deletion of data is permanent and there is no way to undo it. + +##### Format +```sh +delete --application INDEX +``` +Deletes the job application at the given `INDEX`. +* If you wish to know more about the requirements for each parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). + +##### Valid examples + +| Command | Reason | +|--------------------------|---------------------------------------------------------------------| +| `delete --application 1`{:.language-sh} | Deletes the application at index 1. | + +##### Invalid examples + +| Command | Reason | +|--------------------------|---------------------------------------------------| +| `delete --application 0`{:.language-sh} | Invalid index, as index starts from 1. | + + +### Listing data - `list` +
Organization Recruiter Job Application
+ +:trophy: How to list organizations and recruiters in Jobby by conditions Intermediate + +##### Format +```sh +list [--org / --rec / --toapply] +``` +Lists all contacts. If you provide a parameter, the contacts listed will be only those that fit the given parameter. + +* Supplying `--org`{:.language-sh} lists only Organizations while supplying `--rec`{:.language-sh} lists only Recruiters. Specifying neither will list all contacts. + +* Supplying `--toapply`{:.language-sh} lists Organizations you have not applied to. + +##### Valid examples + +| Command | Reason | +|------------------|-------------------------------------------------------------------| +| `list` | List all **contacts**. | +| `list --org`{:.language-sh} | Lists all **organization contacts**. | +| `list --rec`{:.language-sh} | Lists all **recruiter contacts**. | +| `list --toapply`{:.language-sh} | Lists all **organization contacts** that have not been applied to. | + + +### Searching contacts - `find` +
Organization Recruiter
+ +:trophy: How to find organizations and recruiters by keyword Beginner + +##### Format +```sh +find KEYWORD... +``` + +Finds the contacts whose `NAME` or `ID` contains the given `KEYWORD`. +* You can supply multiple keywords as long as they are separated by [whitespace](#glossary). +* If you wish to know more about the requirements for the parameter, check out the [given appendix](#appendix-a-acceptable-values-for-parameters). + +##### Valid examples + +| Command | Reason | +|-------------------|-----------------------------------------------------------------------------------------------------| +| `find jo` | Finds contacts whose `NAME` or `ID` contains the [substring](#glossary) "jo". | +| `find 1231` | Finds contacts whose `NAME` or `ID` contains the substring "1231". | +| `find alex david` | Finds contacts whose `NAME` or `ID` contains the substring "alex" or "david". | + +##### Rules +Intermediate + +* The search is case-insensitive. e.g `hans` will match `Hans` and `1231` will match `id_1231`. +* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans`. +* You can match partial keywords. e.g. searching for `ha` will match with `hamburger`. +* Contacts matching at least one keyword will be returned (i.e. `OR` search) + e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`. + + +### Sorting data - `sort` +
Organization Recruiter Job Application
+ +:trophy: How to sort contacts and job applications in Jobby Intermediate + +##### Format +```sh +sort --FLAG_TO_SORT [--ascending / --descending] +``` + +Sorts contacts or job applications for you by the specified flag. +`--FLAG_TO_SORT`{:.language-sh} represents a parameter of the contact or job application (i.e. `--phone`{:.language-sh} represents the phone number of a contact). + +##### Supported primary parameters (only 1 may be provided) + +###### Fields for Contacts +* `--address`{:.language-sh} - Will sort alphabetically. +* `--email`{:.language-sh} - Will sort alphabetically. +* `--id`{:.language-sh} - Will sort alphabetically. +* `--name`{:.language-sh} - Will sort alphabetically. +* `--phone`{:.language-sh} - Will sort alphabetically. +* `--url`{:.language-sh} - Will sort alphabetically. + +###### Fields for Job Applications +* `--by`{:.language-sh} - Will sort chronologically. +* `--stage`{:.language-sh} - Will sort by stage order. +* `--stale`{:.language-sh} - Will sort chronologically. +* `--status`{:.language-sh} - Will sort by status order. +* `--title`{:.language-sh} - Will sort alphabetically. + +###### Resetting the sort order +* `--none`{:.language-sh} - Will reset the sorting order of Contacts and Job Applications. -Format: `exit` +##### Supported secondary parameters + +###### Changing the sort order +* `--ascending`{:.language-sh} - The specified flag will sort in ascending order. +* `--descending`{:.language-sh} - The specified flag will sort in descending order. + + + +* If neither `--ascending`{:.language-sh} or `--descending`{:.language-sh} are provided, the list will be sorted in ascending order by default. + +* Neither `--ascending`{:.language-sh} nor `--descending`{:.language-sh} may be specified if the flag is `--none`{:.language-sh}. + +* Sorting will work even if no Contacts or Job Applications exist. In that case, nothing will happen. + +##### Sample demonstration +* To order your Job Applications by order of earliest deadline, you can use the command `sort --by`{:.language-sh}. +* In the Application Details section of Jobby, you should see your Job Applications now ordered by most urgent deadline. + +
+ +
+ +##### Valid examples + +| Command | Reason | +|-----------------------------|------------------------------------------------------------------------------------------------------------| +| `sort --title --ascending`{:.language-sh} | Sorts **job applications** by title, in ascending alphabetical order. | +| `sort --url`{:.language-sh} | Sorts **contacts** by url, in the default order - ascending alphabetical. | +| `sort --stale --descending`{:.language-sh} | Sorts **job applications** by last updated time, in reverse chronological order, from most recent to least. | +| `sort --none`{:.language-sh} | Resets the sorting order of **contacts** and **job applications**. | + +##### Invalid examples + +| Command | Reason | +|----------------------------|---------------------------------------------| +| `sort` | No field provided. | +| `sort --org`{:.language-sh} | Invalid field. | +| `sort --none --descending`{:.language-sh} | `--none`{:.language-sh} and `--descending`{:.language-sh} both specified. | +| `sort --title --name`{:.language-sh} | More than 1 field specified. | + + +### Reminding about deadlines - `remind` +
Job Application
+ +:trophy: How to get reminders of deadlines in Jobby Intermediate + +##### Format +```sh +remind --earliest / --latest +``` + +Reminds you of upcoming deadlines for job applications. + +##### Sample demonstration +* To see your application deadlines from the earliest to latest, use the command `remind --earliest`{:.language-sh}. + +![Remind Earliest](images/starter-guide/remind-earliest.jpg) + +##### Valid examples + +| Command | Reason | +|---------------------|--------------------------------------------------------------------------------------| +| `remind --earliest`{:.language-sh} | Lists the application deadlines in order of urgency, from earliest to latest. | +| `remind --latest`{:.language-sh} | Lists the application deadlines in order of reverse urgency, from latest to earliest. | + +##### Invalid examples + +| Command | Reason | +|---------------------------------------|-----------------------------------------------------| +| `remind` | No urgency level specified. | + + +### Viewing help - `help` + +:trophy: How to find help on Jobby's commands Beginner + +##### Format +```sh +help +``` + +Shows a message explaining how to access the help page. + +![Help Message](images/helpMessage.png) + + +### Clearing all data - `clear` +
Organization Recruiter Job Application
+ +:trophy: How to clear all contacts and job applications in Jobby Intermediate
+ +:warning: The deletion of all data is permanent and there is no way to undo it. + +##### Format +```sh +clear +``` + +Clears all Contact and Job Application data from Jobby. + + +### Exiting the program - `exit` + +:trophy: How to exit Jobby Beginner + +##### Format +```sh +exit +``` + +Exits the program. ### 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. +Jobby's data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +Jobby's data are saved automatically as a JSON file `[JAR file location]/data/jobby.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. +
+:warning: **Caution:** If your changes to the data file makes its format invalid, Jobby will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-### Archiving data files `[coming in v2.0]` +-------------------------------------------------------------------------------------------------------------------- + +## Command Summary -_Details coming soon ..._ +### Commands for Handling Contacts + +| Action | Format, Examples | +|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add Organization** | `add --org --name NAME [--id ID] [--phone NUMBER] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]...`{:.language-sh}
e.g., `add --org --name NUS --phone 0123456789 --email example@nus.edu.sg --url https://www.nus.edu.sg/`{:.language-sh} | +| **Add Recruiter** | `add --rec --name NAME [--id ID] [--oid ORG_ID] [--phone NUMBER] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]...`{:.language-sh}
e.g., `add --rec --name John Doe --oid paypal-sg`{:.language-sh} | +| **Delete Contact** | `delete INDEX/ID [--recursive]`{:.language-sh}
e.g., `delete 3`, `delete id-55tg` | +| **Edit Contact** | `edit INDEX/ID [--name NAME] [--id ID] [--phone PHONE] [--email EMAIL] [--url URL] [--address ADDRESS] [--tag TAG]...`{:.language-sh} | +| **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` | +| **List** | `list [--org / --rec / --toapply]`{:.language-sh} | +| **Sort Contacts** | `sort --address / --email / --id / --name / --phone / --url [--ascending / --descending]`{:.language-sh} | + +### Commands for Handling Job Applications + +
+ + +| Action | Format, Examples | +|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Delete Application** | `delete --application INDEX`{:.language-sh}
e.g., `delete --application 2`{:.language-sh} | +| **Edit Application** | `edit --application INDEX [--title TITLE] [--description DESCRIPTION] [--by DEADLINE] [--status STATUS] [--stage STAGE]`{:.language-sh}
e.g., `edit --application 2 --title Analyst`{:.language-sh} | +| **Apply** | `apply INDEX/ID --title TITLE [--description DESCRIPTION] [--by DEADLINE] [--stage STAGE] [--status STATUS]`{:.language-sh} | +| **Sort Applications** | `sort --by / --stage / --stale / --status / --title [--ascending / --descending]`{:.language-sh} | + +### Other Commands + +| Action | Format, Examples | +|-----------|------------------| +| **Clear** | `clear` | +| **Help** | `help` | +| **Exit** | `exit` | + +
-------------------------------------------------------------------------------------------------------------------- -## FAQ +## Glossary + +| Term | Definition | +|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Index** | An index is a number that is used to identify a contact or job application in a list.

(e.g. `2` would be the index of the contact labelled **2.** in the contacts list). | +| **Whitespace** | In the context of this application, a whitespace is any number of spaces that is in the input. | +| **Contact** | A contact in Jobby can be an Organization or a Recruiter. | +| **Substring** | A substring is a contiguous sequence of characters within a string

(e.g. "pp" is a substring of "apple", "mac" is a substring of "macDonald" and "intimacy"). | +| **Subsequence** | A subsequence is a sequence obtainable from another sequence by deleting some or no elements without changing the order of the remaining elements

(e.g. "abc", "1b2", "123" are all subsequences of "a1b2c3"). | +| **Top Level Domain** | A Top Level Domain (TLD) is the part of the website address where it comes after the last dot (i.e. ".com", ".org", ".net") and before the first slash

(e.g. www.example.**com**/path). | + +## Appendices + +### Appendix A: Acceptable values for parameters + +| Parameter | Used by | Requirements | Examples | +|-----------|---------|--------------|----------| +| `INDEX` | [`edit`](#editing-contacts---edit)

[`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application)

[`delete`](#deleting-contacts---delete)

[`delete --application`{:.language-sh}](#deleting-job-applications---delete---application) | A valid index can accept any positive integer up to the number of items displayed in the contact or job application list where applicable. | `1`
`10` | +| `NAME` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid name can accept any non-empty value. | `Ryan Koh`
`小明` | +| `ID` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit)

[`apply`](#applying-to-organizations---apply)

[`delete`](#deleting-contacts---delete) | A valid ID has to start with a letter.

It can consist of alphanumeric characters and basic symbols (i.e. `a-z`, `A-Z`, `0-9`, `-`, `_`). However it cannot have consecutive underscores and dashes. | `woogle123`
`ryan_soc-rec` | +| `NUMBER` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid phone number can consist of only numbers with no whitespace.

It must be at least 3 digits. | `999`
`91824137` | +| `EMAIL` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid email should be in the form of `local-part@domain` where the `local-part` and `domain` must be separated by a single **@**.

The `local-part` can consist of any character except whitespace.

The `domain` name can comprise of one or more labels separated by periods, and each label can include any character except whitespace. The last `domain` label must be a minimum of two characters long. | `ryankoh@nus`
`ryan-koh@nus.edu.sg` | +| `URL` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid url should include a part in the form of `domain.tld` where the `domain` and the `tld` (top level domain) must be separated by a period. | `example.com`
`example.more.com`
`https://example.com`
`example.com/more` | +| `ADDRESS`| [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid address can accept any non-empty value.

For a contact, it designates its physical address. | `21 Lower Kent Ridge Rd` | +| `TAG` | [`add --org`{:.language-sh}](#adding-organizations---add---org)

[`add --rec`{:.language-sh}](#adding-recruiters---add---rec)

[`edit`](#editing-contacts---edit) | A valid tag can consist of only alphanumeric characters. | `internship`
`network`
`parttime`
`jobPortal` | +| `ORG_ID` | [`add --rec`{:.language-sh}](#adding-recruiters---add---rec) | A valid organization ID is subject to the same requirements as the ID parameter.

It must belong to an Organization contact in the address book. | `woogle123`
`meta_sg-1` | +| `TITLE` | [`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application) | A valid title can accept multiple words separated with spaces, as long as the characters are alphanumeric. | `Software Engineer`
`Level 3 Engineer` | +| `DESCRIPTION` | [`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application) | A valid description can accept any non-empty value. | `Senior Role`
`Hourly rate: $25` | +| `DEADLINE` | [`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application) | A valid deadline should be a date in the form of `DD-MM-YYYY`.

The day (`DD`) and month (`MM`) can be either single or double digits. | `09-02-2022`
`9-2-2022`
`19-11-2022` | +| `STAGE` | [`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application) | A valid job application stage can accept only one of the three values: `resume`, `online assessment`, `interview`.

The values are ranked in the order shown. | `resume`
`online assessment`
`interview` | +| `STATUS` | [`apply`](#applying-to-organizations---apply)

[`edit --application`{:.language-sh}](#editing-job-applications---edit---application) | A valid job application status can accept only one of the four values: `pending`, `offered`, `accepted`, `turned down`.

The values are ranked in the order shown. | `pending`
`offered`
`accepted`
`turned down` | +| `KEYWORD` | [`find`](#searching-contacts---find) | A valid keyword is a single word that can accept any non-empty value. | `software`
`Ryan` | -**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. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## Frequently Asked Questions + +##### _How do I transfer my data to another device?_ +* Jobby currently does not directly support data transfer. + You can transfer your contact data and job application data by copying the data folder in your old _jobby.jar_ home folder to the new home folder for _jobby.jar_. + +##### _I want to try out Jobby with some sample data. How can I do so?_ +* You can delete the data folder in the home folder of _jobby.jar_, and launch Jobby again. + There will be sample data generated on launch. + * Alternatively, you can move the data folder somewhere else if you still want to keep the data. -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +##### _I am currently facing issues with Jobby._ +* We would like to hear the details of the issues that you are having. + You can report them through our [bug tracker](https://github.com/AY2324S1-CS2103T-W08-3/tp/issues). + +##### _I would like to suggest a new feature for Jobby._ +* We are always looking for suggestions to improve Jobby! + You can suggest a new feature to us via the [issue tracker](https://github.com/AY2324S1-CS2103T-W08-3/tp/issues). -------------------------------------------------------------------------------------------------------------------- +## Issues + +1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the _preferences.json_ file created by the application before running the application again. + +2. **When requesting to sort applications after a call to `list --rec`{:.language-sh}**, the command will succeed but display nothing, since no organizations are currently listed, and so no linked applications will display. The remedy is to call `list` before sorting applications and calling the sort command once more. -## 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` +3. Parameter names use either the `-` or `--` prefix, but **all commands as of the current version only use the `--` prefix.** While the `-` prefix is currently unused, it is reserved (so user input cannot take that format), and it will be relevant in future updates. diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..74ac03cf341 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "Jobby" theme: minima header_pages: @@ -8,7 +8,10 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +kramdown: + toc_levels: "2,3,4" + +repository: "ay2324s1-cs2103t-w08-3/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml index 8f3e50cb601..f1aee680705 100644 --- a/docs/_data/projects.yml +++ b/docs/_data/projects.yml @@ -1,23 +1,4 @@ -- name: "AB-1" - url: https://se-edu.github.io/addressbook-level1 +- name: "Jobby" + url: https://ay2324s1-cs2103t-w08-3.github.io/tp/ -- name: "AB-2" - url: https://se-edu.github.io/addressbook-level2 -- name: "AB-3" - url: https://se-edu.github.io/addressbook-level3 - -- name: "AB-4" - url: https://se-edu.github.io/addressbook-level4 - -- name: "Duke" - url: https://se-edu.github.io/duke - -- name: "Collate" - url: https://se-edu.github.io/collate - -- name: "Book" - url: https://se-edu.github.io/se-book - -- name: "Resources" - url: https://se-edu.github.io/resources diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..e4e06cb2a9b 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -179,6 +179,8 @@ pre { border: 0; padding-right: 0; padding-left: 0; + + white-space: pre-wrap; } } @@ -203,17 +205,17 @@ pre { margin-left: auto; padding-right: $spacing-unit / 2; padding-left: $spacing-unit / 2; + background-color: white; @extend %clearfix; @media screen and (min-width: $on-large) { - max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); - padding-right: $spacing-unit; - padding-left: $spacing-unit; + max-width: calc(#{$content-width} - (#{$spacing-unit} * 3)); + padding-right: $spacing-unit * 1.5; + padding-left: $spacing-unit * 1.5; } } - /** * Clearfix */ @@ -288,7 +290,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "Jobby"; font-size: 32px; } } diff --git a/docs/_sass/minima/_layout.scss b/docs/_sass/minima/_layout.scss index ca99f981701..7e3ffb77e6c 100644 --- a/docs/_sass/minima/_layout.scss +++ b/docs/_sass/minima/_layout.scss @@ -126,6 +126,26 @@ flex: 1 0 auto; } +@media screen and (min-width: $on-large) { + /* Paper styling on larger screens */ + .page-content { + background: #fafafa; + + & > .wrapper { + padding-top: 48px; + padding-bottom: 48px; + background-color: white; + box-shadow: $paper-box-shadow; + } + } + + .site-header { + border-bottom: none; + background-color: white; + box-shadow: $paper-box-shadow; + } +} + .page-heading { @include relative-font-size(2); } @@ -176,8 +196,41 @@ .post-content { margin-bottom: $spacing-unit; - h1, h2, h3 { margin-top: $spacing-unit * 2 } - h4, h5, h6 { margin-top: $spacing-unit } + h1, h2, h3 { + margin-top: $spacing-unit * 2.25; + margin-bottom: $spacing-unit * 0.5; + font-weight: 600; + } + + h4 { + margin-top: $spacing-unit * 1.75; + margin-bottom: $spacing-unit * 0.25; + font-weight: 500; + } + + h5 { + margin-top: $spacing-unit * 1.25; + margin-bottom: $spacing-unit * 0.2; + font-weight: 500; + font-style: italic; + } + + h6 { + margin-top: $spacing-unit * 1.2; + margin-bottom: $spacing-unit * 0.1; + font-weight: 700; + font-style: italic; + } + + h1, h2, h3, h4, h5, h6 { + code { + font-weight: 400; + } + } + + h2 + h3, h3 + h4, h4 + h5, h5 + h6 { + margin-top: $spacing-unit * 0.5 !important; + } h2 { @include relative-font-size(1.75); @@ -202,6 +255,7 @@ h5 { @include relative-font-size(1.125); } + h6 { @include relative-font-size(1.0625); } @@ -261,3 +315,63 @@ width: calc(50% - (#{$spacing-unit} / 2)); } } + + +/** + * Advanced print formatting + */ +@media print { + @page { + margin: 1.5cm; + } + + body { + zoom: $print-scale; + } + + h2:not(:first-child), h3:not(:first-child):not( + h2 + h3, + h2 + p + h3, + h2 + p + p + h3, + h2 + p + p + p + h3, + h2 + p + p + p + p + h3, + h2 + p + p + p + p + p + h3, + h2 + .h2-summary + h3 + ) { + page-break-before: always; + } + + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + } + + img, code, .alert, .pill, .jobby-data-class { + page-break-inside: avoid; + } + + ul ul, ul ol, ol ul, ol ol { + page-break-before: avoid; + page-break-inside: avoid; + } + + .reset-page-break-defaults, .h2-summary { + &, & * { + page-break-before: auto !important; + page-break-inside: auto !important; + page-break-after: auto !important; + } + } + + .page-break, .page-break-after { + page-break-after: always !important; + } + + .page-break-before { + page-break-before: always !important; + } + + tr { + page-break-inside: avoid; + } + +} diff --git a/docs/_sass/minima/custom-styles.scss b/docs/_sass/minima/custom-styles.scss index 56b5d56b430..1da9573ec20 100644 --- a/docs/_sass/minima/custom-styles.scss +++ b/docs/_sass/minima/custom-styles.scss @@ -1,8 +1,15 @@ // Placeholder to allow defining custom styles that override everything else. // (Use `_sass/minima/custom-variables.scss` to override variable defaults) -h2, h3, h4, h5, h6 { +h2, h3, h4 { color: #e46c0a; } +h5 { + color: #a94325; +} +h6 { + color: $text-color; +} + // Bootstrap style alerts .alert { @@ -32,3 +39,142 @@ h2, h3, h4, h5, h6 { } } +// Custom basic overrides +.icon { + height: 21px; + width: 21px; +} + +hr { + opacity: 0.3; +} + +img.emoji { + height: 16px; + width: 16px; + vertical-align: baseline; +} + +// Syntax highlighting term overrides +// - Multiline +.language-sh, .language-bash, .language-shell { + .highlight code { + .nb { + /* parsing of this seems inconsistent */ + color: unset; + } + .nt { + /* do not wrap --flags */ + white-space: nowrap; + } + } +} +// - Inline +code.language-plaintext, code.language-sh, code.language-bash, code.language-shell { + .nt { + /* do not wrap --flags */ + white-space: nowrap; + } +} + +// Pill styles +.pill { + + border: 1px solid transparent; + border-radius: 16px; + padding: 2px 12px; + margin: 0 2px; + + background-color: #88888822; + + font-family: verdana; + font-size: 75%; + vertical-align: text-bottom; + + display: inline-block; + + .pill { + /* so nesting pills will look the same */ + font-size: 100%; + vertical-align: baseline; + } + + img.emoji { + height: 12px; + width: 12px; + vertical-align: unset; + margin: -2px 0; + } + + &.beginner { + background-color: #00cc0022; + border-color: #00cc00; + color: #008800; + } + + &.intermediate { + background-color: #ffa50022; + border-color: #cc8800; + color: #aa5500; + } + + &.expert { + background-color: #ff000022; + border-color: #cc0000; + color: #cc0000; + } + + &.learning-outcome { + background-color: #ffeb99; + color: #552800; + } + + &.information { + background-color: #33aaff22; + color: #3377aa; + } + + &.danger { + background-color: #cc3333; + color: #ffffff; + } + + &.warning { + background-color: #fdcc99; + color: #331800; + } + + &.applies-to { + background-color: #fdfbf9; + border-color: #e8e6e5; + color: #444444; + + padding-right: 2px; + margin: 4px 2px; + + &.jobby-data-class:not(:first-child) { + margin-left: 0; + } + + &::before { + content: "Applies to "; + } + } + + &.jobby-data-class { + border-color: #a8583333; + } +} + +// Jobby data class highlighting +.jobby-data-class { + background-color: #ff886633; + color: #633412; + + &:not(.pill) { + border-radius: 4px; + padding: 0 4px; + } +} + + diff --git a/docs/_sass/minima/initialize.scss b/docs/_sass/minima/initialize.scss index 30288811151..17f703a96d2 100644 --- a/docs/_sass/minima/initialize.scss +++ b/docs/_sass/minima/initialize.scss @@ -13,15 +13,20 @@ $spacing-unit: 30px !default; $table-text-align: left !default; +$print-scale: 0.8 !default; + // Width of the content area -$content-width: 800px !default; +$content-width: 1024px !default; $on-palm: 600px !default; -$on-laptop: 800px !default; +$on-laptop: 840px !default; $on-medium: $on-palm !default; $on-large: $on-laptop !default; +// Paper styling on large screens +$paper-box-shadow: 0 2px 6px 1px #00000018; + // Use media queries like this: // @include media-query($on-palm) { // .wrapper { diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5ec6976efa..cbe4bf58b7c 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -6,7 +6,4 @@ "minima/skins/{{ site.minima.skin | default: 'classic' }}", "minima/initialize"; -.icon { - height: 21px; - width: 21px -} + diff --git a/docs/diagrams/AddRecruiterActivityDiagram.puml b/docs/diagrams/AddRecruiterActivityDiagram.puml new file mode 100644 index 00000000000..08487b07961 --- /dev/null +++ b/docs/diagrams/AddRecruiterActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start + +:AddRecruiterCommand is executed; + +if () then ([oid value was passed in]) + + if () then ([id belongs to an organization]) + :Create new Recruiter with parent; + else ([else]) + :throw CommandException; + stop + endif + +else ([else]) + :Create new Recruiter with no parent; +endif + +:Add Recruiter to AddressBook; +:return CommandResult; + +stop + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index 48b6cc4333c..b5c4b8cce84 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -14,7 +14,7 @@ activate ui UI_COLOR ui -[UI_COLOR]> logic : execute("delete 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteContact(p) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic diff --git a/docs/diagrams/AutocompleteClasses.puml b/docs/diagrams/AutocompleteClasses.puml new file mode 100644 index 00000000000..dfed450f38b --- /dev/null +++ b/docs/diagrams/AutocompleteClasses.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.2 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +skinparam class { + 'workaround to render generics properly without breaking the rest + BorderColor LOGIC_COLOR + BorderThickness 0 + + StereotypeFontColor LOGIC_COLOR_T2 + StereotypeFontSize 14 +} + +'hidden boxes +class " " as HiddenOutside1 <> +class " " as HiddenOutside2 <> +skinparam class { + BorderColor<> #FFFFFF + BackgroundColor<> #FFFFFF + StereotypeFontColor<> #FFFFFF + StereotypeFontSize<> 1 +} + +'packages +package Autocomplete as AutocompletePackage <> { + class AutocompleteGenerator + class AutocompleteSupplier + + class "AutocompleteItemSet" as AutocompleteItemSet + class "<>\nAutocompleteConstraint" as AutocompleteConstraint + + class "<>\nFlagValueSupplier" as FlagValueSupplier + class PartitionedCommand +} + +package Model as ModelPackage { +} +package "Parser classes" <> { + class Flag +} + +'relationships +HiddenOutside1 .down.> AutocompleteGenerator +HiddenOutside2 .right.> AutocompleteSupplier + +AutocompleteGenerator -down-> "1" AutocompleteSupplier : uses > + +AutocompleteSupplier --> "1" AutocompleteItemSet +AutocompleteSupplier --> "*" FlagValueSupplier +AutocompleteSupplier ..> Flag + +AutocompleteItemSet --> "*" AutocompleteConstraint : contains > + +AutocompleteGenerator ..> PartitionedCommand : creates > +FlagValueSupplier .right.> PartitionedCommand : uses > + +FlagValueSupplier ..down.> ModelPackage : uses > +PartitionedCommand ..down.> Flag +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..ba3edcffabe 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,17 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList +AddressBook *-right-> "1" UniqueContactList AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +UniqueTagList -[hidden]down- UniqueContactList +UniqueTagList -[hidden]down- UniqueContactList UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniqueContactList -right-> Contact -Person -up-> "*" Tag +Contact -up-> "*" Tag + +Organization -up-|> Contact +Recruiter -up-|> Contact -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 40ea6c9dc4c..bc0fa839b53 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -49,7 +49,7 @@ deactivate AddressBookParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deleteContact(1) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index a57720890ee..920760fde92 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -6,7 +6,7 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic as LogicPackage { -Class AddressBookParser +Class AppParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command @@ -27,8 +27,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager -right->"1" AppParser +AppParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..5ec24fc0fdd 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -12,13 +12,13 @@ Class AddressBook Class ModelManager Class UserPrefs -Class UniquePersonList -Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag +Class UniqueContactList +Class "{abstract}\nContact" as Contact +Class Organization +Class Recruiter + +Class JobApplicationList +Class JobApplication Class I #FFFFFF } @@ -35,20 +35,16 @@ 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" UniqueContactList +UniqueContactList --> "~* all" Contact + +ModelManager *-down-> "1" JobApplicationList +JobApplicationList -down-> "*" JobApplication -Person -[hidden]up--> I -UniquePersonList -[hidden]right-> I +Contact -[hidden]up--> I -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Organization -up-|> Contact +Recruiter -up-|> Contact -ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Contact @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..07f01f1bfbf 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -9,30 +9,30 @@ Class XYZCommand package "Parser classes"{ Class "<>\nParser" as Parser -Class AddressBookParser +Class AppParser Class XYZCommandParser Class CliSyntax Class ParserUtil Class ArgumentMultimap Class ArgumentTokenizer -Class Prefix +Class Flag } Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +HiddenOutside ..> AppParser -AddressBookParser .down.> XYZCommandParser: creates > +AppParser .down.> XYZCommandParser: creates > XYZCommandParser ..> XYZCommand : creates > -AddressBookParser ..> Command : returns > +AppParser ..> Command : returns > XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer ArgumentTokenizer .left.> ArgumentMultimap XYZCommandParser ..> CliSyntax -CliSyntax ..> Prefix +CliSyntax ..> Flag XYZCommandParser ..> ParserUtil -ParserUtil .down.> Prefix -ArgumentTokenizer .down.> Prefix +ParserUtil .down.> Flag +ArgumentTokenizer .down.> Flag XYZCommand -up-|> Command @enduml diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..f6a5d7261f0 --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":SortCommandParser" as SortCommandParser LOGIC_COLOR +participant ":SortCommand" as SortCommand 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("sort --status") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("sort --status") +activate AddressBookParser + +create SortCommandParser +AddressBookParser -> SortCommandParser +activate SortCommandParser + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser + +AddressBookParser -> SortCommandParser : parse("--status") +activate SortCommandParser + +create SortCommand +SortCommandParser -> SortCommand +activate SortCommand + +SortCommand --> SortCommandParser +deactivate SortCommand + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> SortCommand : execute() +activate SortCommand + +SortCommand -> Model : updateSortedApplicationList(STATUS_COMPARATOR) +activate Model + +Model --> SortCommand +deactivate Model + +create CommandResult +SortCommand -> CommandResult +activate CommandResult + +CommandResult --> SortCommand +deactivate CommandResult + +SortCommand --> LogicManager : result +deactivate SortCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..457b68ee498 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -18,7 +18,8 @@ package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook -Class JsonAdaptedPerson +Class JsonAdaptedContact +Class JsonAdaptedApplication Class JsonAdaptedTag } @@ -37,7 +38,8 @@ Storage -right-|> AddressBookStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSerializableAddressBook --> "*" JsonAdaptedContact +JsonAdaptedContact --> "*" JsonAdaptedTag +JsonAdaptedContact --> "*" JsonAdaptedApplication @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..73a5e0b8ebd 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,10 +11,13 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class ContactListPanel +Class ContactCard +Class ApplicationListPanel +Class ApplicationCard Class StatusBarFooter Class CommandBox +Class AutocompleteTextField } package Model <> { @@ -32,26 +35,34 @@ UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" ContactListPanel +MainWindow *-down-> "1" ApplicationListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +ContactListPanel -down-> "*" ContactCard +ApplicationListPanel -down-> "*" ApplicationCard -MainWindow -left-|> UiPart +MainWindow --|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart +ContactListPanel --|> UiPart +ContactCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +ApplicationListPanel --|> UiPart +ApplicationCard --|> UiPart -PersonCard ..> Model +CommandBox -down--> "1" AutocompleteTextField + +ContactCard ..> Model +ApplicationCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow +ContactListPanel -[hidden]left- HelpWindow +ApplicationListPanel -[hidden]left- ContactListPanel HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/apply-command/ApplyCommand.puml b/docs/diagrams/apply-command/ApplyCommand.puml new file mode 100644 index 00000000000..e156fb81cbb --- /dev/null +++ b/docs/diagrams/apply-command/ApplyCommand.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AppParser" as AddressBookParser LOGIC_COLOR +participant ":ApplyCommandParser" as ApplyCommandParser LOGIC_COLOR +participant ":ApplyCommand" as ApplyCommand 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("apply 1 --title SWE") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("apply 1 --title SWE") +activate AddressBookParser + +create ApplyCommandParser +AddressBookParser -> ApplyCommandParser +activate ApplyCommandParser + +ApplyCommandParser --> AddressBookParser +deactivate ApplyCommandParser + +AddressBookParser -> ApplyCommandParser : parse(" --title SWE") +activate ApplyCommandParser + +create ApplyCommand +ApplyCommandParser -> ApplyCommand +activate ApplyCommand + +ApplyCommand --> ApplyCommandParser : +deactivate ApplyCommand + +ApplyCommandParser --> AddressBookParser : +deactivate ApplyCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ApplyCommandParser -[hidden]-> AddressBookParser +destroy ApplyCommandParser + +AddressBookParser --> LogicManager : +deactivate AddressBookParser + +LogicManager -> ApplyCommand : execute() +activate ApplyCommand + +ApplyCommand -> Model : addJobApplication() +activate Model + +Model --> ApplyCommand +deactivate Model + +create CommandResult +ApplyCommand -> CommandResult +activate CommandResult + +CommandResult --> ApplyCommand +deactivate CommandResult + +ApplyCommand --> LogicManager : result +deactivate ApplyCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/apply-command/style.puml b/docs/diagrams/apply-command/style.puml new file mode 100644 index 00000000000..f7d7347ae84 --- /dev/null +++ b/docs/diagrams/apply-command/style.puml @@ -0,0 +1,79 @@ +/' + 'Commonly used styles and colors across diagrams. + 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more + 'comprehensive list of skinparams. + '/ + + +'T1 through T4 are shades of the original color from lightest to darkest + +!define UI_COLOR #1D8900 +!define UI_COLOR_T1 #83E769 +!define UI_COLOR_T2 #3FC71B +!define UI_COLOR_T3 #166800 +!define UI_COLOR_T4 #0E4100 + +!define LOGIC_COLOR #3333C4 +!define LOGIC_COLOR_T1 #C8C8FA +!define LOGIC_COLOR_T2 #6A6ADC +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +!define MODEL_COLOR #9D0012 +!define MODEL_COLOR_T1 #F97181 +!define MODEL_COLOR_T2 #E41F36 +!define MODEL_COLOR_T3 #7B000E +!define MODEL_COLOR_T4 #51000A + +!define STORAGE_COLOR #A38300 +!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR_T2 #EDC520 +!define STORAGE_COLOR_T3 #806600 +!define STORAGE_COLOR_T2 #544400 + +!define USER_COLOR #000000 + +skinparam Package { + BackgroundColor #FFFFFF + BorderThickness 1 + FontSize 16 +} + +skinparam Class { + FontColor #FFFFFF + FontSize 15 + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #FFFFFF + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFFF + Padding 20 +} + +skinparam ArrowFontStyle bold +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +hide footbox +hide members +hide circle diff --git a/docs/diagrams/enhancements/FlagChecker.puml b/docs/diagrams/enhancements/FlagChecker.puml new file mode 100644 index 00000000000..cdd1e0f8df9 --- /dev/null +++ b/docs/diagrams/enhancements/FlagChecker.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AppParser" as AddressBookParser LOGIC_COLOR +participant ":EditCommandParser" as EditCommandParser LOGIC_COLOR +end box + +[-> LogicManager : execute("edit --application 1 --name Jay --title SWE") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit --application 1 --name Jay --title SWE") +activate AddressBookParser + +create EditCommandParser +AddressBookParser -> EditCommandParser +activate EditCommandParser + +EditCommandParser --> AddressBookParser +deactivate EditCommandParser + +AddressBookParser -> EditCommandParser : parse("--application 1 --name Jay --title SWE") +activate EditCommandParser + +EditCommandParser -> EditCommandParser : validateFlags() + +EditCommandParser --> AddressBookParser : d +deactivate EditCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCommandParser -[hidden]-> AddressBookParser +destroy EditCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/enhancements/style.puml b/docs/diagrams/enhancements/style.puml new file mode 100644 index 00000000000..f7d7347ae84 --- /dev/null +++ b/docs/diagrams/enhancements/style.puml @@ -0,0 +1,79 @@ +/' + 'Commonly used styles and colors across diagrams. + 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more + 'comprehensive list of skinparams. + '/ + + +'T1 through T4 are shades of the original color from lightest to darkest + +!define UI_COLOR #1D8900 +!define UI_COLOR_T1 #83E769 +!define UI_COLOR_T2 #3FC71B +!define UI_COLOR_T3 #166800 +!define UI_COLOR_T4 #0E4100 + +!define LOGIC_COLOR #3333C4 +!define LOGIC_COLOR_T1 #C8C8FA +!define LOGIC_COLOR_T2 #6A6ADC +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +!define MODEL_COLOR #9D0012 +!define MODEL_COLOR_T1 #F97181 +!define MODEL_COLOR_T2 #E41F36 +!define MODEL_COLOR_T3 #7B000E +!define MODEL_COLOR_T4 #51000A + +!define STORAGE_COLOR #A38300 +!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR_T2 #EDC520 +!define STORAGE_COLOR_T3 #806600 +!define STORAGE_COLOR_T2 #544400 + +!define USER_COLOR #000000 + +skinparam Package { + BackgroundColor #FFFFFF + BorderThickness 1 + FontSize 16 +} + +skinparam Class { + FontColor #FFFFFF + FontSize 15 + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #FFFFFF + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFFF + Padding 20 +} + +skinparam ArrowFontStyle bold +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +hide footbox +hide members +hide circle diff --git a/docs/diagrams/enhancements/warn.puml b/docs/diagrams/enhancements/warn.puml new file mode 100644 index 00000000000..3ed12b2a179 --- /dev/null +++ b/docs/diagrams/enhancements/warn.puml @@ -0,0 +1,54 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AppParser" as AddressBookParser LOGIC_COLOR +participant ":ClearCommand" as ClearCommand LOGIC_COLOR +participant ":WarnMessageGenerator" as WarnCommand LOGIC_COLOR +end box + +[-> LogicManager : execute("clear") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("clear") +activate AddressBookParser + +create ClearCommand +AddressBookParser -> ClearCommand +activate ClearCommand + +ClearCommand --> AddressBookParser +deactivate ClearCommand +'Hidden arrow to position the destroy marker below the end of the activation bar. +ClearCommand -[hidden]-> AddressBookParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> WarnCommand : generateWarningMessage() +activate WarnCommand + +WarnCommand --> LogicManager : warningMessage + +deactivate WarnCommand + + +[<--LogicManager +deactivate LogicManager + +[-> LogicManager : execute("yes") +activate LogicManager + +LogicManager -> ClearCommand : execute() +activate ClearCommand +ClearCommand --> LogicManager : result +deactivate ClearCommand +ClearCommand -[hidden]-> LogicManager +[<-- LogicManager +deactivate LogicManager + + +destroy ClearCommand +@enduml diff --git a/docs/diagrams/enhancements/warn_alt.puml b/docs/diagrams/enhancements/warn_alt.puml new file mode 100644 index 00000000000..ba346d0c169 --- /dev/null +++ b/docs/diagrams/enhancements/warn_alt.puml @@ -0,0 +1,52 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AppParser" as AddressBookParser LOGIC_COLOR +participant ":ClearCommand" as ClearCommand LOGIC_COLOR +end box + +[-> LogicManager : execute("clear") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("clear") +activate AddressBookParser + +create ClearCommand +AddressBookParser -> ClearCommand +activate ClearCommand + +ClearCommand --> AddressBookParser +deactivate ClearCommand +'Hidden arrow to position the destroy marker below the end of the activation bar. +ClearCommand -[hidden]-> AddressBookParser + +AddressBookParser -> AddressBookParser : generateWarningCommand() + +AddressBookParser --> LogicManager : warningMessage +deactivate AddressBookParser + +[<--LogicManager +deactivate LogicManager + +[-> LogicManager : execute("yes") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("yes") +activate AddressBookParser +AddressBookParser --> LogicManager : clearCommand +deactivate AddressBookParser + +LogicManager -> ClearCommand : execute() +activate ClearCommand +ClearCommand --> LogicManager : result +deactivate ClearCommand +ClearCommand -[hidden]-> LogicManager +[<-- LogicManager +deactivate LogicManager + + +destroy ClearCommand +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..7770580f0b7 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -31,6 +31,12 @@ !define STORAGE_COLOR_T3 #806600 !define STORAGE_COLOR_T2 #544400 +!define AUTOCOMPLETE_COLOR #3333C4 +!define AUTOCOMPLETE_COLOR_T1 #C8C8FA +!define AUTOCOMPLETE_COLOR_T2 #6A6ADC +!define AUTOCOMPLETE_COLOR_T3 #1616B0 +!define AUTOCOMPLETE_COLOR_T4 #101086 + !define USER_COLOR #000000 skinparam Package { diff --git a/docs/diagrams/tracing/LogicSequenceDiagram.puml b/docs/diagrams/tracing/LogicSequenceDiagram.puml index 42bf46d3ce8..5eb54978456 100644 --- a/docs/diagrams/tracing/LogicSequenceDiagram.puml +++ b/docs/diagrams/tracing/LogicSequenceDiagram.puml @@ -14,7 +14,7 @@ create ecp abp -> ecp abp -> ecp ++: parse(arguments) create ec -ecp -> ec ++: index, editPersonDescriptor +ecp -> ec ++: index, editContactDescriptor ec --> ecp -- ecp --> abp --: command abp --> logic --: command diff --git a/docs/images/AddRecruiterActivityDiagram.png b/docs/images/AddRecruiterActivityDiagram.png new file mode 100644 index 00000000000..f514f85e064 Binary files /dev/null and b/docs/images/AddRecruiterActivityDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 37ad06a2803..50b47739fbc 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/AutocompleteClasses.png b/docs/images/AutocompleteClasses.png new file mode 100644 index 00000000000..28787acc40c Binary files /dev/null and b/docs/images/AutocompleteClasses.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..13ebd9859ee 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index e3b784310fe..d27e6627eb1 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..f0e24cd9e4e 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 edfd1ff7897..4cdb2ed4dd6 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..c05d66160e4 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..88695b6421b 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..9dd8e2514f3 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/add_recruiter_success.png b/docs/images/add_recruiter_success.png new file mode 100644 index 00000000000..85cea932692 Binary files /dev/null and b/docs/images/add_recruiter_success.png differ diff --git a/docs/images/apply-command/ApplyCommand.png b/docs/images/apply-command/ApplyCommand.png new file mode 100644 index 00000000000..42c3bf1131b Binary files /dev/null and b/docs/images/apply-command/ApplyCommand.png differ diff --git a/docs/images/autocomplete.png b/docs/images/autocomplete.png new file mode 100644 index 00000000000..8484abc1b65 Binary files /dev/null and b/docs/images/autocomplete.png differ diff --git a/docs/images/cj-lee01.png b/docs/images/cj-lee01.png new file mode 100644 index 00000000000..9ee64c59990 Binary files /dev/null and b/docs/images/cj-lee01.png differ diff --git a/docs/images/enhancements/FlagChecker.png b/docs/images/enhancements/FlagChecker.png new file mode 100644 index 00000000000..def4f89cf7e Binary files /dev/null and b/docs/images/enhancements/FlagChecker.png differ diff --git a/docs/images/enhancements/formatting.png b/docs/images/enhancements/formatting.png new file mode 100644 index 00000000000..6b2e7335f8c Binary files /dev/null and b/docs/images/enhancements/formatting.png differ diff --git a/docs/images/enhancements/warn.png b/docs/images/enhancements/warn.png new file mode 100644 index 00000000000..830647c8ce7 Binary files /dev/null and b/docs/images/enhancements/warn.png differ diff --git a/docs/images/enhancements/warn_alt.png b/docs/images/enhancements/warn_alt.png new file mode 100644 index 00000000000..fbfea03c2f1 Binary files /dev/null and b/docs/images/enhancements/warn_alt.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..8ff69f0812b 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/mcnabry.png b/docs/images/mcnabry.png new file mode 100644 index 00000000000..7cecb180ef8 Binary files /dev/null and b/docs/images/mcnabry.png differ diff --git a/docs/images/sort-command/SortCommand.png b/docs/images/sort-command/SortCommand.png new file mode 100644 index 00000000000..0585ce95f85 Binary files /dev/null and b/docs/images/sort-command/SortCommand.png differ diff --git a/docs/images/sort_deadline.png b/docs/images/sort_deadline.png new file mode 100644 index 00000000000..1876f919282 Binary files /dev/null and b/docs/images/sort_deadline.png differ diff --git a/docs/images/starter-guide/add-application.jpg b/docs/images/starter-guide/add-application.jpg new file mode 100644 index 00000000000..45f2b5629e2 Binary files /dev/null and b/docs/images/starter-guide/add-application.jpg differ diff --git a/docs/images/starter-guide/add-john.jpg b/docs/images/starter-guide/add-john.jpg new file mode 100644 index 00000000000..7705e50ea9b Binary files /dev/null and b/docs/images/starter-guide/add-john.jpg differ diff --git a/docs/images/starter-guide/add-woogle.jpg b/docs/images/starter-guide/add-woogle.jpg new file mode 100644 index 00000000000..d0270036e4f Binary files /dev/null and b/docs/images/starter-guide/add-woogle.jpg differ diff --git a/docs/images/starter-guide/delete-recursive.jpg b/docs/images/starter-guide/delete-recursive.jpg new file mode 100644 index 00000000000..f42b96512a3 Binary files /dev/null and b/docs/images/starter-guide/delete-recursive.jpg differ diff --git a/docs/images/starter-guide/edit woogle.jpg b/docs/images/starter-guide/edit woogle.jpg new file mode 100644 index 00000000000..7039ce95f5b Binary files /dev/null and b/docs/images/starter-guide/edit woogle.jpg differ diff --git a/docs/images/starter-guide/edit-application.jpg b/docs/images/starter-guide/edit-application.jpg new file mode 100644 index 00000000000..9ec1306f6fe Binary files /dev/null and b/docs/images/starter-guide/edit-application.jpg differ diff --git a/docs/images/starter-guide/edit-john.jpg b/docs/images/starter-guide/edit-john.jpg new file mode 100644 index 00000000000..6c8f093ee93 Binary files /dev/null and b/docs/images/starter-guide/edit-john.jpg differ diff --git a/docs/images/starter-guide/find-woogle.jpg b/docs/images/starter-guide/find-woogle.jpg new file mode 100644 index 00000000000..ba533eaf8c2 Binary files /dev/null and b/docs/images/starter-guide/find-woogle.jpg differ diff --git a/docs/images/starter-guide/initial-ui.jpg b/docs/images/starter-guide/initial-ui.jpg new file mode 100644 index 00000000000..4742a31eabe Binary files /dev/null and b/docs/images/starter-guide/initial-ui.jpg differ diff --git a/docs/images/starter-guide/list.jpg b/docs/images/starter-guide/list.jpg new file mode 100644 index 00000000000..1aa530deb23 Binary files /dev/null and b/docs/images/starter-guide/list.jpg differ diff --git a/docs/images/starter-guide/remind-earliest.jpg b/docs/images/starter-guide/remind-earliest.jpg new file mode 100644 index 00000000000..c92f8611d74 Binary files /dev/null and b/docs/images/starter-guide/remind-earliest.jpg differ diff --git a/docs/images/tanshiyu1999.png b/docs/images/tanshiyu1999.png new file mode 100644 index 00000000000..894f9e6b6b5 Binary files /dev/null and b/docs/images/tanshiyu1999.png differ diff --git a/docs/images/ug-images/app-added.png b/docs/images/ug-images/app-added.png new file mode 100644 index 00000000000..7b47ca335d8 Binary files /dev/null and b/docs/images/ug-images/app-added.png differ diff --git a/docs/images/ug-images/labelled-gui.png b/docs/images/ug-images/labelled-gui.png new file mode 100644 index 00000000000..54cf7cf5d25 Binary files /dev/null and b/docs/images/ug-images/labelled-gui.png differ diff --git a/docs/images/ug-images/org-added.png b/docs/images/ug-images/org-added.png new file mode 100644 index 00000000000..d52d4700d8c Binary files /dev/null and b/docs/images/ug-images/org-added.png differ diff --git a/docs/images/ug-images/rec-added.png b/docs/images/ug-images/rec-added.png new file mode 100644 index 00000000000..1a5076c11c8 Binary files /dev/null and b/docs/images/ug-images/rec-added.png differ diff --git a/docs/images/wamps-jp.png b/docs/images/wamps-jp.png new file mode 100644 index 00000000000..f43fb34dc46 Binary files /dev/null and b/docs/images/wamps-jp.png differ diff --git a/docs/images/johndoe.png b/docs/images/wxwern.png similarity index 100% rename from docs/images/johndoe.png rename to docs/images/wxwern.png diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..16bbf079408 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,23 @@ --- layout: page -title: AddressBook Level-3 +title: Jobby --- -[![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) +
-![Ui](images/Ui.png) +CI Status +Code Coverage +
+Ui -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +
-* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +**Jobby is a desktop application for managing your job application details, specifically organization and recruiter contacts, plus application info and status.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). + +* If you are interested in using Jobby, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing Jobby, 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) +Refer to the [Developer Guide's Acknowledgements section](DeveloperGuide.html#acknowledgements). diff --git a/docs/team/cj-lee01.md b/docs/team/cj-lee01.md new file mode 100644 index 00000000000..6db280b94d9 --- /dev/null +++ b/docs/team/cj-lee01.md @@ -0,0 +1,62 @@ +--- +layout: page +title: Chun Jie's Project Portfolio Page +--- + +
+ +### Project: Jobby + +Jobby is a desktop address book and job application tracking tool. The user interacts with it using a CLI, and it has a GUI created in JavaFX. It is written in Java. + +Given below are my contributions to the project. + +* **New Feature**: Apply command + * What it does: Allows users to add job applications. + * Justification: For job application tracking, since one person can apply to multiple positions within a company, a separate class is required for job applications. + * Highlights: Created necessary classes for storing information related to job applications. + +* **New Feature**: UI for showing job applications + * What it does: Allows users to view their job applications. + * Justification: To let them see what job applications they have added. + +* **New Feature**: Storage of job applications + * What it does: Stores information on job applications in JSON file. + * Justification: For users to save their job applications. + * Highlights: Decided to store the job applications instead the organization JSON to ensure that the job application is truly associated to it. + +* **New Feature**: Editing of job applications + * What it does: Allows users to edit job applications. + * Justification: In case their job application status changes or they made a mistake while adding the job application. + +* **New Feature**: Deleting of job applications + * What it does: Allows users to delete job applications. + * Justification: Allows users to delete old job applications. + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=AY2324S1-CS2103T-W08-3&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=CJ-Lee01&tabRepo=AY2324S1-CS2103T-W08-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~other~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Identify and allocate work among team: E.g. sections of UG, DG, bugs to fix + +* **Enhancements to existing features**: Delete recursive + * Allows users to delete recruiters linked to an organization when deleting the organization. + * This is due to the parent-child relationship between recruiters and organization. + + +* **Documentation**: + * User Guide: + * Apply, Edit applications and delete command. + * Enhancements to Introduction section and Navigating the Guide section. + * Developer Guide: + * Implementation details for apply command, added planned enhancements + * Add more user stories + * Add more use cases + * Add some non-functional requirements + * Updating manual testing guide + +* **Community**: + * Contributed to forum discussions (examples: #371) + * Reviewed PRs: #39, #42, #60, #67, #99, #147, #150, + +
diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -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. - -Given below are my contributions to the project. - -* **New Feature**: Added the ability to undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/mcnabry.md b/docs/team/mcnabry.md new file mode 100644 index 00000000000..a3800d7fbf1 --- /dev/null +++ b/docs/team/mcnabry.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Bryan's Project Portfolio Page +--- + +
+ +### Project: Jobby + +Jobby is a desktop app for managing job applications and contacts. It can help you manage the tracking of your job applications and contacts in a more streamlined fashion. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to create contacts for recruiters in the AddressBook. + * What it does: Adds a way for the user to track recruiters' contacts. + * Justification: Students would need to track the recruiters that recruited them for the internship application. + +* **New Feature**: Modified certain fields in organization and recruiter contacts to be optional. + * What it does: Users are given the option to leave certain fields blank when adding new organization and recruiter contacts. + * Justification: Users might not have all the information and might not want to specify every detail when creating a new contact. Hence, this improves the user experience by making certain fields optional. + +* **New Feature**: Added the ability to associate recruiters with organizations. + * What it does: Users are able to link recruiters to other organizations added to the AddressBook. + * Justification: Recruiters are associated with the organization they work for. Giving users the ability to store this relationship in the AddressBook improves the product's functionality in managing contacts. + * Highlights: Given how AB3 was structured, implementing this association was a challenge. There was a struggle between loosely coupling or creating a stronger relationship between the two types of contacts in code. Going with the latter resulted in overhauls to the command's execution and storing of contacts in json. Proper care also had to be taken when details of the contacts changed as the association had to be updated to ensure that the contacts were linked to the latest versions of each other. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=AY2324S1-CS2103T-W08-3&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=McNaBry&tabRepo=AY2324S1-CS2103T-W08-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~other~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Forked the team repo and set up team organization. + * Managed release [v1.2.1](https://github.com/AY2324S1-CS2103T-W08-3/tp/releases/tag/v1.2.1) on GitHub. + * Contributed to refactoring `Person` to `Contact` class: [#27](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/27) + * Renamed `person` package to `contact`: [#56](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/56) + +* **Documentation**: + * User Guide: + * Added documentation for adding recruiter: [#165](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/165) + * Added glossary and an appendix detailing acceptable features for the command parameters [#163](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/163) + * Developer Guide: + * Added documentation on the Organization-Recruiter link: [#173](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/173) + * Added appendix on effort taken to evolve AB3 to Jobby: [#190](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/190) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [#7](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/7), [#164](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/164) + * Reported [bugs and suggestions](https://github.com/McNaBry/ped/issues) for other teams. + +
diff --git a/docs/team/tanshiyu1999.md b/docs/team/tanshiyu1999.md new file mode 100644 index 00000000000..b34d8282ef9 --- /dev/null +++ b/docs/team/tanshiyu1999.md @@ -0,0 +1,39 @@ +
+ +### Project: Jobby + +Jobby is a desktop address book and job application tracking tool. The user interacts with it using a CLI, and it has a GUI created in JavaFX. It is written in Java. + +Given below are my contributions to the project. + +* **New Feature**: Add Organization + * What it does: This adds an organization contact into Jobby + * Justification: We want our Jobby application to be able to track both Organization Contact and Recruiter Contact with different parameters, hence we added different functions in order to add these 2 different type of contacts. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=AY2324S1-CS2103T-W08-3&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=tanshiyu1999&tabRepo=AY2324S1-CS2103T-W08-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~other~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Forked the team repo. + +* **Enhancements to existing features**: Modified find command + * What it does: Find now is able to find substring instead of the entire keyword as specified by AB3 + * Justification: The initial find function can only look for the keyword if it is we input the entire keyword. Which is unrealistic as users might not be able to remember the entire keyword. Hence, being able to look up a substring is useful. + +* **Enhancements to existing features**: Modified edit command + * What it does: Edit target can be selected via both index and the substring for name. + * Justification: Initially, edit only works via selection by index. However, we want it to be flexible enough that we can select the target by its name, hence this is being implemented. + +* **Documentation**: + * User Guide: + * Added documentation for Add Organization in the User Guide. + * Created the starter guide for the documentation. + * Help to create GUI Breakdown, and edit the UG for find, edit, apply and delete. + * Developer Guide: + * Documented the implementation of Add Organization into the Developer Guide. + * Added more user stories. + +* **Community**: + * Reported bugs and suggestions for other teams in the class (examples: During PE-D) + * Reviewed PR from Teammates + +
diff --git a/docs/team/wamps-jp.md b/docs/team/wamps-jp.md new file mode 100644 index 00000000000..67d5b1ddb4e --- /dev/null +++ b/docs/team/wamps-jp.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Juanpa's Project Portfolio Page +--- + +
+ +### Project: Jobby + +Jobby is a desktop app for tracking job applications. It saves job contacts and applications in an addressbook, which the user can view with a GUI, and interact wtih using a CLI. + +Given below are my contributions to the project. + +* **New Feature**: Sort command + * What it does: Allows users to sort contacts and job applications. + * Justification: For reference quality of life, since a job applicant can have many applications and contacts. Makes it easier to view relevant data. + * Highlights: Created necessary logic for data to be sorted. + +* **New Feature**: Remind command + * What it does: Allows users to get a reminder of upcoming or future deadlines. + * Justification: Since deadlines can be hard to keep track of, this helps simplify it. Urgent or future deadlines can be viewed easily. + * Highlights: Created necessary logic for reminders. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=AY2324S1-CS2103T-W08-3&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=wamps-jp&tabRepo=AY2324S1-CS2103T-W08-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~other~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + * Helped plan out timeframes for the project. + +* **Enhancements to existing features**: List command + * Allows users to list organizations, recruiters, or organizations with no applications, instead of just all contacts. + +* **Documentation**: + * User Guide: + * List, sort, and reminder command. + * Enhancements to Issues section. + * Developer Guide: + * Implementation details for sort command. + * Added more use cases. + +* **Community**: + * Reviewed PRs: [#20](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/20), [#27](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/27), [#32](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/32), [#46](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/46), [#96](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/96), [#101](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/101) + +
diff --git a/docs/team/wxwern.md b/docs/team/wxwern.md new file mode 100644 index 00000000000..24a0d84732a --- /dev/null +++ b/docs/team/wxwern.md @@ -0,0 +1,70 @@ +--- +layout: page +title: Wern's Project Portfolio Page +--- + +
+ +### Project: Jobby + +Jobby is a desktop application used for managing your job applications and associated organization and recruiter contacts. + +Given below are my contributions to the project. + +* **New Feature**: Command Autocompletion ([PR #63](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/63)) + + * This allows users to autocomplete their commands, just like command terminals or programming IDEs, such as by pressing **TAB**, and even undo when you **BACKSPACE**. + + * Like an IDE, it does a _subsequence_ match, so typing `-dsp` then **TAB** can select `--description`, allowing for fast disambiguation from another similar term like `--descending`. + + * **Rationale:** It allows for faster command input, and reduces command syntax memorization. + + * **Implementation:** The internal implementation is written completely from scratch to support our custom formats - more information available in the Developer Guide. + + * **UI:** The autocompletion UI is adapted from [@floralvikings's AutoCompleteTextBox.java](https://gist.github.com/floralvikings/10290131). The reference text box only has a barebones context menu, but significant enhancements has been made to the styling and behavior to improve readability and UX (like partial term highlighting and undo support), which can be seen when using Jobby. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=AY2324S1-CS2103T-W08-3&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=wxwern&tabRepo=AY2324S1-CS2103T-W08-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~other~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Project management**: + + * Configuring the Issue Tracker Tags and Milestones for the project. + * Setting up GitHub Actions for the project, including Java CI, CodeCov, GitHub Pages, PR Checks. + * Managed releases [v1.2 and v1.3](https://github.com/AY2324S1-CS2103T-W08-3/tp/releases). + * Styling the website for optimal readability and print formatting, including: + + * Styling headers with improved spacing, typography and color for increased readability. + + * Tweaking the page-break rules for different elements, such as preventing page breaks on crucial boxes or enforcing page breaks immediately after certain headers. + + * Styling custom unified UG elements, like the following: + * :trophy: How to perform a task :information_source: An info pill :warning: A warning pill :warning: A danger pill + * Organization Recruiter Job Application + * Beginner Intermediate Expert

+ +* **Enhancements to existing features**: + + * Revamped the parameter syntax to use a prefix of `--param`. + * This allows for improved autocompletion UX as compared to `param/`, since we can immediately determine if the user intends to type a parameter based on the first character. + * It is also much less likely to clash with an existing user input.

+ * Swapped out random ID generation with an implementation to derive IDs from an input name. + * This allows for improved UX when editing details that require an ID, combined with autocomplete integration. + * e.g., `google-sg-a8b3fe` can be derived from an input of `Google SG`.

+ +* **Documentation**: + + * User Guide: + * Added a structured command syntax introduction, and instructions to interpret command formats. + * Added usage guides for command autocompletion. + * Styling the website for improved overall readability and automated print formatting (see above in Project Management).

+ * Developer Guide: + * Integrated explanations of how "Autocomplete classes" work in the context of the `Logic` package. + * Updated how `AppParser` (formerly `AddressBookParser`) operates in the context of our app, since we now dynamically look up details and also support autocompletion. + * Added a complete high-level explanation of Jobby's internal autocomplete implementation. + * Added use cases for autocompletion.

+ +* **Community**: + * Detailed PR Reviews (e.g., [#32](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/32), [#34](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/34), [#39](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/39), [#69](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/69), [#183](https://github.com/AY2324S1-CS2103T-W08-3/tp/pull/183)) + * Forum contributions (e.g., [#30](https://github.com/nus-cs2103-AY2324S1/forum/issues/30), [#103](https://github.com/nus-cs2103-AY2324S1/forum/issues/103)) + * Reported bugs and suggestions for other teams (e.g., during PE-D) + +
diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index d98f38982e7..3f0a73c4c0f 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -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 contact in the address book. */ public class RemarkCommand extends Command { @@ -65,8 +65,8 @@ Following the convention in other commands, we add relevant messages as constant ``` java public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Edits the remark of the person identified " - + "by the index number used in the last person listing. " + + ": Edits the remark of the contact identified " + + "by the index number used in the last contact listing. " + "Existing remark will be overwritten by the input.\n" + "Parameters: INDEX (must be a positive integer) " + "r/ [REMARK]\n" @@ -101,8 +101,8 @@ public class RemarkCommand extends Command { private final String remark; /** - * @param index of the person in the filtered person list to edit the remark - * @param remark of the person to be updated to + * @param index of the contact in the filtered contact list to edit the remark + * @param remark of the contact to be updated to */ public RemarkCommand(Index index, String remark) { requireAllNonNull(index, remark); @@ -151,27 +151,27 @@ Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. ``` java /** * Tokenizes an arguments string and returns an {@code ArgumentMultimap} - * object that maps prefixes to their respective argument values. Only the - * given prefixes will be recognized in the arguments string. + * object that maps flags to their respective argument values. Only the + * given flags will be recognized in the arguments string. * * @param argsString Arguments string of the form: - * {@code preamble value value ...} - * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their + * {@code preamble value value ...} + * @param flags Prefixes to tokenize the arguments string with + * @return ArgumentMultimap object that maps flags to their * arguments */ ``` -We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` and it will return us an instance of `ArgumentMultimap`. Now let’s find out what we need to do in order to obtain the Index and String that we need. Let’s look through `ArgumentMultimap` : +We can tell `ArgumentTokenizer#tokenize()` to look out for our new flag `r/` and it will return us an instance of `ArgumentMultimap`. Now let’s find out what we need to do in order to obtain the Index and String that we need. Let’s look through `ArgumentMultimap` : **`ArgumentMultimap.java`:** ``` java /** - * Returns the last value of {@code prefix}. + * Returns the last value of {@code flag}. */ -public Optional getValue(Prefix prefix) { - List values = getAllValues(prefix); +public Optional getValue(Prefix flag) { + List values = getAllValues(flag); return values.isEmpty() ? Optional.empty() : Optional.of(values.get(values.size() - 1)); } @@ -223,11 +223,11 @@ If you are stuck, check out the sample ## Add `Remark` to the model -Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person. +Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of contact data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the contact’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a contact. ### 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.contact`. 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. @@ -238,9 +238,9 @@ Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` ## Add a placeholder element for remark to the UI -Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each person. +Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each contact. -Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). +Simply add the following to [`seedu.address.ui.ContactCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). **`PersonCard.java`:** @@ -293,7 +293,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/jobby.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
@@ -309,9 +309,9 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c **`PersonCard.java`:** ``` java -public PersonCard(Person person, int displayedIndex) { +public PersonCard(Person contact, int displayedIndex) { //... - remark.setText(person.getRemark().value); + remark.setText(contact.getRemark().value); } ``` @@ -335,31 +335,31 @@ save it with `Model#setPerson()`. //... @Override public CommandResult execute(Model model) throws CommandException { - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getDisplayedContactList(); if (index.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = new Person( - personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), - personToEdit.getAddress(), remark, personToEdit.getTags()); + Person contactToEdit = lastShownList.get(index.getZeroBased()); + Person editedContact = new Person( + contactToEdit.getName(), contactToEdit.getPhone(), contactToEdit.getEmail(), + contactToEdit.getAddress(), remark, contactToEdit.getTags()); - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.setPerson(contactToEdit, editedContact); + model.updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); - return new CommandResult(generateSuccessMessage(editedPerson)); + return new CommandResult(generateSuccessMessage(editedContact)); } /** * Generates a command execution success message based on whether * the remark is added to or removed from - * {@code personToEdit}. + * {@code contactToEdit}. */ - private String generateSuccessMessage(Person personToEdit) { + private String generateSuccessMessage(Person contactToEdit) { String message = !remark.value.isEmpty() ? MESSAGE_ADD_REMARK_SUCCESS : MESSAGE_DELETE_REMARK_SUCCESS; - return String.format(message, personToEdit); + return String.format(message, contactToEdit); } ``` diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..4a7c86bb3ef 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.contact.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) @@ -100,7 +100,7 @@ In `src/test/data/`, data meant for testing purposes are stored. While keeping t ```json { - "persons": [ { + "contacts": [ { "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..7ae6741d0fc 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -189,22 +189,22 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ @Override public CommandResult execute(Model model) throws CommandException { ... - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + Person contactToEdit = lastShownList.get(index.getZeroBased()); + Person editedContact = createEditedPerson(contactToEdit, editPersonDescriptor); + if (!contactToEdit.isSamePerson(editedContact) && model.hasContact(editedContact)) { 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)); + model.setPerson(contactToEdit, editedContact); + model.updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedContact)); } ``` 1. As suspected, `command#execute()` does indeed make changes to the `model` object. Specifically, - * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the person data. - * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
- FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
- To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked. + * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the contact data. + * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ contacts.
+ FYI, The 'filtered list' is the list of contacts resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the contacts so that the user can see the edited contact along with all other contacts. If this was a `find` command, we would be setting that list to contain the search results instead.
+ To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of contacts is being tracked.
* :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) @@ -231,7 +231,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ * {@code JsonSerializableAddressBook}. */ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll( + contacts.addAll( source.getPersonList() .stream() .map(JsonAdaptedPerson::new) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..77a90798a34 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 8cf8e15a0f0..78aad4016c4 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -20,7 +20,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "jobby.log"; private static final Logger logger; // logger for this class private static Logger baseLogger; // to be used as the parent of all other loggers created by this class. private static Level currentLogLevel = Level.INFO; diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/address/commons/core/Version.java index 491d24559b4..35323cf6808 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/address/commons/core/Version.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonValue; /** - * Represents a version with major, minor and patch number + * Represents a version with major, minor and patch number. */ public class Version implements Comparable { diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java b/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java new file mode 100644 index 00000000000..ea12750e5ad --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java @@ -0,0 +1,21 @@ +package seedu.address.commons.exceptions; + +/** + * Exception thrown when attempting to make illegal. + */ +public class IllegalOperationException extends Exception { + /** + * @param message that informs the user that it has attempted an illegal operation. + */ + public IllegalOperationException(String message) { + super(message); + } + + /** + * @param message that informs the user that it has attempted an illegal operation. + * @param cause of the main exception. + */ + public IllegalOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/address/commons/util/EnumUtil.java b/src/main/java/seedu/address/commons/util/EnumUtil.java new file mode 100644 index 00000000000..4c0b6166592 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/EnumUtil.java @@ -0,0 +1,46 @@ +package seedu.address.commons.util; + +import java.util.Arrays; + +/** + * Helper class to for enums to perform common operations. + */ +public class EnumUtil { + + private static final String ENUM_LOOKUP_FAILURE_STRING_FORMAT = + "'%s' is not a valid string representation for any enum constants of type '%s'"; + + /** + * Obtains the enum constant by looking up the enum's {@link Enum#toString()} value that matches the given input. + * + * @param input The input string to match against the enum's string with. + * @param cls The enum class. + * @param The enum type. + * @return The enum constant. + * @throws IllegalArgumentException if the string does not match any of the enum's values. + */ + public static > E lookupByToString(Class cls, String input) + throws IllegalArgumentException { + + return Arrays.stream(cls.getEnumConstants()) + .filter(e -> e.toString().equals(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(String.format( + ENUM_LOOKUP_FAILURE_STRING_FORMAT, + input, cls.getSimpleName() + ))); + } + + /** + * Checks whether there exists an enum constant whose {@link Enum#toString()} value matches the given input. + * + * @param input The input string to match against the enum's string with. + * @param cls The enum class to enumerate through. + * @param The enum type. + * @return true if an enum constant with the given input string as text representation exists, false otherwise. + */ + public static > boolean hasMatchingToString(Class cls, String input) { + return Arrays.stream(cls.getEnumConstants()).anyMatch(e -> e.toString().equals(input)); + } + +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..eaf78a2c5a5 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -65,4 +65,99 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Formats the given values with the format string, but return null if any of the given values are null or empty. + * + * @param format The format string to use. + * @param values The values to insert into the format string. + * @return The formatted string, or null if any of the values are null. + */ + public static String formatWithNullFallback(String format, Object... values) { + if (format == null) { + return null; + } + + for (Object v : values) { + if (v == null || v.toString().isBlank()) { + return null; + } + } + + return String.format(format, values); + } + + /** + * Returns true if the inputString is a fuzzy match of the targetString, + * false otherwise. + * + *

+ * A fuzzy search is an approximate search algorithm. This implementation computes a fuzzy match by determining + * if there exists a subsequence match in linear time. + *

+ * + *

+ * As an example, "abc" is considered to be a fuzzy match of "aa1b2ccc", since one may + * construct the subsequence "abc" by removing extra characters "a1", "2cc" + * from aa1b2ccc. + *

+ * + * @param inputString The partial fuzzy input string. + * @param targetString The target string to check if the input fuzzily matches with. + * @return true if the input fuzzy-matches (is fuzzily contained in) the target, false otherwise. + */ + public static boolean isFuzzyMatch(String inputString, String targetString) { + if (inputString == null || targetString == null) { + return inputString == null && targetString == null; // both null => true, otherwise false + } + + int inputI = 0; + int targetI = 0; + + while (inputI < inputString.length() && targetI < targetString.length()) { + char c = inputString.charAt(inputI); + char t = targetString.charAt(targetI); + + if (c == t) { + inputI++; + } + targetI++; + } + + return inputI >= inputString.length(); + } + + /** + * Returns a score representing how close it is to matching characters at the beginning of the target. + * The higher the value, the better it is. A failed match will have {@code -targetString.length()}, while + * a complete match will have {@code inputString.length()}. + *

+ */ + public static int getFuzzyMatchScore(String inputString, String targetString) { + if (inputString == null || targetString == null) { + return 0; + } + + int inputI = 0; + int targetI = 0; + + int errorCount = 0; + + while (inputI < inputString.length() && targetI < targetString.length()) { + char c = inputString.charAt(inputI); + char t = targetString.charAt(targetI); + + if (c == t) { + errorCount += targetI - inputI - errorCount; + inputI++; + } + targetI++; + } + + if (inputI < inputString.length()) { + return -targetString.length(); + } + + return inputI - errorCount; + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..65f0c9444c3 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,6 +1,7 @@ package seedu.address.logic; import java.nio.file.Path; +import java.util.stream.Stream; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; @@ -8,7 +9,8 @@ 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.contact.Contact; +import seedu.address.model.jobapplication.JobApplication; /** * API of the Logic component @@ -16,6 +18,7 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. @@ -23,6 +26,14 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; + /** + * Parses the command and returns any autocompletion results. + * + * @param commandText The command as entered by the user. + * @return the result of the command execution. + */ + Stream generateCompletions(String commandText); + /** * Returns the AddressBook. * @@ -30,8 +41,11 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered and sorted list of contacts. */ + ObservableList getDisplayedContactList(); + + /** Returns an unmodifiable view of the filtered and sorted list of applications. */ + ObservableList getDisplayedApplicationList(); /** * Returns the user prefs' address book file path. @@ -44,7 +58,7 @@ public interface Logic { GuiSettings getGuiSettings(); /** - * Set the user prefs' GUI settings. + * Sets the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..7a7322fd063 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -4,6 +4,7 @@ import java.nio.file.AccessDeniedException; import java.nio.file.Path; import java.util.logging.Logger; +import java.util.stream.Stream; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; @@ -11,11 +12,12 @@ import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.AppParser; 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.contact.Contact; +import seedu.address.model.jobapplication.JobApplication; import seedu.address.storage.Storage; /** @@ -31,7 +33,7 @@ public class LogicManager implements Logic { private final Model model; private final Storage storage; - private final AddressBookParser addressBookParser; + private final AppParser appParser; /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. @@ -39,7 +41,7 @@ public class LogicManager implements Logic { public LogicManager(Model model, Storage storage) { this.model = model; this.storage = storage; - addressBookParser = new AddressBookParser(); + appParser = new AppParser(); } @Override @@ -47,7 +49,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE logger.info("----------------[USER COMMAND][" + commandText + "]"); CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); + Command command = appParser.parseCommand(commandText); commandResult = command.execute(model); try { @@ -61,14 +63,26 @@ public CommandResult execute(String commandText) throws CommandException, ParseE return commandResult; } + @Override + public Stream generateCompletions(String commandText) { + return appParser + .parseCompletionGenerator(commandText) + .generateCompletions(commandText, model); + } + @Override public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getDisplayedContactList() { + return model.getDisplayedContactList(); + } + + @Override + public ObservableList getDisplayedApplicationList() { + return model.getDisplayedApplicationList(); } @Override diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..4141eade543 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -1,11 +1,12 @@ package seedu.address.logic; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import seedu.address.logic.parser.Prefix; -import seedu.address.model.person.Person; +import seedu.address.logic.parser.Flag; +import seedu.address.model.contact.Contact; /** * Container for user visible messages. @@ -14,38 +15,110 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX = "The contact index provided is invalid"; + public static final String MESSAGE_INVALID_APPLICATION_DISPLAYED_INDEX = "The job application index provided is " + + "invalid"; + + public static final String MESSAGE_NO_SUCH_CONTACT = "No such contact"; + public static final String MESSAGE_CONTACTS_LISTED_OVERVIEW = "%1$d contacts listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued option(s): "; + public static final String MESSAGE_EXTRA_FIELDS = + "Extra irrelevant options found in the command: "; + public static final String MESSAGE_UNEXPECTED_NON_EMPTY_FIELDS = + "The following options may not have any value: "; + public static final String MESSAGE_SIMULTANEOUS_USE_DISALLOWED_FIELDS = + "The following options conflict and cannot be set together: "; + + public static final String MESSAGE_INVALID_FIELD = + "The term '%s' is not a valid option!"; /** - * Returns an error message indicating the duplicate prefixes. + * Returns an error message indicating the duplicate flags. */ - public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePrefixes) { - assert duplicatePrefixes.length > 0; + public static String getErrorMessageForDuplicateFlags(Flag... duplicateFlags) { + assert duplicateFlags.length > 0; Set duplicateFields = - Stream.of(duplicatePrefixes).map(Prefix::toString).collect(Collectors.toSet()); + Stream.of(duplicateFlags).map(Flag::toString).collect(Collectors.toSet()); + + return MESSAGE_DUPLICATE_FIELDS + String.join(", ", duplicateFields); + } + + /** + * Returns an error message indicating the extraneous flags. + */ + public static String getErrorMessageForExtraneousFlags(Flag... extraneousFlags) { + assert extraneousFlags.length > 0; + + Set extraneousFields = + Stream.of(extraneousFlags).map(Flag::toString).collect(Collectors.toSet()); + + return MESSAGE_EXTRA_FIELDS + String.join(", ", extraneousFields); + } + + /** + * Returns an error message indicating the flags have unexpected values. + */ + public static String getErrorMessageForNonEmptyValuedFlags(Flag... nonEmptyValuedFlags) { + assert nonEmptyValuedFlags.length > 0; + + Set nonEmptyValuedFields = + Stream.of(nonEmptyValuedFlags).map(Flag::toString).collect(Collectors.toSet()); - return MESSAGE_DUPLICATE_FIELDS + String.join(" ", duplicateFields); + return MESSAGE_UNEXPECTED_NON_EMPTY_FIELDS + String.join(", ", nonEmptyValuedFields); } /** - * Formats the {@code person} for display to the user. + * Returns an error message indicating the flags should not exist in the same command together. */ - public static String format(Person person) { + public static String getErrorMessageForSimultaneousUseDisallowedFlags(Flag... conflictingFlags) { + assert conflictingFlags.length > 1; + + Set conflictingFields = + Stream.of(conflictingFlags).map(Flag::toString).collect(Collectors.toSet()); + + return MESSAGE_SIMULTANEOUS_USE_DISALLOWED_FIELDS + String.join(", ", conflictingFields); + } + + /** + * Returns an error message indicating the invalid flag. + */ + public static String getErrorMessageForInvalidFlagString(String flagString) { + return String.format( + MESSAGE_INVALID_FIELD, flagString + ); + } + + /** + * Formats the {@code contact} for display to the user. + */ + public static String format(Contact contact) { final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) + builder.append(contact.getName()) + .append("; Id: ") + .append(contact.getId()) .append("; Phone: ") - .append(person.getPhone()) + .append(contact.getPhone().map(p -> p.value).orElse("(none)")) .append("; Email: ") - .append(person.getEmail()) + .append(contact.getEmail().map(e -> e.value).orElse("(none)")) + .append("; Url: ") + .append(contact.getUrl().map(u -> u.value).orElse("(none)")) .append("; Address: ") - .append(person.getAddress()) + .append(contact.getAddress().map(a -> a.value).orElse("(none)")) .append("; Tags: "); - person.getTags().forEach(builder::append); + contact.getTags().forEach(builder::append); return builder.toString(); } + /** + * Formats the given {@code childrenContacts} for display to the user. + */ + public static String formatChildren(List childrenContacts) { + return childrenContacts.stream() + .map(c -> Messages.format(c) + "\n") + .reduce((c1, c2) -> c1 + c2) + .orElse("No other contacts found"); + } + } diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java new file mode 100644 index 00000000000..5db6a0a6e06 --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteGenerator.java @@ -0,0 +1,163 @@ +package seedu.address.logic.autocomplete; + +import java.util.Comparator; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.autocomplete.components.PartitionedCommand; +import seedu.address.logic.parser.Flag; +import seedu.address.model.Model; + +/** + * Creates a generator based on the given supplier or reference commands, so as it can generate + * auto-completions when requested. + */ +public class AutocompleteGenerator { + + /** An autocompletion generator that generates no results. */ + public static final AutocompleteGenerator NO_RESULTS = new AutocompleteGenerator(Stream::empty); + + + /** A comparator used to order fuzzily matched strings where better matches against the input go first. */ + private static final Function> TEXT_FUZZY_MATCH_COMPARATOR = (input) -> (s1, s2) -> { + // Get how well s1 is ahead of s2 (note: higher is better). + int score = StringUtil.getFuzzyMatchScore(input, s1) - StringUtil.getFuzzyMatchScore(input, s2); + + return -score; // -ve implies s1 < s2 + }; + + /** A comparator used to order fuzzily matched flags where better matches against the input go first. */ + private static final Function> FLAG_FUZZY_MATCH_COMPARATOR = (input) -> (f1, f2) -> { + // Get how well f1 is ahead of f2 in both metrics (note: higher is better). + int score = Math.max( + StringUtil.getFuzzyMatchScore(input, f1.getFlagString()), + StringUtil.getFuzzyMatchScore(input, f1.getFlagAliasString()) + ) - Math.max( + StringUtil.getFuzzyMatchScore(input, f2.getFlagString()), + StringUtil.getFuzzyMatchScore(input, f2.getFlagAliasString()) + ); + + return -score; // -ve implies f1 < f2 + }; + + + + /** The cached instance of the result evaluation function. */ + private final BiFunction> resultEvaluationFunction; + + /** + * Constructs an autocomplete generator based on the given supplier of full command strings. + */ + public AutocompleteGenerator(Supplier> referenceCommandsSupplier) { + resultEvaluationFunction = (partialCommand, model) -> { + String input = partialCommand == null ? "" : partialCommand; + + return referenceCommandsSupplier.get() + .filter(term -> StringUtil.isFuzzyMatch(input, term)) + .sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(partialCommand)) + .distinct(); + }; + } + + /** + * Constructs an autocomplete generator based on the given {@link AutocompleteSupplier}. + */ + public AutocompleteGenerator(AutocompleteSupplier supplier) { + resultEvaluationFunction = (partialCommand, model) -> { + + PartitionedCommand command = new PartitionedCommand(partialCommand == null ? "" : partialCommand); + String trailingText = command.getTrailingText(); + + // Compute the possible flags and flag-values. + Stream possibleFlags = getPossibleFlags(command, supplier) + .filter(flag -> StringUtil.isFuzzyMatch(trailingText, flag.getFlagString()) + || StringUtil.isFuzzyMatch(trailingText, flag.getFlagAliasString())) + .sorted(FLAG_FUZZY_MATCH_COMPARATOR.apply(trailingText)) + .map(Flag::getFlagString); + + Stream possibleValues = getPossibleValues(command, supplier, model) + .map(s -> s.filter(term -> StringUtil.isFuzzyMatch(trailingText, term)) + .sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(trailingText))) + .orElse(possibleFlags); + + // Decide which stream to use based on whether it's of a flag syntax or not. + Stream possibleTerminalValues; + if (command.hasFlagSyntaxPrefixInTrailingText()) { + possibleTerminalValues = possibleFlags; + } else { + possibleTerminalValues = possibleValues; + } + + // Return the results as a full completion string, distinct. + return possibleTerminalValues + .map(command::toStringWithNewTrailingTerm) + .distinct(); + }; + } + + /** + * Generates a stream of completions based on the partial command given and no model. + * Note that omitting the model may limit the ability to return useful results. + */ + public Stream generateCompletions(String command) { + return resultEvaluationFunction.apply(command, null); + } + + /** + * Generates a stream of completions based on the partial command given and the model. + */ + public Stream generateCompletions(String command, Model model) { + return resultEvaluationFunction.apply(command, model); + } + + + + + /** + * Obtains the set of possible flags based on the partitioned command and supplier. + */ + private static Stream getPossibleFlags( + PartitionedCommand command, + AutocompleteSupplier supplier + ) { + Flag[] allPossibleFlags = supplier.getAllPossibleFlags().toArray(Flag[]::new); + + Set existingCommandFlags = command.getConfirmedFlagStrings() + .stream() + .map(flagStr -> Flag.findMatch(flagStr, allPossibleFlags)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + + return supplier.getOtherPossibleFlagsAsideFromFlagsPresent(existingCommandFlags).stream(); + } + + /** + * Obtains the optional set of possible values based on the partitioned command, supplier, and model. + * If this optional is empty, that means it is explicitly specified that the flag cannot accept values. + */ + private static Optional> getPossibleValues( + PartitionedCommand command, + AutocompleteSupplier supplier, + Model model + ) { + Optional flagString = command.getLastConfirmedFlagString(); + if (flagString.isEmpty()) { + return supplier.getValidValuesForFlag(null, command, model); + } + + Optional targetFlag = Flag.findMatch( + flagString.get(), + supplier.getAllPossibleFlags().toArray(Flag[]::new) + ); + return targetFlag.flatMap(f -> supplier.getValidValuesForFlag(f, command, model)); + } + +} diff --git a/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java new file mode 100644 index 00000000000..1c259b387d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/AutocompleteSupplier.java @@ -0,0 +1,147 @@ +package seedu.address.logic.autocomplete; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.logic.autocomplete.components.FlagValueSupplier; +import seedu.address.logic.autocomplete.components.PartitionedCommand; +import seedu.address.logic.parser.Flag; +import seedu.address.model.Model; + +/** + * Supplies autocompletion details for arbitrary commands. + */ +public class AutocompleteSupplier { + + private final AutocompleteItemSet flags; + private final Map values; + + /** + * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command. + * The ordering supplied via the parameters are followed when generating results. + * + * @param flags The set of flags that should be used as part of the autocomplete results. + * @param values A map of auto-completable values for each flag that may be obtained via a model. + */ + public AutocompleteSupplier( + AutocompleteItemSet flags, + Map values + ) { + // Create new copies to prevent external modification. + this.flags = flags.copy(); + this.values = new LinkedHashMap<>(values); + } + + /** + * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command. + * The ordering supplied via the parameters are followed when generating results. + * + * @param flags The set of flags that should be used as part of the autocomplete results. + * + * @see #AutocompleteSupplier(AutocompleteItemSet, Map) + */ + public static AutocompleteSupplier from(AutocompleteItemSet flags) { + return new AutocompleteSupplier(flags, Map.of()); + } + + /** + * Constructs an autocomplete supplier that is capable of supplying useful details for autocompleting some command. + * The ordering supplied via the parameters are followed when generating results. + * + * @param flagSets The sets of flags that should be used together as part of the autocomplete results. + */ + @SafeVarargs + public static AutocompleteSupplier from(AutocompleteItemSet... flagSets) { + return from(AutocompleteItemSet.concat(flagSets)); + } + + /** + * Constructs an autocomplete supplier that is capable of supplying all the unique flags (flags which may appear + * at most once) for autocompleting some command. + * The ordering supplied via the parameters are followed when generating results. + * + * @param uniqueFlags A set of flags that each may appear at most once in the command. + */ + public static AutocompleteSupplier fromUniqueFlags(Flag... uniqueFlags) { + return AutocompleteSupplier.from( + AutocompleteItemSet.onceForEachOf(uniqueFlags) + ); + } + + /** + * Constructs an autocomplete supplier that is capable of supplying all the repeatable flags (flags which may + * appear any number of times in the command) for autocompleting some command. + * The ordering supplied via the parameters are followed when generating results. + * + * @param repeatableFlags A set of flags that may appear at any point in the command any number of times. + */ + public static AutocompleteSupplier fromRepeatableFlags(Flag... repeatableFlags) { + return AutocompleteSupplier.from( + AutocompleteItemSet.anyNumberOf(repeatableFlags) + ); + } + + /** + * Returns a set of all possible flags that can be used in the command. + * The set has predictable iteration order: it follows the ordering supplied via the original inputs. + */ + public Set getAllPossibleFlags() { + return flags.copy(); + } + + /** + * Returns a set of other possible flags given the list of flags that are already present in the command. + * If there are conflicting constraints specified, this will use the tightest possible constraint. + * The set has predictable iteration order: it follows the ordering supplied via the original inputs. + */ + public Set getOtherPossibleFlagsAsideFromFlagsPresent(Set flagsPresent) { + return flags.getElementsAfterConsuming(flagsPresent); + } + + /** + * Returns an optional stream of possible values for a flag when computed against a given model. + * If this optional is empty, then this flag is explicitly specified to not have any values, + * and not just the lack of completion suggestions. + * + * @param flag The flag to check against. This may be null to represent the preamble. + * @param currentCommand The current command structure. This should not be null. + * @param model The model to be supplied for generation. This may be null if the model is unavailable. + */ + public Optional> getValidValuesForFlag(Flag flag, PartitionedCommand currentCommand, Model model) { + try { + return Optional.ofNullable( + this.values.getOrDefault(flag, (c, m) -> Stream.empty()) + ).map(flagValueSupplier -> flagValueSupplier.apply(currentCommand, model)); + + } catch (RuntimeException e) { + // Guard against errors like NPEs due to supplied lambdas not handling them. + e.printStackTrace(); + // We simply return that we don't know what to auto-complete by. + return Optional.of(Stream.of()); + } + } + + /** + * Configures the set of flags within this autocomplete supplier using the given {@code operator}. + * This also returns {@code this} instance, which is useful for chaining. + */ + public AutocompleteSupplier configureFlagSet(Consumer> operator) { + operator.accept(this.flags); + return this; + } + + /** + * Configures the map of values within this autocomplete supplier using the given {@code operator}. + * This also returns {@code this} instance, which is useful for chaining. + */ + public AutocompleteSupplier configureValueMap(Consumer> operator) { + operator.accept(this.values); + return this; + } + +} diff --git a/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java new file mode 100644 index 00000000000..aadeac2d128 --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteConstraint.java @@ -0,0 +1,164 @@ +package seedu.address.logic.autocomplete.components; + +import java.util.Collection; +import java.util.Set; + +/** + * Constraints autocompletion to a certain restriction. + * This functional interface takes in an input and a {@link Set} of existing elements, + * and determines whether the input should be allowed to be added among the existing elements. + */ +@FunctionalInterface +public interface AutocompleteConstraint { + + /** + * Returns whether {@code input} can be added to the set of {@code existingElements}. + */ + boolean isAllowed(T input, Set existingElements); + + + // Constraint operators + + /** + * Creates a constraint that returns true as long as any given constraints return true. + */ + static AutocompleteConstraint anyOf(Collection> constraints) { + return (input, existingElements) -> { + for (var c : constraints) { + if (c.isAllowed(input, existingElements)) { + return true; + } + } + return false; + }; + } + + /** + * Creates a constraint that returns true as long as all given constraints return true. + */ + static AutocompleteConstraint allOf(Collection> constraints) { + return (input, existingElements) -> { + for (var c : constraints) { + if (!c.isAllowed(input, existingElements)) { + return false; + } + } + return true; + }; + } + + // Simple constraint templates + + /** + * Creates a constraint that enforces all provided {@code items} each may exist at most once. + */ + @SafeVarargs + static AutocompleteConstraint onceForEachOf(T... items) { + Set itemsSet = Set.of(items); + + return (input, existingElements) -> { + if (!itemsSet.contains(input)) { + // Not part of consideration. True by default. + return true; + } + return !existingElements.contains(input); // Input does not exists <--> input can exist. + }; + } + + /** + * Creates a constraint that enforces at most one item within the entire {@code items} may exist at a time. + */ + @SafeVarargs + static AutocompleteConstraint oneAmongAllOf(T... items) { + Set itemsSet = Set.of(items); + + return (input, existingElements) -> { + if (!itemsSet.contains(input)) { + // Not part of consideration. True by default. + return true; + } + + for (T item : items) { + if (existingElements.contains(item)) { + // Some set element is already present -> no more allowed. + return false; + } + } + + return true; + }; + } + + // Advanced relational constraint templates + + /** + * Represents an item that will be part of the constraint. + * This exists to improve readability of relational factory methods, + * along the lines of {@code .where(x).isSomethingTo(y...)} + */ + class Item { + + final T item; + + private Item(T item) { + this.item = item; + } + + /** + * Creates a constraint that enforces that {@code prerequisite}, i.e. this item, + * must be present before any of {@code dependents} may exist. + */ + @SafeVarargs + public final AutocompleteConstraint isPrerequisiteFor(T... dependents) { + T prerequisite = this.item; + + Set dependentsSet = Set.of(dependents); + + return (input, existingElements) -> { + if (!dependentsSet.contains(input)) { + // Not part of dependents. True by default. + return true; + } + return existingElements.contains(prerequisite); // Prerequisite exists <--> dependents can exist. + }; + } + + /** + * Creates a constraint that enforces that this current item + * cannot be present if any of {@code incompatibleItems} exist. + */ + @SafeVarargs + public final AutocompleteConstraint cannotExistAlongsideAnyOf(T... incompatibleItems) { + T currentItem = this.item; + + Set incompatibleItemsSet = Set.of(incompatibleItems); + + return (input, existingElements) -> { + if (!currentItem.equals(input)) { + // Not the current item. + + // If current item is already in existing elements, + // ensure no incompatible elements pass through. + return !(existingElements.contains(currentItem) + && incompatibleItemsSet.contains(input)); + } + + // Is thh current item. + + // No items in incompatible set exists in existing set --> current item may exist. + return incompatibleItemsSet.stream().noneMatch(existingElements::contains); + }; + } + } + + /** + * Represents an item that is a part of a constraint relationship. + * This should be immediately followed by the relevant constraint methods, + * along the lines of {@code .where(x).isSomethingTo(y...)} + */ + static Item where(T item) { + return new Item(item); + } + + +} diff --git a/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java new file mode 100644 index 00000000000..2e732ff9836 --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/components/AutocompleteItemSet.java @@ -0,0 +1,278 @@ +package seedu.address.logic.autocomplete.components; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A set of items that can be used for autocompletion. It stores any corresponding constraints on them in itself. + * + *

+ * Suppose a command already has specific items, and some items have restrictions such as being only able to + * be used once. Then, one may specify the constraints when setting up this set, and then any time determine + * what items are still available for autocompletion by passing items already used into + * {@link #getElementsAfterConsuming(Set)}. + *

+ * + *

+ * The convenience factory methods {@link #onceForEachOf}, {@link #oneAmongAllOf} and {@link #anyNumberOf} + * may be useful to quickly create a set of items with these common constraints, while convenience methods like + * {@link #concat}, {@link #addDependents}, {@link #addConstraints} can be used for chaining and composing rules. + *

+ * + *

+ * This is a subclass of {@link LinkedHashSet}, so it inherits all its properties like its set functions and + * preservation of insertion order. + *

+ */ +public final class AutocompleteItemSet extends LinkedHashSet { + + private final Set> constraints = new LinkedHashSet<>(); + + /** + * Creates an empty {@link AutocompleteItemSet}. + */ + public AutocompleteItemSet() { + super(); + } + + /** + * Creates a {@link AutocompleteItemSet} with the given elements and constraints. + * + *

+ * This is mainly useful if your rules are complex. Otherwise, the convenience factory + * methods {@link #onceForEachOf}, {@link #oneAmongAllOf} and {@link #anyNumberOf} + * may be useful to quickly create an instance with these common constraints. + *

+ * + * @param collection The collection of items. + * @param constraints The constraints for the given items. + * + * @see #onceForEachOf + * @see #oneAmongAllOf + * @see #anyNumberOf + */ + private AutocompleteItemSet( + Collection collection, + Collection> constraints + ) { + super(collection); + this.constraints.addAll(constraints); + } + + /** + * Creates an {@link AutocompleteItemSet} with the given elements, + * and the constraint that each given element may exist at most once in a command. + */ + @SafeVarargs + public static AutocompleteItemSet onceForEachOf(T... items) { + return new AutocompleteItemSet( + List.of(items), + List.of(AutocompleteConstraint.onceForEachOf(items)) + ); + } + + /** + * Creates an {@link AutocompleteItemSet} with the given elements, + * and the constraint that only one of the given elements may exist in a command. + */ + @SafeVarargs + public static AutocompleteItemSet oneAmongAllOf(T... items) { + return new AutocompleteItemSet( + List.of(items), + List.of(AutocompleteConstraint.oneAmongAllOf(items)) + ); + } + + /** + * Creates an {@link AutocompleteItemSet} with the given elements, + * and the constraint that all given elements may exist any number of times in a command. + */ + @SafeVarargs + public static AutocompleteItemSet anyNumberOf(T... items) { + return new AutocompleteItemSet( + List.of(items), + List.of() + ); + } + + /** + * Concatenates all provided {@link AutocompleteItemSet}s. This includes merging all information between them, + * i.e., all items and constraints. + */ + @SafeVarargs + public static AutocompleteItemSet concat(AutocompleteItemSet... sets) { + return new AutocompleteItemSet( + Arrays.stream(sets).flatMap(Collection::stream).collect(Collectors.toList()), + Arrays.stream(sets).flatMap(s -> s.constraints.stream()).collect(Collectors.toList()) + ); + } + + /** + * Returns a copy of the current instance. + */ + public AutocompleteItemSet copy() { + return new AutocompleteItemSet(this, constraints); + } + + + + /** + * Updates the current set to include {@code dependencies}, with the condition that + * "some element in {@code this} current set exists" is a prerequisite for + * elements in {@code dependencies} to exist in a command. + */ + @SafeVarargs + public final AutocompleteItemSet addDependents(AutocompleteItemSet... dependencies) { + + AutocompleteItemSet mergedDependencies = AutocompleteItemSet.concat(dependencies); + + // Create a dependency array + // - The unchecked cast is required for generics since generic arrays cannot be made. + @SuppressWarnings("unchecked") + T[] dependencyArray = mergedDependencies.toArray((T[]) new Object[mergedDependencies.size()]); + + // Add the constraints to enforce dependency relationship + this.addConstraint(AutocompleteConstraint.anyOf(this.stream() + .map(item -> AutocompleteConstraint.where(item).isPrerequisiteFor(dependencyArray)) + .collect(Collectors.toList()) + )); + + // Once done, add all elements and constraints, ordered after existing elements in the set. + this.addElements(mergedDependencies.getElements()); + this.addConstraints(mergedDependencies.getConstraints()); + + return this; + } + + + /** + * Adds a given constraint to the evaluation rules. + * + * @param constraint The constraint to add. + * @return A reference to {@code this} instance, useful for chaining. + */ + public AutocompleteItemSet addConstraint(AutocompleteConstraint constraint) { + this.constraints.add(constraint); + return this; + } + + /** + * Removes a given constraint from the evaluation rules. + * + * @param constraint The constraint to remove. + * @return A reference to {@code this} instance, useful for chaining. + */ + public AutocompleteItemSet removeConstraint(AutocompleteConstraint constraint) { + this.constraints.remove(constraint); + return this; + } + + /** + * Adds a given collection of constraints to the evaluation rules. + * + * @param constraints The constraints to add. + * @return A reference to {@code this} instance, useful for chaining. + */ + public AutocompleteItemSet addConstraints(Collection> constraints) { + this.constraints.addAll(constraints); + return this; + } + + /** + * Removes a given collection of constraints from the evaluation rules. + * + * @param constraints The constraints to remove. + * @return A reference to {@code this} instance, useful for chaining. + */ + public AutocompleteItemSet removeConstraints(Collection> constraints) { + this.constraints.removeAll(constraints); + return this; + } + + /** + * Returns the current set of constraints applied for evaluating possible auto-completions. + */ + public Set> getConstraints() { + return Collections.unmodifiableSet(constraints); + } + + + + /** + * Adds the item to the set. + * Equivalent to {@link #add}, but returns {@code this}, so is useful for chaining. + */ + public AutocompleteItemSet addElement(T e) { + this.add(e); + return this; + } + + /** + * Removes the item from the set. + * Equivalent to {@link #remove}, but returns {@code this}, so is useful for chaining. + */ + public AutocompleteItemSet removeElement(T e) { + this.remove(e); + return this; + } + + /** + * Adds the items to the set. + * Equivalent to {@link #addAll}, but returns {@code this}, so is useful for chaining. + */ + public AutocompleteItemSet addElements(Collection e) { + this.addAll(e); + return this; + } + + /** + * Removes the items from the set. + * Equivalent to {@link #removeAll}, but returns {@code this}, so is useful for chaining. + */ + public AutocompleteItemSet removeElements(Collection e) { + this.removeAll(e); + return this; + } + + /** + * Returns the elements in this instance in a new {@link Set} instance. + * Iteration order is preserved. + */ + public Set getElements() { + return new LinkedHashSet<>(this); + } + + + /** + * Returns the elements remaining by supposing {@code existingElements} are present and applying all + * configured {@link AutocompleteConstraint}s, as a new {@link Set} instance. + */ + public Set getElementsAfterConsuming(Set existingElements) { + Set resultsSet = new LinkedHashSet<>(); + for (T item: this) { + if (constraints.stream().allMatch(c -> c.isAllowed(item, existingElements))) { + resultsSet.add(item); + } + } + return resultsSet; + } + + + @Override + public boolean equals(Object o) { + + // instanceof checks null implicitly. + if (!(o instanceof AutocompleteItemSet)) { + return false; + } + + AutocompleteItemSet otherSet = (AutocompleteItemSet) o; + + return super.equals(o) && this.constraints.equals(otherSet.constraints); + } +} diff --git a/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java b/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java new file mode 100644 index 00000000000..31a8bcf4f2d --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/components/FlagValueSupplier.java @@ -0,0 +1,49 @@ +package seedu.address.logic.autocomplete.components; + +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import seedu.address.model.Model; + +/** + * Supplies a list of values for auto-completing a flag's value, given the model. + * + *

+ * Any implementation of {@link FlagValueSupplier} must conform to the specifications noted on the + * {@link #apply} method. + *

+ */ +public interface FlagValueSupplier extends BiFunction> { + + + /** + * Returns a stream of possible values that a particular flag could take. + * + *

+ * This will always return all possible values - any additional conditional filters like prefix matching + * should be up to the receiver of the stream to apply. The {@code currentCommand} field is required + * in case the supplier wants to provide entirely different set of suggestions based on the initial input + * (e.g., omit suggestions when a numeric index is provided, but provide suggestions for standard text). + *

+ * + *

+ * There are a two forms of possible return results: + *

    + *
  • + * If the returned stream is non-empty, it indicates suggestions where + * {@code currentValue} could be replaced with. + *
  • + *
  • If the returned stream is empty, it indicates the lack of suggestions.
  • + *
  • If the returned stream given is null, it indicates this flag should not have a value.
  • + *
+ *

+ * + * @param currentCommand The current command typed so far. Should never be null. + * @param model The model that can be used. May be null. + * + * @return A stream of suggestions (can be an empty stream for "nothing to suggest"), + * or null indicating to skip suggestions entirely (for "this should not have any value"). + */ + @Override + Stream apply(PartitionedCommand currentCommand, Model model); +} diff --git a/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java b/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java new file mode 100644 index 00000000000..76bda07dde4 --- /dev/null +++ b/src/main/java/seedu/address/logic/autocomplete/components/PartitionedCommand.java @@ -0,0 +1,184 @@ +package seedu.address.logic.autocomplete.components; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.parser.Flag; + +/** + * A wrapper around a command string as partitions, useful for computing results for autocompletion. + */ +public class PartitionedCommand { + private final String name; + private final String middleText; + private final String trailingText; + + private final boolean hasFlagSyntaxPrefixInTrailingText; + private final List confirmedFlagStrings; + + /** + * Initializes and prepares the given command as distinct partitions. + */ + public PartitionedCommand(String partialCommand) { + List words = List.of(partialCommand.split(" ", -1)); // -1 stops stripping adjacent spaces. + + if (words.size() <= 1) { + this.name = ""; + this.middleText = ""; + this.trailingText = words.isEmpty() ? "" : words.get(0); + this.hasFlagSyntaxPrefixInTrailingText = false; + this.confirmedFlagStrings = List.of(); + return; + } + + // Compute whether the trailing term is likely a flag part. + this.hasFlagSyntaxPrefixInTrailingText = + words.get(words.size() - 1).startsWith(Flag.DEFAULT_PREFIX) + || words.get(words.size() - 1).startsWith(Flag.DEFAULT_ALIAS_PREFIX); + + // Compute all matched flags. + this.confirmedFlagStrings = words.subList(1, words.size() - 1) + .stream() + .filter(Flag::isFlagSyntax) + .collect(Collectors.toUnmodifiableList()); + + // Compute rightmost index of flag. + int lastKnownFlagIndex = words.size() - 1; + while (lastKnownFlagIndex > 0) { + if (Flag.isFlagSyntax(words.get(lastKnownFlagIndex))) { + break; + } + lastKnownFlagIndex--; + } + + // Compute trailing text's start index. It must be at least 1 (i.e., after the command name). + // + // - if the rightmost term is possibly a new flag, the trailing text should be that one. + // e.g., "name --flag1 abc def --fl" should be split to ("name", "--flag abc def", "--fl") + // + // - otherwise, it should be the text beyond the last known flag location. + // e.g., "name --flag1 abc def" should be split to ("name", "--flag", "abc def") + int trailingTextStartIndex = Math.max( + 1, + this.hasFlagSyntaxPrefixInTrailingText + ? words.size() - 1 + : lastKnownFlagIndex + 1 + ); + + // Compute the name, middle, trailing parts by splicing the appropriate ranges. + this.name = words.get(0); + this.middleText = String.join(" ", words.subList(1, trailingTextStartIndex)); + this.trailingText = String.join(" ", words.subList(trailingTextStartIndex, words.size())); + } + + /** + * Gets the command name. + */ + public String getName() { + return name; + } + + /** + * Gets the leading text. + * It is the front part of the command that should not be modified as part of autocompletion. + */ + public String getLeadingText() { + return String.join(" ", List.of(name, middleText)); + } + + /** + * Gets the middle text. + * It is the part after name but before leadingText. + */ + public String getMiddleText() { + return middleText; + } + + /** + * Gets the leading text. + * It is the part of the text that may be autocompleted, either by extending or replacing it. + */ + public String getTrailingText() { + return trailingText; + } + + /** + * Gets the trailing part of the text that should be replaced with an autocompletion value. + * Equivalent to {@link #getTrailingText}. + */ + public String getAutocompletableText() { + return getTrailingText(); + } + + /** + * Gets the rightmost flag string detected in this command. + */ + public Optional getLastConfirmedFlagString() { + return confirmedFlagStrings.isEmpty() + ? Optional.empty() + : Optional.of(confirmedFlagStrings.get(confirmedFlagStrings.size() - 1)); + } + + /** + * Gets all flags detected in this command. + */ + public List getConfirmedFlagStrings() { + return this.confirmedFlagStrings; + } + + /** + * Returns true if the trailing text seems to have signs of a flag prefix, false otherwise. + */ + public boolean hasFlagSyntaxPrefixInTrailingText() { + return this.hasFlagSyntaxPrefixInTrailingText; + } + + /** + * Returns the current command as a string, but with the trailing term replaced. + * This is useful to generate an autocompleted result. + */ + public String toStringWithNewTrailingTerm(String newTrailingTerm) { + return Stream.of(name, middleText, newTrailingTerm) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(" ")) + .trim(); + } + + /** + * Returns the current command as a string. + * This is usually equivalent to the original input string. + */ + @Override + public String toString() { + return Stream.of(name, middleText, trailingText) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(" ")) + .trim(); + } + + /** + * Returns true if the command partitions for this and the other command are equal, false otherwise. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PartitionedCommand + other = (PartitionedCommand) o; + return Objects.equals(name, other.name) + && Objects.equals(middleText, other.middleText) + && Objects.equals(trailingText, other.trailingText); + } + + @Override + public int hashCode() { + return Objects.hash(name, middleText, trailingText); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..641d17054fe 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,84 +1,189 @@ 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.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECRUITER; +import static seedu.address.logic.parser.CliSyntax.FLAG_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Type; +import seedu.address.model.contact.Url; +import seedu.address.model.tag.Tag; /** - * Adds a person to the address book. + * Adds a contact to the address book. */ -public class AddCommand extends Command { +public abstract class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AutocompleteItemSet.oneAmongAllOf( + FLAG_ORGANIZATION, FLAG_RECRUITER + ).addDependents( + AutocompleteItemSet.onceForEachOf( + FLAG_NAME, FLAG_ID, + FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, + FLAG_ORGANIZATION_ID + ), + AutocompleteItemSet.anyNumberOf(FLAG_TAG) + ).addConstraints(List.of( + AutocompleteConstraint.where(FLAG_RECRUITER) + .isPrerequisiteFor(FLAG_ORGANIZATION_ID) + )) + ).configureValueMap(map -> { + // Add value autocompletion for: + map.put(FLAG_ORGANIZATION_ID, (command, model) -> model.getAddressBook() + .getContactList() + .stream() + .filter(c -> c.getType() == Type.ORGANIZATION) + .map(o -> o.getId().value) + ); + + // Disable value autocompletion for: + map.put(null /* preamble */, null); + map.put(FLAG_ORGANIZATION, null); + map.put(FLAG_RECRUITER, null); + }); + + + public static final String MESSAGE_ORGANIZATION_USAGE = "Adds an organization. " + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + FLAG_ORGANIZATION + " " + + FLAG_NAME + " NAME " + + "[" + FLAG_ID + " ID] " + + "[" + FLAG_PHONE + " PHONE] " + + "[" + FLAG_EMAIL + " EMAIL] " + + "[" + FLAG_URL + " URL] " + + "[" + FLAG_ADDRESS + " ADDRESS] " + + "[" + FLAG_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"; + + FLAG_ORGANIZATION + " " + + FLAG_NAME + " JobsInc " + + FLAG_ID + " id_12345-1 " + + FLAG_PHONE + " 98765432 " + + FLAG_EMAIL + " jobsInc@example.com " + + FLAG_URL + " www.jobsinc.com " + + FLAG_ADDRESS + " 311, Clementi Ave 2, #02-25 " + + FLAG_TAG + " softwareEngineering " + + FLAG_TAG + " competitive "; + + public static final String MESSAGE_RECRUITER_USAGE = "Adds a recruiter. " + + "Parameters: " + + FLAG_RECRUITER + " " + + FLAG_NAME + " NAME " + + "[" + FLAG_ID + " ID] " + + "[" + FLAG_ORGANIZATION_ID + " ORG_ID] " + + "[" + FLAG_PHONE + " PHONE] " + + "[" + FLAG_EMAIL + " EMAIL] " + + "[" + FLAG_URL + " URL] " + + "[" + FLAG_ADDRESS + " ADDRESS] " + + "[" + FLAG_TAG + " TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + FLAG_RECRUITER + " " + + FLAG_NAME + " Steve " + + FLAG_ID + " id_98765-1 " + + FLAG_PHONE + " 83452145 " + + FLAG_EMAIL + " steveJobsInc@example.com " + + FLAG_URL + " www.linkedin.com/in/steve/ " + + FLAG_ADDRESS + " 311 W Coast Walk, #02-30 " + + FLAG_TAG + " friendly "; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a contact to the address book of the class type Organization or Recruiter." + + " The input format varies depending on the class:\n\n" + + MESSAGE_ORGANIZATION_USAGE + "\n\n" + + MESSAGE_RECRUITER_USAGE; + + public static final String MESSAGE_SUCCESS = "New %s added: %s"; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the address book"; - 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 static final Logger logger = LogsCenter.getLogger(AddCommand.class); - private final Person toAdd; + // Identity fields + protected final Name name; + protected final Id id; + protected final Phone phone; + protected final Email email; + + // Data fields + protected final Url url; + protected final Address address; + protected final Set tags = new HashSet<>(); /** - * Creates an AddCommand to add the specified {@code Person} + * Creates an AddCommand to add a {@code Contact} to the address book with the given parameters. */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; + public AddCommand(Name name, Id id, Phone phone, Email email, Url url, Address address, Set tags) { + requireAllNonNull(name, id, tags); + this.name = name; + this.id = id; + this.phone = phone; + this.email = email; + this.url = url; + this.address = address; + this.tags.addAll(tags); } @Override public CommandResult execute(Model model) throws CommandException { + Contact toAdd = createContact(); + logger.fine(String.format("Adding contact: %s", toAdd)); + requireNonNull(model); - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (model.hasContact(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); } - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + model.addContact(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd.getType(), Messages.format(toAdd))); } - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } + protected abstract Contact createContact(); - // instanceof handles nulls - if (!(other instanceof AddCommand)) { - return false; - } + @Override + public abstract boolean equals(Object other); - AddCommand otherAddCommand = (AddCommand) other; - return toAdd.equals(otherAddCommand.toAdd); + protected ToStringBuilder toStringBuilder() { + return new ToStringBuilder(this) + .add("name", name) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("url", url) + .add("address", address) + .add("tags", tags); } @Override public String toString() { - return new ToStringBuilder(this) - .add("toAdd", toAdd) - .toString(); + return toStringBuilder().toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/AddOrganizationCommand.java b/src/main/java/seedu/address/logic/commands/AddOrganizationCommand.java new file mode 100644 index 00000000000..7e9d5ae5aa4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddOrganizationCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Url; +import seedu.address.model.tag.Tag; + +/** + * Adds an organization to the address book. + */ +public class AddOrganizationCommand extends AddCommand { + + /** + * Creates an AddCommand to add a {@code Organization} to the address book with the given parameters. + */ + public AddOrganizationCommand( + Name name, + Id id, + Phone phone, + Email email, + Url url, + Address address, + Set tags + ) { + super(name, id, phone, email, url, address, tags); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + return super.execute(model); + } + + @Override + protected Organization createContact() { + return new Organization(name, id, phone, email, url, address, tags); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddOrganizationCommand)) { + return false; + } + + AddOrganizationCommand otherAddCommand = (AddOrganizationCommand) other; + return id.equals(otherAddCommand.id) + && name.equals(otherAddCommand.name) + && Objects.equals(phone, otherAddCommand.phone) + && Objects.equals(email, otherAddCommand.email) + && Objects.equals(address, otherAddCommand.address) + && Objects.equals(url, otherAddCommand.url) + && tags.equals(otherAddCommand.tags); + } + + @Override + protected ToStringBuilder toStringBuilder() { + return new ToStringBuilder(this) + .add("name", name) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("url", url) + .add("address", address) + .add("tags", tags); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddRecruiterCommand.java b/src/main/java/seedu/address/logic/commands/AddRecruiterCommand.java new file mode 100644 index 00000000000..ad5bceaf059 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddRecruiterCommand.java @@ -0,0 +1,108 @@ +package seedu.address.logic.commands; + +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Recruiter; +import seedu.address.model.contact.Type; +import seedu.address.model.contact.Url; +import seedu.address.model.tag.Tag; + +/** + * Adds a recruiter to the address book. + */ +public class AddRecruiterCommand extends AddCommand { + + public static final String MESSAGE_INVALID_ORGANIZATION = + "The organization id you supplied does not match any organization in the address book."; + + protected final Id oid; + private Organization organization; + + /** + * Creates an AddCommand to add a {@code Recruiter} to the address book with the given parameters. + */ + public AddRecruiterCommand( + Name name, + Id id, + Phone phone, + Email email, + Url url, + Address address, + Set tags, + Id oid + ) { + super(name, id, phone, email, url, address, tags); + this.oid = oid; + this.organization = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + if (oid == null) { + return super.execute(model); + } + + Contact contact = model.getContactById(oid); + if (contact == null || contact.getType() != Type.ORGANIZATION) { + throw new CommandException(MESSAGE_INVALID_ORGANIZATION); + } + + organization = (Organization) contact; + assert organization.getId().equals(oid); + + return super.execute(model); + } + + @Override + protected Recruiter createContact() { + return new Recruiter(name, id, phone, email, url, address, tags, organization); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddRecruiterCommand)) { + return false; + } + + AddRecruiterCommand otherAddCommand = (AddRecruiterCommand) other; + return id.equals(otherAddCommand.id) + && name.equals(otherAddCommand.name) + && Objects.equals(phone, otherAddCommand.phone) + && Objects.equals(email, otherAddCommand.email) + && Objects.equals(address, otherAddCommand.address) + && Objects.equals(url, otherAddCommand.url) + && tags.equals(otherAddCommand.tags) + && Objects.equals(oid, otherAddCommand.oid) + && Objects.equals(organization, otherAddCommand.organization); + } + + @Override + protected ToStringBuilder toStringBuilder() { + return new ToStringBuilder(this) + .add("name", name) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("url", url) + .add("address", address) + .add("tags", tags) + .add("oid", oid) + .add("organization", organization); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ApplyCommand.java b/src/main/java/seedu/address/logic/commands/ApplyCommand.java new file mode 100644 index 00000000000..cc5ec68aa90 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ApplyCommand.java @@ -0,0 +1,142 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; + +import java.util.Arrays; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Type; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.Deadline; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobDescription; +import seedu.address.model.jobapplication.JobStatus; +import seedu.address.model.jobapplication.JobTitle; + +/** + * Adds a job application to the address book. + */ +public class ApplyCommand extends Command { + + public static final String COMMAND_WORD = "apply"; + + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AutocompleteItemSet.onceForEachOf( + FLAG_TITLE, FLAG_DESCRIPTION, + FLAG_DEADLINE, FLAG_STAGE, FLAG_STATUS + ) + ).configureValueMap(map -> { + // Add value autocompletion data for: + map.put(null /* preamble */, (command, model) -> { + + String partialText = command.getAutocompletableText(); + if (partialText.isEmpty() || StringUtil.isNonZeroUnsignedInteger(partialText)) { + // Preamble is likely of type Index + return Stream.empty(); + + } else { + // Preamble is likely of type Id + // * Constraint: Applications must be towards organizations + return model.getAddressBook() + .getContactList() + .stream() + .filter(c -> c.getType() == Type.ORGANIZATION) + .map(o -> o.getId().value); + } + }); + + map.put(FLAG_STAGE, (command, model) + -> Arrays.stream(ApplicationStage.values()) + .map(ApplicationStage::toString)); + + map.put(FLAG_STATUS, (command, model) + -> Arrays.stream(JobStatus.values()) + .map(JobStatus::toString)); + }); + + public static final String MESSAGE_USAGE = "Adds a new job application.\n" + + "Parameters: " + + "INDEX/ID " + + FLAG_TITLE + " TITLE " + + "[" + FLAG_DESCRIPTION + " DESCRIPTION] " + + "[" + FLAG_DEADLINE + " DEADLINE: DD-MM-YYYY] " + + "[" + FLAG_STAGE + " APPLICATION STAGE: resume/online assessment/interview] " + + "[" + FLAG_STATUS + " STATUS: pending/offered/accepted/turned down] "; + + public static final String MESSAGE_APPLY_SUCCESS = "Added application: %1$s to %2$s"; + + public static final String MESSAGE_DUPLICATE_APPLICATION = "This application already exists " + + "for the organization you are trying to apply to"; + public static final String MESSAGE_ATTEMPT_TO_ADD_TO_NON_ORG = "Attempted to apply to a non-organization: %1$s"; + + private static final Logger logger = LogsCenter.getLogger(ApplyCommand.class); + + private final Id oid; + + private final Index index; + + private final JobTitle title; + + private final JobDescription description; + + private final Deadline deadline; + + private final JobStatus status; + + private final ApplicationStage applicationStage; + + /** + * Creates an executable ApplyCommand to add {@code JobApplication} + */ + public ApplyCommand(Id oid, Index index, JobTitle title, JobDescription description, Deadline deadline, + JobStatus status, ApplicationStage applicationStage) { + this.oid = oid; + this.index = index; + this.title = title; + this.description = description; // optional. No default value + this.deadline = deadline; // optional with default value + this.applicationStage = applicationStage; // optional with default value + this.status = status; // optional with default value + } + + @Override + public CommandResult execute(Model model) throws CommandException { + Organization org = getOrganization(index, oid, model); + JobApplication ja = new JobApplication(org, title, description, deadline, status, applicationStage); + if (org.hasJobApplication(ja)) { + throw new CommandException(MESSAGE_DUPLICATE_APPLICATION); + } + org.addJobApplication(ja); + model.addApplication(ja); + return new CommandResult(String.format(MESSAGE_APPLY_SUCCESS, ja, org)); + } + + private static Organization getOrganization(Index index, Id id, Model model) throws CommandException { + Contact org; + try { + org = model.getContactByIdXorIndex(id, index); + } catch (IllegalValueException e) { + throw new CommandException(e.getMessage()); + } + if (org.getType() != Type.ORGANIZATION) { + throw new CommandException(String.format(MESSAGE_ATTEMPT_TO_ADD_TO_NON_ORG, org)); + } + return (Organization) org; + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..b9bcae1b1e1 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -1,5 +1,13 @@ package seedu.address.logic.commands; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.autocomplete.AutocompleteSupplier; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; @@ -17,4 +25,70 @@ public abstract class Command { */ public abstract CommandResult execute(Model model) throws CommandException; + + /** + * Obtains the {@code AUTOCOMPLETE_SUPPLIER} which can supply autocompletion results for a given command. + * + * @param cls The subclass of {@link Command} to obtain the autocomplete supplier for. + * @return The autocomplete supplier as an optional. + */ + public static Optional getAutocompleteSupplier(Class cls) { + AutocompleteSupplier supplier = null; + try { + Field field = cls.getDeclaredField("AUTOCOMPLETE_SUPPLIER"); + Object data = field.get(cls); + if (data instanceof AutocompleteSupplier) { + supplier = (AutocompleteSupplier) data; + } + + } catch (NoSuchFieldException | IllegalAccessException e) { + // No available autocompletion results... too bad. + } + + return Optional.ofNullable(supplier); + } + + /** + * Obtains the {@code COMMAND_WORD} which represents the command name for a given command. + * + * @param cls The subclass of {@link Command} to obtain the command word for. + * @return The command word as an optional string. + */ + public static Optional getCommandWord(Class cls) { + String value = null; + try { + Field field = cls.getDeclaredField("COMMAND_WORD"); + Object data = field.get(cls); + if (data instanceof String) { + value = (String) data; + } + + } catch (NoSuchFieldException | IllegalAccessException e) { + // Leave value as null... + } + + return Optional.ofNullable(value); + } + + /** + * Obtains a stream of {@code COMMAND_WORD}s which represent the command names for all given commands. + * + * @param clsStream The subclasses of {@link Command} to obtain the command word for. + * @return The command words as a list of optional strings. + */ + public static Stream> getCommandWords(Stream> clsStream) { + return clsStream.map(Command::getCommandWord); + } + + /** + * Obtains a list of {@code COMMAND_WORD}s which represent the command names for all given commands. + * + * @see #getCommandWords(Stream) + */ + public static List> getCommandWords(Collection> clsList) { + return getCommandWords(clsList.stream()).collect(Collectors.toList()); + } + + + } diff --git a/src/main/java/seedu/address/logic/commands/DeleteApplicationCommand.java b/src/main/java/seedu/address/logic/commands/DeleteApplicationCommand.java new file mode 100644 index 00000000000..36eadca3756 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteApplicationCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.jobapplication.JobApplication; + +/** + * Delete command but for application. + */ +public class DeleteApplicationCommand extends DeleteCommand { + + public static final String MESSAGE_DELETE_APPLICATION_SUCCESS = "Deleted application: %1$s"; + + private final Index index; + + /** + * Creates new DeleteCommand with the index of the application. + */ + public DeleteApplicationCommand(Index targetIndex) { + super(targetIndex); + this.index = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List applications = model.getDisplayedApplicationList(); + + if (index.getZeroBased() >= applications.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICATION_DISPLAYED_INDEX); + } + JobApplication application = applications.get(index.getZeroBased()); + try { + model.deleteApplication(application); + } catch (IllegalValueException e) { + throw new CommandException(e.getMessage()); + } + return new CommandResult(String.format(MESSAGE_DELETE_APPLICATION_SUCCESS, application)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof DeleteApplicationCommand)) { + return false; + } + + DeleteApplicationCommand cmd = (DeleteApplicationCommand) other; + return this.index.equals(cmd.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..ef615852c40 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,48 +1,175 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.FLAG_APPLICATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECURSIVE; import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Recruiter; +import seedu.address.model.contact.Type; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a contact identified using its displayed index or its contact id from the address book. */ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AutocompleteItemSet.oneAmongAllOf( + FLAG_RECURSIVE, FLAG_APPLICATION + ) + ).configureValueMap(map -> { + // Add value autocompletion data for: + map.put(null /* preamble */, (command, model) -> { + + String partialText = command.getAutocompletableText(); + if (partialText.isEmpty() || StringUtil.isNonZeroUnsignedInteger(partialText)) { + // Preamble is likely of type Index + return Stream.empty(); + + } else { + // Preamble is likely of type Id + return model.getAddressBook() + .getContactList() + .stream() + .map(o -> o.getId().value); + } + }); + }); + + public static final String MESSAGE_ILLEGAL_DELETE = "Contacts of type %s cannot have links to a parent contact."; + + public static final String MESSAGE_CONTACT_USAGE = "Deletes a contact.\n" + + "Parameters: " + + "INDEX/ID " + + "[" + FLAG_RECURSIVE + "] " + + "\n" + + "Example 1: " + COMMAND_WORD + " 1\n" + + "Example 2: " + COMMAND_WORD + " amazon-sg\n" + + "Example 3: " + COMMAND_WORD + " 1 --recursive\n"; + + public static final String MESSAGE_APPLICATION_USAGE = "Deletes a job application.\n" + + "Parameters: " + + FLAG_APPLICATION + " INDEX " + + "\n" + + "Example 1: " + COMMAND_WORD + " --application 1\n"; + 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"; + + ": Deletes the contact identified by the index number used in the displayed contact list." + + " Also can delete job applications by index displayed in the application list." + + MESSAGE_CONTACT_USAGE + "\n\n" + + MESSAGE_APPLICATION_USAGE; + + public static final String MESSAGE_DELETE_CONTACT_SUCCESS = "Deleted %s: %s"; + + protected final CommandException commandException; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + private final Object selector; // TODO: This is very sus but this will only be used for equals comparison - private final Index targetIndex; + private final Function contactFunction; + /** + * @param targetIndex of the contact to be deleted + */ public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; + this.selector = targetIndex; + this.contactFunction = (Model model) -> { + List lastShownList = model.getDisplayedContactList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + return null; + } + + return lastShownList.get(targetIndex.getZeroBased()); + }; + this.commandException = new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + + /** + * @param targetId of the contact to be deleted + */ + public DeleteCommand(Id targetId) { + this.selector = targetId; + this.contactFunction = (Model model) -> model.getContactById(targetId); + this.commandException = new CommandException(Messages.MESSAGE_NO_SUCH_CONTACT); + } + + /** + * Creates an executable DeleteCommand based on whether to delete recursively. + * + * @param targetIndex of the contact to delete in the current list + * @param shouldDeleteChildren specifies if child contacts should be deleted + */ + public static DeleteCommand selectIndex(Index targetIndex, boolean shouldDeleteChildren) { + // TODO: Add documentation to DG + requireNonNull(targetIndex); + if (shouldDeleteChildren) { + return new DeleteWithChildrenCommand(targetIndex); + } + return new DeleteCommand(targetIndex); + } + + /** + * Creates an executable DeleteCommand based on whether to delete recursively. + * + * @param id of the contact to delete in the current list + * @param shouldDeleteChildren specifies if child contacts should be deleted + */ + public static DeleteCommand selectId(Id id, boolean shouldDeleteChildren) { + // TODO: Add documentation to DG + requireNonNull(id); + if (shouldDeleteChildren) { + return new DeleteWithChildrenCommand(id); + } + return new DeleteCommand(id); } @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); + Contact contactToDelete = this.contactFunction.apply(model); + if (contactToDelete == null) { + throw commandException; } + model.deleteContact(contactToDelete); + handleChildren(model, contactToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_CONTACT_SUCCESS, + contactToDelete.getType(), Messages.format(contactToDelete))); + } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + protected void handleChildren(Model model, Contact contactToDelete) throws CommandException { + requireNonNull(model); + // At this point if the contact is null, the superclass would have thrown exception. + // Superclass would have also deleted the contact from the list. + assert contactToDelete != null; + List childrenContacts = contactToDelete.getChildren(model); + for (Contact child : childrenContacts) { + // Enforce that only recruiters can have links to parent organizations. + if (child.getType() != Type.RECRUITER) { + throw new CommandException(String.format(MESSAGE_ILLEGAL_DELETE, child.getType().toString())); + } + // Create a new recruiter with no link to the deleted organization. + Recruiter newRecruiter = new Recruiter( + child.getName(), child.getId(), child.getPhone().orElse(null), + child.getEmail().orElse(null), child.getUrl().orElse(null), + child.getAddress().orElse(null), child.getTags(), null + ); + model.setContact(child, newRecruiter); + } } @Override @@ -57,13 +184,23 @@ public boolean equals(Object other) { } DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + return selector.equals(otherDeleteCommand.selector); } @Override public String toString() { + // TODO: replace this toString method with sth better than targetIndex + // To not replace yet until we do the tests return new ToStringBuilder(this) - .add("targetIndex", targetIndex) + .add("targetIndex", selector) .toString(); } + + /** + * Gives the contact that the DeleteCommand is going to delete if a model is given. + * If such a contact does not exist, gives null. + */ + protected Contact getContact(Model model) { + return contactFunction.apply(model); + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteWithChildrenCommand.java b/src/main/java/seedu/address/logic/commands/DeleteWithChildrenCommand.java new file mode 100644 index 00000000000..e561e2cb866 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteWithChildrenCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; + +/** + * Deletes a contact and also deletes its child contacts. + */ +public class DeleteWithChildrenCommand extends DeleteCommand { + + public static final String MESSAGE_DELETE_CONTACT_SUCCESS = DeleteCommand.MESSAGE_DELETE_CONTACT_SUCCESS + " with" + + ":\n%2$s"; + + + /** + * @param targetIndex of the contact to be deleted in the current contact list + */ + public DeleteWithChildrenCommand(Index targetIndex) { + // TODO add documentation in DG + super(targetIndex); + } + + public DeleteWithChildrenCommand(Id targetId) { + super(targetId); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Contact contactToDelete = super.getContact(model); + if (contactToDelete == null) { + throw commandException; + } + List childrenContacts = contactToDelete.getChildren(model); + super.execute(model); + return new CommandResult(String.format( + MESSAGE_DELETE_CONTACT_SUCCESS, + Messages.format(contactToDelete), + Messages.formatChildren(childrenContacts) + )); + } + + @Override + protected void handleChildren(Model model, Contact contactToDelete) { + // At this point if the contact is null, the superclass would have thrown exception. + // Superclass would have also deleted the contact from the list. + assert contactToDelete != null; + List childrenContacts = contactToDelete.getChildren(model); + childrenContacts.forEach(model::deleteContact); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditApplicationCommand.java b/src/main/java/seedu/address/logic/commands/EditApplicationCommand.java new file mode 100644 index 00000000000..8a2f0c1fd9e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditApplicationCommand.java @@ -0,0 +1,203 @@ +package seedu.address.logic.commands; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.Deadline; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobDescription; +import seedu.address.model.jobapplication.JobStatus; +import seedu.address.model.jobapplication.JobTitle; +import seedu.address.model.jobapplication.LastUpdatedTime; + +/** + * Edit command but for application. + */ +public class EditApplicationCommand extends EditCommand { + + public static final String MESSAGE_EDIT_APPLICATION_SUCCESS = "Edited job application: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + + private final Index targetIndex; + private final EditApplicationDescriptor editApplicationDescriptor; + + /** + * Creates a command based on the index of the application in the list and the description given. + * @param index of the application in the list + * @param editApplicationDescriptor of the application to edit + */ + public EditApplicationCommand(Index index, EditApplicationDescriptor editApplicationDescriptor) { + super(index, new EditContactDescriptor()); // dummy things to make the compiler happy. + this.targetIndex = index; + this.editApplicationDescriptor = new EditApplicationDescriptor(editApplicationDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + if (!editApplicationDescriptor.isAnyFieldEdited()) { + throw new CommandException(MESSAGE_NOT_EDITED); + } + List lastShownList = model.getDisplayedApplicationList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICATION_DISPLAYED_INDEX); + } + + JobApplication jobApplication = lastShownList.get(targetIndex.getZeroBased()); + JobApplication newApplication = createApplication(jobApplication, editApplicationDescriptor); + + try { + model.replaceApplication(jobApplication, newApplication); + } catch (IllegalValueException e) { + throw new CommandException(e.getMessage()); + } + + return new CommandResult(String.format(MESSAGE_EDIT_APPLICATION_SUCCESS, newApplication)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EditApplicationCommand)) { + return false; + } + EditApplicationCommand cmd = (EditApplicationCommand) other; + return this.targetIndex.equals(cmd.targetIndex) + && this.editApplicationDescriptor.equals(cmd.editApplicationDescriptor); + } + + private static JobApplication createApplication(JobApplication jobApplication, + EditApplicationDescriptor editApplicationDescriptor) { + Objects.requireNonNull(jobApplication); + Objects.requireNonNull(editApplicationDescriptor); + + Id oid = jobApplication.getOrganizationId(); + Name name = jobApplication.getOrgName(); + JobTitle jobTitle = editApplicationDescriptor.getTitle().orElse(jobApplication.getJobTitle()); + JobDescription jobDescription = + editApplicationDescriptor.getDescription().orElse(jobApplication.getJobDescription() + .orElse(null)); + JobStatus status = editApplicationDescriptor.getStatus().orElse(jobApplication.getStatus()); + ApplicationStage stage = editApplicationDescriptor.getStage().orElse(jobApplication.getApplicationStage()); + Deadline deadline = editApplicationDescriptor.getDeadline().orElse(jobApplication.getDeadline()); + + return new JobApplication(oid, name, jobTitle, jobDescription, deadline, status, stage, new LastUpdatedTime()); + } + + /** + * Class to store information on details to edit. + */ + public static class EditApplicationDescriptor { + + + private JobTitle jobTitle; + + private JobDescription jobDescription; + + private Deadline deadline; + + private JobStatus status; + + private ApplicationStage applicationStage; + + public EditApplicationDescriptor() {} + + /** + * Creates a shallow copy of {@code EditApplicationDescriptor} + */ + public EditApplicationDescriptor(EditApplicationDescriptor toCopy) { + setDeadline(toCopy.deadline); + setApplicationStage(toCopy.applicationStage); + setJobDescription(toCopy.jobDescription); + setJobTitle(toCopy.jobTitle); + setStatus(toCopy.status); + } + + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(deadline, applicationStage, jobDescription, jobTitle, + status); + } + + public void setStatus(JobStatus status) { + this.status = status; + } + + public Optional getStatus() { + return Optional.ofNullable(status); + } + + public void setJobTitle(JobTitle title) { + this.jobTitle = title; + } + + public Optional getTitle() { + return Optional.ofNullable(jobTitle); + } + + public void setJobDescription(JobDescription description) { + this.jobDescription = description; + } + + public Optional getDescription() { + return Optional.ofNullable(jobDescription); + } + + public void setDeadline(Deadline deadline) { + this.deadline = deadline; + } + + public Optional getDeadline() { + return Optional.ofNullable(deadline); + } + + public void setApplicationStage(ApplicationStage applicationStage) { + this.applicationStage = applicationStage; + } + + public Optional getStage() { + return Optional.ofNullable(applicationStage); + } + + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("jobTitle", jobTitle) + .add("description", jobDescription) + .add("status", status) + .add("stage", applicationStage) + .toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EditApplicationDescriptor)) { + return false; + } + + EditApplicationDescriptor editDescriptor = (EditApplicationDescriptor) other; + + // Objects.equals handles null cases as well. + return Objects.equals(this.jobDescription, editDescriptor.jobDescription) + && Objects.equals(this.applicationStage, editDescriptor.applicationStage) + && Objects.equals(this.deadline, editDescriptor.deadline) + && Objects.equals(this.jobTitle, editDescriptor.jobTitle) + && Objects.equals(this.status, editDescriptor.status); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..f2cdc695a38 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,107 +1,307 @@ 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 static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_APPLICATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; + +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Flag; 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.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Recruiter; +import seedu.address.model.contact.Type; +import seedu.address.model.contact.Url; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobStatus; import seedu.address.model.tag.Tag; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing contact 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" + public static final AutocompleteItemSet AUTOCOMPLETE_SET_STANDARD = AutocompleteItemSet.concat( + AutocompleteItemSet.onceForEachOf( + FLAG_NAME, FLAG_ID, + FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, + FLAG_ORGANIZATION_ID + ), + AutocompleteItemSet.anyNumberOf(FLAG_TAG) + ); + + public static final AutocompleteItemSet AUTOCOMPLETE_SET_APPLICATION = AutocompleteItemSet + .onceForEachOf(FLAG_APPLICATION) + .addDependents( + AutocompleteItemSet.onceForEachOf( + FLAG_TITLE, FLAG_DESCRIPTION, FLAG_DEADLINE, FLAG_STAGE, FLAG_STATUS + )) + .addConstraint( + AutocompleteConstraint.where(FLAG_APPLICATION).cannotExistAlongsideAnyOf( + AUTOCOMPLETE_SET_STANDARD.getElements().toArray(Flag[]::new) + ) + ); + + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AUTOCOMPLETE_SET_STANDARD, + AUTOCOMPLETE_SET_APPLICATION + ).configureValueMap(map -> { + + // Add value autocompletion data for: + map.put(null /* preamble*/, (command, model) -> { + + String partialText = command.getAutocompletableText(); + if (partialText.isEmpty() || StringUtil.isNonZeroUnsignedInteger(partialText)) { + // Preamble is likely of type Index + return Stream.empty(); + + } else { + // Preamble is likely of type Id + return model.getAddressBook() + .getContactList() + .stream() + .map(o -> o.getId().value); + } + }); + + map.put(FLAG_ORGANIZATION_ID, (command, model) -> model.getAddressBook() + .getContactList() + .stream() + .filter(c -> c.getType() == Type.ORGANIZATION) + .map(o -> o.getId().value) + ); + + map.put(FLAG_STAGE, (command, model) + -> Arrays.stream(ApplicationStage.values()) + .map(ApplicationStage::toString)); + + map.put(FLAG_STATUS, (command, model) + -> Arrays.stream(JobStatus.values()) + .map(JobStatus::toString)); + }); + + public static final String MESSAGE_ORGANIZATION_USAGE = "Edits an organization.\n" + + "Parameters: INDEX/ID " + + "[" + FLAG_NAME + " NAME] " + + "[" + FLAG_ID + " ID] " + + "[" + FLAG_PHONE + " PHONE] " + + "[" + FLAG_EMAIL + " EMAIL] " + + "[" + FLAG_URL + " URL] " + + "[" + FLAG_ADDRESS + " ADDRESS] " + + "[" + FLAG_TAG + " TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 "; + + public static final String MESSAGE_RECRUITER_USAGE = "Edits a recruiter.\n" + + "Parameters: INDEX/ID " + + "[" + FLAG_NAME + " NAME] " + + "[" + FLAG_ID + " ID] " + + "[" + FLAG_PHONE + " PHONE] " + + "[" + FLAG_EMAIL + " EMAIL] " + + "[" + FLAG_URL + " URL] " + + "[" + FLAG_ADDRESS + " ADDRESS] " + + "[" + FLAG_ORGANIZATION_ID + " OID] " + + "[" + FLAG_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"; + + FLAG_PHONE + " 91234567 " + + FLAG_EMAIL + " rexrecruiter@example.com"; + + public static final String MESSAGE_APPLICATION_USAGE = "Edits a job application.\n" + + "Parameters: " + + FLAG_APPLICATION + " INDEX " + + "[" + FLAG_TITLE + " TITLE] " + + "[" + FLAG_DESCRIPTION + " DESCRIPTION] " + + "[" + FLAG_DEADLINE + " DEADLINE] " + + "[" + FLAG_STAGE + " STAGE] " + + "[" + FLAG_STATUS + " STATUS] \n" + + "Example: " + COMMAND_WORD + " " + FLAG_APPLICATION + " 1 " + FLAG_TITLE + " SWE"; + + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the details of the contact of the class type Organization or Recruiter," + + " identified by its index in the displayed contact list or its id." + + " Note that existing values will be overwritten by the input values." + + " Also can edit job applications in the list identified by its index" + + " The input format varies depending on the class:\n\n" + + MESSAGE_ORGANIZATION_USAGE + "\n\n" + + MESSAGE_RECRUITER_USAGE + "\n\n" + + MESSAGE_APPLICATION_USAGE; + + public static final String MESSAGE_EDIT_CONTACT_SUCCESS = "Edited %s: %s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the address book."; + public static final String MESSAGE_INVALID_ORGANIZATION = + "The organization id you supplied does not match any organization in the address book."; - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; + private Index index; + + private Id targetId = null; + private EditContactDescriptor editContactDescriptor; /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with + * @param index of the contact in the filtered contact list to edit + * @param editContactDescriptor details to edit the contact with */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + public EditCommand(Index index, EditContactDescriptor editContactDescriptor) { requireNonNull(index); - requireNonNull(editPersonDescriptor); + requireNonNull(editContactDescriptor); this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + this.editContactDescriptor = new EditContactDescriptor(editContactDescriptor); } + /** + * @param targetId of the contact to be editted + */ + public EditCommand(Id targetId, EditContactDescriptor editContactDescriptor) { + this.targetId = targetId; + this.editContactDescriptor = new EditContactDescriptor(editContactDescriptor); + } + + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + if (this.targetId != null) { + Contact contactToEdit = model.getContactById(targetId); + if (contactToEdit == null) { + throw new CommandException(Messages.MESSAGE_NO_SUCH_CONTACT); + } + return updateModelAndGetCommandResult(model, contactToEdit); + } + + List lastShownList = model.getDisplayedContactList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + Contact contactToEdit = lastShownList.get(index.getZeroBased()); + return updateModelAndGetCommandResult(model, contactToEdit); + } + + private CommandResult updateModelAndGetCommandResult(Model model, Contact contactToEdit) throws CommandException { + Contact editedContact = createEditedContact(model, contactToEdit, editContactDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (!contactToEdit.isSameContact(editedContact) && model.hasContact(editedContact)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); } - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + if (editedContact.getType() == Type.ORGANIZATION) { + updateLinkedRecruiters(model, (Organization) contactToEdit, (Organization) editedContact); + } + + model.setContact(contactToEdit, editedContact); + model.updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); + return new CommandResult(String.format(MESSAGE_EDIT_CONTACT_SUCCESS, + editedContact.getType(), Messages.format(editedContact))); } /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. + * Creates and returns a {@code Contact} with the details of {@code contactToEdit} + * edited with {@code editContactDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; + private static Contact createEditedContact(Model model, Contact contactToEdit, + EditContactDescriptor editContactDescriptor) throws CommandException { + assert contactToEdit != null; + + Name updatedName = editContactDescriptor.getName() + .orElse(contactToEdit.getName()); + Id updatedId = editContactDescriptor.getId() + .orElse(contactToEdit.getId()); + Phone updatedPhone = editContactDescriptor.getPhone() + .orElse(contactToEdit.getPhone().orElse(null)); + Email updatedEmail = editContactDescriptor.getEmail() + .orElse(contactToEdit.getEmail().orElse(null)); + Url updatedUrl = editContactDescriptor.getUrl() + .orElse(contactToEdit.getUrl().orElse(null)); + Address updatedAddress = editContactDescriptor.getAddress() + .orElse(contactToEdit.getAddress().orElse(null)); + Set updatedTags = editContactDescriptor.getTags() + .orElse(contactToEdit.getTags()); + + // TODO: Refactor into two methods to handle the two cases. + if (contactToEdit.getType() == Type.ORGANIZATION) { + Organization org = (Organization) contactToEdit; + List applications = Arrays.asList(org.getJobApplications()); + + return new Organization(updatedName, updatedId, updatedPhone, updatedEmail, + updatedUrl, updatedAddress, updatedTags, applications); + + } else if (contactToEdit.getType() == Type.RECRUITER) { + Optional updatedOid = editContactDescriptor + .getOrganizationId() + .or(() -> ((Recruiter) contactToEdit).getOrganizationId()); + + Organization linkedOrganization = (Organization) updatedOid.map(model::getContactById) + .filter(c -> c.getType() == Type.ORGANIZATION) + .orElse(null); + + if (updatedOid.isPresent() && linkedOrganization == null) { + throw new CommandException(MESSAGE_INVALID_ORGANIZATION); + } - 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 Recruiter(updatedName, updatedId, updatedPhone, updatedEmail, updatedUrl, + updatedAddress, updatedTags, linkedOrganization); + } - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + throw new IllegalStateException("Contact being edited should be of type Recruiter or Organization"); + } + + /** + * Updates all recruiters linked to the {@code oldOrganization} to link to the {@code updatedOrganization}. + */ + private static void updateLinkedRecruiters(Model model, + Organization oldOrganization, + Organization updatedOrganization) { + // Updates all recruiters linked to the old organization to link to the updated one. + List childrenContacts = oldOrganization.getChildren(model); + for (Contact child : childrenContacts) { + assert child.getType() == Type.RECRUITER; + Recruiter updatedRecruiter = new Recruiter( + child.getName(), child.getId(), child.getPhone().orElse(null), + child.getEmail().orElse(null), child.getUrl().orElse(null), + child.getAddress().orElse(null), child.getTags(), updatedOrganization + ); + model.setContact(child, updatedRecruiter); + } } @Override @@ -117,57 +317,79 @@ public boolean equals(Object other) { EditCommand otherEditCommand = (EditCommand) other; return index.equals(otherEditCommand.index) - && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); + && editContactDescriptor.equals(otherEditCommand.editContactDescriptor); } @Override public String toString() { return new ToStringBuilder(this) .add("index", index) - .add("editPersonDescriptor", editPersonDescriptor) + .add("editContactDescriptor", editContactDescriptor) .toString(); } /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. + * Stores the details to edit the contact with. Each non-empty field value will replace the + * corresponding field value of the contact. */ - public static class EditPersonDescriptor { + public static class EditContactDescriptor { private Name name; + private Id id; private Phone phone; private Email email; + private Url url; private Address address; private Set tags; + private Id oid; - public EditPersonDescriptor() {} + public EditContactDescriptor() {} /** * Copy constructor. * A defensive copy of {@code tags} is used internally. */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { + public EditContactDescriptor(EditContactDescriptor toCopy) { setName(toCopy.name); + setId(toCopy.id); setPhone(toCopy.phone); setEmail(toCopy.email); + setUrl(toCopy.url); setAddress(toCopy.address); setTags(toCopy.tags); + setOid(toCopy.oid); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags, id, url, oid); } public void setName(Name name) { this.name = name; } + public Optional getOrganizationId() { + return Optional.ofNullable(oid); + } + + public void setOid(Id id) { + this.oid = id; + } + public Optional getName() { return Optional.ofNullable(name); } + public Optional getId() { + return Optional.ofNullable(id); + } + + public void setId(Id id) { + this.id = id; + } + public void setPhone(Phone phone) { this.phone = phone; } @@ -184,6 +406,14 @@ public Optional getEmail() { return Optional.ofNullable(email); } + public Optional getUrl() { + return Optional.ofNullable(url); + } + + public void setUrl(Url url) { + this.url = url; + } + public void setAddress(Address address) { this.address = address; } @@ -216,16 +446,16 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { + if (!(other instanceof EditContactDescriptor)) { return false; } - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; - return Objects.equals(name, otherEditPersonDescriptor.name) - && Objects.equals(phone, otherEditPersonDescriptor.phone) - && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + EditContactDescriptor otherEditContactDescriptor = (EditContactDescriptor) other; + return Objects.equals(name, otherEditContactDescriptor.name) + && Objects.equals(phone, otherEditContactDescriptor.phone) + && Objects.equals(email, otherEditContactDescriptor.email) + && Objects.equals(address, otherEditContactDescriptor.address) + && Objects.equals(tags, otherEditContactDescriptor.tags); } @Override @@ -236,6 +466,7 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("url", url) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..224031f00f7 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -5,17 +5,17 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.contact.NameContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all contacts 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 " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts 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"; @@ -29,9 +29,10 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + model.updateFilteredContactList(predicate); return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + String.format(Messages.MESSAGE_CONTACTS_LISTED_OVERVIEW, + model.getDisplayedContactList().size())); } @Override diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..c890346be0b 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,24 +1,72 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.logic.parser.CliSyntax.FLAG_NOT_APPLIED; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECRUITER; +import static seedu.address.model.Model.PREDICATE_SHOW_NOT_APPLIED_ORGANIZATIONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ONLY_ORGANIZATIONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ONLY_RECRUITERS; +import java.util.function.Predicate; + +import seedu.address.logic.autocomplete.AutocompleteSupplier; import seedu.address.model.Model; +import seedu.address.model.contact.Contact; /** - * Lists all persons in the address book to the user. + * Lists all contacts in the address book to the user. */ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.fromUniqueFlags( + FLAG_ORGANIZATION, FLAG_RECRUITER, FLAG_NOT_APPLIED + ); + + public static final String MESSAGE_SUCCESS_ALL_CONTACTS = "Listed all contacts"; + public static final String MESSAGE_SUCCESS_ORGANIZATIONS = "Listed all organizations"; + public static final String MESSAGE_SUCCESS_RECRUITERS = "Listed all recruiters"; + public static final String MESSAGE_SUCCESS_TO_APPLY = "Listed all organizations that have not been applied to."; + private final Predicate predicate; + + /** + * Creates a ListCommand listing the {@code Contact} entries of the specified type. + * @param predicate the predicate determining the type of {@code Contact} to be listed + */ + public ListCommand(Predicate predicate) { + requireNonNull(predicate); + this.predicate = predicate; + } @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); + model.updateFilteredContactList(predicate); + if (predicate.equals(PREDICATE_SHOW_ONLY_ORGANIZATIONS)) { + return new CommandResult(MESSAGE_SUCCESS_ORGANIZATIONS); + } else if (predicate.equals(PREDICATE_SHOW_ONLY_RECRUITERS)) { + return new CommandResult(MESSAGE_SUCCESS_RECRUITERS); + } else if (predicate.equals(PREDICATE_SHOW_NOT_APPLIED_ORGANIZATIONS)) { + return new CommandResult(MESSAGE_SUCCESS_TO_APPLY); + } + return new CommandResult(MESSAGE_SUCCESS_ALL_CONTACTS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ListCommand)) { + return false; + } + + ListCommand otherListCommand = (ListCommand) other; + return predicate.equals(otherListCommand.predicate); } } diff --git a/src/main/java/seedu/address/logic/commands/ReminderCommand.java b/src/main/java/seedu/address/logic/commands/ReminderCommand.java new file mode 100644 index 00000000000..d3cd072ee5e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ReminderCommand.java @@ -0,0 +1,69 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.FLAG_EARLIEST; +import static seedu.address.logic.parser.CliSyntax.FLAG_LATEST; + +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.model.Model; +import seedu.address.model.jobapplication.JobApplication; + +/** + * Reminds the user of urgent or stale applications, similar to the usage of {@code SortCommand}. + */ +public class ReminderCommand extends Command { + public static final String COMMAND_WORD = "remind"; + + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AutocompleteItemSet.oneAmongAllOf( + FLAG_EARLIEST, FLAG_LATEST + ) + ).configureValueMap(map -> { + // Disable value autocompletion for: + map.put(null /* preamble */, null); + }); + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Reminds the user of applications based on the specified flag.\n" + + "Parameters: " + FLAG_EARLIEST + "/" + FLAG_LATEST + "\n" + + "Example 1: " + COMMAND_WORD + " --earliest\n" + + "Example 2: " + COMMAND_WORD + " --latest\n"; + + public static final String MESSAGE_REMINDED_EARLIEST = "Reminded user of high priority applications"; + public static final String MESSAGE_REMINDED_LATEST = "Reminded user of low priority applications"; + private final Boolean isUrgent; + + /** + * Creates a ReminderCommand sorting the {@code JobApplication} entries by deadline. + * @param isUrgent checks if the {@code ReminderCommand} should display urgent or stale applications. + */ + public ReminderCommand(Boolean isUrgent) { + this.isUrgent = isUrgent; + } + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (isUrgent) { + model.updateSortedApplicationList(JobApplication.DEADLINE_COMPARATOR); + return new CommandResult(MESSAGE_REMINDED_EARLIEST); + } + model.updateSortedApplicationList(JobApplication.DEADLINE_COMPARATOR.reversed()); + return new CommandResult(MESSAGE_REMINDED_LATEST); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ReminderCommand)) { + return false; + } + + ReminderCommand otherReminderCommand = (ReminderCommand) other; + return this.isUrgent.equals(otherReminderCommand.isUrgent); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..de6de129df8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,127 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_ASCENDING; +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCENDING; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_NONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STALE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; + +import java.util.Comparator; +import java.util.Objects; + +import seedu.address.logic.autocomplete.AutocompleteSupplier; +import seedu.address.logic.autocomplete.components.AutocompleteConstraint; +import seedu.address.logic.autocomplete.components.AutocompleteItemSet; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.jobapplication.JobApplication; + +/** + * Sorts contacts and job applications in the address book. + */ +public class SortCommand extends Command { + public static final String COMMAND_WORD = "sort"; + + public static final AutocompleteSupplier AUTOCOMPLETE_SUPPLIER = AutocompleteSupplier.from( + AutocompleteItemSet.concat( + AutocompleteItemSet.oneAmongAllOf( + FLAG_NAME, FLAG_ID, FLAG_PHONE, FLAG_EMAIL, FLAG_ADDRESS, FLAG_URL, + FLAG_STALE, FLAG_STAGE, FLAG_STATUS, FLAG_DEADLINE, FLAG_TITLE, + FLAG_NONE + ), AutocompleteItemSet.oneAmongAllOf( + FLAG_ASCENDING, FLAG_DESCENDING + ) + ).addConstraint( + AutocompleteConstraint.oneAmongAllOf( + FLAG_NONE, FLAG_ASCENDING, FLAG_DESCENDING + ) + ) + ).configureValueMap(map -> { + // Disable value autocompletion for: + map.put(null /* preamble */, null); + }); + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Sorts contacts or applications based on the specified flag.\n" + + "Parameters: " + FLAG_NAME + "/" + FLAG_ID + "/" + + FLAG_PHONE + "/" + FLAG_EMAIL + "/" + + FLAG_ADDRESS + "/" + FLAG_URL + "/" + + FLAG_DEADLINE + "/" + FLAG_STATUS + "/" + + FLAG_STAGE + "/" + FLAG_STALE + "/" + + FLAG_TITLE + "/" + FLAG_NONE + + " [" + FLAG_ASCENDING + "/" + FLAG_DESCENDING + "]\n" + + "Example 1: " + COMMAND_WORD + " --name\n" + + "Example 2: " + COMMAND_WORD + " --id" + " --descending\n" + + "Example 3: " + COMMAND_WORD + " --stale" + " --ascending\n"; + + public static final String MESSAGE_SORTED_CONTACTS = "Sorted contacts as specified"; + public static final String MESSAGE_SORTED_APPLICATIONS = "Sorted applications as specified"; + public static final String MESSAGE_RESET_SORTING = "Sorting reset to default order"; + + private final Comparator comparatorContact; + private final Comparator comparatorApplication; + private final Boolean isReset; + + /** + * Creates a SortCommand sorting the {@code Contact} entries and the {@code JobApplication} + * entries by the specified comparators. + * @param contactComparator the comparator determining the sorting of {@code Contact} entries. + * May be null, should the SortCommand seek to sort applications instead. + * @param applicationComparator the comparator determining the sorting of {@code JobApplication} entries. + * May be null, should the SortCommand seek to sort contacts. + * @param isReset checks if the {@code SortCommand} is going to reset the sorting order + * to the original order. + */ + public SortCommand(Comparator contactComparator, + Comparator applicationComparator, Boolean isReset) { + this.comparatorContact = contactComparator; + this.comparatorApplication = applicationComparator; + this.isReset = isReset; + } + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (isReset) { + //both comparators should be null here + model.updateSortedContactList(comparatorContact); + model.updateSortedApplicationList(comparatorApplication); + return new CommandResult(MESSAGE_RESET_SORTING); + } + if (comparatorApplication != null) { + model.updateSortedApplicationList(comparatorApplication); + return new CommandResult(MESSAGE_SORTED_APPLICATIONS); + } + model.updateSortedContactList(comparatorContact); + return new CommandResult(MESSAGE_SORTED_CONTACTS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortCommand)) { + return false; + } + + SortCommand otherSortCommand = (SortCommand) other; + if (this.isReset) { + return otherSortCommand.isReset; + } + return Objects.equals(comparatorContact, otherSortCommand.comparatorContact) + && Objects.equals(comparatorApplication, otherSortCommand.comparatorApplication) + && !otherSortCommand.isReset; + } +} 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..f44c0c19186 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -1,5 +1,7 @@ package seedu.address.logic.commands.exceptions; +import seedu.address.logic.commands.Command; + /** * Represents an error which occurs during execution of a {@link Command}. */ diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..41b708feb09 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,22 +1,30 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECRUITER; +import static seedu.address.logic.parser.CliSyntax.FLAG_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; +import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddOrganizationCommand; +import seedu.address.logic.commands.AddRecruiterCommand; 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.contact.Address; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Url; import seedu.address.model.tag.Tag; /** @@ -25,37 +33,77 @@ 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. + * 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); + ArgumentTokenizer.tokenize(args, + AddCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new) + ); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!argMultimap.hasAllOfFlags(FLAG_NAME) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + argMultimap.verifyNoDuplicateFlagsFor(FLAG_NAME, FLAG_ID, FLAG_PHONE, FLAG_EMAIL, FLAG_URL, FLAG_ADDRESS); - Person person = new Person(name, phone, email, address, tagList); + argMultimap.verifyAtMostOneOfFlagsUsedOutOf(FLAG_ORGANIZATION, FLAG_RECRUITER); - return new AddCommand(person); + if (argMultimap.hasFlag(FLAG_ORGANIZATION)) { + return parseAsOrganization(argMultimap); + } else if (argMultimap.hasFlag(FLAG_RECRUITER)) { + return parseAsRecruiter(argMultimap); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } } - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + private AddRecruiterCommand parseAsRecruiter(ArgumentMultimap argMultimap) throws ParseException { + argMultimap.verifyNoDuplicateFlagsFor(FLAG_ORGANIZATION_ID); + Name name = ParserUtil.parseName(argMultimap.getValue(FLAG_NAME).get()); + + Optional idString = argMultimap.getValue(FLAG_ID); + Id id = idString.isPresent() + ? ParserUtil.parseId(idString.get()) + : Id.synthesizeFrom(name.fullName); + + Phone phone = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_PHONE), ParserUtil::parsePhone); + Email email = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_EMAIL), ParserUtil::parseEmail); + Address address = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_ADDRESS), ParserUtil::parseAddress); + Url url = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_URL), ParserUtil::parseUrl); + Id oid = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_ORGANIZATION_ID), ParserUtil::parseId); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(FLAG_TAG)); + + return new AddRecruiterCommand(name, id, phone, email, url, address, tagList, oid); } + private AddOrganizationCommand parseAsOrganization(ArgumentMultimap argMultimap) throws ParseException { + Name name = ParserUtil.parseName(argMultimap.getValue(FLAG_NAME).get()); + + Optional idString = argMultimap.getValue(FLAG_ID); + Id id = idString.isPresent() + ? ParserUtil.parseId(idString.get()) + : Id.synthesizeFrom(name.fullName); + + Phone phone = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_PHONE), ParserUtil::parsePhone); + Email email = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_EMAIL), ParserUtil::parseEmail); + Address address = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_ADDRESS), ParserUtil::parseAddress); + Url url = ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_URL), ParserUtil::parseUrl); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(FLAG_TAG)); + + return new AddOrganizationCommand(name, id, phone, email, url, address, tagList); + } } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index 3149ee07e0b..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,86 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.commons.core.LogsCenter; -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.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - private static final Logger logger = LogsCenter.getLogger(AddressBookParser.class); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - - // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) - // log messages such as the one below. - // Lower level log messages are used sparingly to minimize noise in the code. - logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); - - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(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(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - logger.finer("This user input caused a ParseException: " + userInput); - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AppParser.java b/src/main/java/seedu/address/logic/parser/AppParser.java new file mode 100644 index 00000000000..fd64851f330 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AppParser.java @@ -0,0 +1,149 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.parser.ClassMappings.COMMAND_TO_PARSER_MAP; + +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.autocomplete.AutocompleteGenerator; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Processes user input for the application. + * It is a utility class for parsing and performing actions on command strings app-wide. + */ +public class AppParser { + + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + private static final Logger logger = LogsCenter.getLogger(AppParser.class); + + + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string. + * @return the command based on the user input. + * @throws ParseException if the user input does not conform the expected format. + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) + // log messages such as the one below. + // Lower level log messages are used sparingly to minimize noise in the code. + logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); + + + // Iterate through the available command and parser classes and locate the one matching the command word. + final Optional> commandClass = COMMAND_TO_PARSER_MAP + .keySet() + .stream() + .filter(c -> commandWord.equals( + Command.getCommandWord(c).orElse(null) + )) + .findFirst(); + + final Optional>> parserClass = commandClass + .map(COMMAND_TO_PARSER_MAP::get) + .flatMap(x -> x); + + // Instantiate the class found and return the command instance. + try { + + if (parserClass.isPresent()) { + return parserClass.get() + .getDeclaredConstructor() + .newInstance() + .parse(arguments); + + } else if (commandClass.isPresent()) { + return commandClass + .get() + .getDeclaredConstructor() + .newInstance(); + + } else { + // We don't know what this command is. + // - Note: To add a support for new commands, add them in ClassMappings.java. + logger.finer("This user input has no known mapped commands: " + userInput); + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + + assert false + : "All command and parser classes should be correctly defined to have an no-args constructor, " + + "but " + e.getClass().getName() + " was thrown for command '" + commandWord + "'!"; + + logger.severe("This user input unexpectedly caused an error during instantiation: " + e); + logger.severe("Will report to the user that the command doesn't exist instead."); + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * Parses user input into an evaluator that can be executed to obtain autocompletion results. + * + * @param userInput full user input string. + * @return the command based on the user input. + */ + public AutocompleteGenerator parseCompletionGenerator(String userInput) { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches() && !userInput.isEmpty()) { + return AutocompleteGenerator.NO_RESULTS; + } + final String commandWord = userInput.isEmpty() ? "" : matcher.group("commandWord"); + + logger.finest("Preparing autocomplete: '" + userInput + "'"); + + + + // Case 1: There is no command name followed by a whitespace character. + // - The command name is incomplete - we're still typing the name. + // - Suggest available command names. + if (!userInput.matches(".+\\s.*")) { + return new AutocompleteGenerator(() -> + Command.getCommandWords(COMMAND_TO_PARSER_MAP.keySet().stream()) + .filter(Optional::isPresent) + .map(Optional::get) + ); + } + + // Case 2: There exists a whitespace character. + // - The command name is complete, and we're typing the arguments now. + // - Lookup and suggest with the relevant command supplier. + return COMMAND_TO_PARSER_MAP.keySet() + .stream() + .filter(cls -> commandWord.equals( + Command.getCommandWord(cls).orElse(null) + )) + .findFirst() + .flatMap(Command::getAutocompleteSupplier) + .map(AutocompleteGenerator::new) + .orElse(AutocompleteGenerator.NO_RESULTS); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java b/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java new file mode 100644 index 00000000000..2e1f21e0c8d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java @@ -0,0 +1,66 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; + +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.ApplyCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.Id; + + +/** + * Parses input arguments and creates a new ApplyCommand object + */ +public class ApplyCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ApplyCommand and returns an ApplyCommand + * object + * for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ApplyCommand parse(String args) throws ParseException { + ArgumentMultimap argumentMultimap = + ArgumentTokenizer.tokenize(args, + ApplyCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new) + ); + + if (argumentMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + ApplyCommand.MESSAGE_USAGE)); + } + + Object indexXorId = ParserUtil.parseIndexXorId(argumentMultimap.getPreamble()); + + Optional title = argumentMultimap.getValue(FLAG_TITLE); + Optional description = argumentMultimap.getValue(FLAG_DESCRIPTION); + Optional stage = argumentMultimap.getValue(FLAG_STAGE); + Optional status = argumentMultimap.getValue(FLAG_STATUS); + Optional deadline = argumentMultimap.getValue(FLAG_DEADLINE); + + if (title.isEmpty()) { + throw new ParseException(String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + ApplyCommand.MESSAGE_USAGE)); + } + + // TODO: Tech debt - Use some wrapper for indexXorId + return new ApplyCommand( + indexXorId instanceof Id ? (Id) indexXorId : null, + indexXorId instanceof Index ? (Index) indexXorId : null, + ParserUtil.parseJobTitle(title.get()), + ParserUtil.parseOptionally(description, ParserUtil::parseJobDescription), + ParserUtil.parseOptionally(deadline, ParserUtil::parseDeadline), + ParserUtil.parseOptionally(status, ParserUtil::parseJobStatus), + ParserUtil.parseOptionally(stage, ParserUtil::parseApplicationStage) + ); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 21e26887a83..2eb4d2f96b2 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -11,68 +11,185 @@ import seedu.address.logic.parser.exceptions.ParseException; /** - * Stores mapping of prefixes to their respective arguments. + * Stores mapping of flags to their respective arguments. * Each key may be associated with multiple argument values. * Values for a given key are stored in a list, and the insertion ordering is maintained. * Keys are unique, but the list of argument values may contain duplicate argument values, i.e. the same argument value - * can be inserted multiple times for the same prefix. + * can be inserted multiple times for the same flag. */ public class ArgumentMultimap { - /** Prefixes mapped to their respective arguments**/ - private final Map> argMultimap = new HashMap<>(); + /** Flags mapped to their respective values. **/ + private final Map> argMultimap = new HashMap<>(); + + /** The preamble value (the text before the first valid flag). **/ + private String preamble = ""; /** - * Associates the specified argument value with {@code prefix} key in this map. + * Associates the specified argument value with {@code flag} key in this map. * If the map previously contained a mapping for the key, the new value is appended to the list of existing values. + * Leading and trailing whitespaces are trimmed, and null values are treated as empty strings. + * + * @param flag Flag key with which the specified argument value is to be associated. + * @param argValue Argument value to be associated with the specified flag key. + */ + public void put(Flag flag, String argValue) { + List argValues = getAllValues(flag); + argValues.add(argValue == null ? "" : argValue.trim()); + argMultimap.put(flag, argValues); + } + + /** + * Associates the specified value with the preamble of this map. + * If the map previously contained a preamble, it will be replaced with this one. + * Leading and trailing whitespaces are trimmed, and null values are treated as empty strings. * - * @param prefix Prefix key with which the specified argument value is to be associated - * @param argValue Argument value to be associated with the specified prefix key + * @param preamble Argument value to be associated with the preamble. + */ + public void putPreamble(String preamble) { + this.preamble = preamble == null ? "" : preamble.trim(); + } + + /** + * Returns whether there exists at least one occurrence of the given {@code flag} in this map. + * Invoking {@code .hasFlag(flag)} is equivalent to the result obtained via {@code .getValue(flag).isPresent()}. + */ + public boolean hasFlag(Flag flag) { + return !getAllValues(flag).isEmpty(); + } + + /** + * Returns whether there exists at least one occurrence of all of these {@code flags} in this map. + * Equivalent to the AND of booleans obtained via {@link #hasFlag} for all provided flags. + */ + public boolean hasAllOfFlags(Flag... flags) { + for (Flag flag : flags) { + if (!this.hasFlag(flag)) { + return false; + } + } + return true; + } + + /** + * Returns whether there exists at least one occurrence of at least one of these {@code flags} in this map. + * Equivalent to the OR of booleans obtained via {@link #hasFlag} for all provided flags. */ - public void put(Prefix prefix, String argValue) { - List argValues = getAllValues(prefix); - argValues.add(argValue); - argMultimap.put(prefix, argValues); + public boolean hasAnyOfFlags(Flag... flags) { + for (Flag flag : flags) { + if (this.hasFlag(flag)) { + return true; + } + } + return false; } /** - * Returns the last value of {@code prefix}. + * Returns whether the given {@code flag} has a non-empty value assigned to it. + * This returns true if the flag exists and is set to some non-empty string, and false otherwise. */ - public Optional getValue(Prefix prefix) { - List values = getAllValues(prefix); + public boolean hasNonEmptyValue(Flag flag) { + for (String value : getAllValues(flag)) { + if (!value.isEmpty()) { + return true; + } + } + return false; + } + + /** + * Returns the last value of {@code flag}, if the flag exists. + * Note that an empty string or longer is guaranteed to be given if the flag exists. + */ + public Optional getValue(Flag flag) { + List values = getAllValues(flag); return values.isEmpty() ? Optional.empty() : Optional.of(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. + * Returns all values of {@code flag}. + * If the flag does not exist or has no values assigned (i.e., not even empty strings), this returns an empty list. * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. */ - public List getAllValues(Prefix prefix) { - if (!argMultimap.containsKey(prefix)) { - return new ArrayList<>(); + public List getAllValues(Flag flag) { + // Attempt to look up the flag as is + if (argMultimap.containsKey(flag)) { + return new ArrayList<>(argMultimap.get(flag)); + } + + // Attempt to search for the alias-only version as fallback + Flag aliasOnlyDefinition = flag.getAliasOnlyDefinition(); + if (argMultimap.containsKey(aliasOnlyDefinition)) { + return new ArrayList<>(argMultimap.get(aliasOnlyDefinition)); } - return new ArrayList<>(argMultimap.get(prefix)); + + // No results found + return new ArrayList<>(); } /** - * Returns the preamble (text before the first valid prefix). Trims any leading/trailing spaces. + * Returns the preamble (text before the first valid flag). */ public String getPreamble() { - return getValue(new Prefix("")).orElse(""); + return preamble; + } + + /** + * Throws a {@code ParseException} if any of the flags given in {@code flags} appeared more than + * once among the arguments, i.e., the given flags may only be used once each. + */ + public void verifyNoDuplicateFlagsFor(Flag... flags) throws ParseException { + Flag[] duplicatedFlags = Stream.of(flags).distinct() + .filter(flag -> getAllValues(flag).size() > 1) + .toArray(Flag[]::new); + + if (duplicatedFlags.length > 0) { + throw new ParseException(Messages.getErrorMessageForDuplicateFlags(duplicatedFlags)); + } + } + + /** + * Throws a {@code ParseException} if there exists any more flags than the ones given in {@code flags} + * among the ones put in this map, i.e., the given flags are the maximally allowed set of flags. + */ + public void verifyNoExtraneousFlagsOnTopOf(Flag... flags) throws ParseException { + List referenceFlagsList = List.of(flags); + + Flag[] extraneousFlags = argMultimap.keySet().stream() + .filter(f -> !referenceFlagsList.contains(f)) + .toArray(Flag[]::new); + + if (extraneousFlags.length > 0) { + throw new ParseException(Messages.getErrorMessageForExtraneousFlags(extraneousFlags)); + } + } + + /** + * Throws a {@code ParseException} if any of the flags given in {@code flags} have a non-empty value + * assigned to it, i.e., the given flags must be empty and not have values set. + */ + public void verifyAllEmptyValuesAssignedFor(Flag... flags) throws ParseException { + Flag[] flagsWithUsefulValues = Stream.of(flags).distinct() + .filter(argMultimap::containsKey) + .filter(f -> argMultimap.get(f).stream().anyMatch(s -> !s.isEmpty())) + .toArray(Flag[]::new); + + if (flagsWithUsefulValues.length > 0) { + throw new ParseException(Messages.getErrorMessageForNonEmptyValuedFlags(flagsWithUsefulValues)); + } } /** - * Throws a {@code ParseException} if any of the prefixes given in {@code prefixes} appeared more than - * once among the arguments. + * Throws a {@code ParseException} if there are more than one of the given {@code flags} simultanouesly + * put in this map, i.e., the given flags cannot be used together. */ - public void verifyNoDuplicatePrefixesFor(Prefix... prefixes) throws ParseException { - Prefix[] duplicatedPrefixes = Stream.of(prefixes).distinct() - .filter(prefix -> argMultimap.containsKey(prefix) && argMultimap.get(prefix).size() > 1) - .toArray(Prefix[]::new); + public void verifyAtMostOneOfFlagsUsedOutOf(Flag... flags) throws ParseException { + Flag[] existingFlags = Stream.of(flags).distinct() + .filter(flag -> getAllValues(flag).size() > 0) + .toArray(Flag[]::new); - if (duplicatedPrefixes.length > 0) { - throw new ParseException(Messages.getErrorMessageForDuplicatePrefixes(duplicatedPrefixes)); + if (existingFlags.length > 1) { + throw new ParseException(Messages.getErrorMessageForSimultaneousUseDisallowedFlags(existingFlags)); } } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..b77fb3c3e1b 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -1,148 +1,168 @@ package seedu.address.logic.parser; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; + +import seedu.address.logic.parser.exceptions.ParseException; /** - * Tokenizes arguments string of the form: {@code preamble value value ...}
- * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
- * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
- * 2. Leading and trailing whitespaces of an argument value will be discarded.
- * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} - * in the above example.
+ * Tokenizes arguments string of the form: {@code preamble value value ...}
+ * e.g. {@code some preamble text -t 11.00 -t 12.00 -k -m July} where flags are {@code -t -k -m}.
+ * + *
    + *
  1. + * An argument's (flag's) value can be an empty string, e.g., the value of {@code -k} + * in the above example. + *
  2. + *
  3. + * Leading and trailing whitespaces of an argument value will be discarded. + *
  4. + *
  5. + * Flags must be surrounded by whitespace on both sides to be tokenized as flags. + *
  6. + *
  7. + * An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} + * in the above example. + *
  8. + *
*/ public class ArgumentTokenizer { /** - * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps prefixes to their - * respective argument values. Only the given prefixes will be recognized in the arguments string. + * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps flag to their + * respective argument values. Only the given flags will be tokenized and added to the multimap, while + * extraneous flags found that match the expected syntax (see: {@link Flag#isFlagSyntax}} + * will throw an exception. + * + *

+ * Unlike {@link #autoTokenize(String, Flag...)}, this will throw an error when unspecified flags + * are found. This means you must provide all the necessary flags you intend to use via the {@code flags} + * parameter. + *

+ * + *

+ * Calling this method is equivalent to using the results from + * {@link ArgumentTokenizer#autoTokenize(String, Flag...)} and verifying there exists no extraneous flags with + * {@link ArgumentMultimap#verifyNoExtraneousFlagsOnTopOf(Flag...)}. + *

+ * + * @param argsString Arguments string of the form: {@code preamble value value ...}. + * @param flags Flags to tokenize the arguments string with. + * @return ArgumentMultimap object that maps flag to their arguments. + * @throws ParseException if there are extraneous flags detected on top of the provided flags. * - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their arguments + * @see #autoTokenize(String, Flag...) */ - public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { - List positions = findAllPrefixPositions(argsString, prefixes); - return extractArguments(argsString, positions); + public static ArgumentMultimap tokenize(String argsString, Flag... flags) throws ParseException { + ArgumentMultimap multimap = autoTokenize(argsString, flags); + multimap.verifyNoExtraneousFlagsOnTopOf(flags); + return multimap; } /** - * Finds all zero-based prefix positions in the given arguments string. + * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps flag to their + * respective argument. All flags provided via the {@code flags} parameter, and all unknown flags + * successfully obtained via {@link Flag#parse}, will in both cases be added to the multimap. * - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param prefixes Prefixes to find in the arguments string - * @return List of zero-based prefix positions in the given arguments string + *

+ * Unlike {@link #tokenize(String, Flag...)}, this will not throw an error when unspecified flags are + * found. In other words, an unknown flag not present in {@code mainFlags} is always accepted. + *

+ * + * @param argsString Arguments string of the form: {@code preamble value value ...}. + * @param mainFlags Optional set of primary flags to prioritize tokenizing the arguments string with. + * @return ArgumentMultimap object that maps flag to their arguments. + * + * @see #tokenize(String, Flag...) */ - private static List findAllPrefixPositions(String argsString, Prefix... prefixes) { - return Arrays.stream(prefixes) - .flatMap(prefix -> findPrefixPositions(argsString, prefix).stream()) - .collect(Collectors.toList()); + public static ArgumentMultimap autoTokenize(String argsString, Flag... mainFlags) { + String[] words = splitByWords(argsString); + return extractArguments(words, mainFlags); } /** - * {@see findAllPrefixPositions} + * Splits an arguments string into individual words, separated by space. + * + * @param argsString Arguments string of the form: {@code preamble value value ...}. + * @return The terms formed after splitting the arguments string by the space character. */ - private static List findPrefixPositions(String argsString, Prefix prefix) { - List positions = new ArrayList<>(); - - int prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), 0); - while (prefixPosition != -1) { - PrefixPosition extendedPrefix = new PrefixPosition(prefix, prefixPosition); - positions.add(extendedPrefix); - prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition); - } - - return positions; + private static String[] splitByWords(String argsString) { + return argsString.split(" "); } /** - * Returns the index of the first occurrence of {@code prefix} in - * {@code argsString} starting from index {@code fromIndex}. An occurrence - * is valid if there is a whitespace before {@code prefix}. Returns -1 if no - * such occurrence can be found. + * Locates all the locations in the words list that represent flags. * - * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns -1 as there are no valid - * occurrences of "p/" with whitespace before it. However, if - * {@code argsString} = "e/hi p/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns 5. + * @param wordsArray An array of words. + * @param targetedFlags An array of flags should be checked explicitly. + * @return The list of indices where a flag can be found. */ - private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { - int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); - return prefixIndex == -1 ? -1 - : prefixIndex + 1; // +1 as offset for whitespace + private static List findFlagIndices(String[] wordsArray, Flag[] targetedFlags) { + + List flagIndices = new ArrayList<>(); + for (int i = 0; i < wordsArray.length; i++) { + String word = wordsArray[i]; + + if (Flag.isFlagSyntax(word) || Flag.findMatch(word, targetedFlags).isPresent()) { + flagIndices.add(i); + } + } + + return flagIndices; } /** - * Extracts prefixes and their argument values, and returns an {@code ArgumentMultimap} object that maps the - * extracted prefixes to their respective arguments. Prefixes are extracted based on their zero-based positions in + * Extracts flag and their argument values, and returns an {@code ArgumentMultimap} object that maps the + * extracted flag to their respective arguments. Flags are extracted based on their zero-based positions in * {@code argsString}. * - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} - * @return ArgumentMultimap object that maps prefixes to their arguments + * @param words An array of words derived from the arguments string. + * @param targetedFlags An array of flags should be checked explicitly. + * @return ArgumentMultimap object that maps flags to their arguments. */ - private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) { + private static ArgumentMultimap extractArguments(String[] words, Flag[] targetedFlags) { - // Sort by start position - prefixPositions.sort((prefix1, prefix2) -> prefix1.getStartPosition() - prefix2.getStartPosition()); + List wordsList = List.of(words); - // Insert a PrefixPosition to represent the preamble - PrefixPosition preambleMarker = new PrefixPosition(new Prefix(""), 0); - prefixPositions.add(0, preambleMarker); + // Define an "end of range" to be the end of a value or preamble. + // We prepare a list that marks the end of ranges via *exclusive* indices (i.e., end index + 1). + // In other words, if the list has [3, 5], it means there are two ranges [0,3) and [3,5). + List endOfRangeIndices = new ArrayList<>(); - // Add a dummy PrefixPosition to represent the end of the string - PrefixPosition endPositionMarker = new PrefixPosition(new Prefix(""), argsString.length()); - prefixPositions.add(endPositionMarker); + endOfRangeIndices.addAll(findFlagIndices(words, targetedFlags)); + endOfRangeIndices.add(words.length); - // Map prefixes to their argument values (if any) + // Search through the ranges and map flag to their argument values (if any) ArgumentMultimap argMultimap = new ArgumentMultimap(); - for (int i = 0; i < prefixPositions.size() - 1; i++) { - // Extract and store prefixes and their arguments - Prefix argPrefix = prefixPositions.get(i).getPrefix(); - String argValue = extractArgumentValue(argsString, prefixPositions.get(i), prefixPositions.get(i + 1)); - argMultimap.put(argPrefix, argValue); - } - - return argMultimap; - } + for (int i = 0; i < endOfRangeIndices.size(); i++) { - /** - * Returns the trimmed value of the argument in the arguments string specified by {@code currentPrefixPosition}. - * The end position of the value is determined by {@code nextPrefixPosition}. - */ - private static String extractArgumentValue(String argsString, - PrefixPosition currentPrefixPosition, - PrefixPosition nextPrefixPosition) { - Prefix prefix = currentPrefixPosition.getPrefix(); + // Note that the bounds are [start, end), i.e., start <= x < end. + int start = i == 0 ? 0 : endOfRangeIndices.get(i - 1); + int end = endOfRangeIndices.get(i); - int valueStartPos = currentPrefixPosition.getStartPosition() + prefix.getPrefix().length(); - String value = argsString.substring(valueStartPos, nextPrefixPosition.getStartPosition()); + if (start >= end) { + continue; + } - return value.trim(); - } + // Case 1: Preamble (if we reach here in the first loop iteration). + if (i == 0) { + String preamble = String.join(" ", wordsList.subList(start, end)).trim(); + argMultimap.putPreamble(preamble); + continue; + } - /** - * Represents a prefix's position in an arguments string. - */ - private static class PrefixPosition { - private int startPosition; - private final Prefix prefix; + // Case 2: Flag + Possible Argument Value (if we reach here in 2nd+ loop iterations). + String flagString = words[start]; + String valueString = String.join(" ", wordsList.subList(start + 1, end)).trim(); - PrefixPosition(Prefix prefix, int startPosition) { - this.prefix = prefix; - this.startPosition = startPosition; - } + Flag flag = Flag.findMatch(flagString, targetedFlags) + .or(() -> Flag.parseOptional(flagString)) + .orElseThrow(); // We should never get here since the flags are validated in findFlagIndices. - int getStartPosition() { - return startPosition; + argMultimap.put(flag, valueString); } - Prefix getPrefix() { - return prefix; - } + return argMultimap; } } diff --git a/src/main/java/seedu/address/logic/parser/ClassMappings.java b/src/main/java/seedu/address/logic/parser/ClassMappings.java new file mode 100644 index 00000000000..dfd1e76f6a5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClassMappings.java @@ -0,0 +1,119 @@ +package seedu.address.logic.parser; + +import java.lang.reflect.InvocationTargetException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ApplyCommand; +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.ReminderCommand; +import seedu.address.logic.commands.SortCommand; + +/** + * A utility class that provides a mapping of all available class values. + */ +public class ClassMappings { + + /** Provides information about commands as a map of command classes to their parser classes. */ + public static final Map, Optional>>> + COMMAND_TO_PARSER_MAP = getCommandToParserMap(); + + /** + * Creates and returns an ordered map of command classes as keys, and optionally their corresponding parsers + * if they accept arguments. + * + *

+ * Developer's note: This method may be modified to include support for new commands. + *

+ */ + private static Map, Optional>>> + getCommandToParserMap() { + + // Create a map that preserves insertion order. + Map, Optional>>> + orderedMap = new LinkedHashMap<>(); + + // Insert the command and parser pairs in order. + orderedMap.put(AddCommand.class, Optional.of(AddCommandParser.class)); + orderedMap.put(ApplyCommand.class, Optional.of(ApplyCommandParser.class)); + orderedMap.put(DeleteCommand.class, Optional.of(DeleteCommandParser.class)); + orderedMap.put(EditCommand.class, Optional.of(EditCommandParser.class)); + + orderedMap.put(ListCommand.class, Optional.of(ListCommandParser.class)); + orderedMap.put(FindCommand.class, Optional.of(FindCommandParser.class)); + orderedMap.put(SortCommand.class, Optional.of(SortCommandParser.class)); + orderedMap.put(ReminderCommand.class, Optional.of(ReminderCommandParser.class)); + + orderedMap.put(HelpCommand.class, Optional.empty()); + orderedMap.put(ClearCommand.class, Optional.empty()); + orderedMap.put(ExitCommand.class, Optional.empty()); + + // Validate that the resulting map is valid. + assert isCommandToParserMapOperational(orderedMap); + + // Return the result. + return orderedMap; + } + + /** + * This is a helper method to validate whether the given map of commands and parsers meet the expected + * specifications. + * + *
    + *
  • All commands must have a command word.
  • + *
  • All commands must either have a parser that can initialize with no arguments, or itself be initalized + * with no-arguments
  • + *
+ * + *

+ * This method's purpose for validating the specifications is because we would be retrieving values + * directly via Java's Reflection API and initializing the instances that way, which does not have compile time + * checks. Adding an assertion to ensure this works helps validate that no programmer error has slipped by. + *

+ */ + private static boolean isCommandToParserMapOperational( + Map, Optional>>> map + ) { + try { + for (var entry: map.entrySet()) { + + // We must have a command word for every command. + assert Command.getCommandWord(entry.getKey()).isPresent() + : "All commands must have COMMAND_WORD set, but " + + entry.getKey().getSimpleName() + " does not!"; + + if (entry.getValue().isPresent()) { + // If there's a parser class, we must be able to initialize them with no args without errors. + entry.getValue().get().getDeclaredConstructor().newInstance(); + } else { + // Otherwise, we must be able to initialize the command class directly with no args without errors. + entry.getKey().getDeclaredConstructor().newInstance(); + } + } + + return true; + + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + + assert false + : "A parser class must have a no-args constructor. " + + "If a command has no parser class, then the command class must have a no-args constructor. " + + "However, an initialization has unexpectedly failed with error: " + e.getMessage(); + } + + return false; + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..8e0147b4743 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -5,11 +5,33 @@ */ 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/"); + /* Flag definitions */ + public static final Flag FLAG_ID = new Flag("id"); + public static final Flag FLAG_ORGANIZATION = new Flag("org"); + public static final Flag FLAG_RECRUITER = new Flag("rec"); + public static final Flag FLAG_APPLICATION = new Flag("application"); + + public static final Flag FLAG_NAME = new Flag("name"); + public static final Flag FLAG_PHONE = new Flag("phone"); + public static final Flag FLAG_EMAIL = new Flag("email"); + public static final Flag FLAG_ADDRESS = new Flag("address"); + public static final Flag FLAG_TAG = new Flag("tag"); + public static final Flag FLAG_URL = new Flag("url"); + public static final Flag FLAG_STATUS = new Flag("status"); + public static final Flag FLAG_RECURSIVE = new Flag("recursive"); + public static final Flag FLAG_ORGANIZATION_ID = new Flag("oid"); + public static final Flag FLAG_RECRUITER_ID = new Flag("rid"); + public static final Flag FLAG_TITLE = new Flag("title"); + public static final Flag FLAG_DEADLINE = new Flag("by"); + public static final Flag FLAG_STAGE = new Flag("stage"); + public static final Flag FLAG_DESCRIPTION = new Flag("description"); + public static final Flag FLAG_NOT_APPLIED = new Flag("toapply"); + public static final Flag FLAG_NONE = new Flag("none"); + public static final Flag FLAG_ASCENDING = new Flag("ascending"); + public static final Flag FLAG_DESCENDING = new Flag("descending"); + public static final Flag FLAG_STALE = new Flag("stale"); + public static final Flag FLAG_EARLIEST = new Flag("earliest"); + public static final Flag FLAG_LATEST = new Flag("latest"); + } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..adde1852370 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,10 +1,14 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.FLAG_APPLICATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECURSIVE; import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteApplicationCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.Id; /** * Parses input arguments and creates a new DeleteCommand object @@ -17,13 +21,51 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { + ArgumentMultimap argumentMultimap = + ArgumentTokenizer.tokenize(args, + DeleteCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new)); + + boolean hasApplicationFlag = argumentMultimap.getValue(FLAG_APPLICATION).isPresent(); + boolean hasContactIdOrIndex = !argumentMultimap.getPreamble().isEmpty(); + boolean isRecursive = argumentMultimap.getValue(FLAG_RECURSIVE).isPresent(); + + if (hasApplicationFlag && hasContactIdOrIndex) { + // example: delete 1 --application 1 throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + if (hasApplicationFlag) { + return handleDeleteApplication(argumentMultimap); + } + + if (!hasContactIdOrIndex) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + Object indexXorId = ParserUtil.parseIndexXorId(argumentMultimap.getPreamble()); + + if (indexXorId instanceof Index) { + return DeleteCommand.selectIndex((Index) indexXorId, isRecursive); + } + + if (indexXorId instanceof Id) { + return DeleteCommand.selectId((Id) indexXorId, isRecursive); } + + assert false : "If indexXorId is neither Index nor Id, then ParserUtil should've thrown ParseException!"; + throw new IllegalStateException(); + } + + private static DeleteCommand handleDeleteApplication(ArgumentMultimap argMultimap) throws ParseException { + if (argMultimap.getValue(FLAG_APPLICATION).isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + Index index = ParserUtil.parseIndex(argMultimap.getValue(FLAG_APPLICATION).get()); + + return new DeleteApplicationCommand(index); } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..680d392471d 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,11 +2,20 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_APPLICATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TAG; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; import java.util.Collection; import java.util.Collections; @@ -14,11 +23,18 @@ import java.util.Set; import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditApplicationCommand; +import seedu.address.logic.commands.EditApplicationCommand.EditApplicationDescriptor; import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditCommand.EditContactDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.Id; import seedu.address.model.tag.Tag; + + + + /** * Parses input arguments and creates a new EditCommand object */ @@ -32,41 +48,91 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, + EditCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new)); + + boolean hasApplicationFlag = argMultimap.getValue(FLAG_APPLICATION).isPresent(); + boolean hasContactIdOrIndex = !argMultimap.getPreamble().isEmpty(); + + if (hasApplicationFlag && hasContactIdOrIndex) { + // example: edit 1 --application 1 + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + if (hasApplicationFlag) { + return handleEditApplication(argMultimap); + } Index index; + Id targetId; try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + String preambleStr = argMultimap.getPreamble(); + + // TODO: Tech debt - Create a wrapper that will parse indexXorId + Object indexXorId = ParserUtil.parseIndexXorId(preambleStr); + + if (indexXorId instanceof Id) { + targetId = (Id) indexXorId; + index = null; + } else if (indexXorId instanceof Index) { + index = (Index) indexXorId; + targetId = null; + } else { + assert false + : "If indexXorId is neither an Index nor an Id, " + + "ParserUtil should've thrown ParseException!"; + throw new IllegalStateException(); + } + } catch (ParseException pe) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + argMultimap.verifyNoDuplicateFlagsFor(FLAG_NAME, FLAG_PHONE, FLAG_EMAIL, + FLAG_ADDRESS, FLAG_URL, FLAG_ID, FLAG_ORGANIZATION_ID); + + EditContactDescriptor editContactDescriptor = new EditContactDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + // TODO: Tech debt - parseOptionally + if (argMultimap.getValue(FLAG_NAME).isPresent()) { + editContactDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(FLAG_NAME).get())); } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + if (argMultimap.getValue(FLAG_PHONE).isPresent()) { + editContactDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(FLAG_PHONE).get())); } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + if (argMultimap.getValue(FLAG_EMAIL).isPresent()) { + editContactDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(FLAG_EMAIL).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + if (argMultimap.getValue(FLAG_ADDRESS).isPresent()) { + editContactDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(FLAG_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + if (argMultimap.getValue(FLAG_URL).isPresent()) { + editContactDescriptor.setUrl(ParserUtil.parseUrl(argMultimap.getValue(FLAG_URL).get())); + } + if (argMultimap.getValue(FLAG_ID).isPresent()) { + editContactDescriptor.setId(ParserUtil.parseId(argMultimap.getValue(FLAG_ID).get())); + } + if (argMultimap.getValue(FLAG_ORGANIZATION_ID).isPresent()) { + editContactDescriptor.setOid(ParserUtil.parseId(argMultimap.getValue(FLAG_ORGANIZATION_ID).get())); + } + parseTagsForEdit(argMultimap.getAllValues(FLAG_TAG)).ifPresent(editContactDescriptor::setTags); - if (!editPersonDescriptor.isAnyFieldEdited()) { + if (!editContactDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + if (targetId == null) { + return new EditCommand(index, editContactDescriptor); + } else { + return new EditCommand(targetId, editContactDescriptor); + } + } + + /** * 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 @@ -82,4 +148,47 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + private static EditCommand handleEditApplication(ArgumentMultimap argMultimap) throws ParseException { + EditApplicationDescriptor editApplicationDescriptor = new EditApplicationDescriptor(); + + Optional indexStringOptional = argMultimap.getValue(FLAG_APPLICATION).filter(s -> !s.isEmpty()); + + Index index; + if (indexStringOptional.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + try { + index = ParserUtil.parseIndex(indexStringOptional.get()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE, pe)); + } + + editApplicationDescriptor.setDeadline( + ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_DEADLINE), + ParserUtil::parseDeadline)); + editApplicationDescriptor.setApplicationStage( + ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_STAGE), + ParserUtil::parseApplicationStage)); + editApplicationDescriptor.setJobTitle( + ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_TITLE), + ParserUtil::parseJobTitle)); + editApplicationDescriptor.setStatus( + ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_STATUS), + ParserUtil::parseJobStatus)); + editApplicationDescriptor.setJobDescription( + ParserUtil.parseOptionally( + argMultimap.getValue(FLAG_DESCRIPTION), + ParserUtil::parseJobDescription)); + + if (!editApplicationDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + return new EditApplicationCommand(index, editApplicationDescriptor); + } + } diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..4bffa076c12 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -6,7 +6,7 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.contact.NameContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object diff --git a/src/main/java/seedu/address/logic/parser/Flag.java b/src/main/java/seedu/address/logic/parser/Flag.java new file mode 100644 index 00000000000..4ecc1bd4541 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/Flag.java @@ -0,0 +1,383 @@ +package seedu.address.logic.parser; + +import java.util.Objects; +import java.util.Optional; + +import seedu.address.logic.Messages; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * A flag is an argument in and of itself. It functions as an option specifier, or as a marker for the beginning of a + * command argument. + * + *

+ * For example, '--t' in 'add James --t friend' is a flag with name 't' and prefix '--'. + *

+ */ +public class Flag { + + public static final String DEFAULT_PREFIX = "--"; + public static final String DEFAULT_POSTFIX = ""; + + public static final String DEFAULT_ALIAS_PREFIX = "-"; + public static final String DEFAULT_ALIAS_POSTFIX = ""; + + private static final String FULL_OR_ALIAS_NAME_VALIDATION_REGEX = "[a-zA-Z][a-zA-Z0-9]*"; + private static final String FULL_OR_ALIAS_NAME_FORMAT_ERROR = + "Flags must only have alphanumeric characters (a-z, A-Z, 0-9), " + + "may not start with a number, and may not be empty."; + + private final String name; + private final String prefix; + private final String postfix; + + private final String alias; + private final String aliasPrefix; + private final String aliasPostfix; + + /** + * Constructs a flag with the {@link #DEFAULT_PREFIX} and {@link #DEFAULT_POSTFIX} surrounding the name. + * No alias will be assigned to this flag. If the name has any leading or trailing whitespace, it'll be trimmed. + * + * @param name The name of the flag. May be null, which will set it to an empty string. + */ + public Flag(String name) { + this( + name, DEFAULT_PREFIX, DEFAULT_POSTFIX, + null, null, null + ); + } + + /** + * Constructs a flag with a custom alias. + * Any fields with leading or trailing whitespace are trimmed. + * + * @param name The name of the flag. May be null, which will set it to an empty string. + * @param alias The prefix of the flag. May be null, which will behave as if no alias is set. + */ + public Flag(String name, String alias) { + this( + name, + DEFAULT_PREFIX, + DEFAULT_POSTFIX, + alias, + alias == null ? null : DEFAULT_ALIAS_PREFIX, + alias == null ? null : DEFAULT_ALIAS_POSTFIX + ); + } + + /** + * Constructs a flag with the given properties for the name and alias. + */ + private Flag(String name, String prefix, String postfix, String alias, String aliasPrefix, String aliasPostfix) { + this.name = name == null ? "" : name.trim(); + this.prefix = prefix == null ? "" : prefix.trim(); + this.postfix = postfix == null ? "" : postfix.trim(); + + boolean isAliasAvailable = alias != null && !alias.isBlank(); + + if (isAliasAvailable) { + this.alias = alias.trim(); + this.aliasPrefix = aliasPrefix == null ? this.prefix : aliasPrefix.trim(); + this.aliasPostfix = aliasPostfix == null ? this.postfix : aliasPostfix.trim(); + } else { + this.alias = this.name; + this.aliasPrefix = this.prefix; + this.aliasPostfix = this.postfix; + } + + if (!this.name.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX) + || !this.alias.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX)) { + + throw new IllegalArgumentException(FULL_OR_ALIAS_NAME_FORMAT_ERROR); + } + } + + /** + * Constructs a flag with a custom name, custom prefix and custom postfix. + * Any fields with leading or trailing whitespace are trimmed. + * + * @param name The name of the flag. May be null, which will set it to an empty string. + * @param prefix The prefix of the flag. May be null, which will set it to an empty string. + * @param postfix The postfix of the flag. May be null, which will set it to an empty string. + */ + public static Flag ofCustomFormat(String name, String prefix, String postfix) { + return new Flag(name, prefix, postfix, null, null, null); + } + + /** + * Parses the given string using the default prefix and postfix format into a {@link Flag}. + * + *

+ * This will work for both full flag strings and flag aliases. However, this will not return the same instance + * as an existing flag, especially if it has both a full value and alias pair. For those, try {@link #findMatch} + * instead. + *

+ * + *

+ * Full flag strings passed into this method and flags constructed in {@link #Flag(String)} are equal + * if they have the same name, prefix, and postfix. + *

+ * + *

+ * Aliased flag strings passed into this method and flags constructed in {@link #Flag(String)}, and then + * obtaining an alias-only definition via {@link #getAliasOnlyDefinition()}, will have an exact equivalent + * representation. + *

+ * + * @param string The string to parse as a flag. + * @return The corresponding {@link Flag} instance. + * @throws ParseException if the flag syntax is invalid. + */ + public static Flag parse(String string) throws ParseException { + + if (string != null && string.startsWith(DEFAULT_PREFIX) && string.endsWith(DEFAULT_POSTFIX)) { + String flag = string.substring( + DEFAULT_PREFIX.length(), + string.length() - DEFAULT_POSTFIX.length() + ); + if (flag.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX)) { + return new Flag(flag); + } + } + + if (string != null && string.startsWith(DEFAULT_ALIAS_PREFIX) && string.endsWith(DEFAULT_ALIAS_POSTFIX)) { + String alias = string.substring( + DEFAULT_ALIAS_PREFIX.length(), + string.length() - DEFAULT_ALIAS_POSTFIX.length() + ); + if (alias.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX)) { + return new Flag(alias, alias); // Treat alias as name, since we don't know any better. + } + } + + throw new ParseException( + Messages.getErrorMessageForInvalidFlagString(string) + ); + } + + /** + * Parses the given string using the default prefix and postfix format into an optional {@link Flag}, + * which will return an empty optional if it's invalid. + * + * @param string The string to check for flag-like formats. + * @return An optional containing the flag if it is a valid flag format. + * @see #parse(String) + */ + public static Optional parseOptional(String string) { + try { + return Optional.of(parse(string)); + } catch (ParseException e) { + return Optional.empty(); + } + } + + /** + * Finds a {@link Flag} from the given {@code flags} that matches the given string representation. + * + * @param string The string to check for a corresponding matching flag or flag-like formats. + * @param flags The array of flags to check from. + * @return An optional instance with the result if there is a successful match. + * @throws IllegalArgumentException if the flag is invalid. + */ + public static Optional findMatch(String string, Flag[] flags) { + for (Flag flag : flags) { + if (string == null) { + break; + } + if (string.equals(flag.getFlagString()) + || string.equals(flag.getFlagAliasString())) { + return Optional.of(flag); + } + } + return Optional.empty(); + } + + /** + * Checks whether the given string representation resembles a flag. + * If this is true, then it resembles the default prefix-name-postfix format specified in {@link Flag}, + * or resembles the equivalent format for the alias counterpart, and thus a plausible output from + * {@link Flag#getFlagString()} or {@link Flag#getFlagAliasString()}. + * + * @param string The string to check for flag-like formats. + * @return true if the string resembles a flag, false otherwise. + */ + public static boolean isFlagSyntax(String string) { + if (string == null) { + return false; + } + + if (string.startsWith(DEFAULT_PREFIX) && string.endsWith(DEFAULT_POSTFIX)) { + String part = string.substring( + DEFAULT_PREFIX.length(), + string.length() - DEFAULT_POSTFIX.length() + ); + return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); + } + + if (string.startsWith(DEFAULT_ALIAS_PREFIX) && string.endsWith(DEFAULT_ALIAS_POSTFIX)) { + String part = string.substring( + DEFAULT_ALIAS_PREFIX.length(), + string.length() - DEFAULT_ALIAS_POSTFIX.length() + ); + return part.matches(FULL_OR_ALIAS_NAME_VALIDATION_REGEX); + } + + return false; + } + + + + public String getName() { + return name; + } + + public String getPrefix() { + return prefix; + } + + public String getPostfix() { + return postfix; + } + + public String getAlias() { + return alias; + } + + public String getAliasPrefix() { + return aliasPrefix; + } + + public String getAliasPostfix() { + return aliasPostfix; + } + + /** + * Returns whether the flag has a distinct alias from its full string form. + */ + public boolean hasAlias() { + return !this.getFlagString().equals(this.getFlagAliasString()); + } + + /** + * Returns the full string that would be used by the user to input a flag. + * + *

+ * This is the full string that would be used by a user to input a flag. + * This means it's the concatenated result of prefix, name, postfix together. + *

+ */ + public String getFlagString() { + return this.getPrefix() + this.getName() + this.getPostfix(); + } + + /** + * Returns the alias string that would be used by the user to input a flag. + * It may be the same as {@link #getFlagString()} if there's no alias assigned to this flag. + * + *

+ * This is the full alias string that would be used by a user to input a flag. + * This means it's the concatenated result of alias prefix, alias, alias postfix together. + *

+ */ + public String getFlagAliasString() { + return this.getAliasPrefix() + this.getAlias() + this.getAliasPostfix(); + } + + /** + * Returns a flag variant derived from the current one which uses the alias as the name. + * + *

+ * If this flag does not have an alias (as per {@link #hasAlias()}), the newly created flag would be equivalent to + * the current flag in all properties. + *

+ */ + public Flag getAliasOnlyDefinition() { + return new Flag(alias, prefix, postfix, alias, aliasPrefix, aliasPostfix); + } + + /** + * Returns a flag variant derived from the current one which has no alias. + * + *

+ * If this flag already does not have an alias (as per {@link #hasAlias()}), the newly created flag would be + * equivalent to the current flag in all properties. + *

+ */ + public Flag getNameOnlyDefinition() { + return new Flag(name, prefix, postfix, null, null, null); + } + + /** + * Returns a string representation of this flag. + * Equivalent to the result from {@link #getFlagString()}. + * + * @return The string representation of this flag. + */ + @Override + public String toString() { + return this.getFlagString(); + } + + @Override + public int hashCode() { + return Objects.hash(name, prefix, postfix); + } + + /** + * Returns whether the two flags have the same full flag string formats. + */ + public boolean equalsFlagString(Flag other) { + if (other == null) { + return false; + } + + return this.getFlagString().equals(other.getFlagString()); + } + + /** + * Returns whether the two flags have the same flag alias formats. + */ + public boolean equalsFlagAliasString(Flag other) { + if (other == null) { + return false; + } + + return this.getFlagAliasString().equals(other.getFlagAliasString()); + } + + /** + * Returns whether the two objects are equal. + * + *

+ * Two flags are equal as long as the full form is equal for all properties, or if not, + * if at least one of them doesn't have a name, they're equal if their alias forms are equal for all properties. + *

+ */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Flag)) { + return false; + } + + Flag otherFlag = (Flag) other; + + boolean isFullFormEqual = Objects.equals(name, otherFlag.name) + && Objects.equals(prefix, otherFlag.prefix) + && Objects.equals(postfix, otherFlag.postfix); + + boolean isAliasFormEqual = Objects.equals(alias, otherFlag.alias) + && Objects.equals(aliasPrefix, otherFlag.aliasPrefix) + && Objects.equals(aliasPostfix, otherFlag.aliasPostfix); + + boolean requiresAliasOnlyMatch = + alias.equals(name) || otherFlag.alias.equals(otherFlag.name); + + return isFullFormEqual || (requiresAliasOnlyMatch && isAliasFormEqual); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..46d5c062acd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,44 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.FLAG_NOT_APPLIED; +import static seedu.address.logic.parser.CliSyntax.FLAG_ORGANIZATION; +import static seedu.address.logic.parser.CliSyntax.FLAG_RECRUITER; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONTACTS; +import static seedu.address.model.Model.PREDICATE_SHOW_NOT_APPLIED_ORGANIZATIONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ONLY_ORGANIZATIONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ONLY_RECRUITERS; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ListCommand object + */ +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns a ListCommand object for execution. + */ + public ListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, + ListCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new)); + + if (argMultimap.hasAllOfFlags(FLAG_ORGANIZATION, FLAG_RECRUITER)) { + return new ListCommand(PREDICATE_SHOW_ALL_CONTACTS); + } + + if (argMultimap.hasFlag(FLAG_ORGANIZATION)) { + return new ListCommand(PREDICATE_SHOW_ONLY_ORGANIZATIONS); + } else if (argMultimap.hasFlag(FLAG_RECRUITER)) { + return new ListCommand(PREDICATE_SHOW_ONLY_RECRUITERS); + } + if (argMultimap.hasFlag(FLAG_NOT_APPLIED)) { + return new ListCommand(PREDICATE_SHOW_NOT_APPLIED_ORGANIZATIONS); + } + + return new ListCommand(PREDICATE_SHOW_ALL_CONTACTS); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..db651c9a146 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -4,15 +4,23 @@ import java.util.Collection; import java.util.HashSet; +import java.util.Optional; import java.util.Set; 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.contact.Address; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Url; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.Deadline; +import seedu.address.model.jobapplication.JobDescription; +import seedu.address.model.jobapplication.JobStatus; +import seedu.address.model.jobapplication.JobTitle; import seedu.address.model.tag.Tag; /** @@ -23,8 +31,9 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. + * 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 +44,39 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses a {@code String id} into an {@code Id} and returns it. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code id} is invalid. + */ + public static Id parseId(String id) throws ParseException { + requireNonNull(id); + String trimmedId = id.trim(); + if (!Id.isValidId(id)) { + throw new ParseException(Id.MESSAGE_CONSTRAINTS); + } + return new Id(trimmedId); + } + + /** + * Parses a {@code String id} into a {@code Id} or {@code Index}, depending on the format of the string. + * Leading and trailing whitespaces will be trimmed. + * + * @return {@link Object} that is either an {@link Id} or an {@link Index} instance. + * @throws ParseException if the given {@code str} is neither an index nor an id. + */ + public static Object parseIndexXorId(String str) throws ParseException { + String trimmedStr = str.trim(); + Object result; + if (trimmedStr.matches("^[0-9]*$")) { + result = ParserUtil.parseIndex(trimmedStr); + } else { + result = ParserUtil.parseId(trimmedStr); + } + return result; + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -65,6 +107,21 @@ public static Phone parsePhone(String phone) throws ParseException { return new Phone(trimmedPhone); } + /** + * Parses a {@code String url} into an {@code Url}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code url} is invalid. + */ + public static Url parseUrl(String url) throws ParseException { + requireNonNull(url); + String trimmedUrl = url.trim(); + if (!Url.isValidUrl(trimmedUrl)) { + throw new ParseException(Url.MESSAGE_CONSTRAINTS); + } + return new Url(trimmedUrl); + } + /** * Parses a {@code String address} into an {@code Address}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +178,103 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code Collection ids} into a {@code Set}. + */ + public static Set parseIds(Collection ids) throws ParseException { + requireNonNull(ids); + final Set idSet = new HashSet<>(); + for (String idName : ids) { + idSet.add(parseId(idName)); + } + return idSet; + } + + + /** + * Parses {@code String deadline} into a {@code Deadline}. + */ + 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 Deadline(trimmedDeadline); + } + + /** + * Parses {@code String jobTitle} into a {@code JobTitle}. + */ + public static JobTitle parseJobTitle(String jobtitle) throws ParseException { + requireNonNull(jobtitle); + String trimmedTitle = jobtitle.trim(); + if (!JobTitle.isValidJobTitle(trimmedTitle)) { + throw new ParseException(JobTitle.MESSAGE_CONSTRAINTS); + } + return new JobTitle(trimmedTitle); + } + + /** + * Parses {@code String jobStatus} into a {@code JobStatus}. + */ + public static JobStatus parseJobStatus(String status) throws ParseException { + requireNonNull(status); + String trimmedStatus = status.trim(); + if (!JobStatus.isValidJobStatus(trimmedStatus)) { + throw new ParseException(JobStatus.MESSAGE_CONSTRAINTS); + } + return JobStatus.fromString(trimmedStatus); + } + + /** + * Parses {@code String applicationStage} into a {@code ApplicationStage}. + */ + public static ApplicationStage parseApplicationStage(String applicationStage) throws ParseException { + requireNonNull(applicationStage); + String trimmedDeadline = applicationStage.trim(); + if (!ApplicationStage.isValidApplicationStage(trimmedDeadline)) { + throw new ParseException(ApplicationStage.MESSAGE_CONSTRAINTS); + } + return ApplicationStage.fromString(trimmedDeadline); + } + + /** + * Parses {@code String jobDescription} into a {@code JobDescription} + */ + public static JobDescription parseJobDescription(String jobDescription) throws ParseException { + requireNonNull(jobDescription); + String trimmedDescription = jobDescription.trim(); + if (!JobDescription.isValidJobDescription(trimmedDescription)) { + throw new ParseException(JobDescription.MESSAGE_CONSTRAINTS); + } + return new JobDescription(trimmedDescription); + } + + /** + * References a function that parses a string into an expected output within the {@link ParserUtil} utility class. + * @param The return result. + */ + @FunctionalInterface + public interface StringParserFunction { + R parse(String value) throws ParseException; + } + + /** + * Returns an object of type R that is given by passing the given string into {@code parseFunction} if + * {@code optionalString} is non-empty, otherwise returns null. + * + * @param The type of object returned by parsing the optionalString. + * + * @throws ParseException if the given {@code optionalString} is invalid as determined by {@code parseFunction} + */ + public static R parseOptionally(Optional optionalString, StringParserFunction parseFunction) + throws ParseException { + + if (optionalString.isPresent()) { + return parseFunction.parse(optionalString.get()); + } + return null; + } } diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/address/logic/parser/Prefix.java deleted file mode 100644 index 348b7686c8a..00000000000 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.parser; - -/** - * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. - */ -public class Prefix { - private final String prefix; - - public Prefix(String prefix) { - this.prefix = prefix; - } - - public String getPrefix() { - return prefix; - } - - @Override - public String toString() { - return getPrefix(); - } - - @Override - public int hashCode() { - return prefix == null ? 0 : prefix.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Prefix)) { - return false; - } - - Prefix otherPrefix = (Prefix) other; - return prefix.equals(otherPrefix.prefix); - } -} diff --git a/src/main/java/seedu/address/logic/parser/ReminderCommandParser.java b/src/main/java/seedu/address/logic/parser/ReminderCommandParser.java new file mode 100644 index 00000000000..a2e34f5f585 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ReminderCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.FLAG_EARLIEST; +import static seedu.address.logic.parser.CliSyntax.FLAG_LATEST; + +import seedu.address.logic.commands.ReminderCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ReminderCommand} object + */ +public class ReminderCommandParser implements Parser { + // TODO: Tech debt - implement tests + /** + * Parses the given {@code String} of arguments in the context of the + * {@code ReminderCommand} and returns a {@code ReminderCommand} object for execution. + */ + public ReminderCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, + ReminderCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new)); + + argMultimap.verifyAtMostOneOfFlagsUsedOutOf(FLAG_EARLIEST, FLAG_LATEST); + + if (argMultimap.hasFlag(FLAG_EARLIEST)) { + return new ReminderCommand(true); + } else if (argMultimap.hasFlag(FLAG_LATEST)) { + return new ReminderCommand(false); + } + + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ReminderCommand.MESSAGE_USAGE)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..c0261eb82a6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,152 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.FLAG_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.FLAG_ASCENDING; +import static seedu.address.logic.parser.CliSyntax.FLAG_DEADLINE; +import static seedu.address.logic.parser.CliSyntax.FLAG_DESCENDING; +import static seedu.address.logic.parser.CliSyntax.FLAG_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FLAG_ID; +import static seedu.address.logic.parser.CliSyntax.FLAG_NAME; +import static seedu.address.logic.parser.CliSyntax.FLAG_NONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_PHONE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STAGE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STALE; +import static seedu.address.logic.parser.CliSyntax.FLAG_STATUS; +import static seedu.address.logic.parser.CliSyntax.FLAG_TITLE; +import static seedu.address.logic.parser.CliSyntax.FLAG_URL; +import static seedu.address.model.Model.COMPARATOR_ADDRESS; +import static seedu.address.model.Model.COMPARATOR_ADDRESS_REVERSED; +import static seedu.address.model.Model.COMPARATOR_EMAIL; +import static seedu.address.model.Model.COMPARATOR_EMAIL_REVERSED; +import static seedu.address.model.Model.COMPARATOR_ID; +import static seedu.address.model.Model.COMPARATOR_NAME; +import static seedu.address.model.Model.COMPARATOR_PHONE; +import static seedu.address.model.Model.COMPARATOR_PHONE_REVERSED; +import static seedu.address.model.Model.COMPARATOR_URL; +import static seedu.address.model.Model.COMPARATOR_URL_REVERSED; + +import java.util.Comparator; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.contact.Contact; +import seedu.address.model.jobapplication.JobApplication; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + + // TODO: Tech debt - implement tests + + /** + * Parses the given {@code String} of arguments in the context of the SortCommand + * and returns a SortCommand object for execution. + */ + public SortCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, + SortCommand.AUTOCOMPLETE_SUPPLIER.getAllPossibleFlags().toArray(Flag[]::new)); + + argMultimap.verifyAtMostOneOfFlagsUsedOutOf(FLAG_NONE, FLAG_ADDRESS, FLAG_EMAIL, + FLAG_ID, FLAG_NAME, FLAG_PHONE, FLAG_URL, FLAG_DEADLINE, FLAG_STAGE, FLAG_STALE, + FLAG_STATUS, FLAG_TITLE); + argMultimap.verifyAtMostOneOfFlagsUsedOutOf(FLAG_NONE, FLAG_ASCENDING, FLAG_DESCENDING); + + if (argMultimap.hasFlag(FLAG_NONE)) { + return new SortCommand(null, null, true); + } + + Boolean isReverse = false; + Comparator contactComparator = null; + Comparator applicationComparator = null; + + if (argMultimap.hasFlag(FLAG_DESCENDING)) { + isReverse = true; + } + + if (argMultimap.hasFlag(FLAG_ADDRESS)) { + if (isReverse) { + contactComparator = COMPARATOR_ADDRESS_REVERSED; + } else { + contactComparator = COMPARATOR_ADDRESS; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_EMAIL)) { + if (isReverse) { + contactComparator = COMPARATOR_EMAIL_REVERSED; + } else { + contactComparator = COMPARATOR_EMAIL; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_NAME)) { + if (isReverse) { + contactComparator = COMPARATOR_NAME.reversed(); + } else { + contactComparator = COMPARATOR_NAME; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_ID)) { + if (isReverse) { + contactComparator = COMPARATOR_ID.reversed(); + } else { + contactComparator = COMPARATOR_ID; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_PHONE)) { + if (isReverse) { + contactComparator = COMPARATOR_PHONE_REVERSED; + } else { + contactComparator = COMPARATOR_PHONE; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_URL)) { + if (isReverse) { + contactComparator = COMPARATOR_URL_REVERSED; + } else { + contactComparator = COMPARATOR_URL; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_STALE)) { + if (isReverse) { + applicationComparator = JobApplication.LAST_UPDATED_COMPARATOR.reversed(); + } else { + applicationComparator = JobApplication.LAST_UPDATED_COMPARATOR; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_STAGE)) { + if (isReverse) { + applicationComparator = JobApplication.STAGE_COMPARATOR.reversed(); + } else { + applicationComparator = JobApplication.STAGE_COMPARATOR; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_STATUS)) { + if (isReverse) { + applicationComparator = JobApplication.STATUS_COMPARATOR.reversed(); + } else { + applicationComparator = JobApplication.STATUS_COMPARATOR; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_DEADLINE)) { + if (isReverse) { + applicationComparator = JobApplication.DEADLINE_COMPARATOR.reversed(); + } else { + applicationComparator = JobApplication.DEADLINE_COMPARATOR; + } + return new SortCommand(contactComparator, applicationComparator, false); + } else if (argMultimap.hasFlag(FLAG_TITLE)) { + if (isReverse) { + applicationComparator = JobApplication.JOB_TITLE_COMPARATOR.reversed(); + } else { + applicationComparator = JobApplication.JOB_TITLE_COMPARATOR; + } + return new SortCommand(contactComparator, applicationComparator, false); + } + + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..4a42ea215ee 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -6,16 +6,16 @@ import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.UniqueContactList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed (by .isSameContact comparison) */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniqueContactList contacts; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -25,13 +25,13 @@ public class AddressBook implements ReadOnlyAddressBook { * among constructors. */ { - persons = new UniquePersonList(); + contacts = new UniqueContactList(); } public AddressBook() {} /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Contacts in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -41,11 +41,11 @@ 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 contact list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + public void setContacts(List contacts) { + this.contacts.setContacts(contacts); } /** @@ -54,44 +54,44 @@ public void setPersons(List persons) { public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + setContacts(newData.getContactList()); } - //// person-level operations + //// contact-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Adds a contact to the address book. + * The contact must not already exist in the address book. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public void addContact(Contact p) { + contacts.add(p); } /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. + * Replaces the given contact {@code target} in the list with {@code editedContact}. * {@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. + * The contact identity of {@code editedContact} must not be the same as another existing one in the address book. */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void setContact(Contact target, Contact editedContact) { + requireNonNull(editedContact); - persons.setPerson(target, editedPerson); + contacts.setContact(target, editedContact); } /** * Removes {@code key} from this {@code AddressBook}. * {@code key} must exist in the address book. */ - public void removePerson(Person key) { - persons.remove(key); + public void removeContact(Contact key) { + contacts.remove(key); + } + + /** + * Returns true if a contact with the same identity as {@code contact} exists in the address book. + */ + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return contacts.contains(contact); } //// util methods @@ -99,13 +99,13 @@ public void removePerson(Person key) { @Override public String toString() { return new ToStringBuilder(this) - .add("persons", persons) + .add("contacts", contacts) .toString(); } @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + public ObservableList getContactList() { + return contacts.asUnmodifiableObservableList(); } @Override @@ -120,11 +120,11 @@ public boolean equals(Object other) { } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + return contacts.equals(otherAddressBook.contacts); } @Override public int hashCode() { - return persons.hashCode(); + return contacts.hashCode(); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..cc88c0388c6 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,18 +1,62 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Type; +import seedu.address.model.jobapplication.JobApplication; /** * 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_CONTACTS = contact -> true; + Predicate PREDICATE_SHOW_ONLY_ORGANIZATIONS = contact -> contact.getType() == Type.ORGANIZATION; + Predicate PREDICATE_SHOW_ONLY_RECRUITERS = contact -> contact.getType() == Type.RECRUITER; + Predicate PREDICATE_SHOW_NOT_APPLIED_ORGANIZATIONS = + contact -> contact.getType() == Type.ORGANIZATION + && ((Organization) contact).getJobApplications().length == 0; + + Comparator COMPARATOR_ADDRESS = Comparator.comparing(contact -> + contact.getAddress().map(address -> address.value).orElse(null), + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_ADDRESS_NULLS_FIRST = Comparator.comparing(contact -> + contact.getAddress().map(address -> address.value).orElse(null), + Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_ADDRESS_REVERSED = COMPARATOR_ADDRESS_NULLS_FIRST.reversed(); + Comparator COMPARATOR_EMAIL = Comparator.comparing(contact -> + contact.getEmail().map(email -> email.value).orElse(null), + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_EMAIL_NULLS_FIRST = Comparator.comparing(contact -> + contact.getEmail().map(email -> email.value).orElse(null), + Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_EMAIL_REVERSED = COMPARATOR_EMAIL_NULLS_FIRST.reversed(); + Comparator COMPARATOR_ID = Comparator.comparing(contact -> + contact.getId().value, String.CASE_INSENSITIVE_ORDER); + Comparator COMPARATOR_NAME = Comparator.comparing(contact -> + contact.getName().fullName, String.CASE_INSENSITIVE_ORDER); + Comparator COMPARATOR_PHONE = Comparator.comparing(contact -> + contact.getPhone().map(phone -> phone.value).orElse(null), + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_PHONE_NULLS_FIRST = Comparator.comparing(contact -> + contact.getPhone().map(phone -> phone.value).orElse(null), + Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_PHONE_REVERSED = COMPARATOR_PHONE_NULLS_FIRST.reversed(); + Comparator COMPARATOR_URL = Comparator.comparing(contact -> + contact.getUrl().map(url -> url.value).orElse(null), + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_URL_NULLS_FIRST = Comparator.comparing(contact -> + contact.getUrl().map(url -> url.value).orElse(null), + Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)); + Comparator COMPARATOR_URL_REVERSED = COMPARATOR_URL_NULLS_FIRST.reversed(); /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -53,35 +97,75 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a contact with the same identity as {@code contact} exists in the address book. */ - boolean hasPerson(Person person); + boolean hasContact(Contact contact); /** - * Deletes the given person. - * The person must exist in the address book. + * Deletes the given contact. + * The contact must exist in the address book. */ - void deletePerson(Person target); + void deleteContact(Contact target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given contact. + * {@code contact} must not already exist in the address book. */ - void addPerson(Person person); + void addContact(Contact contact); /** - * Replaces the given person {@code target} with {@code editedPerson}. + * Replaces the given contact {@code target} with {@code editedContact}. * {@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. + * The contact identity of {@code editedContact} must not be the same as another existing one in the address book. */ - void setPerson(Person target, Person editedPerson); + void setContact(Contact target, Contact editedContact); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** + * Adds the given application. + */ + void addApplication(JobApplication application); + + /** + * Gives a contact which matches the given id. + * Gives null if no such contact is found. + * Given id must not be null. + */ + Contact getContactById(Id id); + + /** + * Guarantees a contact given an id or index. + * + * @throws IllegalValueException if both are given or not given, or if model cannot access the contact. + */ + Contact getContactByIdXorIndex(Id id, Index index) throws IllegalValueException; + + /** + * Replaces the old {@code JobApplication} with the new {@code JobApplication}. + */ + void replaceApplication(JobApplication oldApplication, JobApplication newApplication) throws IllegalValueException; /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. + * Removes the application from the list. */ - void updateFilteredPersonList(Predicate predicate); + void deleteApplication(JobApplication application) throws IllegalValueException; + + + /** Returns an unmodifiable view of the displayed contact list. */ + ObservableList getDisplayedContactList(); + + /** + * Updates the filtered contact list to filter by the given {@code predicate}. May not be null. + */ + void updateFilteredContactList(Predicate predicate); + + /** + * Updates the sorted contact list to sort by the given {@code comparator}. May be null to disable sorting. + */ + void updateSortedContactList(Comparator comparator); + + + /** Returns an unmodifiable view of the displayed application list. */ + ObservableList getDisplayedApplicationList(); + + void updateSortedApplicationList(Comparator comparator); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..8eb4e88abce 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,14 +4,29 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.Messages; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Type; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobApplicationList; /** * Represents the in-memory model of the address book data. @@ -21,7 +36,13 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private final FilteredList displayedContacts; + private final FilteredList filteredContacts; + private final SortedList sortedContacts; + private final JobApplicationList jobApplicationList; + private final SortedList sortedApplications; + private final FilteredList filteredApplications; + private final FilteredList displayedApplications; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -33,7 +54,15 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + this.sortedContacts = new SortedList<>(this.addressBook.getContactList()); + this.filteredContacts = new FilteredList<>(sortedContacts); + this.displayedContacts = filteredContacts; + ObservableList applicationList = + FXCollections.observableArrayList(extractApplicationsFromContacts(filteredContacts)); + this.jobApplicationList = new JobApplicationList(applicationList); + this.sortedApplications = new SortedList<>(applicationList); + this.filteredApplications = new FilteredList<>(this.sortedApplications, s->true); + this.displayedApplications = filteredApplications; } public ModelManager() { @@ -80,6 +109,7 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); + this.jobApplicationList.setAll(extractApplicationsFromContacts(addressBook.getContactList())); } @Override @@ -88,44 +118,155 @@ public ReadOnlyAddressBook getAddressBook() { } @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return addressBook.hasContact(contact); } @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); + public void deleteContact(Contact target) { + addressBook.removeContact(target); + if (target.getType() == Type.ORGANIZATION) { + for (JobApplication i: ((Organization) target).getJobApplications()) { + jobApplicationList.remove(i); + } + } } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public void addContact(Contact contact) { + addressBook.addContact(contact); + updateFilteredContactList(PREDICATE_SHOW_ALL_CONTACTS); + } @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void setContact(Contact target, Contact editedContact) { + requireAllNonNull(target, editedContact); + + addressBook.setContact(target, editedContact); + + if (target.getType() != Type.ORGANIZATION || editedContact.getType() != Type.ORGANIZATION) { + // guard clause + return; + } + if (target.getName().equals(editedContact.getName()) && target.getId().equals(editedContact.getId())) { + // guard clause + return; + } + + updateApplicationNames((Organization) editedContact, (Organization) target); - addressBook.setPerson(target, editedPerson); } - //=========== Filtered Person List Accessors ============================================================= + @Override + public Contact getContactById(Id id) { + return addressBook.getContactById(id); + } + + @Override + public Contact getContactByIdXorIndex(Id id, Index index) throws IllegalValueException { + Contact contact; + if (id == null && index == null) { + throw new IllegalValueException("No contact specified"); + } + if (id != null && index != null) { + throw new IllegalValueException( + Messages.MESSAGE_SIMULTANEOUS_USE_DISALLOWED_FIELDS + "INDEX, ID"); + } + if (id != null) { + contact = getContactById(id); + } else { // else index is not null instead + + List lastShownList = this.getDisplayedContactList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new IllegalValueException(Messages.MESSAGE_INVALID_CONTACT_DISPLAYED_INDEX); + } + contact = lastShownList.get(index.getZeroBased()); + } + + if (contact == null) { + throw new IllegalValueException(Messages.MESSAGE_NO_SUCH_CONTACT); + } + return contact; + } + + @Override + public void replaceApplication(JobApplication oldApplication, + JobApplication newApplication) throws IllegalValueException { + + Contact contact = getContactById(newApplication.getOrganizationId()); + if (contact == null || contact.getType() != Type.ORGANIZATION) { + throw new IllegalValueException("Id field is invalid!"); // TODO: Should I change this? + } + + int index = jobApplicationList.indexOf(oldApplication); + + if (index < 0) { + throw new IllegalValueException("Job application does not exist."); // should never reach here. + } + + Organization organization = (Organization) contact; + try { + organization.replaceJobApplication(oldApplication, newApplication); + } catch (IllegalOperationException e) { + throw new IllegalValueException(e.getMessage()); + } + + jobApplicationList.set(index, newApplication); + + } + + @Override + public void deleteApplication(JobApplication application) throws IllegalValueException { + Contact contact = getContactById(application.getOrganizationId()); + if (contact == null || contact.getType() != Type.ORGANIZATION) { + throw new IllegalValueException("Id field is invalid!"); + } + Organization org = (Organization) contact; + this.jobApplicationList.remove(application); + org.deleteJobApplication(application); + } + + @Override + public void addApplication(JobApplication application) { + jobApplicationList.add(application); + // TODO: Tech debt - need separate declaration for the predicates + filteredApplications.setPredicate(c -> true); + } + + //=========== Filtered Contact 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 {@code Contact} backed by the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; + public ObservableList getDisplayedContactList() { + return this.displayedContacts; } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void updateFilteredContactList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredContacts.setPredicate(predicate); + // TODO: Tech debt - inefficient? + filteredApplications.setPredicate(a -> predicate.test(getContactById(a.getOrganizationId()))); + } + + @Override + public void updateSortedContactList(Comparator comparator) { + this.sortedContacts.setComparator(comparator); + } + + @Override + public ObservableList getDisplayedApplicationList() { + return displayedApplications; + } + + @Override + public void updateSortedApplicationList(Comparator comparator) { + sortedApplications.setComparator(comparator); } @Override @@ -142,7 +283,30 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredContacts.equals(otherModelManager.filteredContacts); + } + + private void updateApplicationNames(Organization newOrg, Organization oldOrg) { + List oldApplications = List.of(oldOrg.getJobApplications()); + List newApplications = List.of(newOrg.getJobApplications()); + + for (JobApplication newApplication: newApplications) { + for (JobApplication oldApplication: oldApplications) { + if (!newApplication.looseEquals(oldApplication)) { + continue; + } + int index = jobApplicationList.indexOf(oldApplication); + jobApplicationList.set(index, newApplication); + } + } + } + + private List extractApplicationsFromContacts(List contacts) { + return contacts.stream() + .filter(c -> c.getType() == Type.ORGANIZATION) + .flatMap(c -> Arrays.stream(((Organization) c).getJobApplications())) + .sorted(JobApplication.LAST_UPDATED_COMPARATOR) + .collect(Collectors.toList()); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..e922899cc79 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,10 @@ package seedu.address.model; +import static java.util.Objects.requireNonNull; + import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; /** * Unmodifiable view of an address book @@ -9,9 +12,24 @@ 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 contacts list. + * This list will not contain any duplicate contacts. + */ + ObservableList getContactList(); + + /** + * Gives a contact which id matches the given id. + * Gives null if a contact with such id does not exist. + * Given id must not be null. */ - ObservableList getPersonList(); + default Contact getContactById(Id id) { + requireNonNull(id); + for (Contact c: getContactList()) { + if (id.equals(c.getId())) { + return c; + } + } + return null; + } } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..b665a525e90 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "jobby.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/contact/Address.java similarity index 94% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/seedu/address/model/contact/Address.java index 469a2cc9a1e..936de0d6e9a 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/contact/Address.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. + * Represents a Contact's address in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { diff --git a/src/main/java/seedu/address/model/contact/Contact.java b/src/main/java/seedu/address/model/contact/Contact.java new file mode 100644 index 00000000000..f681202099d --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Contact.java @@ -0,0 +1,155 @@ +package seedu.address.model.contact; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; + +/** + * Represents a Contact in the address book. + * Guarantees: name and id are present and not null, field values are immutable and if present, are validated. + */ +public abstract class Contact { + + // Identity fields + private final Name name; + private final Id id; + private final Optional phone; + private final Optional email; + + // Data fields + private final Optional url; + private final Optional
address; + private final Set tags = new HashSet<>(); + + private final Optional parent; + + /** + * Name and id fields must be non-null. + * Tags must be non-null but can be empty as well. + * The other fields can be null. + */ + public Contact(Name name, Id id, Phone phone, Email email, Url url, Address address, Set tags, + Contact parent) { + requireAllNonNull(name, id, tags); + this.name = name; + this.id = id; + this.phone = Optional.ofNullable(phone); + this.email = Optional.ofNullable(email); + this.url = Optional.ofNullable(url); + this.address = Optional.ofNullable(address); + this.tags.addAll(tags); + this.parent = Optional.ofNullable(parent); + } + + public abstract Type getType(); + + public Name getName() { + return name; + } + + public Id getId() { + return id; + } + + public Optional getPhone() { + return phone; + } + + public Optional getEmail() { + return email; + } + + public Optional
getAddress() { + return address; + } + + public Optional getUrl() { + return url; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both contacts have the same id. + * This defines a weaker notion of equality between two contacts. + */ + public boolean isSameContact(Contact otherContact) { + if (otherContact == this) { + return true; + } + + return otherContact != null + && otherContact.getId().equals(getId()); + } + + /** + * Gives the parent of this contact. + */ + public Optional getParent() { + return parent; + } + + /** + * Gives the array of contacts that are linked under this contact. + */ + public List getChildren(Model model) { + // TODO add to DG + return model.getAddressBook().getContactList().stream() + .filter(contact -> contact.getParent() + .map(parent -> parent.equals(this)) + .orElse(false)) + .collect(Collectors.toList()); + } + + /** + * Returns true if both contacts have the same identity and data fields. + * This defines a stronger notion of equality between two contacts. + */ + @Override + public abstract boolean equals(Object other); + + @Override + public int hashCode() { + return Objects.hash(id, getType(), name, phone, email, url, address, tags); + } + + /** + * Returns a builder for the {@link #toString} method of this class using {@code ToStringBuilder}. + * This can be overriden by subclasses to add properties to the builder. + * + * @return An instance of {@code ToStringBuilder} capable of crafting a string representation of this instance. + */ + protected ToStringBuilder toStringBuilder() { + return new ToStringBuilder(this) + .add("name", name) + .add("type", getType()) + .add("id", id) + .add("phone", phone) + .add("email", email) + .add("url", url) + .add("address", address) + .add("tags", tags); + } + + @Override + public String toString() { + return toStringBuilder().toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/contact/Email.java similarity index 53% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/seedu/address/model/contact/Email.java index c62e512bc29..fe1f47ead16 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/contact/Email.java @@ -1,32 +1,26 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. + * Represents a Contact's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { - private static final String SPECIAL_CHARACTERS = "+_.-"; + public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" - + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " - + "separated by periods.\n" - + "The domain name must:\n" - + " - 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 + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE - + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; + + "and adhere to the following constraints:\n\n" + + "1. There must be exactly one '@' sign separating the local part and domain name.\n" + + "2. Both parts may not contain whitespace.\n" + + "3. The domain name is made up of domain labels separated by periods, " + + " and must end with a domain label at least 2 characters long.\n"; + + private static final String ALL_ACCEPTED_CHARS = "[^\\s@]"; // Very loose match to support International Emails + private static final String LOCAL_PART_REGEX = "^" + ALL_ACCEPTED_CHARS + "+"; + private static final String DOMAIN_PART_REGEX = ALL_ACCEPTED_CHARS + + "(-" + ALL_ACCEPTED_CHARS + ")*"; private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; diff --git a/src/main/java/seedu/address/model/contact/Id.java b/src/main/java/seedu/address/model/contact/Id.java new file mode 100644 index 00000000000..162373d5050 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Id.java @@ -0,0 +1,126 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.Random; +import java.util.UUID; + +/** + * Represents a Contact's unique Id in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidId(String)} + */ +public class Id { + public static final String MESSAGE_CONSTRAINTS = + "Id should only contain alphanumeric characters, underscores and dashes, and it should not be blank. " + + "Ids must also start with alphabets, and may not have consecutive underscores and/or dashes."; + + /** + * The first character of the id must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[a-zA-Z](([_\\-]?[a-zA-Z0-9])*)"; + + public final String value; + + /** + * Constructs a {@code Id}. + * + * @param id A valid id. + */ + public Id(String id) { + requireNonNull(id); + checkArgument(isValidId(id), MESSAGE_CONSTRAINTS); + value = id; + } + + /** + * Constructs an autogenerated {@code Id}. + */ + public Id() { + value = "i-" + UUID.randomUUID().toString(); + } + + /** + * Synthesizes an {@link Id} from a given string. + */ + public static Id synthesizeFrom(String input) { + if (input == null || input.isBlank()) { + return new Id(); + } + + String resultingIdString = input.trim().toLowerCase(); + + // Ensure leading letters + if (!resultingIdString.matches("^[a-zA-Z].*")) { + resultingIdString = "i-" + resultingIdString; + } + + // Ensure no non-accepted characters + resultingIdString = resultingIdString + .replaceAll("[ \t\n]", "-") + .replaceAll("[^a-zA-Z0-9_\\-]", "x"); + + // Ensure no repeated _ or - + resultingIdString = resultingIdString + .replaceAll("(-_+|_-+)", "-") + .replaceAll("_{2,}", "_") + .replaceAll("-{2,}", "-"); + + // Add a random trailing 6-digit hex to minimize collisions + Random random = new Random(); + int hexValue = random.nextInt(0x1_000_000); + + if (resultingIdString.matches("(.*)[^_\\-]$")) { + // Append a trailing dash (if one isn't present) before the hex value + resultingIdString += "-"; + } + resultingIdString += String.format("%06x", hexValue); + + // Construct and return our new id + try { + return new Id(resultingIdString); + } catch (IllegalArgumentException e) { + // If we still fail due to an invalid format, generate a random one. + // + // Note: It is best to always synthesize user-friendly id values. So, if we have values that result in + // parse errors, consider checking what cases have been missed and normalize them to the rules. + return new Id(); + } + } + + + /** + * Returns true if a given string is a valid id. + */ + public static boolean isValidId(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Id)) { + return false; + } + + Id otherId = (Id) other; + return value.equals(otherId.value); + } + + @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/contact/Name.java similarity index 81% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/address/model/contact/Name.java index 173f15b9b00..b856447e2ab 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/contact/Name.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Contact's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { @@ -12,11 +12,11 @@ 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. + /** + * Name must not be blank, must not contain any leading and trailing whitespaces + * and can have any number of characters. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[^\\s](.*[^\\s]|)"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/contact/NameContainsKeywordsPredicate.java similarity index 51% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/contact/NameContainsKeywordsPredicate.java index 62d19be2977..41931154f5b 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/contact/NameContainsKeywordsPredicate.java @@ -1,15 +1,16 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import java.util.List; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; /** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + * Tests that a {@code Contact}'s {@code Name} matches any of the keywords given. */ -public class NameContainsKeywordsPredicate implements Predicate { +public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; public NameContainsKeywordsPredicate(List keywords) { @@ -17,9 +18,25 @@ public NameContainsKeywordsPredicate(List keywords) { } @Override - public boolean test(Person person) { + public boolean test(Contact contact) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> { + String pattern = "(?i)" + keyword; + String contactName = contact.getName().fullName; + Pattern regexPattern = Pattern.compile(pattern); + Matcher matcher = regexPattern.matcher(contactName); + if (matcher.find()) { + return true; + } + String contactId = contact.getId().value; + matcher = regexPattern.matcher(contactId); + if (matcher.find()) { + return true; + } + return false; + + }); + } @Override diff --git a/src/main/java/seedu/address/model/contact/Organization.java b/src/main/java/seedu/address/model/contact/Organization.java new file mode 100644 index 00000000000..11d306156db --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Organization.java @@ -0,0 +1,154 @@ +package seedu.address.model.contact; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.tag.Tag; + +/** + * Represents an Organisation in the address book. + * Guarantees: Guarantees: name and id are present and not null, + * field values are immutable and if present, are validated. + */ +public class Organization extends Contact { + // TODO: Override the getChildren method + + private final List jobApplications = new ArrayList<>(); + + /** + * Name and id fields must be non-null. + * Tags must be non-null but can be empty as well. + * The other fields can be null. + */ + public Organization( + Name name, Id id, Phone phone, Email email, Url url, + Address address, Set tags + ) { + this(name, id, phone, email, url, address, tags, new ArrayList<>()); + } + + /** + * Name and id fields must be non-null. + * Tags must be non-null but can be empty as well. + * List of applications must not be null. + * The other fields can be null. + */ + public Organization( + Name name, Id id, Phone phone, Email email, Url url, + Address address, Set tags, List jobApplications + ) { + super(name, id, phone, email, url, address, tags, null); + // Ensure that the new job applications are modified correctly. + List newApplications = jobApplications + .stream() + .map(a -> a.changeCompanyDetails(name, id)) + .collect(Collectors.toList()); + + this.jobApplications.addAll(newApplications); + } + + @Override + public Type getType() { + return Type.ORGANIZATION; + } + + /** + * Returns a list of {@code JobApplication} made to this organization. + */ + public JobApplication[] getJobApplications() { + return this.jobApplications.toArray(new JobApplication[]{}); + } + + /** + * Checks if the organization has the given {@code JobApplication}. + */ + public boolean hasJobApplication(JobApplication jobApplication) { + return this.jobApplications.stream() + .anyMatch(application -> application.isSameApplication(jobApplication)); + } + + /** + * Adds a {@code JobApplication} to the list of applications. + */ + public void addJobApplication(JobApplication jobApplication) { + this.jobApplications.add(jobApplication); + } + + /** + * Replaces the old job application in the list with the new one. + */ + public void replaceJobApplication(JobApplication oldApplication, JobApplication newApplication) throws + IllegalOperationException { + assert newApplication.getOrganizationId().equals(this.getId()); + assert newApplication.getOrganizationId().equals(oldApplication.getOrganizationId()); + if (hasApplicationWithSameNameWithExclusion(newApplication, oldApplication)) { + throw new IllegalOperationException("Job Application with same name found. Set a different name"); + } + this.jobApplications.remove(oldApplication); + this.jobApplications.add(newApplication); + } + + /** + * Deletes the job application in the list. + */ + public void deleteJobApplication(JobApplication application) { + this.jobApplications.remove(application); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls implicitly + if (!(other instanceof Organization)) { + return false; + } + + Organization otherContact = (Organization) other; + return getId().equals(otherContact.getId()) + && getType().equals(otherContact.getType()) + && getName().equals(otherContact.getName()) + && getPhone().equals(otherContact.getPhone()) + && getEmail().equals(otherContact.getEmail()) + && getAddress().equals(otherContact.getAddress()) + && getUrl().equals(otherContact.getUrl()) + && getTags().equals(otherContact.getTags()); + } + + @Override + public int hashCode() { + return Objects.hash( + getId(), getType(), getName(), getPhone(), getEmail(), getAddress(), getTags() + ); + } + + @Override + public ToStringBuilder toStringBuilder() { + return super.toStringBuilder(); + } + + private boolean hasApplicationWithSameNameWithExclusion(JobApplication application, + JobApplication... excludedApplications) { + List applicationsExcludedList = + Arrays.stream(excludedApplications).collect(Collectors.toList()); + for (JobApplication a: jobApplications) { + if (applicationsExcludedList.contains(a)) { + continue; + } + if (application.getJobTitle().equals(a.getJobTitle())) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/contact/Phone.java similarity index 93% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/seedu/address/model/contact/Phone.java index d733f63d739..acdddbec472 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/contact/Phone.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's phone number in the address book. + * Represents a Contact's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { diff --git a/src/main/java/seedu/address/model/contact/Recruiter.java b/src/main/java/seedu/address/model/contact/Recruiter.java new file mode 100644 index 00000000000..0e94d793329 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Recruiter.java @@ -0,0 +1,80 @@ +package seedu.address.model.contact; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.tag.Tag; + +/** + * Represents a Recruiter in the address book. + * Guarantees: Guarantees: name and id are present and not null, + * field values are immutable and if present, are validated. + */ +public class Recruiter extends Contact { + + public static final String MESSAGE_INVALID_ORGANIZATION = + "If a recruiter is linked to an organization, " + + "the linked organization should be present in the address book " + + "and have a valid id"; + + /** + * Name and id fields must be non-null. + * Tags must be non-null but can be empty as well. + * The other fields can be null. + */ + public Recruiter(Name name, Id id, Phone phone, Email email, Url url, Address address, Set tags, + Organization organization) { + super(name, id, phone, email, url, address, tags, organization); + } + + public Optional getOrganizationId() { + return getOrganization().map(Contact::getId); + } + + public Optional getOrganization() { + return super.getParent().map(contact -> (Organization) contact); + } + + @Override + public Type getType() { + return Type.RECRUITER; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls implicitly + if (!(other instanceof Recruiter)) { + return false; + } + + Recruiter otherContact = (Recruiter) other; + return getId().equals(otherContact.getId()) + && getType().equals(otherContact.getType()) + && getName().equals(otherContact.getName()) + && getPhone().equals(otherContact.getPhone()) + && getEmail().equals(otherContact.getEmail()) + && getAddress().equals(otherContact.getAddress()) + && getUrl().equals(otherContact.getUrl()) + && getTags().equals(otherContact.getTags()) + && getOrganization().equals(otherContact.getOrganization()); + } + + @Override + public int hashCode() { + return Objects.hash( + getId(), getType(), getName(), getPhone(), getEmail(), getAddress(), getTags(), getOrganization() + ); + } + + @Override + public ToStringBuilder toStringBuilder() { + return super.toStringBuilder() + .add("organization", getOrganization()); + } +} diff --git a/src/main/java/seedu/address/model/contact/Type.java b/src/main/java/seedu/address/model/contact/Type.java new file mode 100644 index 00000000000..08222552f0c --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Type.java @@ -0,0 +1,56 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.EnumUtil; + +/** + * Represents the type of {@code Contact} instances. + */ +public enum Type { + + ORGANIZATION("organization"), + RECRUITER("recruiter"); + + public static final String MESSAGE_CONSTRAINTS = "Contact type must be 'organization' or 'recruiter'."; + + private final String textRepresentation; + + Type(String textRepresentation) { + requireNonNull(textRepresentation); + this.textRepresentation = textRepresentation; + } + + /** + * Returns the {@code ContactType} enum as a string representation. This is reversible, i.e., the string + * representation here can be used to re-obtain the {@code ContactType} enum by using {@link #fromString}. + * + * @return A string representation of the contact type. + */ + @Override + public String toString() { + return this.textRepresentation; + } + + /** + * Returns a corresponding {@code ContactType} enum value matching the given text representation of it. + * + * @param textRepresentation The text representation of the {@code ContactType}. + * @return The corresponding {@code ContactType}. + * @throws IllegalArgumentException if the given input does not represent any known {@code ContactType}. + */ + public static Type fromString(String textRepresentation) throws IllegalArgumentException { + return EnumUtil.lookupByToString(Type.class, textRepresentation); + } + + /** + * Verifies if the given input is a valid contact type. + * + * @param textRepresentation The text representation of the {@code Type}. + * @return Whether the contact type matches a known value. + */ + public static boolean isValidType(String textRepresentation) { + return EnumUtil.hasMatchingToString(Type.class, textRepresentation); + } + +} diff --git a/src/main/java/seedu/address/model/contact/UniqueContactList.java b/src/main/java/seedu/address/model/contact/UniqueContactList.java new file mode 100644 index 00000000000..96565c217f9 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/UniqueContactList.java @@ -0,0 +1,150 @@ +package seedu.address.model.contact; + +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.contact.exceptions.DuplicatePersonException; +import seedu.address.model.contact.exceptions.PersonNotFoundException; + +/** + * A list of contacts that enforces uniqueness between its elements and does not allow nulls. + * A contact is considered unique by comparing using {@code Contact#isSameContact(Contact)}. As such, adding and + * updating of contacts uses Contact#isSameContact(Contact) for equality so as to ensure that the contact being added or + * updated is unique in terms of identity in the UniqueContactList. However, the removal of a contact uses + * Contact#equals(Object) so as to ensure that the contact with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Contact#isSameContact(Contact) + */ +public class UniqueContactList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent contact as the given argument. + */ + public boolean contains(Contact toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameContact); + } + + /** + * Adds a contact to the list. + * The contact must not already exist in the list. + */ + public void add(Contact toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicatePersonException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the contact {@code target} in the list with {@code editedContact}. + * {@code target} must exist in the list. + * The contact identity of {@code editedContact} must not be the same as another existing contact in the list. + */ + public void setContact(Contact target, Contact editedContact) { + requireAllNonNull(target, editedContact); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new PersonNotFoundException(); + } + + if (!target.isSameContact(editedContact) && contains(editedContact)) { + throw new DuplicatePersonException(); + } + + internalList.set(index, editedContact); + } + + /** + * Removes the equivalent contact from the list. + * The contact must exist in the list. + */ + public void remove(Contact toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new PersonNotFoundException(); + } + } + + public void setContacts(UniqueContactList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. + */ + public void setContacts(List contacts) { + requireAllNonNull(contacts); + if (!areUniqueContacts(contacts)) { + throw new DuplicatePersonException(); + } + + internalList.setAll(contacts); + } + + /** + * 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) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueContactList)) { + return false; + } + + UniqueContactList otherUniqueContactList = (UniqueContactList) other; + return internalList.equals(otherUniqueContactList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + /** + * Returns true if {@code contacts} contains only unique contacts. + */ + private boolean areUniqueContacts(List contacts) { + for (int i = 0; i < contacts.size() - 1; i++) { + for (int j = i + 1; j < contacts.size(); j++) { + if (contacts.get(i).isSameContact(contacts.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/contact/Url.java b/src/main/java/seedu/address/model/contact/Url.java new file mode 100644 index 00000000000..427933dcd0e --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Url.java @@ -0,0 +1,70 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Contact's url in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidUrl(String)} + */ +public class Url { + + public static final String MESSAGE_CONSTRAINTS = + "Url should minimally contain a dot surrounded by text, like example.com"; + + public static final String VALIDATION_REGEX = ".+\\..+"; + + public final String value; + + /** + * Constructs a {@code Url}. + * + * @param url A valid url. + */ + public Url(String url) { + requireNonNull(url); + checkArgument(isValidUrl(url), MESSAGE_CONSTRAINTS); + value = url; + } + + /** + * Constructs an empty {@code Url}. + */ + public Url() { + value = ""; + } + + /** + * Returns true if a given string is a valid url. + */ + public static boolean isValidUrl(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Url)) { + return false; + } + + Url otherName = (Url) other; + return value.equals(otherName.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/contact/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/contact/exceptions/DuplicatePersonException.java new file mode 100644 index 00000000000..688dca665e0 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/DuplicatePersonException.java @@ -0,0 +1,11 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation will result in duplicate Contacts (Contacts are considered duplicates if they have the + * same identity). + */ +public class DuplicatePersonException extends RuntimeException { + public DuplicatePersonException() { + super("Operation would result in duplicate contacts"); + } +} diff --git a/src/main/java/seedu/address/model/contact/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/contact/exceptions/PersonNotFoundException.java new file mode 100644 index 00000000000..04938b0c3e5 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/PersonNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation is unable to find the specified contact. + */ +public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java b/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java new file mode 100644 index 00000000000..f868eedda8e --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/ApplicationStage.java @@ -0,0 +1,54 @@ +package seedu.address.model.jobapplication; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.EnumUtil; + +/** + * The different stages of internship application. + */ +public enum ApplicationStage { + RESUME("resume"), + ONLINE_ASSESSMENT("online assessment"), + INTERVIEW("interview"); + + public static final ApplicationStage DEFAULT_STAGE = ApplicationStage.RESUME; + public static final String MESSAGE_CONSTRAINTS = "Applications accept one of these values: resume | online " + + "assessment | interview"; + private final String textRepresentation; + + ApplicationStage(String textRepresentation) { + requireNonNull(textRepresentation); + this.textRepresentation = textRepresentation; + } + + /** + * Returns the {@code ApplicationStage} enum as string representation. This is reversible, i.e., the string + * representation here can be used to re-obtain the {@code ApplicationStage} enum by using {@link #fromString}. + */ + @Override + public String toString() { + return this.textRepresentation; + } + + /** + * Returns a corresponding {@code ApplicationStage} enum value matching the given text representation of it. + * + * @param textRepresentation The text representation of the {@code ApplicationStage}. + * @return The corresponding {@code JobStatus}. + * @throws IllegalArgumentException if the text representation does not match any known values. + */ + public static ApplicationStage fromString(String textRepresentation) { + return EnumUtil.lookupByToString(ApplicationStage.class, textRepresentation); + } + + /** + * Verifies if the given input is a valid job application stage. + * + * @param textRepresentation The text representation of the {@code ApplicationStage}. + * @return Whether the application stage matches a known value. + */ + public static boolean isValidApplicationStage(String textRepresentation) { + return EnumUtil.hasMatchingToString(ApplicationStage.class, textRepresentation); + } +} diff --git a/src/main/java/seedu/address/model/jobapplication/Deadline.java b/src/main/java/seedu/address/model/jobapplication/Deadline.java new file mode 100644 index 00000000000..8e9f4025704 --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/Deadline.java @@ -0,0 +1,137 @@ +package seedu.address.model.jobapplication; + + +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.util.Arrays; +import java.util.List; + +/** + * Represents the various job application deadlines. + * Such as deadlines for applications, deadlines for online assessments. + */ +public class Deadline implements Comparable { + + public static final String MESSAGE_CONSTRAINTS = + "Deadlines should be in the format of DD-MM-YYYY"; + + public static final String VALIDATION_REGEX = "^(0?[1-9]|[12][0-9]|3[01])-(0?[1-9]|1[012])-\\d{4}$"; + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("d-M-y"); + public final LocalDate deadline; + + /** + * Gives a deadline given a string deadline. + * @param deadline in the appropriate format. + */ + public Deadline(String deadline) { + requireNonNull(deadline); + checkArgument(isValidDeadline(deadline), MESSAGE_CONSTRAINTS); + this.deadline = LocalDate.parse(deadline, FORMATTER); + } + + /** + * Gives a deadline given a {@link LocalDate} instance. + * @param deadline as a {@link LocalDate} instance. + */ + public Deadline(LocalDate deadline) { + requireNonNull(deadline); + this.deadline = deadline; + } + + /** + * Gives a default deadline 14 days from now. + */ + public Deadline() { + this.deadline = LocalDate.now().plusDays(14); + } + + /** + * Checks if the given string is strictly a valid date. + */ + public static boolean isValidDeadline(String test) { + if (!test.matches(VALIDATION_REGEX)) { + return false; + } + + String[] arr = test.split("-"); + if (arr.length != 3) { + return false; + } + + int day = Integer.parseInt(arr[0]); + int month = Integer.parseInt(arr[1]); + int year = Integer.parseInt(arr[2]); + + return isValidDate(day, month, year); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof Deadline)) { + return false; + } + + Deadline otherDeadline = (Deadline) other; + return deadline.equals(otherDeadline.deadline); + } + + @Override + public int compareTo(Deadline d) { + return deadline.compareTo(d.deadline); + } + + @Override + public String toString() { + return FORMATTER.format(deadline); + } + + private static boolean isValidDate(int day, int month, int year) { + + List monthsWithThirtyDays = Arrays.asList(4, 6, 9, 11); + + // these are completely invalid + if (day < 1 || day > 31 || month < 1 || month > 12 || year < 0) { + return false; + } + + // these are for months with 30 days + if (monthsWithThirtyDays.contains(month)) { + return day < 31; + } + + // these are for months with 31 days + if (month != 2) { + return true; + } + + // remaining here is february + if (isLeapYear(year)) { + return day < 30; + } + + return day < 29; + + + } + + private static boolean isLeapYear(int year) { + if (year % 400 == 0) { + return true; + } + + if (year % 100 == 0) { + return false; + } + + return year % 4 == 0; + } + +} diff --git a/src/main/java/seedu/address/model/jobapplication/JobApplication.java b/src/main/java/seedu/address/model/jobapplication/JobApplication.java new file mode 100644 index 00000000000..321c96df42e --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/JobApplication.java @@ -0,0 +1,250 @@ +package seedu.address.model.jobapplication; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; + +/** + * Represents a Job Application in the address book. + */ +public class JobApplication { + + public static final Comparator STATUS_COMPARATOR = (a, b) -> + a.status.compareTo(b.status); + + public static final Comparator STAGE_COMPARATOR = (a, b) -> + a.applicationStage.compareTo(b.applicationStage); + + public static final Comparator DEADLINE_COMPARATOR = (a, b) -> + a.deadline.compareTo(b.deadline); + + public static final Comparator LAST_UPDATED_COMPARATOR = (a, b) -> + a.lastUpdatedTime.compareTo(b.lastUpdatedTime); + + public static final Comparator JOB_TITLE_COMPARATOR = Comparator.comparing( + application -> application.getJobTitle().title, String.CASE_INSENSITIVE_ORDER); + + private final Id oid; + private final Name orgName; + + private final JobTitle jobTitle; + + private final Optional jobDescription; + + private final LastUpdatedTime lastUpdatedTime; + + private final Deadline deadline; + + private final JobStatus status; + + private final ApplicationStage applicationStage; + + /** + * Constructs a job application. + * + * @param org that is being applied to + * @param jobTitle of the postion applied to. + * @param jobDescription of the positon applied to. + * @param deadline of the application or interview if relevant. + * @param status of the application + */ + public JobApplication(Organization org, JobTitle jobTitle, JobDescription jobDescription, + Deadline deadline, JobStatus status, ApplicationStage applicationStage) { + this(org, jobTitle, jobDescription, deadline, status, applicationStage, new LastUpdatedTime()); + } + + /** + * Constructs a job application with default status and application stage. + * + * @param org that is being applied to + * @param jobTitle of the postion applied to. + * @param jobDescription of the positon applied to. + * @param deadline of the application or interview if relevant. + */ + public JobApplication(Organization org, JobTitle jobTitle, JobDescription jobDescription, + Deadline deadline) { + this(org, jobTitle, jobDescription, deadline, JobStatus.DEFAULT_STATUS, ApplicationStage.DEFAULT_STAGE); + } + + /** + * Constructs a job application with default status. + * + * @param org that is being applied to + * @param jobTitle of the postion applied to. + * @param jobDescription of the positon applied to. + * @param deadline of the application or interview if relevant. + * @param applicationStage of the application. + */ + public JobApplication(Organization org, JobTitle jobTitle, JobDescription jobDescription, + Deadline deadline, ApplicationStage applicationStage) { + this(org, jobTitle, jobDescription, deadline, JobStatus.DEFAULT_STATUS, applicationStage); + } + + /** + * Constructs a job application (should be directly used by Jackson only) + * + * @param oid of the organization that is being applied to. + * @param orgName of the organization that is being applied to. + * @param jobTitle of the postion applied to. + * @param jobDescription of the positon applied to. + * @param deadline of the application or interview if relevant. + * @param status of the application + * @param lastUpdatedTime of the application + */ + public JobApplication( + Id oid, Name orgName, JobTitle jobTitle, JobDescription jobDescription, + Deadline deadline, JobStatus status, ApplicationStage applicationStage, + LastUpdatedTime lastUpdatedTime) { + requireNonNull(oid); + requireNonNull(orgName); + requireNonNull(jobTitle); + this.oid = oid; + this.orgName = orgName; + this.jobTitle = jobTitle; + this.jobDescription = Optional.ofNullable(jobDescription); + + this.deadline = deadline == null ? new Deadline() : deadline; + this.status = status == null ? JobStatus.DEFAULT_STATUS : status; + this.applicationStage = applicationStage == null ? ApplicationStage.DEFAULT_STAGE : applicationStage; + this.lastUpdatedTime = lastUpdatedTime; + } + + /** + * Constructs a job application (should be directly used by Jackson only) + * + * @param org that is being applied to + * @param jobTitle of the postion applied to. + * @param jobDescription of the positon applied to. + * @param deadline of the application or interview if relevant. + * @param status of the application + * @param lastUpdatedTime of the application + */ + public JobApplication( + Organization org, JobTitle jobTitle, JobDescription jobDescription, + Deadline deadline, JobStatus status, ApplicationStage applicationStage, + LastUpdatedTime lastUpdatedTime) { + this(org.getId(), org.getName(), jobTitle, jobDescription, deadline, status, applicationStage, + lastUpdatedTime); + } + + /** + * Returns true if both job applications have the same job title. + * This defines a weaker notion of equality between two job applications. + */ + public boolean isSameApplication(JobApplication otherApplication) { + if (otherApplication == this) { + return true; + } + + return otherApplication != null + && otherApplication.getOrganizationId().equals(getOrganizationId()) + && otherApplication.getJobTitle().equals(getJobTitle()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof JobApplication)) { + return false; + } + + JobApplication otherApplication = (JobApplication) other; + return oid.equals(otherApplication.oid) + && jobTitle.equals(otherApplication.jobTitle) + && jobDescription.equals(otherApplication.jobDescription) + && deadline.equals(otherApplication.deadline) + && lastUpdatedTime.equals(otherApplication.lastUpdatedTime) + && status.equals(otherApplication.status) + && applicationStage.equals(otherApplication.applicationStage); + } + + public Id getOrganizationId() { + return oid; + } + + public JobTitle getJobTitle() { + return jobTitle; + } + + public Optional getJobDescription() { + return jobDescription; + } + + public Deadline getDeadline() { + return deadline; + } + + public LastUpdatedTime getLastUpdatedTime() { + return lastUpdatedTime; + } + + public JobStatus getStatus() { + return status; + } + + public ApplicationStage getApplicationStage() { + return applicationStage; + } + public Name getOrgName() { + return orgName; + } + + /** + * Checks if the details of the job application is the same excluding org name and id and last updated time. + */ + public boolean looseEquals(JobApplication other) { + return this.jobTitle.equals(other.jobTitle) + && this.jobDescription.equals(other.jobDescription) + && this.applicationStage.equals(other.applicationStage) + && this.status.equals(other.status); + } + + /** + * Gives a new job application given change in company details. + */ + public JobApplication changeCompanyDetails(Name orgName, Id oid) { + return new JobApplication(oid, orgName, this.jobTitle, this.jobDescription.orElse(null), this.deadline, + this.status, this.applicationStage, new LastUpdatedTime()); + } + + @Override + public int hashCode() { + return Objects.hash( + oid.toString(), + jobTitle.toString(), + jobDescription.map(JobDescription::toString).orElse("None"), + deadline.toString(), + lastUpdatedTime.toString(), + status.toString(), + applicationStage.toString() + ); + } + + /** + * Returns a builder for the {@link #toString} method of this class + * @return an instance if {@code ToStringBuilder} + */ + protected ToStringBuilder toStringBuilder() { + return new ToStringBuilder("") + .add("title", jobTitle) + .add("\nstage", applicationStage) + .add("\nstatus", status) + .add("\ndeadline", deadline) + .add("\ndescription", jobDescription.map(JobDescription::toString).orElse("None")); + } + + @Override + public String toString() { + return toStringBuilder().toString(); + } +} diff --git a/src/main/java/seedu/address/model/jobapplication/JobApplicationList.java b/src/main/java/seedu/address/model/jobapplication/JobApplicationList.java new file mode 100644 index 00000000000..9597cc0c909 --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/JobApplicationList.java @@ -0,0 +1,70 @@ +package seedu.address.model.jobapplication; + + +import java.util.Collection; + +import javafx.collections.ObservableList; + +/** + * A wrapper for ObservableList of {@link JobApplication} + */ +public class JobApplicationList { + + private ObservableList applications; + + /** + * Creates a wrapper for the observable list. + */ + public JobApplicationList(ObservableList applications) { + this.applications = applications; + } + + /** + * Gives the list of applications. + */ + public ObservableList getApplications() { + return applications; + } + + /** + * Removes application from the list. + */ + public void remove(JobApplication application) { + applications.remove(application); + } + + /** + * Adds application to list. + */ + public void add(JobApplication jobApplication) { + applications.add(jobApplication); + } + + /** + * Sets application at the specified index. + */ + public void set(int index, JobApplication jobApplication) { + applications.set(index, jobApplication); + } + + /** + * Gets the application at the specified index. + */ + public JobApplication get(int index) { + return applications.get(index); + } + + /** + * Gets the index of the application in the list. Returns -1 if not found. + */ + public int indexOf(JobApplication application) { + return applications.indexOf(application); + } + + /** + * Replaces all the applications in the list with the new applications + */ + public void setAll(Collection applications) { + this.applications.setAll(applications); + } +} diff --git a/src/main/java/seedu/address/model/jobapplication/JobDescription.java b/src/main/java/seedu/address/model/jobapplication/JobDescription.java new file mode 100644 index 00000000000..c8e01de193d --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/JobDescription.java @@ -0,0 +1,59 @@ +package seedu.address.model.jobapplication; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + + +/** + * Describes the functions of the job to be applied to. + */ +public class JobDescription { + public static final String MESSAGE_CONSTRAINTS = + "Job description should only contain alphanumeric characters, underscores and dashes"; + + /** + * The first character of the id must not be a whitespace, otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "^(?!\\s*$).+"; + + public final String description; + + /** + * Constructs a {@code JobDescription}. + * + * @param description A valid description. + */ + public JobDescription(String description) { + requireNonNull(description); + checkArgument(isValidJobDescription(description), MESSAGE_CONSTRAINTS); + this.description = description; + } + + /** + * Returns true if a given string is a valid description. + */ + public static boolean isValidJobDescription(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.description; + } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof JobDescription)) { + return false; + } + + JobDescription otherDescription = (JobDescription) other; + return description.equals(otherDescription.description); + } + +} diff --git a/src/main/java/seedu/address/model/jobapplication/JobStatus.java b/src/main/java/seedu/address/model/jobapplication/JobStatus.java new file mode 100644 index 00000000000..95f24a2e466 --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/JobStatus.java @@ -0,0 +1,58 @@ +package seedu.address.model.jobapplication; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.EnumUtil; + +/** + * Information on the status of the job application: pending, rejected, offered, accepted, turned-down + */ +public enum JobStatus { + PENDING("pending"), + REJECTED("rejected"), + OFFERED("offered"), + ACCEPTED("accepted"), + TURNED_DOWN("turned down"); + + public static final JobStatus DEFAULT_STATUS = JobStatus.PENDING; + + public static final String MESSAGE_CONSTRAINTS = "Job status are one of the values: pending | rejected | offered " + + "| accepted | turned down"; + + private final String textRepresentation; + + JobStatus(String textRepresentation) { + requireNonNull(textRepresentation); + this.textRepresentation = textRepresentation; + } + + /** + * Returns the {@code JobStatus} enum as string representation. This is reversible, i.e., the string + * representation here can be used to re-obtain the {@code JobStatus} enum by using {@link #fromString}. + */ + @Override + public String toString() { + return this.textRepresentation; + } + + /** + * Returns a corresponding {@code JobStatus} enum value matching the given text representation of it. + * + * @param textRepresentation The text representation of the {@code JobStatus}. + * @return The corresponding {@code JobStatus}. + * @throws IllegalArgumentException if the text representation does not match any known values. + */ + public static JobStatus fromString(String textRepresentation) { + return EnumUtil.lookupByToString(JobStatus.class, textRepresentation); + } + + /** + * Verifies if the given input is a valid job status. + * + * @param textRepresentation The text representation of the {@code JobStatus}. + * @return Whether the job status matches a known value. + */ + public static boolean isValidJobStatus(String textRepresentation) { + return EnumUtil.hasMatchingToString(JobStatus.class, textRepresentation); + } +} diff --git a/src/main/java/seedu/address/model/jobapplication/JobTitle.java b/src/main/java/seedu/address/model/jobapplication/JobTitle.java new file mode 100644 index 00000000000..a32e7e24fa5 --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/JobTitle.java @@ -0,0 +1,55 @@ +package seedu.address.model.jobapplication; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * The title of the job that is applied to. + */ +public class JobTitle { + public static final String MESSAGE_CONSTRAINTS = + "Job title should only contain alphanumeric characters, underscores and dashes, and it should not be blank"; + + /** + * Job titles must not be blank, must not contain any leading and trailing whitespaces + * and can have any number of characters. + */ + public static final String VALIDATION_REGEX = "[^\\s](.*[^\\s]|)"; + + public final String title; + + /** + * Constructs a {@code JobTitle} + * + * @params title a valid title. + */ + public JobTitle(String title) { + requireNonNull(title); + checkArgument(isValidJobTitle(title), MESSAGE_CONSTRAINTS); + this.title = title; + } + + public static boolean isValidJobTitle(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.title; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof JobTitle)) { + return false; + } + + JobTitle otherTitle = (JobTitle) other; + return title.equals(otherTitle.title); + } +} diff --git a/src/main/java/seedu/address/model/jobapplication/LastUpdatedTime.java b/src/main/java/seedu/address/model/jobapplication/LastUpdatedTime.java new file mode 100644 index 00000000000..c50d8f64e73 --- /dev/null +++ b/src/main/java/seedu/address/model/jobapplication/LastUpdatedTime.java @@ -0,0 +1,82 @@ +package seedu.address.model.jobapplication; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * The latest time that the job application was updated. + * Completely immutable, users are not able to specify the updated time. + */ +public class LastUpdatedTime implements Comparable { + + private static final String INVALID_FORMAT_MESSAGE = "The given last modified datetime is invalid: %s\n Delete " + + "the last updated time section to reset the last updated time."; + + private static final DateTimeFormatter displayFormat = + DateTimeFormatter.ofPattern( + "dd-MM-yyyy h:mma", + Locale.ENGLISH + ); + + public final LocalDateTime lastUpdatedTime; + + /** + * Constructs an autogenerated {@code LastUpdatedTime} + */ + public LastUpdatedTime() { + this.lastUpdatedTime = LocalDateTime.now(); + } + + /** + * Constructs a {@code LastUpdatedTime} for testing purposes only. + * @param dateTime of the updated time. + */ + public LastUpdatedTime(LocalDateTime dateTime) { + this.lastUpdatedTime = dateTime; + } + + /** + * Generates a {@code LastUpdatedTime} for the purpose of storage. + */ + public static LastUpdatedTime generateLastUpdatedTime(String test) throws IllegalValueException { + try { + return new LastUpdatedTime( + LocalDateTime.parse(test) + ); + } catch (DateTimeParseException e) { + throw new IllegalValueException(String.format(INVALID_FORMAT_MESSAGE, test)); + } + } + + public String toDisplayString() { + return lastUpdatedTime.format(displayFormat); + } + + @Override + public String toString() { + return lastUpdatedTime.toString(); + } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LastUpdatedTime)) { + return false; + } + + LastUpdatedTime otherLastUpdate = (LastUpdatedTime) other; + return lastUpdatedTime.equals(otherLastUpdate.lastUpdatedTime); + } + + @Override + public int compareTo(LastUpdatedTime o) { + return this.lastUpdatedTime.compareTo(o.lastUpdatedTime); + } +} 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 abe8c46b535..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,117 +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.commons.util.ToStringBuilder; -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; - } - - // instanceof handles nulls - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return name.equals(otherPerson.name) - && phone.equals(otherPerson.phone) - && email.equals(otherPerson.email) - && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); - } - - @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() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index cc0a68d79f9..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,150 +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) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof UniquePersonList)) { - return false; - } - - UniquePersonList otherUniquePersonList = (UniquePersonList) other; - return internalList.equals(otherUniquePersonList.internalList); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - @Override - public String toString() { - return internalList.toString(); - } - - /** - * 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/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..15c592ab939 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,49 +1,85 @@ package seedu.address.model.util; +import java.time.LocalDate; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.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.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Recruiter; +import seedu.address.model.contact.Url; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.Deadline; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobStatus; +import seedu.address.model.jobapplication.JobTitle; import seedu.address.model.tag.Tag; /** * 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 Contact[] getSampleContacts() { + Organization alexYeohInc = new Organization( + new Name("Alex Yeoh Inc"), + new Id("alex_yeoh_inc"), new Phone("87438807"), + new Email("contact@alexyeoh.example.com"), new Url("alexyeoh.example.com"), + null, getTagSet("parttime") + ); + + Organization google = new Organization( + new Name("Google"), new Id("google"), new Phone("65218000"), null, + new Url("careers.google.com"), + new Address("70 Pasir Panjang Road, #03-71, " + + "Mapletree Business City, " + + "Singapore 117371"), + getTagSet("bigtech", "internship", "competitive") + ); + + Organization jobSeekerPlus = new Organization( + new Name("Job Seeker Plus"), new Id("job_seeker_plus"), new Phone("93210283"), + new Email("jobseekerplus@example.com"), null, + new Address("Blk 16 Real Street 128, #08-04"), + getTagSet("startup", "internship") + ); + + alexYeohInc.addJobApplication(new JobApplication(alexYeohInc, new JobTitle("AI Engineer"), + null, new Deadline(LocalDate.now().plusDays(42)), + JobStatus.PENDING, ApplicationStage.RESUME)); + alexYeohInc.addJobApplication(new JobApplication(alexYeohInc, new JobTitle("Marketing Advisor"), + null, new Deadline(LocalDate.now().minusDays(3)), + JobStatus.TURNED_DOWN, ApplicationStage.ONLINE_ASSESSMENT)); + google.addJobApplication(new JobApplication(google, new JobTitle("Full-Stack Developer"), + null, new Deadline(LocalDate.now().plusDays(5)), + JobStatus.PENDING, ApplicationStage.INTERVIEW)); + jobSeekerPlus.addJobApplication(new JobApplication(jobSeekerPlus, new JobTitle("Job Seeking Pro"), + null, new Deadline(LocalDate.now().minusDays(17)), + JobStatus.REJECTED, ApplicationStage.RESUME)); + + return new Contact[] { + alexYeohInc, google, jobSeekerPlus, + new Recruiter(new Name("David Li"), new Id("david_li"), new Phone("91031282"), + new Email("davidli@alexyeoh.example.com"), null, + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + getTagSet("direct", "network"), alexYeohInc), + new Recruiter(new Name("Roy Balakrishnan"), new Id("roy_balakrishnan"), new Phone("92624417"), + new Email("royb@example.com"), new Url("www.nus.edu.sg"), + null, getTagSet("friendly"), null) }; } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); + for (Contact sampleContact : getSampleContacts()) { + sampleAb.addContact(sampleContact); } return sampleAb; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedApplication.java b/src/main/java/seedu/address/storage/JsonAdaptedApplication.java new file mode 100644 index 00000000000..3ca8abd0fd5 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedApplication.java @@ -0,0 +1,133 @@ +package seedu.address.storage; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.jobapplication.ApplicationStage; +import seedu.address.model.jobapplication.Deadline; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobDescription; +import seedu.address.model.jobapplication.JobStatus; +import seedu.address.model.jobapplication.JobTitle; +import seedu.address.model.jobapplication.LastUpdatedTime; + + +/** + * Jackson-friendly version of {@link JobApplication}. + */ +public class JsonAdaptedApplication { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Job Application's %s field is missing!"; + + private final String title; + private final String description; + private final String lastUpdatedTime; + private final String deadline; + private final String status; + private final String stage; + + /** + * Constructs a {@code JsonAdaptedApplication} with the given application details. + */ + @JsonCreator + public JsonAdaptedApplication(@JsonProperty("title") String title, + @JsonProperty("description") String description, + @JsonProperty("lastUpdatedTime") String lastUpdatedTime, + @JsonProperty("deadline") String deadline, + @JsonProperty("status") String status, + @JsonProperty("stage") String stage) { + this.title = title; + this.description = description; + this.lastUpdatedTime = lastUpdatedTime; + this.deadline = deadline; + this.status = status; + this.stage = stage; + } + + /** + * Converts a given {@code JobApplication} into this class for Jackson use. + */ + public JsonAdaptedApplication(JobApplication source) { + + this.title = source + .getJobTitle() + .toString(); + this.description = source + .getJobDescription() + .map(JobDescription::toString) + .orElse(null); + this.lastUpdatedTime = source + .getLastUpdatedTime() + .toString(); + this.deadline = source + .getDeadline() + .toString(); + this.status = source + .getStatus() + .toString(); + this.stage = source + .getApplicationStage() + .toString(); + } + + /** + * Converts this Jackson-friendly adapted application object into the model's {@code JobApplication} object. + * + * @throws IllegalValueException if there are any data constraints violated in the adapted application. + */ + public JobApplication toModelType(String id, String name) throws IllegalValueException { + final Id oid; + final Name orgName; + final JobTitle title; + final Optional description; + final Deadline deadline; + final LastUpdatedTime lastUpdatedTime; + final JobStatus status; + final ApplicationStage stage; + + if (id == null || !Id.isValidId(id)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + oid = new Id(id); + if (this.title == null || !JobTitle.isValidJobTitle(this.title)) { + throw new IllegalValueException(JobTitle.MESSAGE_CONSTRAINTS); + } + title = new JobTitle(this.title); + if (this.description != null && !JobDescription.isValidJobDescription(this.description)) { + throw new IllegalValueException(JobDescription.MESSAGE_CONSTRAINTS); + } + description = Optional.ofNullable(this.description).map(JobDescription::new); + if (this.deadline == null || !Deadline.isValidDeadline(this.deadline)) { + throw new IllegalValueException(Deadline.MESSAGE_CONSTRAINTS); + } + deadline = new Deadline(this.deadline); + lastUpdatedTime = LastUpdatedTime.generateLastUpdatedTime(this.lastUpdatedTime); + if (this.status == null || !JobStatus.isValidJobStatus(this.status)) { + throw new IllegalValueException(JobStatus.MESSAGE_CONSTRAINTS); + } + status = JobStatus.fromString(this.status); + if (this.stage == null || !ApplicationStage.isValidApplicationStage(this.stage)) { + throw new IllegalValueException(ApplicationStage.MESSAGE_CONSTRAINTS); + } + stage = ApplicationStage.fromString(this.stage); + if (name == null || !Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + orgName = new Name(name); + + return new JobApplication( + oid, + orgName, + title, + description.orElse(null), + deadline, + status, + stage, + lastUpdatedTime + ); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedContact.java b/src/main/java/seedu/address/storage/JsonAdaptedContact.java new file mode 100644 index 00000000000..63ee0219481 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedContact.java @@ -0,0 +1,226 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.Arrays; +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.ReadOnlyAddressBook; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Organization; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Recruiter; +import seedu.address.model.contact.Type; +import seedu.address.model.contact.Url; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.tag.Tag; + + +/** + * Jackson-friendly version of {@link Contact}. + */ +class JsonAdaptedContact implements Comparable { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Contact's %s field is missing!"; + + private final String type; + private final String name; + private final String phone; + private final String email; + private final String address; + private final String id; + private final String url; + private String oid; + private final List tags = new ArrayList<>(); + private final List applications = new ArrayList<>(); + + + /** + * Constructs a {@code JsonAdaptedContact} with the given contact details. + */ + @JsonCreator + public JsonAdaptedContact(@JsonProperty("type") String type, + @JsonProperty("name") String name, @JsonProperty("id") String id, + @JsonProperty("phone") String phone, @JsonProperty("email") String email, + @JsonProperty("url") String url, @JsonProperty("address") String address, + @JsonProperty("oid") String oid, @JsonProperty("tags") List tags, + @JsonProperty("applications") List applications) { + this.type = type; + this.name = name; + this.id = id; + this.phone = phone; + this.email = email; + this.url = url; + this.address = address; + this.oid = oid; + if (tags != null) { + this.tags.addAll(tags); + } + if (applications != null) { + this.applications.addAll(applications); + } + } + + /** + * Converts a given {@code Contact} into this class for Jackson use. + */ + public JsonAdaptedContact(Contact source) { + if (source.getType() == Type.ORGANIZATION) { + oid = null; + } else if (source.getType() == Type.RECRUITER) { + Recruiter recruiter = (Recruiter) source; + oid = recruiter.getOrganizationId() + .map(oid -> oid.value) + .orElse(null); + } + + type = source.getType().toString(); + name = source.getName().fullName; + id = source.getId().value; + phone = source.getPhone().map(phone -> phone.value).orElse(null); + email = source.getEmail().map(email -> email.value).orElse(null); + url = source.getUrl().map(url -> url.value).orElse(null); + address = source.getAddress().map(address -> address.value).orElse(null); + tags.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + if (source.getType() == Type.ORGANIZATION) { + Organization org = (Organization) source; + List applicationList = Arrays.asList(org.getJobApplications()); + applications.addAll(applicationList.stream() + .map(JsonAdaptedApplication::new) + .collect(Collectors.toList())); + } + } + + /** + * Returns the id string stored in this {@code JsonAdaptedContact} + */ + public String getId() { + return this.id; + } + + /** + * Converts this Jackson-friendly adapted contact object into the model's {@code Contact} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted contact. + */ + public Contact toModelType(ReadOnlyAddressBook reference) throws IllegalValueException { + final List contactTags = new ArrayList<>(); + final List jobApplications = new ArrayList<>(); + for (JsonAdaptedTag tag : tags) { + contactTags.add(tag.toModelType()); + } + for (JsonAdaptedApplication application: applications) { + jobApplications.add(application.toModelType(id, name)); + } + + 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 (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Id.class.getSimpleName())); + } + if (!Id.isValidId(id)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + final Id modelId = new Id(id); + + if (phone != null && !Phone.isValidPhone(phone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelPhone = phone == null ? null : new Phone(phone); + + if (email != null && !Email.isValidEmail(email)) { + throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } + final Email modelEmail = email == null ? null : new Email(email); + + if (url != null && !Url.isValidUrl(url)) { + throw new IllegalValueException(Url.MESSAGE_CONSTRAINTS); + } + final Url modelUrl = url == null ? null : new Url(url); + + if (address != null && !Address.isValidAddress(address)) { + throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = address == null ? null : new Address(address); + + final Set modelTags = new HashSet<>(contactTags); + + if (type == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Type.class.getSimpleName())); + } + if (!Type.isValidType(type)) { + throw new IllegalValueException(Type.MESSAGE_CONSTRAINTS); + } + + final Type modelType = Type.fromString(type); + + switch (modelType) { + case ORGANIZATION: { + return new Organization( + modelName, modelId, modelPhone, modelEmail, modelUrl, modelAddress, + modelTags, jobApplications + ); + } + case RECRUITER: { + if (oid != null && !Id.isValidId(oid)) { + throw new IllegalValueException(Id.MESSAGE_CONSTRAINTS); + } + final Id modelOid = oid == null ? null : new Id(oid); + final Organization modelOrg; + if (modelOid == null) { + modelOrg = null; + } else { + Contact contact = reference.getContactById(modelOid); + if (contact == null || contact.getType() != Type.ORGANIZATION) { + throw new IllegalValueException(Recruiter.MESSAGE_INVALID_ORGANIZATION); + } + modelOrg = (Organization) contact; + assert modelOrg.getId().equals(modelOid); + } + + return new Recruiter( + modelName, modelId, modelPhone, modelEmail, modelUrl, modelAddress, + modelTags, modelOrg + ); + } + default: + assert false : "We should not reach this stage - there is a developer error and the contact type " + + modelType + "is not handled!"; + + throw new IllegalStateException(); + } + } + + @Override + public int compareTo(JsonAdaptedContact o) { + boolean isThisTypeValid = Type.isValidType(this.type); + boolean isOtherTypeValid = Type.isValidType(o.type); + + if (!isThisTypeValid) { + return isOtherTypeValid ? 1 : 0; + } + if (!isOtherTypeValid) { + return -1; + } + + return Type.fromString(this.type).compareTo(Type.fromString(o.type)); + } +} 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 bd1ca0f56c8..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 tags = 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("tags") List tags) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tags != null) { - this.tags.addAll(tags); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * 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 : tags) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..0df68646f89 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -1,8 +1,13 @@ package seedu.address.storage; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,7 +16,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * An Immutable AddressBook that is serializable to JSON format. @@ -19,16 +24,16 @@ @JsonRootName(value = "addressbook") class JsonSerializableAddressBook { - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_CONTACT = "Contacts list contains duplicate contact(s)."; - private final List persons = new ArrayList<>(); + private final List contacts = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given contacts. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); + public JsonSerializableAddressBook(@JsonProperty("contacts") List contacts) { + this.contacts.addAll(contacts); } /** @@ -37,7 +42,10 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List orderMap; + try { + orderMap = IntStream.range(0, contacts.size()) + .boxed() + .collect(Collectors.toMap(i -> contacts.get(i).getId(), Function.identity())); + } catch (IllegalStateException s) { + // Having two duplicate keys in the order map will trigger an IllegalStateException. + // In this case, duplicate keys mean duplicate ids which implies duplicate contacts. + throw new IllegalValueException(MESSAGE_DUPLICATE_CONTACT); + } + + Comparator originalOrderComparator = Comparator.comparingInt(c -> orderMap.get(c.getId().value)); + + // Create all contacts. + List sortedJsonContacts = new ArrayList<>(contacts); + Collections.sort(sortedJsonContacts); + List newContacts = new ArrayList<>(); + AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); + for (JsonAdaptedContact jsonAdaptedContact : sortedJsonContacts) { + Contact contact = jsonAdaptedContact.toModelType(addressBook); + // Defensive check. + if (addressBook.getContactById(contact.getId()) != null) { + throw new IllegalValueException(MESSAGE_DUPLICATE_CONTACT); } - addressBook.addPerson(person); + addressBook.addContact(contact); + newContacts.add(contact); } + + // Add them into the book in the original order. + newContacts.sort(originalOrderComparator); + addressBook.setContacts(newContacts); + return addressBook; } diff --git a/src/main/java/seedu/address/ui/ApplicationCard.java b/src/main/java/seedu/address/ui/ApplicationCard.java new file mode 100644 index 00000000000..5830ca96867 --- /dev/null +++ b/src/main/java/seedu/address/ui/ApplicationCard.java @@ -0,0 +1,78 @@ +package seedu.address.ui; + +import java.util.function.Supplier; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.model.jobapplication.JobApplication; +import seedu.address.model.jobapplication.JobDescription; + +/** + * A UI component that displays information of a {@code JobApplication}. + */ +public class ApplicationCard extends UiPart { + private static final String FXML = "ApplicationListCard.fxml"; + private final JobApplication jobApplication; + + @FXML + private HBox cardPane; + @FXML + private VBox cardPaneInnerVbox; + @FXML + private Label index; + @FXML + private Label linkedParentOrganization; + @FXML + private Label title; + @FXML + private Label description; + @FXML + private Label status; + @FXML + private Label deadline; + @FXML + private Label stage; + @FXML + private Label lastUpdatedTime; + + /** + * Creates a {@code ApplicationCard} with the given {@code JobApplication} and index to display. + */ + public ApplicationCard(JobApplication application, int displayedIndex) { + super(FXML); + this.jobApplication = application; + index.setText(String.format("%d. ", displayedIndex)); + linkedParentOrganization.setText(application.getOrgName().fullName); + title.setText(application.getJobTitle().title); + status.setText(application.getStatus().toString()); + deadline.setText("Deadline: " + application.getDeadline().toString()); + stage.setText(application.getApplicationStage().toString()); + lastUpdatedTime.setText("Last Updated: " + application.getLastUpdatedTime().toDisplayString()); + setVboxInnerLabelText( + description, () -> application.getJobDescription().map(JobDescription::toString).orElse(null)); + } + + /** + * Configures the inner label contained within the vbox container to show the given string, or remove the label + * entirely if the string is empty or null. + * + * @param label The label to set the text to. + * @param valueSupplier The string value supplier. This may be expressed as a lambda function. + */ + private void setVboxInnerLabelText(Label label, Supplier valueSupplier) { + if (label == null) { + return; + } + + String value = valueSupplier.get(); + if (value == null || value.isBlank()) { + label.setText(null); + cardPaneInnerVbox.getChildren().remove(label); + } else { + label.setText(value); + } + } +} diff --git a/src/main/java/seedu/address/ui/ApplicationListPanel.java b/src/main/java/seedu/address/ui/ApplicationListPanel.java new file mode 100644 index 00000000000..b6d220b7971 --- /dev/null +++ b/src/main/java/seedu/address/ui/ApplicationListPanel.java @@ -0,0 +1,50 @@ +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.jobapplication.JobApplication; + +/** + * A UI component that displays a list of applications. + */ +public class ApplicationListPanel extends UiPart { + private static final String FXML = "ApplicationListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ApplicationListPanel.class); + + @FXML + private ListView applicationListView; + + /** + * Creates a {@code ContactListPanel} with the given {@code ObservableList}. + */ + public ApplicationListPanel(ObservableList applicationList) { + super(FXML); + applicationListView.setItems(applicationList); + applicationListView.setCellFactory(listView -> new ApplicationListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code JobApplication} using a {@code ApplicationCard}. + * Same as {@link seedu.address.ui.ContactListPanel.ContactListViewCell} + */ + class ApplicationListViewCell extends ListCell { + @Override + protected void updateItem(JobApplication application, boolean isEmpty) { + super.updateItem(application, isEmpty); + + if (isEmpty || application == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ApplicationCard(application, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/AutocompleteTextField.java b/src/main/java/seedu/address/ui/AutocompleteTextField.java new file mode 100644 index 00000000000..3afa991b164 --- /dev/null +++ b/src/main/java/seedu/address/ui/AutocompleteTextField.java @@ -0,0 +1,431 @@ +package seedu.address.ui; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.Stack; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Side; +import javafx.scene.Node; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; + +/** + * A text field capable of displaying and performing autocomplete. + * + *

+ * At a basic level, it extends {@link TextField} + * to provide an autocomplete result menu configurable via {@link #setCompletionGenerator}, + * the ability to trigger auto-completions via {@link #triggerImmediateAutocompletion()} or the GUI, + * and the ability to undo the latest auto-completions via {@link #undoLastImmediateAutocompletion()}. + *


+ * + *

+ * The base implementation was referenced from a proof-of-concept implementation from + * floralvikings/AutoCompleteTextBox.java. + * Additional functionality like dynamic completion suppliers and autocomplete history tracking for undo + * are custom-implemented. + *

+ */ +public class AutocompleteTextField extends TextField { + + /** A snapshot of a text-change due to autocompletion. */ + protected static class AutocompleteSnapshot { + public final String partialValue; + public final String completedValue; + + public AutocompleteSnapshot(String partialValue, String completedValue) { + this.partialValue = partialValue; + this.completedValue = completedValue; + } + + @Override + public String toString() { + return "AutocompleteSnapshot{'" + partialValue + "' to '" + completedValue + "'}"; + } + } + + /** An enum of execution status. */ + public enum ActionResult { + EXECUTED, NOT_EXECUTED; + } + + /** + * A functional interface that generates a stream of auto-completion results + * based on the given partial input. + */ + @FunctionalInterface + public interface CompletionGenerator extends Function> { } + + /** Internal ID prefix to note the exact menu item per index. */ + private static final String AUTOCOMPLETE_MENU_ITEM_ID_PREFIX = "autocomplete-completion-item-"; + + // GUI elements + private final ContextMenu autocompletePopup; + + + // Configuration variables + private CompletionGenerator completionGenerator = s -> Stream.empty(); + private String autocompleteHintString = "[Select to autocomplete]"; + private int popupLimit = 10; + + + // History tracking for autocomplete undo operations + private final Stack autocompleteHistory = new Stack<>(); + + /** + * Constructs a new text field with the ability to perform autocompletion. + */ + public AutocompleteTextField() { + super(); + this.autocompletePopup = new ContextMenu(); + + // Setup default behaviours + autocompletePopup.setHideOnEscape(true); + autocompletePopup.setAutoFix(true); + autocompletePopup.setAutoHide(true); + autocompletePopup.addEventFilter(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.SPACE) { + // SPACE dismisses the popup if none is selected via tab focus. + // We disallow this by intercepting it before it does. + e.consume(); + } + + if (e.getCode() == KeyCode.ENTER) { + // The default behaviour of ENTER seems buggy. + // We will intercept it and do our own processing. + // This uses JavaFX private APIs to locate the focused element, as found here: + // https://stackoverflow.com/questions/27332981/contextmenu-and-programmatically-selecting-an-item + e.consume(); + + int highlightedIndex = 0; + + Set items = autocompletePopup.getSkin().getNode().lookupAll(".menu-item"); + for (Node item : items) { + if (!item.isFocused() && !item.isHover()) { + continue; + } + + if (!item.getId().startsWith(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX)) { + continue; + } + + try { + String indexPart = item.getId().substring(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX.length()); + highlightedIndex = Integer.parseInt(indexPart, 10); + break; + } catch (NumberFormatException exception) { + // Ignore... + } + } + + this.triggerImmediateAutocompletion(highlightedIndex); + } + }); + + // Setup autocompletion popup menu UI updates + this.textProperty().addListener(e -> refreshPopupState()); + this.focusedProperty().addListener(e -> refreshPopupState()); + + // Setup autocompletion undo data cleanup + this.textProperty().addListener((e, oldValue, newValue) -> updateUndoHistoryState(oldValue, newValue)); + } + + /** + * Sets the autocompletion generator for this text field. + */ + public void setCompletionGenerator(CompletionGenerator completionGenerator) { + this.completionGenerator = completionGenerator == null + ? s -> Stream.empty() + : completionGenerator; + } + + /** + * Sets the maximum number of entries that can be shown in the autocomplete popup. + */ + public void setPopupLimit(int popupLimit) { + this.popupLimit = popupLimit; + } + + /** + * Gets the current maximum number of entries that can be shown in the autocomplete popup. + */ + public int getPopupLimit() { + return popupLimit; + } + + /** + * Triggers autocompletion immediately using the first suggested value, if any. + */ + public ActionResult triggerImmediateAutocompletion() { + return triggerImmediateAutocompletion(0); + } + + /** + * Triggers autocompletion immediately using the given suggested value index, if any. + */ + public ActionResult triggerImmediateAutocompletion(int index) { + ObservableList menuItems = autocompletePopup.getItems(); + + if (!isPopupVisible() || menuItems.size() <= index || index < 0) { + return ActionResult.NOT_EXECUTED; + } + + menuItems.get(index).fire(); + return ActionResult.EXECUTED; + } + + /** + * Triggers autocompletion immediately using the given result. + */ + private void triggerImmediateAutocompletionUsingResult(String result) { + String oldText = this.getText(); + + // Add data for undoing if there's a change + if (!Objects.equals(oldText, result)) { + autocompleteHistory.add(new AutocompleteSnapshot(oldText, result)); + } + + // Update the text field + this.setText(result + " "); // add a new space character to allow for faster continuation + + // Update the view and cursor location + this.requestFocus(); + this.end(); + this.refreshPopupState(); + } + + /** + * Undoes the last immediate autocompleted result. + * This only does something when invoked at a stage where the current text matches + * a previously autocompleted result. + */ + public ActionResult undoLastImmediateAutocompletion() { + if (autocompleteHistory.isEmpty()) { + return ActionResult.NOT_EXECUTED; + } + + AutocompleteSnapshot snapshot = autocompleteHistory.peek(); + + // Verify that the current text correctly matches the latest snapshot + if (!this.getText().trim().equals(snapshot.completedValue)) { + return ActionResult.NOT_EXECUTED; + } + + // Pop the result + autocompleteHistory.pop(); + + // Set the old value back in + this.setText(snapshot.partialValue); + + // Update the view and cursor location + this.requestFocus(); + this.end(); + this.refreshPopupState(); + + return ActionResult.EXECUTED; + } + + /** + * Sets the string used to hint that an option can be autocompleted. + */ + public void setAutocompleteHintString(String autocompleteHintString) { + this.autocompleteHintString = autocompleteHintString; + } + + /** + * Returns true if the autocomplete popup menu is visible, false otherwise. + */ + public boolean isPopupVisible() { + return this.autocompletePopup.isShowing(); + } + + /** + * Hides the autocomplete popup menu if it is visible. + * This is temporary and may be shown again if the user types anything, + * or if {@link #refreshPopupState()} is invoked. + */ + public void hidePopup() { + this.autocompletePopup.hide(); + } + + /** + * Shows the autocomplete popup menu if there are contents to show but it is not yet visible. + * This is equivalent to calling {@link #refreshPopupState()}, since the popup is visible by default when there + * are elements. + */ + public void showPopup() { + this.refreshPopupState(); + } + + /** + * Updates the state of the popup indicating the autocompletion entries. + */ + public void refreshPopupState() { + String text = getText(); + if ((!isFocused() && !autocompletePopup.isFocused()) + || popupLimit <= 0) { + + autocompletePopup.hide(); + return; + } + + // Obtain the list of completions + List completions = completionGenerator.apply(text) + .limit(popupLimit + 1) // (+ 1 so we can tell if it exceeds the limit) + .collect(Collectors.toList()); + + // Obtain the length of the prefix part of the completion strings that can be truncated + int hidableCompletionPrefixLength = completions.stream() + .min(Comparator.comparingInt(String::length)) + .map(String::length) + .map(l -> Math.max(0, l - 48)) // Keep the last 48 characters in view + .orElse(0); + + // Populate the menu items + List menuItems = new LinkedList<>(); + for (int i = 0; i < completions.size(); i++) { + + // Create the relevant labels + String completion = completions.get(i); + String[] completionParts = splitIntoAutocompletionComponents(getText(), completion); + + String prefixMatchPart = truncateFrontWithEllipsis(completionParts[0], hidableCompletionPrefixLength); + String postfixCompletionPart = completionParts[1]; + + Label completionLabelFront = new Label(prefixMatchPart); + Label completionLabelBack = new Label(postfixCompletionPart); + + completionLabelFront.getStyleClass().add("completion-prefix"); + completionLabelBack.getStyleClass().add("completion-data"); + + // Create the horizontal box + HBox completionBox = new HBox(completionLabelFront, completionLabelBack); + completionBox.getStyleClass().add("autocomplete-box"); + completionBox.setPadding(new Insets(0, 8, 0, 8)); + completionBox.setAlignment(Pos.BASELINE_LEFT); + + // Create the context menu item + CustomMenuItem item = new CustomMenuItem(completionBox, false); + item.setOnAction(e -> triggerImmediateAutocompletionUsingResult(completion)); + item.setId(AUTOCOMPLETE_MENU_ITEM_ID_PREFIX + i); + menuItems.add(item); + + // Handle special case styling + if (i == 0) { + // Special Case 1: First completion option + Label completionHint = new Label(autocompleteHintString); + completionHint.getStyleClass().add("completion-hint"); + completionHint.setPadding(new Insets(0, 8, 0, 8)); + + completionBox.getStyleClass().add("primary"); + completionBox.getChildren().add(completionHint); + + } else if (i >= popupLimit) { + // Special Case 2: Options exceeding limit and not first element + completionLabelFront.setText("... (more options hidden)"); + completionLabelBack.setText(null); + item.setDisable(true); + item.setOnAction(e -> {}); + break; // Stop further processing immediately - only one of this should be displayed. + + } + } + + // Replace the current menu items with the new set + autocompletePopup.getItems().clear(); + autocompletePopup.getItems().addAll(menuItems); + + // Hide the popup regardless if it should be shown or not. + // + // This is done to work around a weird quirk/bug in JavaFX where the text field would steal focus after an + // autocompletion result has been invoked, leading to the inability to use arrow keys to navigate the + // autocomplete list. + autocompletePopup.hide(); + + // Show the popup given that we have items in the list to show. + if (menuItems.size() > 0) { + autocompletePopup.show(AutocompleteTextField.this, Side.BOTTOM, 0, 0); + } + } + + /** + * Updates undo history tracked state based on the change for text field old values to new values. + */ + protected void updateUndoHistoryState(String previousValue, String currentValue) { + + // Heuristic for clearing history: + // IF current value is no longer a prefix of the latest autocomplete result OR is of the undone state + // THEN said autocompletion snapshot is no longer applicable. + + while (autocompleteHistory.size() > 0 + && (!currentValue.startsWith(autocompleteHistory.peek().completedValue) + || currentValue.trim().equals(autocompleteHistory.peek().partialValue)) + ) { + autocompleteHistory.pop(); + } + } + + /** + * Splits the given autocomplete result by the closest prefix matched phrase and the autocompleted result. + * + *

+ * For example, providing the input {@code "abc def 1234"} with autocompletion {@code "abc def 12345 ghi"} + * would yield a split of {@code new String[] { "abc def", "12345 ghi" }} + *

+ * + * @return an array of length 2 that contains the (prefix part, autocompletion part). + */ + private String[] splitIntoAutocompletionComponents(String input, String autocompletionResult) { + int secondPartIndex = 0; + + // Step 1: Find the index beyond the prefix match. + while (secondPartIndex < input.length() + && secondPartIndex < autocompletionResult.length() + && input.charAt(secondPartIndex) == autocompletionResult.charAt(secondPartIndex)) { + secondPartIndex++; + } + + // Step 2: Backtrack till a space is found. + while (secondPartIndex - 1 >= 0 + && autocompletionResult.charAt(secondPartIndex - 1) != ' ') { + secondPartIndex--; + } + + // Step 3: Split by the given index, if possible. + if (secondPartIndex >= 0 && secondPartIndex <= autocompletionResult.length()) { + return new String[] { + autocompletionResult.substring(0, secondPartIndex), + autocompletionResult.substring(secondPartIndex), + }; + } else { + return new String[] { autocompletionResult, "" }; + } + } + + /** + * Truncates the given string by the given {@code truncateAmount} of characters at the front, and adds + * leading ellipsis. It returns just the ellipsis if the number of characters truncated exceeds the string length. + */ + private String truncateFrontWithEllipsis(String str, int truncateAmount) { + if (truncateAmount <= 0) { + return str; + } + return truncateAmount >= str.length() ? "..." : "..." + str.substring(truncateAmount); + } + +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..54f26612b81 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,9 +1,14 @@ package seedu.address.ui; +import java.util.Objects; +import java.util.logging.Logger; + import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -16,36 +21,138 @@ public class CommandBox extends UiPart { public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; + private static final Logger logger = LogsCenter.getLogger(CommandBox.class); + private final CommandExecutor commandExecutor; + private final AutocompleteTextField.CompletionGenerator completionGenerator; @FXML - private TextField commandTextField; + private AutocompleteTextField commandTextField; /** * Creates a {@code CommandBox} with the given {@code CommandExecutor}. */ - public CommandBox(CommandExecutor commandExecutor) { + public CommandBox( + CommandExecutor commandExecutor, AutocompleteTextField.CompletionGenerator completionGenerator + ) { super(FXML); this.commandExecutor = commandExecutor; - // calls #setStyleToDefault() whenever there is a change to the text of the command box. + this.completionGenerator = completionGenerator; + + assert commandTextField != null; + + // Setup completion results and hints + commandTextField.setCompletionGenerator(completionGenerator); + commandTextField.setAutocompleteHintString("[Press TAB or SPACE to autocomplete]"); + + // Setup UI events + commandTextField.setOnAction(e -> this.handleCommandEntered()); + + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyFilter); + commandTextField.addEventFilter(KeyEvent.KEY_RELEASED, this::handleKeyFilter); + commandTextField.addEventFilter(KeyEvent.KEY_TYPED, this::handleKeyFilter); + + // Sets the style to default whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); } /** - * Handles the Enter button pressed event. + * Handles the key filter of a textbox. */ @FXML + private void handleKeyFilter(KeyEvent keyEvent) { + boolean isTextCaretAtEnd = + this.commandTextField.getCaretPosition() == this.commandTextField.getText().length() + && this.commandTextField.getSelectedText().isEmpty(); + boolean isKeyTyped = keyEvent.getEventType() == KeyEvent.KEY_TYPED; + boolean isKeyPressed = keyEvent.getEventType() == KeyEvent.KEY_PRESSED; + + // Note: + // These captures keystrokes at different KeyEvent types, as there some events are captured by + // other elements, and hence not all events are delivered in all situations. + + if (isKeyTyped && isTextCaretAtEnd && Objects.equals(keyEvent.getCharacter(), " ")) { + logger.fine("Intercepted SPACE typed at end"); + this.handleCommandAutocompleted(keyEvent); + + + } else if (isKeyPressed && isTextCaretAtEnd && keyEvent.getCode() == KeyCode.BACK_SPACE) { + logger.fine("Intercepted BACKSPACE typed at end"); + this.handleCommandUndoAutocomplete(keyEvent); + + + } else if (isKeyPressed && keyEvent.getCode() == KeyCode.TAB) { + logger.fine("Intercepted TAB key"); + + keyEvent.consume(); // Consume to prevent element focus change. + + // There are two cases for TAB: + // - Case 1: Autocomplete popup shown --> Triggers autocomplete result + // - Case 2: Autocomplete popup not shown --> Tries to make autocomplete popup visible + + if (commandTextField.isPopupVisible()) { + this.handleCommandAutocompleted(keyEvent); + } else { + commandTextField.showPopup(); + } + } + } + + /** + * Handles the request for finalization of text input. + */ private void handleCommandEntered() { String commandText = commandTextField.getText(); - if (commandText.equals("")) { + + if (commandText.isBlank()) { + // Ignore and reset blank (whitespace-only or empty) inputs + commandTextField.setText(""); return; } try { + // Process the command commandExecutor.execute(commandText); + + // Once successful, reset the text field commandTextField.setText(""); + commandTextField.requestFocus(); + commandTextField.hidePopup(); + } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); + commandTextField.hidePopup(); + } + } + + /** + * Handles the request for autocompletion of text input. + */ + @FXML + private void handleCommandAutocompleted(KeyEvent keyEvent) { + logger.fine("User invoked auto-completion!"); + + var actionResult = commandTextField.triggerImmediateAutocompletion(); + if (actionResult == AutocompleteTextField.ActionResult.EXECUTED) { + keyEvent.consume(); + + commandTextField.requestFocus(); + commandTextField.end(); + } + } + + /** + * Handles the request for undoing autocompletion. + */ + private void handleCommandUndoAutocomplete(KeyEvent keyEvent) { + logger.fine("User invoked undo auto-completion!"); + + var actionResult = commandTextField.undoLastImmediateAutocompletion(); + if (actionResult == AutocompleteTextField.ActionResult.EXECUTED) { + keyEvent.consume(); + + commandTextField.requestFocus(); + commandTextField.end(); } } diff --git a/src/main/java/seedu/address/ui/ContactCard.java b/src/main/java/seedu/address/ui/ContactCard.java new file mode 100644 index 00000000000..7ef863a41e7 --- /dev/null +++ b/src/main/java/seedu/address/ui/ContactCard.java @@ -0,0 +1,123 @@ +package seedu.address.ui; + +import java.util.Comparator; +import java.util.Optional; +import java.util.function.Supplier; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Id; +import seedu.address.model.contact.Recruiter; + +/** + * A UI component that displays information of a {@code Contact}. + */ +public class ContactCard extends UiPart { + + private static final String FXML = "ContactListCard.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 Contact contact; + + @FXML + private HBox cardPane; + @FXML + private VBox cardPaneInnerVbox; + @FXML + private Label name; + @FXML + private Label index; + @FXML + private Label id; + @FXML + private Label linkedParentOrganization; + @FXML + private Label phone; + @FXML + private Label address; + @FXML + private Label email; + @FXML + private Label url; + @FXML + private FlowPane tags; + + /** + * Creates a {@code PersonCode} with the given {@code Contact} and index to display. + */ + public ContactCard(Contact contact, int displayedIndex) { + super(FXML); + this.contact = contact; + + index.setText(String.format("%d. ", displayedIndex)); + id.setText(contact.getId().value); + name.setText(contact.getName().fullName); + + final Label typeLabel = new Label(contact.getType().toString()); + typeLabel.setId("type"); + tags.getChildren().add(typeLabel); // add it to the front of tags + + setVboxInnerLabelText( + phone, () -> contact.getPhone().map(phone -> phone.value).orElse(null)); + setVboxInnerLabelText( + address, () -> contact.getAddress().map(address -> address.value).orElse(null)); + setVboxInnerLabelText( + email, () -> contact.getEmail().map(email -> email.value).orElse(null)); + setVboxInnerLabelText( + url, () -> contact.getUrl().map(url -> url.value).orElse(null)); + + switch (contact.getType()) { + case RECRUITER: { + Recruiter recruiter = (Recruiter) contact; + + final Optional linkedOrgId = recruiter.getOrganizationId(); + + // TODO: This should display organization name instead of ID in the future + setVboxInnerLabelText(linkedParentOrganization, () -> + linkedOrgId.map(oid -> String.format("from %s (%s)", "organization", oid.value)) + .orElse(null) + ); + break; + } + default: + cardPaneInnerVbox.getChildren().removeAll(linkedParentOrganization); + break; + } + + contact.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } + + /** + * Configures the inner label contained within the vbox container to show the given string, or remove the label + * entirely if the string is empty or null. + * + * @param label The label to set the text to. + * @param valueSupplier The string value supplier. This may be expressed as a lambda function. + */ + private void setVboxInnerLabelText(Label label, Supplier valueSupplier) { + if (label == null) { + return; + } + + String value = valueSupplier.get(); + if (value == null || value.isBlank()) { + label.setText(null); + cardPaneInnerVbox.getChildren().remove(label); + } else { + label.setText(value); + } + } +} diff --git a/src/main/java/seedu/address/ui/ContactListPanel.java b/src/main/java/seedu/address/ui/ContactListPanel.java new file mode 100644 index 00000000000..f329592e844 --- /dev/null +++ b/src/main/java/seedu/address/ui/ContactListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.contact.Contact; + +/** + * Panel containing the list of contacts. + */ +public class ContactListPanel extends UiPart { + private static final String FXML = "ContactListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ContactListPanel.class); + + @FXML + private ListView contactListView; + + /** + * Creates a {@code ContactListPanel} with the given {@code ObservableList}. + */ + public ContactListPanel(ObservableList contactList) { + super(FXML); + contactListView.setItems(contactList); + contactListView.setCellFactory(listView -> new ContactListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Contact} using a {@code ContactCard}. + */ + class ContactListViewCell extends ListCell { + @Override + protected void updateItem(Contact contact, boolean isEmpty) { + super.updateItem(contact, isEmpty); + + if (isEmpty || contact == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ContactCard(contact, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..a09fcc282fc 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,8 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://AY2324S1-CS2103T-W08-3.github.io/tp/UserGuide" + + ".html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..952dd105a06 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,6 +1,7 @@ package seedu.address.ui; import java.util.logging.Logger; +import java.util.stream.Stream; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -31,7 +32,8 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ContactListPanel contactListPanel; + private ApplicationListPanel applicationListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -42,7 +44,9 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane contactListPanelPlaceholder; + @FXML + private StackPane applicationListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -110,8 +114,11 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + contactListPanel = new ContactListPanel(logic.getDisplayedContactList()); + contactListPanelPlaceholder.getChildren().add(contactListPanel.getRoot()); + + applicationListPanel = new ApplicationListPanel(logic.getDisplayedApplicationList()); + applicationListPanelPlaceholder.getChildren().add(applicationListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -119,7 +126,7 @@ void fillInnerParts() { StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); + CommandBox commandBox = new CommandBox(this::executeCommand, this::generateCompletions); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } @@ -163,8 +170,12 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + public ContactListPanel getContactListPanel() { + return contactListPanel; + } + + public ApplicationListPanel getApplicationListPanel() { + return applicationListPanel; } /** @@ -193,4 +204,13 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Obtains a list of auto-completed commands based on the current partial text. + * + * @see seedu.address.logic.Logic#generateCompletions(String) + */ + private Stream generateCompletions(String commandText) { + return logic.generateCompletions(commandText); + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 094c42cda82..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.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 Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. - */ - public PersonCard(Person person, int displayedIndex) { - 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))); - } -} 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/resources/view/ApplicationListCard.fxml b/src/main/resources/view/ApplicationListCard.fxml new file mode 100644 index 00000000000..b8e51dcb7c9 --- /dev/null +++ b/src/main/resources/view/ApplicationListCard.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ApplicationListPanel.fxml b/src/main/resources/view/ApplicationListPanel.fxml new file mode 100644 index 00000000000..4fa3379e367 --- /dev/null +++ b/src/main/resources/view/ApplicationListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..d0e9582ce8c 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,9 @@ + - + - diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/ContactListCard.fxml similarity index 76% rename from src/main/resources/view/PersonListCard.fxml rename to src/main/resources/view/ContactListCard.fxml index f5e812e25e6..64fe93a782e 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/ContactListCard.fxml @@ -14,12 +14,12 @@ - + - + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/ContactListPanel.fxml similarity index 77% rename from src/main/resources/view/PersonListPanel.fxml rename to src/main/resources/view/ContactListPanel.fxml index a1bb6bbace8..c4e105169f3 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/ContactListPanel.fxml @@ -4,5 +4,5 @@ - + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..12dc14d9abe 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -149,7 +149,7 @@ .result-display { -fx-background-color: transparent; -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-font-size: 12pt; -fx-text-fill: white; } @@ -328,6 +328,25 @@ -fx-text-fill: white; } +.autocomplete-box.primary { + -fx-background-color: #ffffff22; +} + +.autocomplete-box .completion-prefix { + -fx-font-size: 12pt; + -fx-text-fill: #ffffff88; +} + +.autocomplete-box .completion-data { + -fx-font-size: 12pt; + -fx-text-fill: #ffffffff; +} + +.autocomplete-box .completion-hint { + -fx-font-size: 7pt; + -fx-text-fill: #ffffff88; +} + #filterField, #personListPanel, #personWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } @@ -337,6 +356,12 @@ -fx-background-radius: 0; } +.label#id { + -fx-text-fill: #d0d0d088; + -fx-font-size: 10px; + -fx-font-style: italic; +} + #tags { -fx-hgap: 7; -fx-vgap: 3; @@ -350,3 +375,7 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#tags .label#type { + -fx-background-color: #3f917e; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..eef3b90d32d 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -10,9 +10,9 @@ + - + @@ -33,26 +33,33 @@ - + - + - + - + - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..42a3a1591f8 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -5,5 +5,5 @@ -