Skip to content

Commit ba7f861

Browse files
committed
Added Note Dependencies.
1 parent 15ced77 commit ba7f861

57 files changed

Lines changed: 809 additions & 39 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
# Introduction
44
This tool is my own version of Trello and Clickup (Which ive used very actively in the past but ive always been turned off by their AI features / paid features).
5-
This version has some basic functionality of organizing notes, exporting them as json, adding goals / due dates / assignees and comments.
5+
This version has functionality for organizing notes, exporting them as json, adding goals / due dates / assignees and comments.
66

7+
## Key Features
8+
* Kanban-style boards with customizable columns.
9+
* Hierarchical goals (sub-tasks) with progress tracking.
10+
* Note dependencies (e.g., "blocks", "is blocked by") with visual indicators.
11+
* File attachments and note linking directly within goals (`@NoteName`).
712

813
# Installation
914
Make sure you have the JDK installed or use the JRE that comes with the releases.

src/main/java/com/tarek/notetool/MainApp.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ public class MainApp extends Application {
2828
private NoteManager noteManager;
2929
private Timeline autoSaveTimeline;
3030

31+
/**
32+
* Gets the path to the directory where attachments are stored.
33+
* @return The Path for the application's attachments directory.
34+
*/
35+
public static Path getAttachmentsDirectory() {
36+
return DATA_DIRECTORY_PATH.resolve("attachments");
37+
}
38+
3139
/**
3240
* Determines the appropriate data storage path.
3341
* If a OneDrive folder is detected via environment variables, it's used as the base.
@@ -60,6 +68,15 @@ public void start(Stage stage) throws Exception {
6068
// Check for data in the old format and offer to migrate it before loading anything.
6169
checkForAndOfferMigration();
6270

71+
// Ensure the attachments directory exists
72+
try {
73+
Files.createDirectories(getAttachmentsDirectory());
74+
} catch (IOException e) {
75+
System.err.println("Could not create attachments directory: " + e.getMessage());
76+
// This is not fatal, so we just log it and continue.
77+
}
78+
79+
6380
// Apply the modern theme
6481
Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet());
6582

src/main/java/com/tarek/notetool/MainViewController.java

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import java.io.IOException;
5757
import java.util.Comparator;
5858
import java.util.ArrayList;
59+
import java.util.Collections;
5960
import java.util.EnumMap;
6061
import java.util.Map;
6162
import java.util.Optional;
@@ -68,11 +69,13 @@
6869
import java.util.Arrays;
6970
import javafx.beans.value.ChangeListener;
7071
import org.kordamp.ikonli.javafx.FontIcon;
72+
import org.kordamp.ikonli.materialdesign2.MaterialDesignB;
7173
import org.kordamp.ikonli.materialdesign2.MaterialDesignA;
7274
import org.kordamp.ikonli.materialdesign2.MaterialDesignC;
7375
import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
7476
import org.kordamp.ikonli.materialdesign2.MaterialDesignF;
7577
import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
78+
import org.kordamp.ikonli.materialdesign2.MaterialDesignL;
7679

7780
public class MainViewController {
7881

@@ -549,6 +552,66 @@ private HBox buildCardDetails(Note note) {
549552
Tooltip.install(assigneeLabel, new Tooltip("Assigned to: " + assigneeNames));
550553
detailsBox.getChildren().add(assigneeLabel);
551554
}
555+
556+
// Dependencies
557+
if (note.getDependencies() != null && !note.getDependencies().isEmpty()) {
558+
// --- Active Blockers ---
559+
List<String> activeBlockerTitles = note.getDependencies().stream()
560+
.filter(dep -> dep.type() == Note.DependencyType.BLOCKED_BY)
561+
.filter(dep -> noteManager.findNoteAcrossAllBoards(dep.otherNoteId())
562+
.map(blocker -> blocker.getStatus() != Note.Status.DONE && blocker.getStatus() != Note.Status.ARCHIVED)
563+
.orElse(false)) // If note not found, it's not an active blocker.
564+
.map(Note.Dependency::otherNoteTitle)
565+
.collect(Collectors.toList());
566+
567+
if (!activeBlockerTitles.isEmpty()) {
568+
FontIcon depIcon = new FontIcon(MaterialDesignL.LOCK_OUTLINE);
569+
depIcon.setIconSize(12);
570+
depIcon.getStyleClass().addAll("detail-icon", "danger-icon");
571+
Tooltip.install(depIcon, new Tooltip("Blocked by: " + String.join(", ", activeBlockerTitles)));
572+
detailsBox.getChildren().add(depIcon);
573+
}
574+
575+
// --- Notes this one Blocks ---
576+
List<String> blockedByThisNoteTitles = note.getDependencies().stream()
577+
.filter(dep -> dep.type() == Note.DependencyType.BLOCKS)
578+
.map(Note.Dependency::otherNoteTitle)
579+
.collect(Collectors.toList());
580+
581+
if (!blockedByThisNoteTitles.isEmpty()) {
582+
FontIcon depIcon = new FontIcon(MaterialDesignB.BLOCK_HELPER);
583+
depIcon.setIconSize(12);
584+
depIcon.getStyleClass().addAll("detail-icon", "warning-icon");
585+
Tooltip.install(depIcon, new Tooltip("Blocks: " + String.join(", ", blockedByThisNoteTitles)));
586+
detailsBox.getChildren().add(depIcon);
587+
}
588+
}
589+
590+
// --- Related & Linked Goals Icon ---
591+
List<String> relatedNoteTitles = note.getDependencies().stream()
592+
.filter(dep -> dep.type() == Note.DependencyType.RELATED_TO)
593+
.map(Note.Dependency::otherNoteTitle)
594+
.collect(Collectors.toList());
595+
596+
boolean hasLinkedGoals = note.getGoals().stream().anyMatch(this::goalHasLinks);
597+
598+
if (!relatedNoteTitles.isEmpty() || hasLinkedGoals) {
599+
FontIcon linkIcon = new FontIcon(MaterialDesignL.LINK_VARIANT);
600+
linkIcon.setIconSize(12);
601+
linkIcon.getStyleClass().add("detail-icon");
602+
603+
List<String> tooltipParts = new ArrayList<>();
604+
if (!relatedNoteTitles.isEmpty()) {
605+
tooltipParts.add("Related to: " + String.join(", ", relatedNoteTitles));
606+
}
607+
if (hasLinkedGoals) {
608+
tooltipParts.add("Contains links to other notes in its goals.");
609+
}
610+
611+
Tooltip.install(linkIcon, new Tooltip(String.join("\n", tooltipParts)));
612+
detailsBox.getChildren().add(linkIcon);
613+
}
614+
552615
return detailsBox;
553616
}
554617

@@ -1008,7 +1071,22 @@ private void handleSortColumn(Note.Status status, Comparator<Note> comparator) {
10081071
}
10091072

10101073
private void handleReorderNote(UUID draggedNoteId, Note.Status targetStatus, int newIndexInUI) {
1074+
// Find the note first to perform checks before any UI manipulation
10111075
currentBoard.findNoteById(draggedNoteId).ifPresent(noteToMove -> {
1076+
// --- NEW: Check for blockers before moving to DONE ---
1077+
if (targetStatus == Note.Status.DONE || targetStatus == Note.Status.ARCHIVED) {
1078+
List<String> openBlockers = getOpenBlockers(noteToMove);
1079+
if (!openBlockers.isEmpty()) {
1080+
String blockersList = openBlockers.stream()
1081+
.map(title -> "- " + title)
1082+
.collect(Collectors.joining("\n"));
1083+
showError("Move Blocked", "This note cannot be completed because it is still blocked by:\n\n" +
1084+
blockersList);
1085+
return; // Abort the move
1086+
}
1087+
}
1088+
1089+
// If checks pass, proceed with the move
10121090
VBox oldContainer = noteContainersMap.get(noteToMove.getStatus());
10131091
VBox newContainer = noteContainersMap.get(targetStatus);
10141092

@@ -1180,6 +1258,7 @@ private void showNoteDetailView(Note note, VBox noteCard) {
11801258
controller.setNote(note);
11811259
controller.setUsers(currentBoard.getMembers());
11821260
controller.setAllTags(noteManager.getAllTags());
1261+
controller.setNoteManager(noteManager); // Pass the NoteManager for dependency searching
11831262
if (noteManager.getCurrentUser() != null) {
11841263
controller.setCurrentUser(noteManager.getCurrentUser()); // Set the current user for commenting
11851264
}
@@ -1205,6 +1284,25 @@ private void showNoteDetailView(Note note, VBox noteCard) {
12051284
// Refresh recent notes list after closing
12061285
refreshRecentNotesList();
12071286

1287+
// Check if the user clicked a link to open another note
1288+
Optional<UUID> noteToOpenId = controller.getNoteToOpen();
1289+
if (noteToOpenId.isPresent()) {
1290+
noteManager.findNoteAndBoard(noteToOpenId.get()).ifPresent(pair -> {
1291+
// Use Platform.runLater to avoid issues with opening a new dialog
1292+
// while the old one is still in its closing phase.
1293+
Platform.runLater(() -> {
1294+
// Switch to the board if it's not the current one
1295+
if (currentBoard == null || !currentBoard.getName().equals(pair.board.getName())) {
1296+
boardListView.getSelectionModel().select(pair.board.getName());
1297+
}
1298+
// Open the detail view for the linked note. We don't have a card context here.
1299+
showNoteDetailView(pair.note, null);
1300+
});
1301+
});
1302+
// Stop further processing since we are opening a new note and any other result is irrelevant.
1303+
return;
1304+
}
1305+
12081306
// Check if the user saved the changes by getting the returned note copy
12091307
controller.getResult().ifPresent(result -> {
12101308
Note savedNoteCopy = result.savedNote();
@@ -1242,4 +1340,37 @@ private void showNoteDetailView(Note note, VBox noteCard) {
12421340
showError("Failed to open editor", "Could not load the note detail view. Error: " + e.getMessage());
12431341
}
12441342
}
1343+
1344+
/**
1345+
* Checks a note for any "BLOCKED_BY" dependencies that are not yet completed.
1346+
*
1347+
* @param note The note to check.
1348+
* @return A list of titles of the notes that are actively blocking the given note.
1349+
*/
1350+
private List<String> getOpenBlockers(Note note) {
1351+
if (note == null || note.getDependencies() == null) {
1352+
return Collections.emptyList();
1353+
}
1354+
return note.getDependencies().stream()
1355+
.filter(dep -> dep.type() == Note.DependencyType.BLOCKED_BY)
1356+
.filter(dep -> noteManager.findNoteAcrossAllBoards(dep.otherNoteId())
1357+
.map(blocker -> blocker.getStatus() != Note.Status.DONE && blocker.getStatus() != Note.Status.ARCHIVED)
1358+
.orElse(false)) // Treat a non-existent note as a non-blocker
1359+
.map(Note.Dependency::otherNoteTitle)
1360+
.collect(Collectors.toList());
1361+
}
1362+
1363+
/**
1364+
* Recursively checks if a goal or any of its sub-goals are links to other notes.
1365+
*
1366+
* @param goal The goal to check.
1367+
* @return true if a link is found, false otherwise.
1368+
*/
1369+
private boolean goalHasLinks(Note.Goal goal) {
1370+
if (goal.isLink()) {
1371+
return true;
1372+
}
1373+
// The getSubGoals() method on the Goal class returns an unmodifiable list, which is never null.
1374+
return goal.getSubGoals().stream().anyMatch(this::goalHasLinks);
1375+
}
12451376
}

0 commit comments

Comments
 (0)