mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
todo_list: Add option for modal to create todo-lists.
A button has been introduced to launch a modal for creating todo-lists directly from the compose box. 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_modal.ts`, and `poll_modal.ts` is now deprecated. Co-authored-by: Sujal Shah <sujalshah28092004@gmail.com> Fixes #29779.
This commit is contained in:
@@ -6,6 +6,31 @@
|
||||
|
||||
{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/configure-send-message-keys) 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.
|
||||
|
@@ -177,7 +177,6 @@ EXEMPT_FILES = make_set(
|
||||
"web/src/playground_links_popover.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.ts",
|
||||
@@ -291,6 +290,7 @@ EXEMPT_FILES = make_set(
|
||||
"web/src/user_topics.ts",
|
||||
"web/src/user_topics_ui.ts",
|
||||
"web/src/views_util.ts",
|
||||
"web/src/widget_modal.ts",
|
||||
"web/src/zcommand.ts",
|
||||
"web/src/zform.js",
|
||||
"web/src/zulip_test.ts",
|
||||
|
1
web/shared/icons/todo-list.svg
Normal file
1
web/shared/icons/todo-list.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none" viewBox="0 0 22 22"><path fill="#000" d="M8.28 5.47a.75.75 0 0 1 0 1.06L5.614 9.197a.75.75 0 0 1-1.061 0L3.22 7.864a.75.75 0 0 1 1.06-1.061l.803.803L7.22 5.47a.75.75 0 0 1 1.06 0ZM10.416 7h8a1 1 0 1 1 0 2h-8a1 1 0 0 1 0-2Zm0 7h8a1 1 0 1 1 0 2h-8a1 1 0 0 1 0-2Zm-2.136-.47-2.666 2.667a.75.75 0 0 1-1.061 0L3.22 14.864a.75.75 0 0 1 1.06-1.061l.803.803L7.22 12.47a.75.75 0 0 1 1.06 1.06Z"/></svg>
|
After Width: | Height: | Size: 468 B |
@@ -132,7 +132,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) {
|
||||
|
@@ -141,7 +141,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;
|
||||
@@ -388,7 +390,9 @@ export let start = (raw_opts: ComposeActionsStartOpts): void => {
|
||||
false,
|
||||
replace_all_without_undo_support,
|
||||
);
|
||||
$(".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($("#send_message_form"));
|
||||
|
@@ -3,6 +3,7 @@ import _ from "lodash";
|
||||
|
||||
import {unresolve_name} from "../shared/src/resolved_topic.ts";
|
||||
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.js";
|
||||
import * as compose_actions from "./compose_actions.ts";
|
||||
@@ -23,7 +24,6 @@ import * as message_view from "./message_view.ts";
|
||||
import * as narrow_state from "./narrow_state.ts";
|
||||
import * as onboarding_steps from "./onboarding_steps.ts";
|
||||
import {page_params} from "./page_params.ts";
|
||||
import * as poll_modal from "./poll_modal.ts";
|
||||
import * as popovers from "./popovers.ts";
|
||||
import * as resize from "./resize.ts";
|
||||
import * as rows from "./rows.ts";
|
||||
@@ -36,6 +36,7 @@ import {get_timestamp_for_flatpickr} from "./timerender.ts";
|
||||
import * as ui_report from "./ui_report.ts";
|
||||
import * as upload from "./upload.ts";
|
||||
import * as user_topics from "./user_topics.ts";
|
||||
import * as widget_modal from "./widget_modal.ts";
|
||||
|
||||
export function abort_xhr() {
|
||||
$("#compose-send-button").prop("disabled", false);
|
||||
@@ -88,9 +89,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");
|
||||
}
|
||||
|
||||
if (compose_state.get_is_content_unedited_restored_draft()) {
|
||||
@@ -449,7 +450,7 @@ 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);
|
||||
},
|
||||
on_show() {
|
||||
@@ -460,11 +461,74 @@ export function initialize() {
|
||||
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("input", "#add-todo-modal .todo-input", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(".option-row").each(function () {
|
||||
const todo_name = $(this).find(".todo-input").val();
|
||||
const $todo_description = $(this).find(".todo-description-input");
|
||||
$todo_description.prop("disabled", !todo_name);
|
||||
});
|
||||
});
|
||||
|
||||
$("body").on(
|
||||
"click",
|
||||
".compose_control_button_container:not(.disabled) .add-todo-list",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
function validate_input(e) {
|
||||
let is_valid = true;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(".option-row").each(function () {
|
||||
const todo_name = $(this).find(".todo-input").val();
|
||||
const todo_description = $(this).find(".todo-description-input").val();
|
||||
if (!todo_name && todo_description) {
|
||||
ui_report.error(
|
||||
$t_html({defaultMessage: "Please enter task title."}),
|
||||
undefined,
|
||||
$("#dialog_error"),
|
||||
);
|
||||
is_valid = false;
|
||||
}
|
||||
});
|
||||
return is_valid;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
on_show() {
|
||||
setTimeout(() => {
|
||||
$("#todo-title-input").trigger("select");
|
||||
}, 0);
|
||||
},
|
||||
form_id: "add-todo-form",
|
||||
validate_input,
|
||||
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();
|
||||
|
@@ -231,6 +231,29 @@ export function initialize(): void {
|
||||
},
|
||||
});
|
||||
|
||||
tippy.delegate("body", {
|
||||
target: "#add-todo-modal .todo-description-container",
|
||||
onShow(instance) {
|
||||
const $elem = $(instance.reference);
|
||||
|
||||
/* Due to height: 0, data-reference-hidden for tooltip is set on the tooltip and can
|
||||
cause the tooltip to hide. We should use .show-when-reference-hidden here too since we
|
||||
want data-reference-hidden to work when user scrolls here.*/
|
||||
$(instance.popper).find(".tippy-box").addClass("show-when-reference-hidden");
|
||||
|
||||
if ($elem.find(".todo-description-input").is(":disabled")) {
|
||||
instance.setContent(
|
||||
$t({
|
||||
defaultMessage: "Enter a task before adding a description.",
|
||||
}),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
appendTo: () => document.body,
|
||||
});
|
||||
|
||||
$("body").on(
|
||||
"blur",
|
||||
".message_control_button, .delete-selected-drafts-button-container",
|
||||
|
@@ -2,16 +2,20 @@ 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";
|
||||
|
||||
import * as util from "./util.ts";
|
||||
|
||||
function create_option_row($last_option_row_input: JQuery): void {
|
||||
const row_html = render_poll_modal_option();
|
||||
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(this: HTMLElement): void {
|
||||
function add_option_row(this: HTMLElement, 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
|
||||
@@ -20,7 +24,8 @@ function add_option_row(this: HTMLElement): void {
|
||||
if ($(this).closest(".option-row").next().hasClass("option-row")) {
|
||||
return;
|
||||
}
|
||||
create_option_row($(this));
|
||||
const template = widget_type === "POLL" ? render_poll_modal_option : render_todo_modal_task;
|
||||
create_option_row($(this), template);
|
||||
}
|
||||
|
||||
function delete_option_row(this: HTMLElement): void {
|
||||
@@ -28,6 +33,21 @@ function delete_option_row(this: HTMLElement): void {
|
||||
$row.remove();
|
||||
}
|
||||
|
||||
function setup_sortable_list(selector: string): void {
|
||||
// setTimeout is needed to here to give time for simplebar to initialise
|
||||
setTimeout(() => {
|
||||
SortableJS.create(util.the($($(selector + " .simplebar-content"))), {
|
||||
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);
|
||||
}
|
||||
|
||||
export function poll_options_setup(): void {
|
||||
const $poll_options_list = $("#add-poll-form .poll-options-list");
|
||||
const $submit_button = $("#add-poll-modal .dialog_submit_button");
|
||||
@@ -43,21 +63,22 @@ 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", function (this: HTMLElement) {
|
||||
add_option_row.call(this, "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(util.the($("#add-poll-form .poll-options-list .simplebar-content")), {
|
||||
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 todo_list_tasks_setup(): void {
|
||||
const $todo_options_list = $("#add-todo-form .todo-options-list");
|
||||
$todo_options_list.on("input", "input.todo-input", function (this: HTMLElement) {
|
||||
add_option_row.call(this, "TODO");
|
||||
});
|
||||
$todo_options_list.on("click", "button.delete-option", delete_option_row);
|
||||
|
||||
setup_sortable_list("#add-todo-form .todo-options-list");
|
||||
}
|
||||
|
||||
export function frame_poll_message_content(): string {
|
||||
@@ -70,3 +91,33 @@ export function frame_poll_message_content(): string {
|
||||
.filter(Boolean);
|
||||
return "/poll " + question + "\n" + options.join("\n");
|
||||
}
|
||||
|
||||
export function frame_todo_message_content(): string {
|
||||
let title = $<HTMLInputElement>("input#todo-title-input").val()?.trim();
|
||||
|
||||
if (title === "") {
|
||||
title = "Task list";
|
||||
}
|
||||
const todo_str = `/todo ${title}\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) {
|
||||
let todo = "";
|
||||
|
||||
if (todo_name && todo_description) {
|
||||
todo = `${todo_name}: ${todo_description}`;
|
||||
} else if (todo_name && !todo_description) {
|
||||
todo = todo_name;
|
||||
}
|
||||
todos.push(todo);
|
||||
}
|
||||
});
|
||||
|
||||
return todo_str + todos.join("\n");
|
||||
}
|
@@ -466,7 +466,8 @@
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
#add-poll-modal {
|
||||
#add-poll-modal,
|
||||
#add-todo-modal {
|
||||
/* this height allows 3-4 option rows
|
||||
to fit in without need for scrolling */
|
||||
height: 32.1428em; /* 450px / 14px em */
|
||||
@@ -481,27 +482,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;
|
||||
@@ -525,6 +531,22 @@
|
||||
margin-bottom: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.todo-input {
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: 0;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.todo-description-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
|
||||
.todo-description-input {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-row:first-child {
|
||||
@@ -543,6 +565,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $sm_min) {
|
||||
.option-row {
|
||||
.todo-input {
|
||||
width: 100%;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.todo-description-container {
|
||||
.todo-description-input {
|
||||
width: 100%;
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
13
web/templates/add_todo_list_modal.hbs
Normal file
13
web/templates/add_todo_list_modal.hbs
Normal file
@@ -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" autocomplete="off" value="{{t 'Task list' }}" class="modal_text_input" />
|
||||
</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 data-simplebar-tab-index="-1">
|
||||
{{> todo_modal_task }}
|
||||
{{> todo_modal_task }}
|
||||
{{> todo_modal_task }}
|
||||
</ul>
|
||||
</form>
|
@@ -56,7 +56,10 @@
|
||||
<div class="divider"></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>
|
||||
|
10
web/templates/todo_modal_task.hbs
Normal file
10
web/templates/todo_modal_task.hbs
Normal file
@@ -0,0 +1,10 @@
|
||||
<li class="option-row">
|
||||
<i class="zulip-icon zulip-icon-grip-vertical drag-icon"></i>
|
||||
<input type="text" class="todo-input modal_text_input" placeholder="{{t 'New task'}}" />
|
||||
<div class="todo-description-container">
|
||||
<input type="text" class="todo-description-input modal_text_input" disabled="true" placeholder="{{t 'Task description (optional)'}}" />
|
||||
</div>
|
||||
<button type="button" class="button rounded small btn-secondary delete-option" title="{{t 'Delete' }}">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>
|
@@ -68,6 +68,12 @@
|
||||
<span class="tooltip-inner-content italic">{{t "A poll must be an entire message." }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template id="add-todo-tooltip">
|
||||
<div>
|
||||
<span>{{t "Add to-do list" }}</span><br/>
|
||||
<span class="tooltip-inner-content italic">{{t "A to-do list must be an entire message." }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template id="add-saved-snippet-tooltip">
|
||||
{{t "Add saved snippet" }}
|
||||
{{tooltip_hotkey_hints "Ctrl" "'"}}
|
||||
|
Reference in New Issue
Block a user