Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions Plugin/FocusRelayBridge.omnijs/Resources/BridgeLibrary.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,32 @@
* @param {Task} task - OmniFocus task object
* @returns {boolean} True if task is remaining
*/
function isRemainingStatus(task) {
const st = taskStatus(task);
function parentChainAllowsRemaining(task) {
let parent = safe(() => task.parent);
let depth = 0;
while (parent && depth < 100) {
const parentStatus = taskStatus(parent);
if (isCompletedStatusValue(parentStatus) || isDroppedStatusValue(parentStatus)) {
return false;
}
parent = safe(() => parent.parent);
depth += 1;
}
return true;
}

function isRemainingStatusValue(st) {
return !isCompletedStatusValue(st) && !isDroppedStatusValue(st);
}

function isRemainingStatusWithStatus(task, taskStatusValue) {
return isRemainingStatusValue(taskStatusValue) && parentChainAllowsRemaining(task);
}

function isRemainingStatus(task) {
return isRemainingStatusWithStatus(task, taskStatus(task));
}

/**
* Check if task status indicates availability
* Note: This checks ONLY the task status, not project/parent status
Expand Down Expand Up @@ -251,11 +272,7 @@
if (statusStr.includes("Done")) { return false; }
}

const parent = safe(() => task.parent);
if (parent) {
if (isCompletedStatus(parent)) { return false; }
if (isDroppedStatus(parent)) { return false; }
}
if (!parentChainAllowsRemaining(task)) { return false; }

return isAvailableStatusValue(taskStatusValue);
}
Expand Down Expand Up @@ -455,7 +472,7 @@
const taskCompleted = isCompletedStatusValue(taskStatusValue);
if (taskCompleted !== filterState.completed) return false;
} else if (!isEverything) {
if (isCompletedStatusValue(taskStatusValue) || isDroppedStatusValue(taskStatusValue)) return false;
if (!isRemainingStatusWithStatus(t, taskStatusValue)) return false;
}
if (filterState.flagged !== undefined) {
const taskFlagged = Boolean(t.flagged);
Expand Down Expand Up @@ -1307,7 +1324,7 @@
const taskStatusValue = taskStatus(t);
const taskCompleted = isCompletedStatusValue(taskStatusValue);
const taskDropped = isDroppedStatusValue(taskStatusValue);
const taskRemaining = !taskCompleted && !taskDropped;
const taskRemaining = isRemainingStatusWithStatus(t, taskStatusValue);
let taskFlagged = null;

if (filterState.completed !== undefined) {
Expand Down
4 changes: 1 addition & 3 deletions Sources/OmniFocusAutomation/BridgeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ final class BridgeClient: @unchecked Sendable {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
self.encoder = encoder
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.decoder = decoder
self.decoder = BridgeDateDecoding.makeJSONDecoder()
}

func listTasks(filter: TaskFilter, page: PageRequest, fields: [String]?) throws -> Page<TaskItem> {
Expand Down
55 changes: 55 additions & 0 deletions Sources/OmniFocusAutomation/BridgeDateDecoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation

enum BridgeDateDecoding {
private static let fractionalISO8601Formatter = LockedISO8601Formatter(
formatOptions: [.withInternetDateTime, .withFractionalSeconds]
)

private static let standardISO8601Formatter = LockedISO8601Formatter(
formatOptions: [.withInternetDateTime]
)

static func makeJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
configure(decoder)
return decoder
}

static func configure(_ decoder: JSONDecoder) {
decoder.dateDecodingStrategy = .custom { decoder in
try decodeDate(from: decoder)
}
}

private static func decodeDate(from decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

if let date = fractionalISO8601Formatter.date(from: string)
?? standardISO8601Formatter.date(from: string) {
return date
}

throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Expected ISO8601 date string with or without fractional seconds."
)
}
}

private final class LockedISO8601Formatter: @unchecked Sendable {
private let lock = NSLock()
private let formatter: ISO8601DateFormatter

init(formatOptions: ISO8601DateFormatter.Options) {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = formatOptions
self.formatter = formatter
}

func date(from string: String) -> Date? {
lock.lock()
defer { lock.unlock() }
return formatter.date(from: string)
}
}
67 changes: 40 additions & 27 deletions Sources/OmniFocusAutomation/OmniFocusAutomation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ public final class OmniAutomationService: OmniFocusService {

public init(runner: ScriptRunner = ScriptRunner()) {
self.runner = runner
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.decoder = decoder
self.decoder = BridgeDateDecoding.makeJSONDecoder()
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
self.requestEncoder = encoder
Expand Down Expand Up @@ -367,9 +365,20 @@ private func listTasksOmniAutomationScript(requestJSON: String) -> String {
return taskStatusName(task) === "dropped";
}

function parentChainAllowsRemaining(task) {
var parent = safe(function() { return task.parent; });
var depth = 0;
while (parent && depth < 100) {
if (isCompletedStatus(parent) || isDroppedStatus(parent)) { return false; }
parent = safe(function() { return parent.parent; });
depth += 1;
}
return true;
}

function isRemainingStatus(task) {
var statusName = taskStatusName(task);
return statusName !== "completed" && statusName !== "dropped";
return statusName !== "completed" && statusName !== "dropped" && parentChainAllowsRemaining(task);
}

function isAvailableStatus(task) {
Expand Down Expand Up @@ -400,14 +409,8 @@ private func listTasksOmniAutomationScript(requestJSON: String) -> String {
return false;
}

function parentAllowsAvailability(task) {
var parent = safe(function() { return task.parent; });
if (!parent) { return true; }
return !isCompletedStatus(parent) && !isDroppedStatus(parent);
}

function isTaskAvailable(task) {
if (!parentAllowsAvailability(task)) { return false; }
if (!parentChainAllowsRemaining(task)) { return false; }

var project = safe(function() { return task.containingProject; });
if (project) {
Expand Down Expand Up @@ -949,9 +952,20 @@ private func taskCountsOmniAutomationScript(requestJSON: String) -> String {
return taskStatusName(task) === "dropped";
}

function parentChainAllowsRemaining(task) {
var parent = safe(function() { return task.parent; });
var depth = 0;
while (parent && depth < 100) {
if (isCompletedStatus(parent) || isDroppedStatus(parent)) { return false; }
parent = safe(function() { return parent.parent; });
depth += 1;
}
return true;
}

function isRemainingStatus(task) {
var statusName = taskStatusName(task);
return statusName !== "completed" && statusName !== "dropped";
return statusName !== "completed" && statusName !== "dropped" && parentChainAllowsRemaining(task);
}

function isAvailableStatus(task) {
Expand Down Expand Up @@ -982,14 +996,8 @@ private func taskCountsOmniAutomationScript(requestJSON: String) -> String {
return false;
}

function parentAllowsAvailability(task) {
var parent = safe(function() { return task.parent; });
if (!parent) { return true; }
return !isCompletedStatus(parent) && !isDroppedStatus(parent);
}

function isTaskAvailable(task) {
if (!parentAllowsAvailability(task)) { return false; }
if (!parentChainAllowsRemaining(task)) { return false; }

var project = safe(function() { return task.containingProject; });
if (project) {
Expand Down Expand Up @@ -1256,9 +1264,20 @@ private func projectCountsOmniAutomationScript(requestJSON: String) -> String {
return taskStatusName(task) === "dropped";
}

function parentChainAllowsRemaining(task) {
var parent = safe(function() { return task.parent; });
var depth = 0;
while (parent && depth < 100) {
if (isCompletedStatus(parent) || isDroppedStatus(parent)) { return false; }
parent = safe(function() { return parent.parent; });
depth += 1;
}
return true;
}

function isRemainingStatus(task) {
var statusName = taskStatusName(task);
return statusName !== "completed" && statusName !== "dropped";
return statusName !== "completed" && statusName !== "dropped" && parentChainAllowsRemaining(task);
}

function isAvailableStatus(task) {
Expand Down Expand Up @@ -1289,14 +1308,8 @@ private func projectCountsOmniAutomationScript(requestJSON: String) -> String {
return false;
}

function parentAllowsAvailability(task) {
var parent = safe(function() { return task.parent; });
if (!parent) { return true; }
return !isCompletedStatus(parent) && !isDroppedStatus(parent);
}

function isTaskAvailable(task) {
if (!parentAllowsAvailability(task)) { return false; }
if (!parentChainAllowsRemaining(task)) { return false; }

var project = safe(function() { return task.containingProject; });
if (project) {
Expand Down
Loading
Loading