From 8bfc54a7104f201b69ad6e327c3128e4b58bf5d3 Mon Sep 17 00:00:00 2001 From: pra-navi Date: Tue, 24 Oct 2023 18:41:21 +0800 Subject: [PATCH 1/6] feat: Add tag field to Task --- .../java/seedu/address/logic/Messages.java | 4 +- .../logic/commands/AddTaskCommand.java | 8 ++- .../logic/commands/EditTaskCommand.java | 36 +++++++++++-- .../logic/parser/AddTaskCommandParser.java | 10 +++- .../seedu/address/logic/parser/CliSyntax.java | 2 +- .../logic/parser/EditTaskCommandParser.java | 26 +++++++++- .../java/seedu/address/model/task/Task.java | 38 ++++++++++---- .../address/model/util/SampleDataUtil.java | 4 +- .../address/storage/JsonAdaptedTask.java | 28 ++++++++-- src/main/java/seedu/address/ui/TaskCard.java | 11 +++- src/main/resources/view/TaskListCard.fxml | 2 + .../typicalTasksAddressBook.json | 52 +++++++++++++++---- .../seedu/address/logic/MessagesTest.java | 4 +- .../logic/commands/CommandTestUtil.java | 9 +++- .../commands/EditTaskDescriptorTest.java | 8 ++- .../parser/AddTaskCommandParserTest.java | 25 ++++++++- .../parser/EditPersonCommandParserTest.java | 2 +- .../parser/EditTaskCommandParserTest.java | 42 +++++++++++++-- .../seedu/address/model/task/TaskTest.java | 36 +++++++------ .../model/task/UniqueTaskListTest.java | 2 + .../address/storage/JsonAdaptedTaskTest.java | 52 ++++++++++++++----- .../testutil/EditTaskDescriptorBuilder.java | 16 ++++++ .../seedu/address/testutil/TaskBuilder.java | 18 ++++++- .../java/seedu/address/testutil/TaskUtil.java | 15 ++++++ .../seedu/address/testutil/TypicalTasks.java | 9 ++++ 25 files changed, 381 insertions(+), 78 deletions(-) diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index 00b3f5b0c54..5582f765940 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -59,7 +59,9 @@ public static String format(Task task) { .append("; Note: ") .append(task.getNote()) .append("; Status: ") - .append(task.getStatus()); + .append(task.getStatus()) + .append("; Tags: "); + task.getTags().forEach(builder::append); return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddTaskCommand.java b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java index d8360fd444c..94ecfbd66c4 100644 --- a/src/main/java/seedu/address/logic/commands/AddTaskCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java @@ -1,6 +1,7 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_TITLE; @@ -19,10 +20,13 @@ public class AddTaskCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a task to the address book. " + "Parameters: " + PREFIX_TASK_TITLE + "TITLE " - + PREFIX_TASK_NOTE + "NOTE\n" + + PREFIX_TASK_NOTE + "NOTE" + + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_TASK_TITLE + "Get flowers " - + PREFIX_TASK_NOTE + "Wedding Anniversary"; + + PREFIX_TASK_NOTE + "Wedding Anniversary" + + PREFIX_TAG + "finance " + + PREFIX_TAG + "class "; public static final String MESSAGE_SUCCESS = "New task added: %1$s"; public static final String MESSAGE_DUPLICATE_TASK = "This task already exists in the address book"; diff --git a/src/main/java/seedu/address/logic/commands/EditTaskCommand.java b/src/main/java/seedu/address/logic/commands/EditTaskCommand.java index 1c642d33a82..a8fdc9a8bf5 100644 --- a/src/main/java/seedu/address/logic/commands/EditTaskCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditTaskCommand.java @@ -1,13 +1,17 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_TITLE; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_TASKS; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; @@ -15,6 +19,7 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.tag.Tag; import seedu.address.model.task.Note; import seedu.address.model.task.Task; import seedu.address.model.task.Title; @@ -31,7 +36,8 @@ public class EditTaskCommand extends Command { + "Existing values will be overwritten by the input values.\n" + "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_TASK_TITLE + "TITLE] " - + "[" + PREFIX_TASK_NOTE + "NOTE]\n" + + "[" + PREFIX_TASK_NOTE + "NOTE]" + + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_TASK_TITLE + "Prepare Agenda " + PREFIX_TASK_NOTE + "To book venue"; @@ -85,8 +91,9 @@ private static Task createEditedTask(Task taskToEdit, EditTaskDescriptor editTas Title updatedTitle = editTaskDescriptor.getTitle().orElse(taskToEdit.getTitle()); Note updatedNote = editTaskDescriptor.getNote().orElse(taskToEdit.getNote()); + Set updatedTags = editTaskDescriptor.getTags().orElse(taskToEdit.getTags()); - return new Task(updatedTitle, updatedNote); + return new Task(updatedTitle, updatedNote, updatedTags); } @Override @@ -120,6 +127,7 @@ public String toString() { public static class EditTaskDescriptor { private Title title; private Note note; + private Set tags; public EditTaskDescriptor() { } @@ -130,13 +138,14 @@ public EditTaskDescriptor() { public EditTaskDescriptor(EditTaskDescriptor toCopy) { setTitle(toCopy.title); setNote(toCopy.note); + setTags(toCopy.tags); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(title, note); + return CollectionUtil.isAnyNonNull(title, note, tags); } public void setTitle(Title title) { @@ -155,6 +164,23 @@ public Optional getNote() { return Optional.ofNullable(note); } + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -168,7 +194,8 @@ public boolean equals(Object other) { EditTaskDescriptor otherEditTaskDescriptor = (EditTaskDescriptor) other; return Objects.equals(title, otherEditTaskDescriptor.title) - && Objects.equals(note, otherEditTaskDescriptor.note); + && Objects.equals(note, otherEditTaskDescriptor.note) + && Objects.equals(tags, otherEditTaskDescriptor.tags); } @Override @@ -176,6 +203,7 @@ public String toString() { return new ToStringBuilder(this) .add("title", title) .add("note", note) + .add("tags", tags) .toString(); } } diff --git a/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java index b3b4693a89d..94a3ac0222a 100644 --- a/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java @@ -1,13 +1,16 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_TITLE; +import java.util.Set; import java.util.stream.Stream; import seedu.address.logic.commands.AddTaskCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; import seedu.address.model.task.Note; import seedu.address.model.task.Task; import seedu.address.model.task.Title; @@ -25,7 +28,8 @@ public class AddTaskCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public AddTaskCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TASK_TITLE, PREFIX_TASK_NOTE); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TASK_TITLE, PREFIX_TASK_NOTE, + PREFIX_TAG); if (!arePrefixesPresent(argMultimap, PREFIX_TASK_TITLE, PREFIX_TASK_NOTE) || !argMultimap.getPreamble().isEmpty()) { @@ -35,8 +39,10 @@ public AddTaskCommand parse(String args) throws ParseException { argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TASK_TITLE, PREFIX_TASK_NOTE); Title title = ParserUtil.parseTitle(argMultimap.getValue(PREFIX_TASK_TITLE).get()); Note note = ParserUtil.parseNote(argMultimap.getValue(PREFIX_TASK_NOTE).get()); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Task task = new Task(title, note); + + Task task = new Task(title, note, tagList); return new AddTaskCommand(task); } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index e0c63280ce8..60f08ba35d6 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -12,6 +12,6 @@ public class CliSyntax { public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); public static final Prefix PREFIX_TASK_NOTE = new Prefix("n/"); - public static final Prefix PREFIX_TASK_TITLE = new Prefix("t/"); + public static final Prefix PREFIX_TASK_TITLE = new Prefix("T/"); } diff --git a/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java index 289481493df..94e3eb7beb3 100644 --- a/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java @@ -2,13 +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_TAG; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK_TITLE; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditTaskCommand; import seedu.address.logic.commands.EditTaskCommand.EditTaskDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditTaskCommand object @@ -23,7 +30,7 @@ public class EditTaskCommandParser implements Parser { */ public EditTaskCommand parse(String args) throws ParseException { requireNonNull(args); - ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TASK_TITLE, PREFIX_TASK_NOTE); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TASK_TITLE, PREFIX_TASK_NOTE, PREFIX_TAG); Index index; @@ -45,10 +52,27 @@ public EditTaskCommand parse(String args) throws ParseException { editTaskDescriptor.setNote(ParserUtil.parseNote(argMultimap.getValue(PREFIX_TASK_NOTE).get())); } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editTaskDescriptor::setTags); + if (!editTaskDescriptor.isAnyFieldEdited()) { throw new ParseException(EditTaskCommand.MESSAGE_NOT_EDITED); } return new EditTaskCommand(index, editTaskDescriptor); } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } } diff --git a/src/main/java/seedu/address/model/task/Task.java b/src/main/java/seedu/address/model/task/Task.java index 9805547187b..2b1a3d50752 100644 --- a/src/main/java/seedu/address/model/task/Task.java +++ b/src/main/java/seedu/address/model/task/Task.java @@ -2,9 +2,13 @@ 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 Task in the task list. @@ -16,17 +20,19 @@ public class Task { private final Title title; private final Note note; private Status status; + private final Set tags = new HashSet<>(); /** - * A Task consists of a title and a note. - * Both fields must be present and not null. + * A Task consists of a title, a note, a status and tags. + * All fields must be present and not null. * Tasks will be default not done when created. */ - public Task(Title title, Note note) { - requireAllNonNull(title, note); + public Task(Title title, Note note, Set tags) { + requireAllNonNull(title, note, tags); this.title = title; this.note = note; this.status = new Status(Status.TaskStatus.NOT_DONE); + this.tags.addAll(tags); } /** @@ -35,12 +41,14 @@ public Task(Title title, Note note) { * @param title The title of the task. Must not be null. * @param note The note associated with the task. Must not be null. * @param status The status of the task. Must not be null. + * @param tags The tags linked to the task. Must not be null. */ - public Task(Title title, Note note, Status status) { + public Task(Title title, Note note, Status status, Set tags) { requireAllNonNull(title, note); this.title = title; this.note = note; this.status = status; + this.tags.addAll(tags); } public Title getTitle() { @@ -59,14 +67,22 @@ public Status getStatus() { * Updates the Status of the Task as Done. */ public Task markDone() { - return new Task(title, note, new Status(Status.TaskStatus.DONE)); + return new Task(title, note, new Status(Status.TaskStatus.DONE), tags); } /** * Updates the Status of the Task as not Done. */ public Task unmarkDone() { - return new Task(title, note, new Status(Status.TaskStatus.NOT_DONE)); + return new Task(title, note, new Status(Status.TaskStatus.NOT_DONE), tags); + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); } /** @@ -84,7 +100,7 @@ public boolean isSameTask(Task otherTask) { } /** - * Returns true if both tasks have the same title, note and status. + * Returns true if both tasks have the same title, note, status and tags. */ @Override public boolean equals(Object other) { @@ -100,13 +116,14 @@ public boolean equals(Object other) { Task otherTask = (Task) other; return title.equals(otherTask.title) && note.equals(otherTask.note) - && status.equals(otherTask.status); + && status.equals(otherTask.status) + && tags.equals(otherTask.tags); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(title, note, status); + return Objects.hash(title, note, status, tags); } @Override @@ -115,6 +132,7 @@ public String toString() { .add("title", title) .add("note", note) .add("status", status) + .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 095f485bd22..6e25361e085 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -45,8 +45,8 @@ public static Person[] getSamplePersons() { public static Task[] getSampleTasks() { return new Task[] { - new Task(new Title("Buy Flowers"), new Note("from Gina")), - new Task(new Title("Buy Milk"), new Note("from NTUC")) + new Task(new Title("Find caterer"), new Note("for 221 students"), getTagSet("caterer")), + new Task(new Title("Create budget"), new Note("for CS2102"), getTagSet("finance", "class")) }; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTask.java b/src/main/java/seedu/address/storage/JsonAdaptedTask.java index 566f5748c73..890fe0c26e4 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTask.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedTask.java @@ -1,9 +1,16 @@ 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.tag.Tag; import seedu.address.model.task.Note; import seedu.address.model.task.Status; import seedu.address.model.task.Task; @@ -19,6 +26,7 @@ class JsonAdaptedTask { private final String title; private final String note; private final boolean isDone; + private final List tags = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedTask} with the given task details. @@ -26,10 +34,14 @@ class JsonAdaptedTask { @JsonCreator public JsonAdaptedTask(@JsonProperty("title") String title, @JsonProperty("note") String note, - @JsonProperty("isDone") boolean isDone) { + @JsonProperty("isDone") boolean isDone, + @JsonProperty("tags") List tags) { this.title = title; this.note = note; this.isDone = isDone; + if (tags != null) { + this.tags.addAll(tags); + } } /** @@ -39,6 +51,9 @@ public JsonAdaptedTask(Task source) { title = source.getTitle().toString(); note = source.getNote().toString(); isDone = source.getStatus().equals(Status.STATUS_DONE); + tags.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); } /** @@ -47,6 +62,11 @@ public JsonAdaptedTask(Task source) { * @throws IllegalValueException if there were any data constraints violated in the adapted task. */ public Task toModelType() throws IllegalValueException { + final List taskTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tags) { + taskTags.add(tag.toModelType()); + } + if (title == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Title.class.getSimpleName())); } @@ -63,10 +83,12 @@ public Task toModelType() throws IllegalValueException { } final Note modelNote = new Note(note); + final Set modelTags = new HashSet<>(taskTags); + if (isDone) { - return new Task(modelTitle, modelNote, Status.STATUS_DONE); + return new Task(modelTitle, modelNote, Status.STATUS_DONE, modelTags); } - return new Task(modelTitle, modelNote, Status.STATUS_NOT_DONE); + return new Task(modelTitle, modelNote, Status.STATUS_NOT_DONE, modelTags); } } diff --git a/src/main/java/seedu/address/ui/TaskCard.java b/src/main/java/seedu/address/ui/TaskCard.java index 41c79d96c5c..71f9a47a058 100644 --- a/src/main/java/seedu/address/ui/TaskCard.java +++ b/src/main/java/seedu/address/ui/TaskCard.java @@ -1,9 +1,12 @@ package seedu.address.ui; +import java.util.Comparator; + import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import seedu.address.model.task.Status; @@ -32,12 +35,12 @@ public class TaskCard extends UiPart { private Label title; @FXML private Label note; - @FXML private Label id; - @FXML private ImageView statusIcon; + @FXML + private FlowPane tags; /** * Creates a {@code TaskCode} with the given {@code Task} and index to display. @@ -54,5 +57,9 @@ public TaskCard(Task task, int displayedIndex) { } else { statusIcon.setImage(new Image("/images/not_done.png")); } + + task.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } } diff --git a/src/main/resources/view/TaskListCard.fxml b/src/main/resources/view/TaskListCard.fxml index 127bde88f1a..4d21260d448 100644 --- a/src/main/resources/view/TaskListCard.fxml +++ b/src/main/resources/view/TaskListCard.fxml @@ -9,6 +9,7 @@ + @@ -28,6 +29,7 @@ +