Skip to content

Commit

Permalink
todo_list: Add option for modal to create todo-lists.
Browse files Browse the repository at this point in the history
Previously, the `/todo` slash command was the sole method for
creating todos. Now, a button has been introduced to launch a modal
for creating todo-lists directly from the compose box.
This button becomes enabled only when the compose box is empty,
thereby avoiding complexities associated with losing or having to
save drafts of any messages already being composed.

The modal features a form that, upon submission,
generates a message using the `/todo` syntax and the data
inputted in the form. Subsequently, the content of the compose box
is set to this message, which the user can then send.

This modal closely parallels the UI for adding a poll; therefore,
the poll and todo code has been shifted to a newly created
file named `widget.ts`, and `poll_modal.ts` is now deprecated.

Fixes: #29779.
  • Loading branch information
sujalshah-bit committed May 1, 2024
1 parent 5a4c2c6 commit 8d46022
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 46 deletions.
26 changes: 26 additions & 0 deletions help/collaborative-to-do-lists.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@

{start_tabs}

{tab|via-compose-box-buttons}

{!start-composing.md!}

1. Make sure the compose box is empty.

1. Click the **Add to-do list** (<i class="zulip-icon zulip-icon-todo-list"></i>) icon at
the bottom of the compose box.

1. Fill out todo-list information as desired, and click **Create to-do list** to insert todo-list
formatting.

