feat: enhance input component and task modal UX
- Text selection & clipboard: Add full text selection support with Ctrl+A/C/X/V operations, visual highlight, and proper cursor positioning - Multiple annotations: Fix annotation creation to properly append multiple entries instead of overwriting - Navigation improvements: Improve annotation list navigation to cycle between items and input field - UI layout fixes: Add flex wrapping to tag displays to handle long lists gracefully - Focus management: Fix focus retention when switching between edit/view modes in task modal - Tag suggestions: Prevent auto-acceptance of suggestions on Enter, allowing custom tag input
This commit is contained in:
+265
-42
@@ -14,6 +14,7 @@ pub struct Input {
|
|||||||
placeholder: gpui::SharedString,
|
placeholder: gpui::SharedString,
|
||||||
|
|
||||||
cursor_pos: usize,
|
cursor_pos: usize,
|
||||||
|
selection_anchor: Option<usize>,
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
|
|
||||||
suggestions: Vec<Suggestion>,
|
suggestions: Vec<Suggestion>,
|
||||||
@@ -39,6 +40,7 @@ impl Input {
|
|||||||
placeholder: placeholder.into(),
|
placeholder: placeholder.into(),
|
||||||
|
|
||||||
cursor_pos: 0,
|
cursor_pos: 0,
|
||||||
|
selection_anchor: None,
|
||||||
multiline: false,
|
multiline: false,
|
||||||
|
|
||||||
suggestions: vec![],
|
suggestions: vec![],
|
||||||
@@ -101,6 +103,11 @@ impl Input {
|
|||||||
self.suggestions_open
|
self.suggestions_open
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn close_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.suggestions_open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_value(&mut self, value: impl Into<String>, cx: &mut gpui::Context<Self>) {
|
pub fn set_value(&mut self, value: impl Into<String>, cx: &mut gpui::Context<Self>) {
|
||||||
self.value = value.into();
|
self.value = value.into();
|
||||||
self.cursor_pos = self.value.len();
|
self.cursor_pos = self.value.len();
|
||||||
@@ -130,6 +137,67 @@ impl Input {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_selection(&self) -> bool {
|
||||||
|
self.selection_anchor.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
|
self.selection_anchor.map(|anchor| {
|
||||||
|
let start = anchor.min(self.cursor_pos);
|
||||||
|
let end = anchor.max(self.cursor_pos);
|
||||||
|
start..end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_selection(&mut self) {
|
||||||
|
self.selection_anchor = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selection(&mut self, anchor: usize, cursor: usize) {
|
||||||
|
self.selection_anchor = Some(anchor);
|
||||||
|
self.cursor_pos = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_all(&mut self) {
|
||||||
|
self.selection_anchor = Some(0);
|
||||||
|
self.cursor_pos = self.value.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selection(&mut self, cx: &mut gpui::Context<Self>) -> bool {
|
||||||
|
if let Some(range) = self.selection_range() {
|
||||||
|
self.value.drain(range.clone());
|
||||||
|
self.cursor_pos = range.start;
|
||||||
|
self.clear_selection();
|
||||||
|
if let Some(on_change) = self.on_change.clone() {
|
||||||
|
on_change(&self.value, cx);
|
||||||
|
}
|
||||||
|
self.refresh_suggestions(cx);
|
||||||
|
cx.notify();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_selection(&mut self, text: &str, cx: &mut gpui::Context<Self>) {
|
||||||
|
let range = self.selection_range();
|
||||||
|
let insert_pos = range.as_ref().map(|r| r.start).unwrap_or(self.cursor_pos);
|
||||||
|
let delete_range = range.unwrap_or(self.cursor_pos..self.cursor_pos);
|
||||||
|
|
||||||
|
self.value.replace_range(delete_range.clone(), text);
|
||||||
|
self.cursor_pos = insert_pos + text.len();
|
||||||
|
self.clear_selection();
|
||||||
|
|
||||||
|
if let Some(on_change) = self.on_change.clone() {
|
||||||
|
on_change(&self.value, cx);
|
||||||
|
}
|
||||||
|
self.refresh_suggestions(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text(&self) -> Option<&str> {
|
||||||
|
self.selection_range().map(|range| &self.value[range])
|
||||||
|
}
|
||||||
|
|
||||||
fn word_start_before(&self, pos: usize) -> usize {
|
fn word_start_before(&self, pos: usize) -> usize {
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -167,26 +235,46 @@ impl Input {
|
|||||||
i
|
i
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_left(&mut self) {
|
fn move_left(&mut self, cx: &mut gpui::Context<Self>, clear_selection: bool) {
|
||||||
if self.cursor_pos > 0 {
|
if self.cursor_pos > 0 {
|
||||||
let mut new_pos = self.cursor_pos - 1;
|
let mut new_pos = self.cursor_pos - 1;
|
||||||
while new_pos > 0 && !self.value.is_char_boundary(new_pos) {
|
while new_pos > 0 && !self.value.is_char_boundary(new_pos) {
|
||||||
new_pos -= 1;
|
new_pos -= 1;
|
||||||
}
|
}
|
||||||
self.cursor_pos = new_pos;
|
self.cursor_pos = new_pos;
|
||||||
|
if clear_selection {
|
||||||
|
self.clear_selection();
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_right(&mut self) {
|
fn move_right(&mut self, cx: &mut gpui::Context<Self>, clear_selection: bool) {
|
||||||
if self.cursor_pos < self.value.len() {
|
if self.cursor_pos < self.value.len() {
|
||||||
let mut new_pos = self.cursor_pos + 1;
|
let mut new_pos = self.cursor_pos + 1;
|
||||||
while new_pos < self.value.len() && !self.value.is_char_boundary(new_pos) {
|
while new_pos < self.value.len() && !self.value.is_char_boundary(new_pos) {
|
||||||
new_pos += 1;
|
new_pos += 1;
|
||||||
}
|
}
|
||||||
self.cursor_pos = new_pos;
|
self.cursor_pos = new_pos;
|
||||||
|
if clear_selection {
|
||||||
|
self.clear_selection();
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_word_left(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.cursor_pos = self.word_start_before(self.cursor_pos);
|
||||||
|
self.clear_selection();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_word_right(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.cursor_pos = self.word_end_after(self.cursor_pos);
|
||||||
|
self.clear_selection();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
fn refresh_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
if let Some(suggest) = &self.suggest {
|
if let Some(suggest) = &self.suggest {
|
||||||
self.suggestions = suggest(&self.value);
|
self.suggestions = suggest(&self.value);
|
||||||
@@ -271,23 +359,20 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn insert_text(&mut self, text: &str, cx: &mut gpui::Context<Self>) {
|
fn insert_text(&mut self, text: &str, cx: &mut gpui::Context<Self>) {
|
||||||
self.value.insert_str(self.cursor_pos, text);
|
self.replace_selection(text, cx);
|
||||||
self.cursor_pos += text.len();
|
|
||||||
|
|
||||||
if let Some(on_change) = self.on_change.clone() {
|
|
||||||
on_change(&self.value, cx);
|
|
||||||
}
|
|
||||||
self.refresh_suggestions(cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_backward(&mut self, cx: &mut gpui::Context<Self>) {
|
fn delete_backward(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
if self.has_selection() {
|
||||||
|
self.delete_selection(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.cursor_pos == 0 {
|
if self.cursor_pos == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_pos = self.cursor_pos;
|
let old_pos = self.cursor_pos;
|
||||||
self.move_left();
|
self.move_left(cx, false);
|
||||||
self.value.drain(self.cursor_pos..old_pos);
|
self.value.drain(self.cursor_pos..old_pos);
|
||||||
|
|
||||||
if let Some(on_change) = self.on_change.clone() {
|
if let Some(on_change) = self.on_change.clone() {
|
||||||
@@ -298,6 +383,10 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn delete_forward(&mut self, cx: &mut gpui::Context<Self>) {
|
fn delete_forward(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
if self.has_selection() {
|
||||||
|
self.delete_selection(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.cursor_pos >= self.value.len() {
|
if self.cursor_pos >= self.value.len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -316,6 +405,10 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn delete_word_backward(&mut self, cx: &mut gpui::Context<Self>) {
|
fn delete_word_backward(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
if self.has_selection() {
|
||||||
|
self.delete_selection(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.cursor_pos == 0 {
|
if self.cursor_pos == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,6 +425,10 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn delete_word_forward(&mut self, cx: &mut gpui::Context<Self>) {
|
fn delete_word_forward(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
if self.has_selection() {
|
||||||
|
self.delete_selection(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.cursor_pos >= self.value.len() {
|
if self.cursor_pos >= self.value.len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -381,6 +478,43 @@ impl Input {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctrl {
|
||||||
|
match key {
|
||||||
|
"a" => {
|
||||||
|
self.select_all();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"c" => {
|
||||||
|
if let Some(text) = self.selected_text() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"x" => {
|
||||||
|
if let Some(text) = self.selected_text() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
|
||||||
|
self.delete_selection(cx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"v" => {
|
||||||
|
if let Some(item) = cx.read_from_clipboard() {
|
||||||
|
if let Some(text) = item.text() {
|
||||||
|
let sanitized = if !self.multiline {
|
||||||
|
text.replace('\n', " ").replace("\r\n", " ")
|
||||||
|
} else {
|
||||||
|
text.replace("\r\n", "\n")
|
||||||
|
};
|
||||||
|
self.replace_selection(&sanitized, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"enter" => {
|
"enter" => {
|
||||||
// For multiline, Enter inserts newline (modal handles exit via commands)
|
// For multiline, Enter inserts newline (modal handles exit via commands)
|
||||||
@@ -433,32 +567,59 @@ impl Input {
|
|||||||
|
|
||||||
"left" => {
|
"left" => {
|
||||||
if ctrl {
|
if ctrl {
|
||||||
self.cursor_pos = self.word_start_before(self.cursor_pos);
|
self.move_word_left(cx);
|
||||||
cx.notify();
|
} else if shift {
|
||||||
|
if self.selection_anchor.is_none() {
|
||||||
|
self.selection_anchor = Some(self.cursor_pos);
|
||||||
|
}
|
||||||
|
self.move_left(cx, false);
|
||||||
} else {
|
} else {
|
||||||
self.move_left();
|
self.move_left(cx, true);
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"right" => {
|
"right" => {
|
||||||
if ctrl {
|
if ctrl {
|
||||||
self.cursor_pos = self.word_end_after(self.cursor_pos);
|
self.move_word_right(cx);
|
||||||
cx.notify();
|
} else if shift {
|
||||||
|
if self.selection_anchor.is_none() {
|
||||||
|
self.selection_anchor = Some(self.cursor_pos);
|
||||||
|
}
|
||||||
|
self.move_right(cx, false);
|
||||||
} else {
|
} else {
|
||||||
self.move_right();
|
self.move_right(cx, true);
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"home" => {
|
"home" => {
|
||||||
self.cursor_pos = 0;
|
if shift && self.selection_anchor.is_none() {
|
||||||
cx.notify();
|
self.selection_anchor = Some(self.cursor_pos);
|
||||||
|
self.cursor_pos = 0;
|
||||||
|
cx.notify();
|
||||||
|
} else if shift {
|
||||||
|
self.cursor_pos = 0;
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.cursor_pos = 0;
|
||||||
|
self.clear_selection();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"end" => {
|
"end" => {
|
||||||
self.cursor_pos = self.value.len();
|
let len = self.value.len();
|
||||||
cx.notify();
|
if shift && self.selection_anchor.is_none() {
|
||||||
|
self.selection_anchor = Some(self.cursor_pos);
|
||||||
|
self.cursor_pos = len;
|
||||||
|
cx.notify();
|
||||||
|
} else if shift {
|
||||||
|
self.cursor_pos = len;
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.cursor_pos = len;
|
||||||
|
self.clear_selection();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"backspace" => {
|
"backspace" => {
|
||||||
@@ -481,11 +642,6 @@ impl Input {
|
|||||||
self.delete_word_backward(cx);
|
self.delete_word_backward(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
"a" if ctrl => {
|
|
||||||
self.cursor_pos = 0;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
"e" if ctrl => {
|
"e" if ctrl => {
|
||||||
self.cursor_pos = self.value.len();
|
self.cursor_pos = self.value.len();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -729,22 +885,89 @@ impl gpui::Render for Input {
|
|||||||
gpui::div().id(self.id.clone()).into_any_element()
|
gpui::div().id(self.id.clone()).into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let content = if self.has_selection() {
|
||||||
|
let range = self.selection_range().unwrap();
|
||||||
|
let cursor_at_end = self.cursor_pos >= range.end;
|
||||||
|
|
||||||
|
let before_sel = &self.value[..range.start];
|
||||||
|
let selected_text = &self.value[range.clone()];
|
||||||
|
let after_sel = &self.value[range.end..];
|
||||||
|
|
||||||
|
let cursor = if is_focused {
|
||||||
|
gpui::div().w_px().h_4().bg(theme.accent).into_any_element()
|
||||||
|
} else {
|
||||||
|
gpui::div().into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
|
if cursor_at_end {
|
||||||
|
gpui::div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(before_sel.to_string()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.bg(theme.selection)
|
||||||
|
.text_color(theme.selection_foreground)
|
||||||
|
.child(selected_text.to_string()),
|
||||||
|
)
|
||||||
|
.child(cursor)
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(after_sel.to_string()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
gpui::div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(before_sel.to_string()),
|
||||||
|
)
|
||||||
|
.child(cursor)
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.bg(theme.selection)
|
||||||
|
.text_color(theme.selection_foreground)
|
||||||
|
.child(selected_text.to_string()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(after_sel.to_string()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gpui::div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(before.to_string()),
|
||||||
|
)
|
||||||
|
.child(cursor)
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(after.to_string()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
};
|
||||||
|
|
||||||
gpui::div()
|
gpui::div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.flex()
|
.child(content)
|
||||||
.flex_row()
|
|
||||||
.items_center()
|
|
||||||
.child(
|
|
||||||
gpui::div()
|
|
||||||
.text_color(theme.foreground)
|
|
||||||
.child(before.to_string()),
|
|
||||||
)
|
|
||||||
.child(cursor)
|
|
||||||
.child(
|
|
||||||
gpui::div()
|
|
||||||
.text_color(theme.foreground)
|
|
||||||
.child(after.to_string()),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+43
-46
@@ -401,14 +401,18 @@ impl App {
|
|||||||
annotations_add,
|
annotations_add,
|
||||||
annotations_delete,
|
annotations_delete,
|
||||||
} = update;
|
} = update;
|
||||||
|
|
||||||
|
let has_description = description.is_some();
|
||||||
|
let has_project = project.is_some();
|
||||||
|
let has_priority = priority.is_some();
|
||||||
|
let has_due = due.is_some();
|
||||||
|
let has_tags = tags.is_some();
|
||||||
|
let has_status = status.is_some();
|
||||||
|
let has_annotations = !annotations_add.is_empty() || !annotations_delete.is_empty();
|
||||||
|
|
||||||
let mut latest_task: Option<task::Task> = None;
|
let mut latest_task: Option<task::Task> = None;
|
||||||
|
|
||||||
if description.is_some()
|
if has_description || has_project || has_priority || has_tags || has_due {
|
||||||
|| project.is_some()
|
|
||||||
|| priority.is_some()
|
|
||||||
|| tags.is_some()
|
|
||||||
|| due.is_some()
|
|
||||||
{
|
|
||||||
match self.task_service.update_task(
|
match self.task_service.update_task(
|
||||||
task_id,
|
task_id,
|
||||||
description,
|
description,
|
||||||
@@ -433,39 +437,22 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(status) = status {
|
if has_status {
|
||||||
let status_result = match status {
|
match status {
|
||||||
task::TaskStatus::Completed => self.task_service.complete_task(task_id),
|
Some(task::TaskStatus::Completed) => {
|
||||||
task::TaskStatus::Pending => self.task_service.reopen_task(task_id),
|
latest_task = self.task_service.complete_task(task_id).ok()
|
||||||
task::TaskStatus::Deleted => {
|
|
||||||
self.task_service.delete_task(task_id).and_then(|_| {
|
|
||||||
self.task_service
|
|
||||||
.get_task(task_id)
|
|
||||||
.and_then(|task| task.ok_or(task::TaskError::NotFound(task_id)))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
_ => self
|
Some(task::TaskStatus::Pending) => {
|
||||||
.task_service
|
latest_task = self.task_service.reopen_task(task_id).ok()
|
||||||
.get_task(task_id)
|
|
||||||
.and_then(|task| task.ok_or(task::TaskError::NotFound(task_id))),
|
|
||||||
};
|
|
||||||
|
|
||||||
match status_result {
|
|
||||||
Ok(task) => latest_task = Some(task),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("[App] Failed to update status: {}", e);
|
|
||||||
self.toast_host.update(cx, |host, cx| {
|
|
||||||
host.push(
|
|
||||||
ToastKind::Error,
|
|
||||||
format!("Failed to update status: {}", e),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if let Some(task) = latest_task {
|
|
||||||
self.apply_task_update(task, cx);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
Some(task::TaskStatus::Deleted) => {
|
||||||
|
let _ = self.task_service.delete_task(task_id);
|
||||||
|
latest_task = self.task_service.get_task(task_id).ok().flatten();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
latest_task = self.task_service.get_task(task_id).ok().flatten();
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,10 +468,6 @@ impl App {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if let Some(task) = latest_task {
|
|
||||||
self.sync_task_detail(task, cx);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,15 +484,29 @@ impl App {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if let Some(task) = latest_task {
|
|
||||||
self.sync_task_detail(task, cx);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(task) = latest_task {
|
let task_to_sync = if latest_task.is_some() {
|
||||||
|
latest_task.take()
|
||||||
|
} else if has_description
|
||||||
|
|| has_project
|
||||||
|
|| has_priority
|
||||||
|
|| has_tags
|
||||||
|
|| has_due
|
||||||
|
|| has_status
|
||||||
|
|| has_annotations
|
||||||
|
{
|
||||||
|
self.task_service.get_task(task_id).ok().flatten()
|
||||||
|
} else {
|
||||||
|
self.task_detail_modal.update(cx, |modal, cx| {
|
||||||
|
modal.cancel_edit(None, cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(task) = task_to_sync {
|
||||||
self.apply_task_update(task, cx);
|
self.apply_task_update(task, cx);
|
||||||
} else {
|
} else {
|
||||||
self.task_detail_modal.update(cx, |modal, cx| {
|
self.task_detail_modal.update(cx, |modal, cx| {
|
||||||
|
|||||||
+6
-4
@@ -610,10 +610,12 @@ impl TaskService {
|
|||||||
.map_err(|e| TaskError::Storage(e.to_string()))?
|
.map_err(|e| TaskError::Storage(e.to_string()))?
|
||||||
.ok_or(TaskError::NotFound(uuid))?;
|
.ok_or(TaskError::NotFound(uuid))?;
|
||||||
|
|
||||||
let annotation = taskchampion::Annotation {
|
let mut entry = Utc::now();
|
||||||
entry: Utc::now(),
|
while tc_task.get_annotations().any(|a| a.entry == entry) {
|
||||||
description,
|
entry = entry + chrono::Duration::nanoseconds(1);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
let annotation = taskchampion::Annotation { entry, description };
|
||||||
|
|
||||||
tc_task
|
tc_task
|
||||||
.add_annotation(annotation, &mut ops)
|
.add_annotation(annotation, &mut ops)
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ pub(super) enum AnnotationOrigin {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn annotation_id(entry: DateTime<Utc>, text: &str, index: usize) -> AnnotationId {
|
fn annotation_id(entry: DateTime<Utc>, text: &str) -> AnnotationId {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
entry.timestamp_millis().hash(&mut hasher);
|
entry.timestamp_millis().hash(&mut hasher);
|
||||||
text.hash(&mut hasher);
|
text.hash(&mut hasher);
|
||||||
index.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +53,8 @@ impl AnnotationState {
|
|||||||
let items: Vec<AnnotationView> = detail
|
let items: Vec<AnnotationView> = detail
|
||||||
.annotations
|
.annotations
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|annotation| AnnotationView {
|
||||||
.map(|(index, annotation)| AnnotationView {
|
id: annotation_id(annotation.entry, &annotation.content),
|
||||||
id: annotation_id(annotation.entry, &annotation.content, index),
|
|
||||||
created_at: annotation.entry,
|
created_at: annotation.entry,
|
||||||
text: annotation.content.clone().into(),
|
text: annotation.content.clone().into(),
|
||||||
origin: AnnotationOrigin::Original,
|
origin: AnnotationOrigin::Original,
|
||||||
@@ -78,7 +76,7 @@ impl AnnotationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn add_local(&mut self, text: SharedString, created_at: DateTime<Utc>) {
|
pub(super) fn add_local(&mut self, text: SharedString, created_at: DateTime<Utc>) {
|
||||||
let id = annotation_id(created_at, text.as_ref(), self.items.len());
|
let id = annotation_id(created_at, text.as_ref());
|
||||||
self.items.push(AnnotationView {
|
self.items.push(AnnotationView {
|
||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
|
|||||||
@@ -877,6 +877,8 @@ impl TaskDetailModal {
|
|||||||
self.entities.annotation_input.update(cx, |input, cx| {
|
self.entities.annotation_input.update(cx, |input, cx| {
|
||||||
input.clear(cx);
|
input.clear(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.state.annotation_selected = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1334,6 +1336,10 @@ impl TaskDetailModal {
|
|||||||
|
|
||||||
match self.state.modal_focus {
|
match self.state.modal_focus {
|
||||||
ModalFocus::TagsInput => {
|
ModalFocus::TagsInput => {
|
||||||
|
self.entities.tags_input.update(cx, |input, cx| {
|
||||||
|
input.close_suggestions(cx);
|
||||||
|
});
|
||||||
|
|
||||||
self.sync_form_from_inputs(cx);
|
self.sync_form_from_inputs(cx);
|
||||||
|
|
||||||
if let Some(InlineEditTarget::Tag(i)) = self.state.inline_edit {
|
if let Some(InlineEditTarget::Tag(i)) = self.state.inline_edit {
|
||||||
@@ -1581,7 +1587,6 @@ impl TaskDetailModal {
|
|||||||
ConfirmAction::DeleteTag { index, .. } => {
|
ConfirmAction::DeleteTag { index, .. } => {
|
||||||
if index < self.state.form.tags.len() {
|
if index < self.state.form.tags.len() {
|
||||||
self.state.form.tags.remove(index);
|
self.state.form.tags.remove(index);
|
||||||
// Adjust selection
|
|
||||||
if self.state.form.tags.is_empty() {
|
if self.state.form.tags.is_empty() {
|
||||||
self.state.tag_selected = None;
|
self.state.tag_selected = None;
|
||||||
} else if let Some(sel) = self.state.tag_selected {
|
} else if let Some(sel) = self.state.tag_selected {
|
||||||
@@ -1710,16 +1715,6 @@ impl TaskDetailModal {
|
|||||||
|
|
||||||
match ann.origin {
|
match ann.origin {
|
||||||
AnnotationOrigin::Added => {
|
AnnotationOrigin::Added => {
|
||||||
self.state.inline_edit =
|
|
||||||
Some(InlineEditTarget::Annotation(actual_index));
|
|
||||||
|
|
||||||
let ann_value = ann.text.to_string();
|
|
||||||
self.entities.annotation_input.update(cx, |input, cx| {
|
|
||||||
input.set_value(ann_value, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.enter_edit_field(window, cx);
|
|
||||||
|
|
||||||
return CommandResult::Handled;
|
return CommandResult::Handled;
|
||||||
}
|
}
|
||||||
AnnotationOrigin::Original => {
|
AnnotationOrigin::Original => {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub(super) fn render_task_detail_modal(
|
|||||||
tags_input,
|
tags_input,
|
||||||
status_dropdown,
|
status_dropdown,
|
||||||
priority_dropdown,
|
priority_dropdown,
|
||||||
|
focus_handle,
|
||||||
form_focus_handle,
|
form_focus_handle,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
&theme,
|
&theme,
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ pub(super) fn render_task_detail_panel<OnCloseClick>(
|
|||||||
tags_input: &gpui::Entity<Input>,
|
tags_input: &gpui::Entity<Input>,
|
||||||
status_dropdown: &gpui::Entity<Dropdown>,
|
status_dropdown: &gpui::Entity<Dropdown>,
|
||||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||||
|
focus_handle: &gpui::FocusHandle,
|
||||||
form_focus_handle: &gpui::FocusHandle,
|
form_focus_handle: &gpui::FocusHandle,
|
||||||
scroll_handle: &gpui::ScrollHandle,
|
scroll_handle: &gpui::ScrollHandle,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
@@ -300,7 +301,11 @@ where
|
|||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.track_scroll(scroll_handle)
|
.track_scroll(scroll_handle)
|
||||||
.track_focus(form_focus_handle)
|
.track_focus(if mode == ModalMode::View {
|
||||||
|
focus_handle
|
||||||
|
} else {
|
||||||
|
form_focus_handle
|
||||||
|
})
|
||||||
.px(gpui::rems(1.0))
|
.px(gpui::rems(1.0))
|
||||||
.py(gpui::rems(0.75))
|
.py(gpui::rems(0.75))
|
||||||
.gap_4()
|
.gap_4()
|
||||||
@@ -678,6 +683,7 @@ fn render_tags_section(
|
|||||||
|
|
||||||
gpui::div()
|
gpui::div()
|
||||||
.flex()
|
.flex()
|
||||||
|
.flex_wrap()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(chips)
|
.children(chips)
|
||||||
@@ -694,6 +700,7 @@ fn render_tags_section(
|
|||||||
|
|
||||||
gpui::div()
|
gpui::div()
|
||||||
.flex()
|
.flex()
|
||||||
|
.flex_wrap()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(chips)
|
.children(chips)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -711,7 +718,14 @@ fn render_tags_section(
|
|||||||
.custom(Theme::alpha(theme.muted, 0.2), theme.muted)
|
.custom(Theme::alpha(theme.muted, 0.2), theme.muted)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
});
|
});
|
||||||
div.child(gpui::div().flex().gap_2().children(vchips).text_sm())
|
div.child(
|
||||||
|
gpui::div()
|
||||||
|
.flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_2()
|
||||||
|
.children(vchips)
|
||||||
|
.text_sm(),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ impl Default for SortState {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
column: SortColumn::Priority,
|
column: SortColumn::Priority,
|
||||||
direction: SortDirection::Desc,
|
direction: SortDirection::Asc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user