// // ProjectPanelView.swift // Todos // // Created by Sam Jaffe on 2/28/26. // import SwiftUI import SwiftData struct ProjectPanelView: View { @Environment(\.modelContext) private var modelContext @Bindable var item: Project @State private var showDialogue = false @State private var move = false @State private var taskFilter = "" @State private var statuses = StatusList() var body: some View { HStack { TextField("Project Name", text: $item.name) .font(.title) .padding(.leading, 10) CategoryColorDisplay(category: $item.category) Spacer() Button(action: addItem) { Image(systemName: "plus") } .help("New Task") .padding(.trailing, 10) if move { Label("", systemImage: "arrow.up.arrow.down") .foregroundStyle(.red) .font(.title2) .help("Re-ordering mode is enabled, text fields will be unresponsive") } if !taskFilter.isEmpty { Label("", systemImage: "text.magnifyingglass") .foregroundStyle(.blue) .font(.title2) .help("Only showing text matching '\(taskFilter)'") } if !statuses.all { Label("", systemImage: "exclamationmark.magnifyingglass") .foregroundStyle(.blue) .font(.title2) .help(statuses.description) } Button { showDialogue = !showDialogue } label: { Label("", systemImage: "ellipsis.circle") .foregroundStyle(.gray) .font(.title) } .buttonStyle(.borderless) .popover(isPresented: $showDialogue) { List { Label("Settings", systemImage: "gearshape") .font(.title) CategoryPicker(category: $item.category) Label("Filters", systemImage: "magnifyingglass") .font(.title) HStack { Label("", systemImage: "arrow.up.arrow.down") Toggle("Move Tasks", isOn: $move) } HStack { Label("", systemImage: "text.magnifyingglass") TextField("Filter Tasks", text: $taskFilter) } StatusChecklist(statuses: $statuses) } } Text("") } TextField("Project Notes", text: $item.notes, axis: .vertical) .padding(.leading, 20) List { ForEach(selected($item.tasks), id: \.id) { task in TaskView(task: task) .swipeActions { Button("Delete", systemImage: "trash", role: .destructive) { deleteItem(item: task.wrappedValue, from: item) } } ForEach(selected(task.subtasks), id: \.id) { subtask in SubTaskView(task: subtask) .swipeActions { Button("Delete", systemImage: "trash", role: .destructive) { deleteItem(item: subtask.wrappedValue, from: task.wrappedValue) } } } .onMove(perform: { moveItem(task.wrappedValue, $0, $1) }) .moveDisabled(!move) } .onMove(perform: { moveItem(item, $0, $1) }) .moveDisabled(!move) } } private func selected(_ items: Binding<[T]>) -> [Binding] { return items.sorted(by: T.less).filter({ let value = $0.wrappedValue return value.name.isEmpty || (statuses.test(value.status) && (taskFilter.isEmpty || value.containsText(taskFilter))) }) } private func addItem() { withAnimation { let newTask = Task(parent: item) modelContext.insert(newTask) item.tasks.append(newTask) } } private func moveItem(_ within: any Aggregate, _ fromOffsets: IndexSet, _ toOffset: Int) { withAnimation { within.move(fromOffsets: fromOffsets, toOffset: toOffset) } } private func deleteItem(item: T.Element, from: T) where T.Element: PersistentModel { withAnimation { from.remove(item) modelContext.delete(item) } } } #Preview { @Previewable @State var item = Project() ProjectPanelView(item: item) }