Skip to content
Merged
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
14 changes: 7 additions & 7 deletions Application/DevLogCore/Sources/TodoQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@

import Foundation

public struct TodoQuery: Equatable {
public enum SortTarget: Equatable, Hashable {
public struct TodoQuery: Equatable, Sendable {
public enum SortTarget: Equatable, Hashable, Sendable {
case createdAt
case completedAt
case deletedAt
case updatedAt
case dueDate
}

public enum SortOrder: Equatable, Hashable {
public enum SortOrder: Equatable, Hashable, Sendable {
case latest
case oldest
}

public enum CompletionFilter: Equatable, Hashable {
public enum CompletionFilter: Equatable, Hashable, Sendable {
case all
case incomplete
case completed
}

public enum DueDateFilter: Equatable, Hashable {
public enum DueDateFilter: Equatable, Hashable, Sendable {
case all
case withDueDate
case withoutDueDate
}

public var categoryId: String?
public var keyword: String?
public var isPinned: Bool?
public var isPinned: Bool
public var completionFilter: CompletionFilter
public var dueDateFilter: DueDateFilter
public var sortDateFrom: Date?
Expand All @@ -49,7 +49,7 @@ public struct TodoQuery: Equatable {
public init(
categoryId: String? = nil,
keyword: String? = nil,
isPinned: Bool? = nil,
isPinned: Bool = false,
completionFilter: CompletionFilter = .all,
dueDateFilter: DueDateFilter = .all,
sortDateFrom: Date? = nil,
Expand Down
2 changes: 1 addition & 1 deletion Application/DevLogDomain/Sources/Entity/TodoCursor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public struct TodoCursor {
public struct TodoCursor: Equatable {
public let primarySortDate: Date?
public let secondarySortDate: Date?
public let documentID: String
Expand Down
6 changes: 3 additions & 3 deletions Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class TodoServiceImpl: TodoService {
"sortOrder=\(query.sortOrder == .latest ? "latest" : "oldest")",
query.keyword != nil ? "keywordLength=\(trimmedKeyword.count)" : nil,
query.categoryId != nil ? "category=\(query.categoryId!)" : nil,
query.isPinned != nil ? "pinned=\(query.isPinned!)" : nil,
query.isPinned ? "pinned=true" : nil,
query.completionFilter.isCompletedValue != nil
? "completed=\(query.completionFilter.isCompletedValue!)"
: nil,
Expand All @@ -58,8 +58,8 @@ final class TodoServiceImpl: TodoService {
)
}

if let isPinned = query.isPinned {
firestoreQuery = firestoreQuery.whereField("isPinned", isEqualTo: isPinned)
if query.isPinned {
firestoreQuery = firestoreQuery.whereField("isPinned", isEqualTo: true)
}

if let isCompleted = query.completionFilter.isCompletedValue {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// TodoListFeature+Effects.swift
// DevLogPresentation
//
// Created by opfic on 6/12/26.
//

import ComposableArchitecture
import DevLogCore
import DevLogDomain
import Foundation

extension TodoListFeature {
func searchEffect(
_ keyword: String,
category: TodoCategory
) -> Effect<Action> {
.run { [fetchTodosUseCase] send in
do {
let query = TodoQuery(categoryId: category.storageValue, keyword: keyword)
let page = try await fetchTodosUseCase.execute(query, cursor: nil)
try Task.checkCancellation()
await send(.fetchSearchResults(page.items.compactMap(TodoListItem.init(from:))))
await send(.loading(.end(target: .default, mode: .immediate)))
} catch is CancellationError {
return
} catch {
await send(.setAlert(true))
await send(.loading(.end(target: .default, mode: .immediate)))
}
}
.cancellable(id: CancelID.request, cancelInFlight: true)
}

func toggleCompletedEffect(_ item: TodoListItem) -> Effect<Action> {
.concatenate(
.send(.loading(.begin(target: .default, mode: .delayed))),
.run { [fetchTodoByIdUseCase, upsertTodoUseCase, trackAnalyticsEventUseCase] send in
do {
var todo = try await fetchTodoByIdUseCase.execute(item.id)
let now = Date()
todo.isCompleted.toggle()
todo.completedAt = todo.isCompleted ? now : nil
todo.updatedAt = now
try await upsertTodoUseCase.execute(todo)
if todo.isCompleted {
trackAnalyticsEventUseCase?.execute(.todoComplete)
}
guard let todoListItem = TodoListItem(from: todo) else {
await send(.setAlert(true))
await send(.loading(.end(target: .default, mode: .delayed)))
return
}
await send(.didToggleCompleted(todoListItem))
await send(.loading(.end(target: .default, mode: .delayed)))
} catch {
await send(.setAlert(true))
await send(.loading(.end(target: .default, mode: .delayed)))
}
}
)
}

func togglePinnedEffect(_ item: TodoListItem) -> Effect<Action> {
.concatenate(
.send(.loading(.begin(target: .default, mode: .delayed))),
.run { [fetchTodoByIdUseCase, upsertTodoUseCase] send in
do {
var todo = try await fetchTodoByIdUseCase.execute(item.id)
todo.isPinned.toggle()
todo.updatedAt = Date()
try await upsertTodoUseCase.execute(todo)
guard let todoListItem = TodoListItem(from: todo) else {
await send(.setAlert(true))
await send(.loading(.end(target: .default, mode: .delayed)))
return
}
await send(.didTogglePinned(todoListItem))
await send(.loading(.end(target: .default, mode: .delayed)))
} catch {
await send(.setAlert(true))
await send(.loading(.end(target: .default, mode: .delayed)))
}
}
)
}

func swipeTodoEffect(_ todo: TodoListItem, state: inout State) -> Effect<Action> {
guard state.todos.contains(where: { $0.id == todo.id }) else { return .none }
state.undoTodoId = todo.id
state.deleteToastTodoId = todo.id
Self.setTodoHidden(&state, todoId: todo.id, isHidden: true)
return deleteEffect(todo)
}

func deleteEffect(_ item: TodoListItem) -> Effect<Action> {
.run { [deleteTodoUseCase] send in
do {
try await deleteTodoUseCase.execute(item.id)
} catch {
await send(.setTodoHidden(item.id, false))
await send(.setAlert(true))
}
}
}

func undoDeleteEffect(_ todoId: String) -> Effect<Action> {
.run { [undoDeleteTodoUseCase] send in
do {
try await undoDeleteTodoUseCase.execute(todoId)
} catch {
await send(.setTodoHidden(todoId, true))
await send(.setAlert(true))
}
}
}

static func setAlert(
_ state: inout State,
isPresented: Bool
) {
state.alert = isPresented ? Self.alertState() : nil
}

static func alertState() -> AlertState<Never> {
AlertState {
TextState(String(localized: "common_error_title"))
} actions: {
ButtonState(role: .cancel) {
TextState(String(localized: "common_close"))
}
} message: {
TextState(String(localized: "common_error_message"))
}
}

static func setTodoHidden(
_ state: inout State,
todoId: String,
isHidden: Bool
) {
if let todoIndex = state.todos.firstIndex(where: { $0.id == todoId }) {
state.todos[todoIndex].isHidden = isHidden
}

if let searchResultIndex = state.searchResults.firstIndex(where: { $0.id == todoId }) {
state.searchResults[searchResultIndex].isHidden = isHidden
}
}
}
Loading
Loading