mirror of https://github.com/zulip/zulip.git
todo_widget: Allow tasks to be added through `/todo` command.
Uptil now, users could add tasks to a todo widget only after creating it through the `/todo` command in the compose box. Users can now add an initial list of tasks using the `/todo` command, with each task on a new line in the compose box, where the 1st `:` would separate a task from its (optional) description. Example: `/todo\nTask1:description1\nTask2 without description`. Fixes part of #20213.
This commit is contained in:
parent
6df3ad251a
commit
d8a8364d1a
|
@ -32,6 +32,7 @@ const poll_widget_extra_data_schema = z
|
||||||
export const todo_widget_extra_data_schema = z
|
export const todo_widget_extra_data_schema = z
|
||||||
.object({
|
.object({
|
||||||
task_list_title: z.string().optional(),
|
task_list_title: z.string().optional(),
|
||||||
|
tasks: z.array(z.object({task: z.string(), desc: z.string()})).optional(),
|
||||||
})
|
})
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ export class TaskData {
|
||||||
current_user_id,
|
current_user_id,
|
||||||
is_my_task_list,
|
is_my_task_list,
|
||||||
task_list_title,
|
task_list_title,
|
||||||
|
tasks,
|
||||||
report_error_function,
|
report_error_function,
|
||||||
}) {
|
}) {
|
||||||
this.message_sender_id = message_sender_id;
|
this.message_sender_id = message_sender_id;
|
||||||
|
@ -36,6 +37,14 @@ export class TaskData {
|
||||||
} else {
|
} else {
|
||||||
this.set_task_list_title($t({defaultMessage: "Task list"}));
|
this.set_task_list_title($t({defaultMessage: "Task list"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [i, data] of tasks.entries()) {
|
||||||
|
this.handle.new_task.inbound("canned", {
|
||||||
|
key: i,
|
||||||
|
task: data.task,
|
||||||
|
desc: data.desc,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_task_list_title(new_title) {
|
set_task_list_title(new_title) {
|
||||||
|
@ -219,13 +228,14 @@ export function activate({$elem, callback, extra_data, message}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {data} = parse_result;
|
const {data} = parse_result;
|
||||||
const {task_list_title = ""} = data || {};
|
const {task_list_title = "", tasks = []} = data || {};
|
||||||
const is_my_task_list = people.is_my_user_id(message.sender_id);
|
const is_my_task_list = people.is_my_user_id(message.sender_id);
|
||||||
const task_data = new TaskData({
|
const task_data = new TaskData({
|
||||||
message_sender_id: message.sender_id,
|
message_sender_id: message.sender_id,
|
||||||
current_user_id: people.my_current_user_id(),
|
current_user_id: people.my_current_user_id(),
|
||||||
is_my_task_list,
|
is_my_task_list,
|
||||||
task_list_title,
|
task_list_title,
|
||||||
|
tasks,
|
||||||
report_error_function: blueslip.warn,
|
report_error_function: blueslip.warn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ type ZFormExtraData = {
|
||||||
// TODO: This TodoWidgetExtraData type should be moved to web/src/todo_widget.js when it will be migrated
|
// TODO: This TodoWidgetExtraData type should be moved to web/src/todo_widget.js when it will be migrated
|
||||||
type TodoWidgetExtraData = {
|
type TodoWidgetExtraData = {
|
||||||
task_list_title?: string;
|
task_list_title?: string;
|
||||||
|
tasks?: {task: string; desc: string}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type WidgetExtraData = PollWidgetExtraData | TodoWidgetExtraData | ZFormExtraData | null;
|
type WidgetExtraData = PollWidgetExtraData | TodoWidgetExtraData | ZFormExtraData | null;
|
||||||
|
|
|
@ -6,7 +6,7 @@ from zerver.lib.message import SendMessageRequest
|
||||||
from zerver.models import Message, SubMessage
|
from zerver.models import Message, SubMessage
|
||||||
|
|
||||||
|
|
||||||
def get_widget_data(content: str) -> Tuple[Optional[str], Optional[str]]:
|
def get_widget_data(content: str) -> Tuple[Optional[str], Any]:
|
||||||
valid_widget_types = ["poll", "todo"]
|
valid_widget_types = ["poll", "todo"]
|
||||||
tokens = re.split(r"\s+|\n+", content)
|
tokens = re.split(r"\s+|\n+", content)
|
||||||
|
|
||||||
|
@ -73,33 +73,9 @@ def parse_todo_extra_data(content: str) -> Any:
|
||||||
|
|
||||||
def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) -> Any:
|
def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) -> Any:
|
||||||
if widget_type == "poll":
|
if widget_type == "poll":
|
||||||
# This is used to extract the question from the poll command.
|
return parse_poll_extra_data(content)
|
||||||
# The command '/poll question' will pre-set the question in the poll
|
|
||||||
lines = content.splitlines()
|
|
||||||
question = ""
|
|
||||||
options = []
|
|
||||||
if lines and lines[0]:
|
|
||||||
question = lines.pop(0).strip()
|
|
||||||
for line in lines:
|
|
||||||
# If someone is using the list syntax, we remove it
|
|
||||||
# before adding an option.
|
|
||||||
option = re.sub(r"(\s*[-*]?\s*)", "", line.strip(), count=1)
|
|
||||||
if len(option) > 0:
|
|
||||||
options.append(option)
|
|
||||||
extra_data = {
|
|
||||||
"question": question,
|
|
||||||
"options": options,
|
|
||||||
}
|
|
||||||
return extra_data
|
|
||||||
else:
|
else:
|
||||||
# This is used to extract the task list title from the todo command.
|
return parse_todo_extra_data(content)
|
||||||
# The command '/todo Title' will pre-set the task list title
|
|
||||||
lines = content.splitlines()
|
|
||||||
task_list_title = ""
|
|
||||||
if lines and lines[0]:
|
|
||||||
task_list_title = lines.pop(0).strip()
|
|
||||||
extra_data = {"task_list_title": task_list_title}
|
|
||||||
return extra_data
|
|
||||||
|
|
||||||
|
|
||||||
def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
|
def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
|
||||||
|
|
|
@ -98,13 +98,17 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
self.assertEqual(get_widget_data(content=message), (None, None))
|
self.assertEqual(get_widget_data(content=message), (None, None))
|
||||||
|
|
||||||
# Add positive checks for context
|
# Add positive checks for context
|
||||||
self.assertEqual(get_widget_data(content="/todo"), ("todo", {"task_list_title": ""}))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
get_widget_data(content="/todo Title"), ("todo", {"task_list_title": "Title"})
|
get_widget_data(content="/todo"), ("todo", {"task_list_title": "", "tasks": []})
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_widget_data(content="/todo Title"),
|
||||||
|
("todo", {"task_list_title": "Title", "tasks": []}),
|
||||||
)
|
)
|
||||||
# Test tokenization on newline character
|
# Test tokenization on newline character
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
get_widget_data(content="/todo\nignore"), ("todo", {"task_list_title": ""})
|
get_widget_data(content="/todo\nTask"),
|
||||||
|
("todo", {"task_list_title": "", "tasks": [{"task": "Task", "desc": ""}]}),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_explicit_widget_content(self) -> None:
|
def test_explicit_widget_content(self) -> None:
|
||||||
|
@ -171,7 +175,7 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
|
|
||||||
expected_submessage_content = dict(
|
expected_submessage_content = dict(
|
||||||
widget_type="todo",
|
widget_type="todo",
|
||||||
extra_data={"task_list_title": ""},
|
extra_data={"task_list_title": "", "tasks": []},
|
||||||
)
|
)
|
||||||
|
|
||||||
submessage = SubMessage.objects.get(message_id=message.id)
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
@ -188,7 +192,42 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
|
|
||||||
expected_submessage_content = dict(
|
expected_submessage_content = dict(
|
||||||
widget_type="todo",
|
widget_type="todo",
|
||||||
extra_data={"task_list_title": "Example Task List Title"},
|
extra_data={"task_list_title": "Example Task List Title", "tasks": []},
|
||||||
|
)
|
||||||
|
|
||||||
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
self.assertEqual(submessage.msg_type, "widget")
|
||||||
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
||||||
|
|
||||||
|
# We test for both trailing and leading spaces, along with blank lines
|
||||||
|
# for the tasks.
|
||||||
|
content = "/todo Example Task List Title\n\n task without description\ntask: with description \n\n - task as list : also with description"
|
||||||
|
payload["content"] = content
|
||||||
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
message = self.get_last_message()
|
||||||
|
self.assertEqual(message.content, content)
|
||||||
|
|
||||||
|
expected_submessage_content = dict(
|
||||||
|
widget_type="todo",
|
||||||
|
extra_data=dict(
|
||||||
|
task_list_title="Example Task List Title",
|
||||||
|
tasks=[
|
||||||
|
dict(
|
||||||
|
task="task without description",
|
||||||
|
desc="",
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
task="task",
|
||||||
|
desc="with description",
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
task="task as list",
|
||||||
|
desc="also with description",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
submessage = SubMessage.objects.get(message_id=message.id)
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
@ -267,9 +306,7 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
|
|
||||||
expected_submessage_content = dict(
|
expected_submessage_content = dict(
|
||||||
widget_type="todo",
|
widget_type="todo",
|
||||||
extra_data=dict(
|
extra_data=dict(task_list_title="School Work", tasks=[]),
|
||||||
task_list_title="School Work",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
submessage = SubMessage.objects.get(message_id=message.id)
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
@ -283,10 +320,37 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
result = self.api_post(sender, "/api/v1/messages", payload)
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
expected_submessage_content = dict(
|
||||||
|
widget_type="todo",
|
||||||
|
extra_data=dict(task_list_title="", tasks=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
message = self.get_last_message()
|
||||||
|
self.assertEqual(message.content, content)
|
||||||
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
self.assertEqual(submessage.msg_type, "widget")
|
||||||
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
||||||
|
# Now supply both task list title and tasks.
|
||||||
|
|
||||||
|
content = "/todo School Work\nchemistry homework: assignment 2\nstudy for english test: pages 56-67"
|
||||||
|
payload["content"] = content
|
||||||
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
expected_submessage_content = dict(
|
expected_submessage_content = dict(
|
||||||
widget_type="todo",
|
widget_type="todo",
|
||||||
extra_data=dict(
|
extra_data=dict(
|
||||||
task_list_title="",
|
task_list_title="School Work",
|
||||||
|
tasks=[
|
||||||
|
dict(
|
||||||
|
task="chemistry homework",
|
||||||
|
desc="assignment 2",
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
task="study for english test",
|
||||||
|
desc="pages 56-67",
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue