Overview
NurseTraverse is a desktop application designed to help community nurses manage all the data they may need, including visit management, appointment scheduling, and importing and exporting of patient data. The user interacts with the app using a CLI (Command Line Interface) enhanced with an autocomplete functionality and ability to undo and redo commands. The app has a GUI created with JavaFX and is written in Java. NurseTraverse is built by me and 4 other students.
Summary of contributions
-
Major enhancement: added the ability to undo/redo previous commands
-
What it does: allows the user to undo all previous commands one or many 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 detailed handling of the model state.
-
Credits: No outside code or ideas were referenced in the development of these features (beyond code that already exists within Address Book 3).
-
-
Minor enhancement: added a history GUI panel that displays previous commands, making it much more convenient to use the multiple undo command.
-
Code contributed: An overview of contributed code can be found here. There are filters for functional and test code. Otherwise, code contributed can be found via references in the following issues:
-
Other contributions:
-
Project management:
-
Enhancements to existing features:
-
Make name comparison case insensitive (Pull request #197)
-
-
Community:
-
Contributions to the User Guide
Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users. |
Data Saving, History, Undo, Redo
Data Saving, History, Undo, Redo
As a community nurse, you will often perform commands that modify information managed by the application (e.g. patient data). This section describes the features involving the storage of this data, viewing command history, and undo-ing / redo-ing commands.
Saving your data
Your data are saved in the hard disk automatically after any command that changes the data.
There is no need to save manually.
Viewing the command history
The rightmost panel of the app shows a history of the previous data-modifying commands executed
since the app was started. Examples of data-modifying commands are pat-edit or visit-start.
Commands that do not affect the app data, such as pat-list, are not shown.
Each command is displayed with an index number. The most recent command is shown at the bottom of the history and has the greatest index number. The earliest command in the history is shown at the top with an index of 1.
The app keeps a record of the previous 20 data-modifying commands in the history.
Undoing a command: app-undo
You can undo the changes of the data-modifying commands in the history
with app-undo. All app data will be reverted to how they were before the
undone command was executed.
app-undo can be used in two ways. The first way is to provide no arguments
to the command. This simply undoes the latest command. The second way is to
specify the index of a command in the history. This would cause all commands
after and including that command to be undone.
The command app-undo itself is not affected by app-undo and is not recorded in
the history. To revert an app-undo, use app-redo.
Format: app-undo [INDEX]
Examples:
-
pat-clear
pat-list
app-undo(reverts thepat-clearcommand) -
Suppose the history looks like this:
-
pat-edit 1 n/Ivan Yeoh -
pat-delete 3 -
pat-clear
Now
app-undo 2is executed.pat-clearandpat-delete 3are undone in that order. The app data is now as it was beforepat-delete 3was executed. -
Redoing an undone command: app-redo
You can redo and reapply the changes of the last undone command using app-redo.
This is only possible when the latest data-modifying command (other than
app-redo itself) was an app-undo.
Format: app-redo
Examples:
-
pat-edit 1 n/Ivan Yeoh
app-undo
app-redo(reverts data to afterpat-editwas executed) -
pat-edit 1 n/Ivan Yeoh
pat-clear
app-undo
app-undo
app-redo(redoespat-edit)
app-redo(redoespat-clear) -
Suppose the history looks like this:
-
pat-edit 1 n/Ivan Yeoh -
pat-delete 3 -
pat-clear
Now the following commands are executed:
app-undo 2(revertspat-clearandpat-delete)
app-redo(redoespat-delete)
app-redo(redoespat-clear) -
Contributions to the Developer Guide
Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project. |
This set of functionality consists of one GUI component and two commands, each of which rely on the same core components:
-
HistoryPane- GUI component that displays a numbered list of previous data-modifying commands that can be reverted byapp-undo. -
app-undo- Command that reverts until a specified data-modifying command in the history, or the previous data-modifying command if no argument is specified. -
app-redo- Command that reverts the previous command if and only if it was anapp-undo.
Overview of Changes
For undo/redo/history to function, there must be the following:
-
A way to mark which
Commands are data-modifying commands so that non-data-modifying commands can be ignored. -
A mechanism to clearly demarcate the model state before and after the execution of each data-modifying command.
-
A way to record the state of the model before (or after) each command, together with the
Commandobject responsible. -
Methods to switch the state of the model back and forth from the states stored in the history.
To achieve this, the following classes are introduced:
-
MutatorCommand- An empty subclass ofCommandto indicate that aCommandis a data-modifying command and should be considered for undo/redo/history. -
HistoryRecord- Record of the command and data of one atomic commit in the command history. It consists of theMutatorCommandresponsible for the commit and the state of the model either before or after the commit. All recorded states are deep copies decoupled from their original references. -
HistoryManager- Represents the history of the application. Maintains data structures containingHistoryRecords of each data state change.
Additionally, LogicManager and Model are modified to accommodate this functionality.
The following methods are added to the ModelManager class:
-
Model#commit(MutatorCommand)- Commits the changes made to the address book since the last call to this method, making them permanent and updating the UI data. Creates a newHistoryRecordcontaining the committingMutatorCommandand thePatientBookandAppointmentBookstates before the execution of the command and pushes it to theHistoryManagerfor storage. -
Model#getHistory()- Returns an unmodifiable view of the history. -
Model#undoTo(HistoryRecord)- Reverts current model state to the that contained in the specifiedHistoryRecord(i.e. the state before the command was executed). -
Model#redo()- Redoes the previousMutatorCommandif it was an undo by popping the latestHistoryRecordfrom theHistoryManager's redo stack.
Furthermore, in addition to MutatorCommand which was described earlier, the following logical classes are added:
-
UndoCommand- Undoes a designated command in the history, or the previous one if no argument is specified. TheCOMMAND_WORDfor this command isapp-undo. -
UndoCommandParser- Parses input arguments and creates a new UndoCommand object. -
RedoCommand- Redoes the previous command if it was an undo. TheCOMMAND_WORDfor this command isapp-redo.
HistoryManager checks the classes of commands pushed to the history and does not record
them if they are instances of UndoCommand or RedoCommand. This ensures that successive UndoCommand s do
not undo themselves instead of the desired data-modifying commands, requiring RedoCommand for
the special case of undo reversion.
Example Usage Scenario
The following example usage scenario and the state of historyManager at each step.
Step 1: The user launches the application.
HistoryManager is initialized with empty history and redoStack objects.
historyManagerStep 2: The user executes the command pat-delete 3.
After the DeleteCommand makes its changes on the model, logicManager calls Model#commit(MutatorCommand),
passing the command object into modelManager. In turn, modelManager passes the command object, stagedPatientBook,
and stagedAppointmentBook into historyManager through
HistoryManager#pushRecord(MutatorCommand, PatientBook, AppointmentBook).
historyManager uses those objects to create a new HistoryRecord object which contains the model state before the
command was executed (here labelled hr0) and pushes it into the history.
historyManager after pat-delete 3 is executedStep 3: The user executes the command visit-start.
The interaction between logicManager, modelManager and historyManager is the same as before.
historyManager creates a new HistoryRecord object (here labelled hr1) and pushes it into the history.
historyManager after visit-start is executedStep 4: The user wants to revert to the first item in the history, so he executes the command app-undo 1.
First, the UndoCommand retrieves the HistoryRecord corresponding to the first item in the history by searching the
list returned by Model#getHistory(). Then, UndoCommand calls Model#undoTo(HistoryRecord) passing in the target
record. When this happens, modelManager calls historyManager#popRecordsTo(HistoryRecord) to pop all records after
and including the target record from the history (hr1 and hr0 in the previous step). The historyManager uses
these popped records to create new records of the model state after the commands were executed, and places these
new records (hr2 and hr3) into the redoStack.
historyManager after app-undo 1 is executedFinally, modelManager calls ModelManager#changeBaseTo(PatientBook, AppointmentBook) using the state objects in the
target record. This performs the actual reversion of the state.
The action of the UndoCommand is summarised in the sequence diagram below:
app-undoStep 5: The user wants to redo pat-delete 3, so he executes the command app-redo.
The RedoCommand calls Model#redo(). The modelManager calls HistoryManager#popRedo(PatientBook, AppointmentBook)
passing it the current stagedPatientBook and stagedAppointmentBook.
historyManager pops the record at the top of the redoStack (hr3) and uses its command, together with the
PatientBook and AppointmentBook just passed in by the modelManager, to create a new HistoryRecord (hr4)
describing the model state before that command was executed. It then pushes hr4 into the history.
historyManager after app-redo is executedhistoryManager#popRedo() returns the HistoryRecord (hr3) containing the state after the redo. modelManager
can now call ModelManager#changeBaseTo() to change the state to it.
Step 6: The user executes the command pat-clear.
The action of the logicManager and modelManager is similar what is described in Step 2; only this time, when
modelManager calls HistoryManager#pushRecord(MutatorCommand, PatientBook, AppointmentBook), the
historyManager sees that the committing command is not an UndoCommand or a RedoCommand and clears the
redoStack to avoid branching.
historyManager after pat-clear is executedModel and Logic Design Considerations
Aspect: How to undo and redo between states
-
Alternative 1 (current choice): Save the entire
PatientBookandAppointmentBookobjects to record each model state.-
Pros: Easier to implement.
-
Cons: Consumes more memory.
-
-
Alternative 2: Only save the
MutatorCommandobjects but implement anundo()method for eachMutatorCommandwhich does exactly the reverse of itsexecute()method.-
Pros: Consumes much less memory.
-
Cons: Difficult to implement - doubles the amount of work needed for each command.
-
Aspect: How to record the PatientBook and AppointmentBook states in the history
-
Alternative 1: Simply store references to
PatientBookandAppointmentBook.-
Pros: Easier to implement.
-
Cons: Relies on the assumption that the objects in
PatientBookandAppointmentBookare immutable; if they are not truly immutable, changes to the current model’sPatientBookandAppointmentBookstate may leak and affect the states stored in the history.
-
-
Alternative 2 (current choice): Defensively store deep copies of the
PatientBookandAppointmentBook.-
Pros: Prevents improperly coded
PatientorAppointment(or their associated classes) from breaking undo/redo/history functionality. Can reuse JSON serialization code for persistent storage ofPatientBookandAppointmentBookto create deep copies by serializing then immediately deserializing them. -
Cons: Consumes more memory and CPU time. More difficult to implement - MVC pattern between UI view and models is broken in two. This is because each time the current state is swapped with a state in the history by
ModelManager, theObservableListviewed by the UI must also be updated by theModelManagerinstead of thePatientBookas the currentPatientBookis completely decoupled and placed into the history.
-
Aspect: Which class to place the HistoryManager in
-
Alternative 1 (current choice): Make
HistoryManagera field ofModelManager.-
Pros: Ensures atomicity of the records in the history as pushing a transaction to the
HistoryManagercan only be (and is always) done byModel#commit()itself - records in the history are guaranteed to be products of complete command execution rather than intermediate states. -
Cons: More difficult to test
ModelManageras twoModelManagerobjects may have the same current state but differingHistoryManagerobjects. May violate Single Responsibility Principle asModelManagernow has to manage both its current state and its previous states.
-
-
Alternative 2: Make
HistoryManagera field ofLogicManager.-
Pros: Higher cohesion as
ModelManageronly represents the model’s current state. Easier to testModelManageras only its current state matters. -
Cons: It is possible for intermediate model states to be pushed to the
HistoryManager- trustsLogicManagerto push the transaction to history after (and only after) callingModel#commit(). RequiresCommand#execute()to acceptHistoryManageras a parameter just soUndoCommandandRedoCommandcan work even though the vast majority of commands do not require it.
-
UI
The command history is constantly displayed in a panel on the right side of
the app. This HistoryPanel uses HistoryRecordCard s to display the
user-input text that invoked each command. It is a view of the ObservableList<HistoryRecord>
returned by HistoryManager#asUnmodifiableObservableList().
UI Design Considerations
Aspect: Where to display the history
-
Alternative 1 (current choice): Permanently display it in a dedicated panel.
-
Pros: User does not have to execute a 'history' command to view the history, making it much easier to use the multiple undo function.
-
Cons: Takes up more space in the UI.
-
-
Alternative 2: Display it as a tab in the
TabPane.-
Pros: Saves space in the UI.
-
Cons: User has to switch to the history tab to view it. Less intuitive UX as the other tabs in the
TabPaneall display actual data such asPatient,Visit, andAppointmentinfo, whereas history is app metadata.
-