refactor: task detail modal into its own component
Move task detail modal logic from App into a dedicated TaskDetailModal component. The modal now manages its own state (open/closed, loading, error states) and handles its own focus and scroll. App delegates modal operations to the component and reacts to its events.
This commit is contained in:
+35
-92
@@ -3,16 +3,15 @@ use std::collections::HashMap;
|
||||
use gpui::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components::modal::ModalState,
|
||||
keymap::{Command, CommandDispatcher, ContextId, FocusTarget, KeyChord, KeymapStack},
|
||||
models::{FilterState, ProjectTree},
|
||||
task::{self, TaskDetailState, TaskOverview, TaskService, TaskSummary},
|
||||
task::{self, TaskOverview, TaskService, TaskSummary},
|
||||
theme::ActiveTheme,
|
||||
view::{
|
||||
app_layout,
|
||||
sidebar::{Sidebar, SidebarEvent, SidebarSection, TagItem},
|
||||
status_bar::{StatusBar, StatusBarEvent, SyncState},
|
||||
task_detail_modal,
|
||||
task_detail_modal::{TaskDetailModal, TaskDetailModalEvent},
|
||||
task_table::{TaskTable, TaskTableEvent},
|
||||
},
|
||||
};
|
||||
@@ -25,14 +24,10 @@ pub(super) struct App {
|
||||
pub(super) filter_state: gpui::Entity<FilterState>,
|
||||
pub(super) status_bar: gpui::Entity<StatusBar>,
|
||||
pub(super) task_table: gpui::Entity<TaskTable>,
|
||||
pub(super) task_detail_modal: gpui::Entity<TaskDetailModal>,
|
||||
pub(super) task_service: TaskService,
|
||||
pub(super) tasks: Vec<TaskSummary>,
|
||||
pub(super) selected_task_id: Option<uuid::Uuid>,
|
||||
pub(super) task_detail_state: TaskDetailState,
|
||||
pub(super) modal_state: ModalState,
|
||||
pub(super) modal_focus_handle: gpui::FocusHandle,
|
||||
pub(super) focus_before_modal: FocusTarget,
|
||||
pub(super) modal_scroll_handle: gpui::ScrollHandle,
|
||||
}
|
||||
|
||||
impl gpui::Render for App {
|
||||
@@ -63,22 +58,8 @@ impl gpui::Render for App {
|
||||
}
|
||||
});
|
||||
|
||||
let modal = if self.modal_state.open {
|
||||
let on_close_backdrop =
|
||||
cx.listener(|app, _event: &gpui::MouseDownEvent, window, cx| {
|
||||
app.close_task_detail(Some(window), cx);
|
||||
});
|
||||
let on_close_click = cx.listener(|app, _event: &gpui::MouseDownEvent, window, cx| {
|
||||
app.close_task_detail(Some(window), cx);
|
||||
});
|
||||
Some(task_detail_modal::render_task_detail_modal(
|
||||
&self.task_detail_state,
|
||||
&self.modal_focus_handle,
|
||||
&self.modal_scroll_handle,
|
||||
theme,
|
||||
on_close_backdrop,
|
||||
on_close_click,
|
||||
))
|
||||
let modal = if self.task_detail_modal.read(cx).is_open() {
|
||||
Some(self.task_detail_modal.clone().into_any_element())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -207,7 +188,9 @@ impl App {
|
||||
let context = self.active_context(cx);
|
||||
|
||||
if let Some(command) = self.keymap.resolve(context, &chord) {
|
||||
if self.modal_state.open {
|
||||
let modal_is_open = self.task_detail_modal.read(cx).is_open();
|
||||
|
||||
if modal_is_open {
|
||||
match command {
|
||||
Command::CloseModal
|
||||
| Command::SaveModal
|
||||
@@ -295,7 +278,7 @@ impl App {
|
||||
window: Option<&mut gpui::Window>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if self.modal_state.open {
|
||||
if self.task_detail_modal.read(cx).is_open() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -314,76 +297,26 @@ impl App {
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
self.focus_before_modal = self.focus_target;
|
||||
self.modal_state.open = true;
|
||||
if let Some(window) = window {
|
||||
window.focus(&self.modal_focus_handle);
|
||||
}
|
||||
self.modal_scroll_handle = gpui::ScrollHandle::new();
|
||||
self.modal_scroll_handle.scroll_to_item(0);
|
||||
|
||||
if self.selected_task_id == Some(task_id) {
|
||||
if matches!(self.task_detail_state, TaskDetailState::Ready(_)) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.selected_task_id = Some(task_id);
|
||||
self.task_detail_state = TaskDetailState::Loading(task_id);
|
||||
cx.notify();
|
||||
|
||||
let tasks = self.tasks.clone();
|
||||
match self.task_service.get_task_detail(task_id, &tasks) {
|
||||
Ok(detail) => {
|
||||
self.task_detail_state = TaskDetailState::Ready(detail);
|
||||
self.task_detail_modal.update(cx, |modal, cx| {
|
||||
modal.open_with_detail(detail, window, cx);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
self.task_detail_state = TaskDetailState::Error(task_id, e.to_string());
|
||||
self.task_detail_modal.update(cx, |modal, cx| {
|
||||
modal.open_with_error(task_id, e.to_string(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn close_task_detail(
|
||||
&mut self,
|
||||
window: Option<&mut gpui::Window>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if !self.modal_state.open {
|
||||
return;
|
||||
}
|
||||
|
||||
self.modal_state.open = false;
|
||||
self.selected_task_id = None;
|
||||
self.focus_target = self.focus_before_modal;
|
||||
|
||||
if let Some(window) = window {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn scroll_task_detail(&self, delta: i32, cx: &mut gpui::Context<Self>) {
|
||||
let handle = &self.modal_scroll_handle;
|
||||
let current = if delta > 0 {
|
||||
handle.bottom_item()
|
||||
} else {
|
||||
handle.top_item()
|
||||
};
|
||||
let next = if delta > 0 {
|
||||
current.saturating_add(1)
|
||||
} else {
|
||||
current.saturating_sub(1)
|
||||
};
|
||||
|
||||
handle.scroll_to_item(next);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn active_context(&self, cx: &gpui::Context<Self>) -> ContextId {
|
||||
if self.modal_state.open {
|
||||
if self.task_detail_modal.read(cx).is_open() {
|
||||
return ContextId::Modal;
|
||||
}
|
||||
if matches!(self.focus_target, FocusTarget::Table) {
|
||||
@@ -440,8 +373,12 @@ impl App {
|
||||
|
||||
let task_table = cx
|
||||
.new(|cx| TaskTable::new("main-task-table", filter_state.clone(), cx));
|
||||
|
||||
let task_detail_modal = cx.new(|cx| TaskDetailModal::new(cx));
|
||||
|
||||
let task_table_events = task_table.clone();
|
||||
let sidebar_events = sidebar.clone();
|
||||
let modal_events = task_detail_modal.clone();
|
||||
|
||||
task_table.update(cx, |table, cx| {
|
||||
table.reload_tasks_from_all(task_summaries.clone(), cx);
|
||||
@@ -450,7 +387,7 @@ impl App {
|
||||
let mut keymap = KeymapStack::new();
|
||||
keymap.push_layer(crate::keymap::defaults::build_default_keymap());
|
||||
|
||||
let app = App {
|
||||
let app_instance = App {
|
||||
focus_handle: cx.focus_handle(),
|
||||
focus_target: FocusTarget::Table,
|
||||
keymap,
|
||||
@@ -458,17 +395,13 @@ impl App {
|
||||
filter_state: filter_state.clone(),
|
||||
status_bar: status_bar.clone(),
|
||||
task_table,
|
||||
task_detail_modal,
|
||||
task_service,
|
||||
tasks: task_summaries,
|
||||
selected_task_id: None,
|
||||
task_detail_state: TaskDetailState::default(),
|
||||
modal_state: ModalState::default(),
|
||||
modal_focus_handle: cx.focus_handle(),
|
||||
focus_before_modal: FocusTarget::Table,
|
||||
modal_scroll_handle: gpui::ScrollHandle::new(),
|
||||
};
|
||||
|
||||
window.focus(&app.focus_handle);
|
||||
window.focus(&app_instance.focus_handle);
|
||||
|
||||
cx.observe(&filter_state, |app, _, cx| {
|
||||
app.reload_tasks(cx);
|
||||
@@ -481,6 +414,7 @@ impl App {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&sidebar_events, |app, _sidebar, event, cx| match event {
|
||||
SidebarEvent::Focused(section) => {
|
||||
app.focus_target = match section {
|
||||
@@ -491,16 +425,25 @@ impl App {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&task_table_events, |app, _table, event, cx| match event {
|
||||
TaskTableEvent::OpenTask(task_id) => {
|
||||
if !app.modal_state.open {
|
||||
if !app.task_detail_modal.read(cx).is_open() {
|
||||
app.open_task_detail(*task_id, None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app
|
||||
cx.subscribe(&modal_events, |app, _modal, event, cx| match event {
|
||||
TaskDetailModalEvent::Closed => {
|
||||
app.focus_target = app.focus_before_modal;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app_instance
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
+16
-2
@@ -3,6 +3,20 @@ use crate::{
|
||||
keymap::{Command, CommandDispatcher, FocusTarget},
|
||||
};
|
||||
|
||||
impl App {
|
||||
fn close_task_detail(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
self.task_detail_modal.update(cx, |modal, cx| {
|
||||
modal.close(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn scroll_task_detail(&self, delta: i32, cx: &mut gpui::Context<Self>) {
|
||||
self.task_detail_modal.update(cx, |modal, cx| {
|
||||
modal.scroll(delta, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandDispatcher for App {
|
||||
fn dispatch(&mut self, command: Command, cx: &mut gpui::Context<Self>) -> bool {
|
||||
match command {
|
||||
@@ -94,11 +108,11 @@ impl CommandDispatcher for App {
|
||||
true
|
||||
}
|
||||
Command::CloseModal => {
|
||||
self.close_task_detail(None, cx);
|
||||
self.close_task_detail(cx);
|
||||
true
|
||||
}
|
||||
Command::SaveModal => {
|
||||
self.close_task_detail(None, cx);
|
||||
self.close_task_detail(cx);
|
||||
true
|
||||
}
|
||||
Command::ModalScrollUp => {
|
||||
|
||||
@@ -4,11 +4,163 @@ use std::sync::Arc;
|
||||
use crate::components::label::Label;
|
||||
use crate::components::modal::ModalFrame;
|
||||
use crate::task::model::TaskLinkVm;
|
||||
use crate::task::{self, TaskDetailState};
|
||||
use crate::theme::Theme;
|
||||
use crate::task::{self, TaskDetailState, TaskDetailVm};
|
||||
use crate::theme::{ActiveTheme, Theme};
|
||||
use crate::ui::{DATE_FORMAT, DATE_TIME_FORMAT};
|
||||
|
||||
pub fn render_task_detail_modal(
|
||||
pub enum TaskDetailModalEvent {
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct TaskDetailModal {
|
||||
state: TaskDetailState,
|
||||
is_open: bool,
|
||||
focus_handle: gpui::FocusHandle,
|
||||
scroll_handle: gpui::ScrollHandle,
|
||||
}
|
||||
|
||||
impl TaskDetailModal {
|
||||
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
|
||||
Self {
|
||||
state: TaskDetailState::default(),
|
||||
is_open: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: gpui::ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
|
||||
pub fn focus_handle(&self) -> &gpui::FocusHandle {
|
||||
&self.focus_handle
|
||||
}
|
||||
|
||||
pub fn open_with_detail(
|
||||
&mut self,
|
||||
detail: TaskDetailVm,
|
||||
window: Option<&mut gpui::Window>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if let Some(window) = window {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.is_open = true;
|
||||
self.scroll_handle = gpui::ScrollHandle::new();
|
||||
self.scroll_handle.scroll_to_item(0);
|
||||
self.state = TaskDetailState::Ready(detail);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn open_with_error(
|
||||
&mut self,
|
||||
task_id: uuid::Uuid,
|
||||
error: String,
|
||||
window: Option<&mut gpui::Window>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if let Some(window) = window {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.is_open = true;
|
||||
self.scroll_handle = gpui::ScrollHandle::new();
|
||||
self.scroll_handle.scroll_to_item(0);
|
||||
self.state = TaskDetailState::Error(task_id, error);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn open_loading(
|
||||
&mut self,
|
||||
task_id: uuid::Uuid,
|
||||
window: Option<&mut gpui::Window>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if let Some(window) = window {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.is_open = true;
|
||||
self.scroll_handle = gpui::ScrollHandle::new();
|
||||
self.scroll_handle.scroll_to_item(0);
|
||||
self.state = TaskDetailState::Loading(task_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_detail(&mut self, detail: TaskDetailVm, cx: &mut gpui::Context<Self>) {
|
||||
self.state = TaskDetailState::Ready(detail);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_error(&mut self, task_id: uuid::Uuid, error: String, cx: &mut gpui::Context<Self>) {
|
||||
self.state = TaskDetailState::Error(task_id, error);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
if !self.is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_open = false;
|
||||
self.state = TaskDetailState::Idle;
|
||||
cx.emit(TaskDetailModalEvent::Closed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn scroll(&self, delta: i32, cx: &mut gpui::Context<Self>) {
|
||||
let handle = &self.scroll_handle;
|
||||
let current = if delta > 0 {
|
||||
handle.bottom_item()
|
||||
} else {
|
||||
handle.top_item()
|
||||
};
|
||||
let next = if delta > 0 {
|
||||
current.saturating_add(1)
|
||||
} else {
|
||||
current.saturating_sub(1)
|
||||
};
|
||||
|
||||
handle.scroll_to_item(next);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::EventEmitter<TaskDetailModalEvent> for TaskDetailModal {}
|
||||
|
||||
impl gpui::Render for TaskDetailModal {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
if !self.is_open {
|
||||
return gpui::div().into_any_element();
|
||||
}
|
||||
|
||||
let theme = cx.theme();
|
||||
|
||||
let on_close_backdrop = cx.listener(|modal, _event: &gpui::MouseDownEvent, _window, cx| {
|
||||
modal.close(cx);
|
||||
});
|
||||
let on_close_click = cx.listener(|modal, _event: &gpui::MouseDownEvent, _window, cx| {
|
||||
modal.close(cx);
|
||||
});
|
||||
|
||||
render_task_detail_modal(
|
||||
&self.state,
|
||||
&self.focus_handle,
|
||||
&self.scroll_handle,
|
||||
theme,
|
||||
on_close_backdrop,
|
||||
on_close_click,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_task_detail_modal(
|
||||
detail_state: &TaskDetailState,
|
||||
focus_handle: &gpui::FocusHandle,
|
||||
scroll_handle: &gpui::ScrollHandle,
|
||||
|
||||
Reference in New Issue
Block a user