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:
N-Shar-ma 2022-08-04 19:50:54 +05:30 committed by Tim Abbott
parent 6df3ad251a
commit d8a8364d1a
5 changed files with 89 additions and 37 deletions

View File

@ -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();

View File

@ -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,
}); });

View File

@ -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;

View File

@ -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:

View File

@ -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",
),
],
), ),
) )