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 gpui::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::modal::ModalState,
|
|
||||||
keymap::{Command, CommandDispatcher, ContextId, FocusTarget, KeyChord, KeymapStack},
|
keymap::{Command, CommandDispatcher, ContextId, FocusTarget, KeyChord, KeymapStack},
|
||||||
models::{FilterState, ProjectTree},
|
models::{FilterState, ProjectTree},
|
||||||
task::{self, TaskDetailState, TaskOverview, TaskService, TaskSummary},
|
task::{self, TaskOverview, TaskService, TaskSummary},
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
view::{
|
view::{
|
||||||
app_layout,
|
app_layout,
|
||||||
sidebar::{Sidebar, SidebarEvent, SidebarSection, TagItem},
|
sidebar::{Sidebar, SidebarEvent, SidebarSection, TagItem},
|
||||||
status_bar::{StatusBar, StatusBarEvent, SyncState},
|
status_bar::{StatusBar, StatusBarEvent, SyncState},
|
||||||
task_detail_modal,
|
task_detail_modal::{TaskDetailModal, TaskDetailModalEvent},
|
||||||
task_table::{TaskTable, TaskTableEvent},
|
task_table::{TaskTable, TaskTableEvent},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -25,14 +24,10 @@ pub(super) struct App {
|
|||||||
pub(super) filter_state: gpui::Entity<FilterState>,
|
pub(super) filter_state: gpui::Entity<FilterState>,
|
||||||
pub(super) status_bar: gpui::Entity<StatusBar>,
|
pub(super) status_bar: gpui::Entity<StatusBar>,
|
||||||
pub(super) task_table: gpui::Entity<TaskTable>,
|
pub(super) task_table: gpui::Entity<TaskTable>,
|
||||||
|
pub(super) task_detail_modal: gpui::Entity<TaskDetailModal>,
|
||||||
pub(super) task_service: TaskService,
|
pub(super) task_service: TaskService,
|
||||||
pub(super) tasks: Vec<TaskSummary>,
|
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) focus_before_modal: FocusTarget,
|
||||||
pub(super) modal_scroll_handle: gpui::ScrollHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::Render for App {
|
impl gpui::Render for App {
|
||||||
@@ -63,22 +58,8 @@ impl gpui::Render for App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let modal = if self.modal_state.open {
|
let modal = if self.task_detail_modal.read(cx).is_open() {
|
||||||
let on_close_backdrop =
|
Some(self.task_detail_modal.clone().into_any_element())
|
||||||
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,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -207,7 +188,9 @@ impl App {
|
|||||||
let context = self.active_context(cx);
|
let context = self.active_context(cx);
|
||||||
|
|
||||||
if let Some(command) = self.keymap.resolve(context, &chord) {
|
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 {
|
match command {
|
||||||
Command::CloseModal
|
Command::CloseModal
|
||||||
| Command::SaveModal
|
| Command::SaveModal
|
||||||
@@ -295,7 +278,7 @@ impl App {
|
|||||||
window: Option<&mut gpui::Window>,
|
window: Option<&mut gpui::Window>,
|
||||||
cx: &mut gpui::Context<Self>,
|
cx: &mut gpui::Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.modal_state.open {
|
if self.task_detail_modal.read(cx).is_open() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,76 +297,26 @@ impl App {
|
|||||||
cx: &mut gpui::Context<Self>,
|
cx: &mut gpui::Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.focus_before_modal = self.focus_target;
|
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();
|
let tasks = self.tasks.clone();
|
||||||
match self.task_service.get_task_detail(task_id, &tasks) {
|
match self.task_service.get_task_detail(task_id, &tasks) {
|
||||||
Ok(detail) => {
|
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) => {
|
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();
|
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 {
|
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;
|
return ContextId::Modal;
|
||||||
}
|
}
|
||||||
if matches!(self.focus_target, FocusTarget::Table) {
|
if matches!(self.focus_target, FocusTarget::Table) {
|
||||||
@@ -440,8 +373,12 @@ impl App {
|
|||||||
|
|
||||||
let task_table = cx
|
let task_table = cx
|
||||||
.new(|cx| TaskTable::new("main-task-table", filter_state.clone(), 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 task_table_events = task_table.clone();
|
||||||
let sidebar_events = sidebar.clone();
|
let sidebar_events = sidebar.clone();
|
||||||
|
let modal_events = task_detail_modal.clone();
|
||||||
|
|
||||||
task_table.update(cx, |table, cx| {
|
task_table.update(cx, |table, cx| {
|
||||||
table.reload_tasks_from_all(task_summaries.clone(), cx);
|
table.reload_tasks_from_all(task_summaries.clone(), cx);
|
||||||
@@ -450,7 +387,7 @@ impl App {
|
|||||||
let mut keymap = KeymapStack::new();
|
let mut keymap = KeymapStack::new();
|
||||||
keymap.push_layer(crate::keymap::defaults::build_default_keymap());
|
keymap.push_layer(crate::keymap::defaults::build_default_keymap());
|
||||||
|
|
||||||
let app = App {
|
let app_instance = App {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
focus_target: FocusTarget::Table,
|
focus_target: FocusTarget::Table,
|
||||||
keymap,
|
keymap,
|
||||||
@@ -458,17 +395,13 @@ impl App {
|
|||||||
filter_state: filter_state.clone(),
|
filter_state: filter_state.clone(),
|
||||||
status_bar: status_bar.clone(),
|
status_bar: status_bar.clone(),
|
||||||
task_table,
|
task_table,
|
||||||
|
task_detail_modal,
|
||||||
task_service,
|
task_service,
|
||||||
tasks: task_summaries,
|
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,
|
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| {
|
cx.observe(&filter_state, |app, _, cx| {
|
||||||
app.reload_tasks(cx);
|
app.reload_tasks(cx);
|
||||||
@@ -481,6 +414,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.subscribe(&sidebar_events, |app, _sidebar, event, cx| match event {
|
cx.subscribe(&sidebar_events, |app, _sidebar, event, cx| match event {
|
||||||
SidebarEvent::Focused(section) => {
|
SidebarEvent::Focused(section) => {
|
||||||
app.focus_target = match section {
|
app.focus_target = match section {
|
||||||
@@ -491,16 +425,25 @@ impl App {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.subscribe(&task_table_events, |app, _table, event, cx| match event {
|
cx.subscribe(&task_table_events, |app, _table, event, cx| match event {
|
||||||
TaskTableEvent::OpenTask(task_id) => {
|
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);
|
app.open_task_detail(*task_id, None, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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},
|
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 {
|
impl CommandDispatcher for App {
|
||||||
fn dispatch(&mut self, command: Command, cx: &mut gpui::Context<Self>) -> bool {
|
fn dispatch(&mut self, command: Command, cx: &mut gpui::Context<Self>) -> bool {
|
||||||
match command {
|
match command {
|
||||||
@@ -94,11 +108,11 @@ impl CommandDispatcher for App {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
Command::CloseModal => {
|
Command::CloseModal => {
|
||||||
self.close_task_detail(None, cx);
|
self.close_task_detail(cx);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Command::SaveModal => {
|
Command::SaveModal => {
|
||||||
self.close_task_detail(None, cx);
|
self.close_task_detail(cx);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Command::ModalScrollUp => {
|
Command::ModalScrollUp => {
|
||||||
|
|||||||
@@ -4,11 +4,163 @@ use std::sync::Arc;
|
|||||||
use crate::components::label::Label;
|
use crate::components::label::Label;
|
||||||
use crate::components::modal::ModalFrame;
|
use crate::components::modal::ModalFrame;
|
||||||
use crate::task::model::TaskLinkVm;
|
use crate::task::model::TaskLinkVm;
|
||||||
use crate::task::{self, TaskDetailState};
|
use crate::task::{self, TaskDetailState, TaskDetailVm};
|
||||||
use crate::theme::Theme;
|
use crate::theme::{ActiveTheme, Theme};
|
||||||
use crate::ui::{DATE_FORMAT, DATE_TIME_FORMAT};
|
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,
|
detail_state: &TaskDetailState,
|
||||||
focus_handle: &gpui::FocusHandle,
|
focus_handle: &gpui::FocusHandle,
|
||||||
scroll_handle: &gpui::ScrollHandle,
|
scroll_handle: &gpui::ScrollHandle,
|
||||||
|
|||||||
Reference in New Issue
Block a user