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-clear
command) -
Suppose the history looks like this:
-
pat-edit 1 n/Ivan Yeoh
-
pat-delete 3
-
pat-clear
Now
app-undo 2
is executed.pat-clear
andpat-delete 3
are undone in that order. The app data is now as it was beforepat-delete 3
was 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-edit
was 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-clear
andpat-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
Command
s 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
Command
object 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 ofCommand
to indicate that aCommand
is 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 theMutatorCommand
responsible 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 containingHistoryRecord
s 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 newHistoryRecord
containing the committingMutatorCommand
and thePatientBook
andAppointmentBook
states before the execution of the command and pushes it to theHistoryManager
for 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 previousMutatorCommand
if it was an undo by popping the latestHistoryRecord
from 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_WORD
for 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_WORD
for 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.
historyManager
Step 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-undo
Step 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
PatientBook
andAppointmentBook
objects to record each model state.-
Pros: Easier to implement.
-
Cons: Consumes more memory.
-
-
Alternative 2: Only save the
MutatorCommand
objects but implement anundo()
method for eachMutatorCommand
which 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
PatientBook
andAppointmentBook
.-
Pros: Easier to implement.
-
Cons: Relies on the assumption that the objects in
PatientBook
andAppointmentBook
are immutable; if they are not truly immutable, changes to the current model’sPatientBook
andAppointmentBook
state may leak and affect the states stored in the history.
-
-
Alternative 2 (current choice): Defensively store deep copies of the
PatientBook
andAppointmentBook
.-
Pros: Prevents improperly coded
Patient
orAppointment
(or their associated classes) from breaking undo/redo/history functionality. Can reuse JSON serialization code for persistent storage ofPatientBook
andAppointmentBook
to 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
, theObservableList
viewed by the UI must also be updated by theModelManager
instead of thePatientBook
as the currentPatientBook
is completely decoupled and placed into the history.
-
Aspect: Which class to place the HistoryManager in
-
Alternative 1 (current choice): Make
HistoryManager
a field ofModelManager
.-
Pros: Ensures atomicity of the records in the history as pushing a transaction to the
HistoryManager
can 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
ModelManager
as twoModelManager
objects may have the same current state but differingHistoryManager
objects. May violate Single Responsibility Principle asModelManager
now has to manage both its current state and its previous states.
-
-
Alternative 2: Make
HistoryManager
a field ofLogicManager
.-
Pros: Higher cohesion as
ModelManager
only represents the model’s current state. Easier to testModelManager
as only its current state matters. -
Cons: It is possible for intermediate model states to be pushed to the
HistoryManager
- trustsLogicManager
to push the transaction to history after (and only after) callingModel#commit()
. RequiresCommand#execute()
to acceptHistoryManager
as a parameter just soUndoCommand
andRedoCommand
can 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
TabPane
all display actual data such asPatient
,Visit
, andAppointment
info, whereas history is app metadata.
-