1. Click the **Send** (<i class="zulip-icon zulip-icon-send"></i>) button, or
use a [keyboard
shortcut](/help/mastering-the-compose-box#toggle-between-ctrl-enter-and-enter-to-send-a-message)
to send your message.

!!! tip ""

To reorder the list of todos, click and drag the **vertical dots**
(<i class="zulip-icon zulip-icon-grip-vertical"></i>) to the left of each
option. To delete an option, click the **trash**
(<i class="fa fa-trash-o"></i>) icon to the right of it.

{tab|via-markdown}

{!start-composing.md!}

1. Make sure the compose box is empty.
Expand Down
2 changes: 1 addition & 1 deletion tools/test-js-with-node
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ EXEMPT_FILES = make_set(
"web/src/plotly.js.d.ts",
"web/src/pm_list.ts",
"web/src/pm_list_dom.ts",
"web/src/poll_modal.ts",
"web/src/poll_widget.ts",
"web/src/popover_menus.ts",
"web/src/popover_menus_data.js",
Expand Down Expand Up @@ -287,6 +286,7 @@ EXEMPT_FILES = make_set(
"web/src/user_topics.ts",
"web/src/user_topics_ui.js",
"web/src/views_util.ts",
"web/src/widget_modal.ts",
"web/src/zcommand.ts",
"web/src/zform.js",
"web/src/zulip.js",
Expand Down
1 change: 1 addition & 0 deletions web/shared/icons/todo-list.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion web/src/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export function clear_compose_box() {
compose_banner.clear_uploads();
compose_ui.hide_compose_spinner();
scheduled_messages.reset_selected_schedule_timestamp();
$(".compose_control_button_container:has(.add-poll)").removeClass("disabled-on-hover");
$(".compose_control_button_container:has(.needs-empty-compose)").removeClass(
"disabled-on-hover",
);
}

export function send_message_success(request, data) {
Expand Down
8 changes: 6 additions & 2 deletions web/src/compose_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ function clear_box(): void {
compose_banner.clear_errors();
compose_banner.clear_warnings();
compose_banner.clear_uploads();
$(".compose_control_button_container:has(.add-poll)").removeClass("disabled-on-hover");
$(".compose_control_button_container:has(.needs-empty-compose)").removeClass(
"disabled-on-hover",
);
}

let autosize_callback_opts: ComposeActionsStartOpts;
Expand Down Expand Up @@ -326,7 +328,9 @@ export function start(raw_opts: ComposeActionsStartOpts): void {

if (opts.content !== undefined) {
compose_ui.insert_and_scroll_into_view(opts.content, $("textarea#compose-textarea"), true);
$(".compose_control_button_container:has(.add-poll)").addClass("disabled-on-hover");
$(".compose_control_button_container:has(.needs-empty-compose)").addClass(
"disabled-on-hover",
);
// If we were provided with message content, we might need to
// display that it's too long.
compose_validate.check_overflow_text();
Expand Down
38 changes: 33 additions & 5 deletions web/src/compose_setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import $ from "jquery";

import {unresolve_name} from "../shared/src/resolved_topic";
import render_add_poll_modal from "../templates/add_poll_modal.hbs";
import render_add_todo_list_modal from "../templates/add_todo_list_modal.hbs";

import * as compose from "./compose";
import * as compose_actions from "./compose_actions";
Expand All @@ -19,7 +20,6 @@ import {$t_html} from "./i18n";
import * as message_edit from "./message_edit";
import * as narrow from "./narrow";
import {page_params} from "./page_params";
import * as poll_modal from "./poll_modal";
import * as popovers from "./popovers";
import * as resize from "./resize";
import * as rows from "./rows";
Expand All @@ -32,6 +32,7 @@ import {get_timestamp_for_flatpickr} from "./timerender";
import * as ui_report from "./ui_report";
import * as upload from "./upload";
import * as user_topics from "./user_topics";
import * as widget_modal from "./widget_modal";

export function abort_xhr() {
$("#compose-send-button").prop("disabled", false);
Expand Down Expand Up @@ -84,9 +85,9 @@ export function initialize() {

// The poll widget requires an empty compose box.
if (compose_text_length > 0) {
$(".add-poll").parent().addClass("disabled-on-hover");
$(".needs-empty-compose").parent().addClass("disabled-on-hover");
} else {
$(".add-poll").parent().removeClass("disabled-on-hover");
$(".needs-empty-compose").parent().removeClass("disabled-on-hover");
}
});

Expand Down Expand Up @@ -390,17 +391,44 @@ export function initialize() {
// frame a message using data input in modal, then populate the compose textarea with it
e.preventDefault();
e.stopPropagation();
const poll_message_content = poll_modal.frame_poll_message_content();
const poll_message_content = widget_modal.frame_poll_message_content();
compose_ui.insert_syntax_and_focus(poll_message_content);
},
validate_input,
form_id: "add-poll-form",
id: "add-poll-modal",
post_render: poll_modal.poll_options_setup,
post_render: widget_modal.poll_options_setup,
help_link: "https://zulip.com/help/create-a-poll",
});
});

$("body").on(
"click",
".compose_control_button_container:not(.disabled) .add-todo-list",
(e) => {
e.preventDefault();
e.stopPropagation();

dialog_widget.launch({
html_heading: $t_html({defaultMessage: "Create a collaborative to-do list"}),
html_body: render_add_todo_list_modal(),
html_submit_button: $t_html({defaultMessage: "Create to-do list"}),
close_on_submit: true,
on_click(e) {
// frame a message using data input in modal, then populate the compose textarea with it
e.preventDefault();
e.stopPropagation();
const todo_message_content = widget_modal.frame_todo_message_content();
compose_ui.insert_syntax_and_focus(todo_message_content);
},
form_id: "add-todo-form",
id: "add-todo-modal",
post_render: widget_modal.todo_list_tasks_setup,
help_link: "https://zulip.com/help/collaborative-to-do-lists",
});
},
);

$("#compose").on("click", ".markdown_preview", (e) => {
e.preventDefault();
e.stopPropagation();
Expand Down
82 changes: 65 additions & 17 deletions web/src/poll_modal.ts → web/src/widget_modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ import $ from "jquery";
import SortableJS from "sortablejs";

import render_poll_modal_option from "../templates/poll_modal_option.hbs";
import render_todo_modal_task from "../templates/todo_modal_task.hbs";

function create_option_row($last_option_row_input: JQuery): void {
const row_html = render_poll_modal_option();
function setup_sortable_list(selector: string): void {
// setTimeout is needed to here to give time for simplebar to initialise
setTimeout(() => {
SortableJS.create($(selector + " .simplebar-content")[0], {
onUpdate() {
// Do nothing on drag; the order is only processed on submission.
},
// We don't want the last (empty) row to be draggable, as a new row
// is added on input event of the last row.
filter: "input, .option-row:last-child",
preventOnFilter: false,
});
}, 0);
}

function create_option_row(
$last_option_row_input: JQuery,
template: (context?: unknown) => string,
): void {
const row_html = template();
const $row_container = $last_option_row_input.closest(".simplebar-content");
$row_container.append($(row_html));
}

function add_option_row(e: JQuery.TriggeredEvent): void {
function add_option_row(e: JQuery.TriggeredEvent, widget_type: string): void {
// if the option triggering the input event e is not the last,
// that is, its next sibling has the class `option-row`, we
// do not add a new option row and return from this function
Expand All @@ -18,7 +37,9 @@ function add_option_row(e: JQuery.TriggeredEvent): void {
if ($(e.target).closest(".option-row").next().hasClass("option-row")) {
return;
}
create_option_row($(e.target));

const template = widget_type === "POLL" ? render_poll_modal_option : render_todo_modal_task;
create_option_row($(e.target), template);
}

function delete_option_row(e: JQuery.ClickEvent): void {
Expand All @@ -41,21 +62,12 @@ export function poll_options_setup(): void {
}
});

$poll_options_list.on("input", "input.poll-option-input", add_option_row);
$poll_options_list.on("input", "input.poll-option-input", (event) => {
add_option_row(event, "POLL");
});
$poll_options_list.on("click", "button.delete-option", delete_option_row);

// setTimeout is needed to here to give time for simplebar to initialise
setTimeout(() => {
SortableJS.create($("#add-poll-form .poll-options-list .simplebar-content")[0], {
onUpdate() {
// Do nothing on drag; the order is only processed on submission.
},
// We don't want the last (empty) row to be draggable, as a new row
// is added on input event of the last row.
filter: "input, .option-row:last-child",
preventOnFilter: false,
});
}, 0);
setup_sortable_list("#add-poll-form .poll-options-list");
}

export function frame_poll_message_content(): string {
Expand All @@ -68,3 +80,39 @@ export function frame_poll_message_content(): string {
.filter(Boolean);
return "/poll " + question + "\n" + options.join("\n");
}

export function todo_list_tasks_setup(): void {
const $todo_options_list = $("#add-todo-form .todo-options-list");
$todo_options_list.on("input", "input.todo-input", (event) => {
add_option_row(event, "TODO");
});
$todo_options_list.on("input", "input.todo-description-input", (event) => {
add_option_row(event, "TODO");
});
$todo_options_list.on("click", "button.delete-option", delete_option_row);

setup_sortable_list("#add-todo-form .todo-options-list");
}

export function frame_todo_message_content(): string {
const title = $<HTMLInputElement>("input#todo-title-input").val()?.toString().trim() ?? "";
const todo_str = title ? `/todo ${title}\n` : "/todo Task list\n";

const todos: string[] = [];

$(".option-row").each(function () {
const todo_name = $(this).find(".todo-input").val()?.toString().trim() ?? "";
const todo_description =
$(this).find(".todo-description-input").val()?.toString().trim() ?? "";

if (todo_name || todo_description) {
const todo =
todo_name && todo_description
? `${todo_name}: ${todo_description}`
: todo_name || todo_description;
todos.push(todo);
}
});

return todo_str + todos.join("\n");
}
42 changes: 23 additions & 19 deletions web/styles/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@
}
}

#add-poll-modal {
#add-poll-modal,
#add-todo-modal {
/* this height allows 3-4 option rows
to fit in without need for scrolling */
height: 450px;
Expand All @@ -402,27 +403,32 @@
}
}

#add-poll-form {
#add-poll-form,
#add-todo-form {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;

.poll-label {
.poll-label,
.todo-label {
font-weight: bold;
margin: 5px 0;
}

.poll-question-input-container {
.poll-question-input-container,
.todo-title-input-container {
display: flex;
margin-bottom: 10px;

#poll-question-input {
#poll-question-input,
#todo-title-input {
flex-grow: 1;
}
}

.poll-options-list {
.poll-options-list,
.todo-options-list {
margin: 0;
height: 0;
overflow: auto;
Expand All @@ -441,24 +447,22 @@
color: hsl(0deg 0% 75%);
}

.poll-option-input {
.poll-option-input,
.todo-title-input {
flex-grow: 1;
}
}

.option-row:first-child {
margin-top: 0;
}

.option-row:last-child {
cursor: default;

.delete-option {
visibility: hidden;
&:first-child {
margin-top: 0;
}

.drag-icon {
visibility: hidden;
&:last-child {
cursor: default;

.delete-option,
.drag-icon {
visibility: hidden;
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions web/templates/add_todo_list_modal.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<form id="add-todo-form" class="new-style">
<label class="todo-label">{{t "To-do list title"}}</label>
<div class="todo-title-input-container">
<input type="text" id="todo-title-input" class="modal_text_input" placeholder="{{t 'Task list'}}" />
</div>
<label class="todo-label">{{t "Tasks"}}</label>
<p>{{t "Anyone can add more tasks after the to-do list is posted."}}</p>
<ul class="todo-options-list" data-simplebar>
{{> todo_modal_task }}
{{> todo_modal_task }}
{{> todo_modal_task }}
</ul>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
</div>
{{#unless message_id}}
<div class="compose_control_button_container preview_mode_disabled" data-tooltip-template-id="add-poll-tooltip" data-tippy-maxWidth="none">
<a role="button" class="compose_control_button zulip-icon zulip-icon-poll add-poll" aria-label="{{t 'Add poll' }}" tabindex=0></a>
<a role="button" class="compose_control_button zulip-icon zulip-icon-poll add-poll needs-empty-compose" aria-label="{{t 'Add poll' }}" tabindex=0></a>
</div>
<div class="compose_control_button_container preview_mode_disabled" data-tooltip-template-id="add-todo-tooltip" data-tippy-maxWidth="none">
<a role="button" class="compose_control_button zulip-icon zulip-icon-todo-list add-todo-list needs-empty-compose" aria-label="{{t 'Add to-do list' }}" tabindex=0></a>
</div>
{{/unless}}
<a role="button" class="compose_control_button compose_help_button zulip-icon zulip-icon-question" tabindex=0 data-tippy-content="{{t 'Message formatting' }}" data-overlay-trigger="message-formatting"></a>

0 comments on commit 8d46022

Please sign in to comment.