mirror of
https://github.com/zulip/zulip.git
synced 2025-10-29 19:13:53 +00:00
custom-profile-fields: Fix handling manual date input.
This commit fixes the behavior when manually changing the date input instead of selecting from the picker. Changes done are- - Users can enter date in various formats including the one showed in the input like "June 20, 1999", "MM-DD-YYY", and basically the formats which can be parsed by "Date". - Fixed handling of invalid strings where we show "Invalid date value" error message for some time without sending the request to server. Fixes #19936.
This commit is contained in:
@@ -34,7 +34,7 @@ async function open_settings(page: Page): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function close_settings_and_date_picker(page: Page): Promise<void> {
|
async function close_settings_and_date_picker(page: Page): Promise<void> {
|
||||||
const date_picker_selector = ".custom_user_field_value.datepicker.form-control";
|
const date_picker_selector = ".date-field-alt-input";
|
||||||
await page.click(date_picker_selector);
|
await page.click(date_picker_selector);
|
||||||
|
|
||||||
await page.waitForSelector(".flatpickr-calendar", {visible: true});
|
await page.waitForSelector(".flatpickr-calendar", {visible: true});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import * as settings_components from "./settings_components.ts";
|
|||||||
import * as settings_ui from "./settings_ui.ts";
|
import * as settings_ui from "./settings_ui.ts";
|
||||||
import {current_user, realm} from "./state_data.ts";
|
import {current_user, realm} from "./state_data.ts";
|
||||||
import * as typeahead_helper from "./typeahead_helper.ts";
|
import * as typeahead_helper from "./typeahead_helper.ts";
|
||||||
|
import * as ui_report from "./ui_report.ts";
|
||||||
import type {UserPillWidget} from "./user_pill.ts";
|
import type {UserPillWidget} from "./user_pill.ts";
|
||||||
import * as user_pill from "./user_pill.ts";
|
import * as user_pill from "./user_pill.ts";
|
||||||
|
|
||||||
@@ -200,19 +201,131 @@ export function initialize_custom_user_type_fields(
|
|||||||
return user_pills;
|
return user_pills;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialize_custom_date_type_fields(element_id: string): void {
|
export function format_date(date: Date | undefined, format: string): string {
|
||||||
|
if (date === undefined || date.toString() === "Invalid Date") {
|
||||||
|
return "Invalid Date";
|
||||||
|
}
|
||||||
|
|
||||||
|
return flatpickr.formatDate(date, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize_custom_date_type_fields(element_id: string, user_id: number): void {
|
||||||
const $date_picker_elements = $(element_id).find(".custom_user_field .datepicker");
|
const $date_picker_elements = $(element_id).find(".custom_user_field .datepicker");
|
||||||
if ($date_picker_elements.length === 0) {
|
if ($date_picker_elements.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_date(instance: flatpickr.Instance, date_str: string): void {
|
||||||
|
const $input_elem = $(instance.element);
|
||||||
|
const field_id = Number.parseInt($input_elem.attr("data-field-id")!, 10);
|
||||||
|
|
||||||
|
if (date_str === "Invalid Date") {
|
||||||
|
// Date parses empty string to an invalid value but in
|
||||||
|
// our case it is a valid value when user does not want
|
||||||
|
// to set any value for the custom profile field.
|
||||||
|
if ($input_elem.parent().find(".date-field-alt-input").val() === "") {
|
||||||
|
if (user_id !== people.my_current_user_id()) {
|
||||||
|
// For "Manage user" modal, API request is made after
|
||||||
|
// clicking on "Save changes" button.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
update_user_custom_profile_fields([{id: field_id}], channel.del);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show "Invalid date value" message briefly and set
|
||||||
|
// the input to original value.
|
||||||
|
const $spinner_element = $input_elem
|
||||||
|
.closest(".custom_user_field")
|
||||||
|
.find(".custom-field-status");
|
||||||
|
ui_report.error(
|
||||||
|
$t({defaultMessage: "Invalid date value"}),
|
||||||
|
undefined,
|
||||||
|
$spinner_element,
|
||||||
|
1200,
|
||||||
|
);
|
||||||
|
const original_value = people.get_custom_profile_data(user_id, field_id)?.value ?? "";
|
||||||
|
instance.setDate(original_value);
|
||||||
|
if (user_id !== people.my_current_user_id()) {
|
||||||
|
// Trigger "input" event so that save button state can
|
||||||
|
// be toggled in "Manage user" modal.
|
||||||
|
$input_elem
|
||||||
|
.closest(".custom_user_field")
|
||||||
|
.find(".date-field-alt-input")
|
||||||
|
.trigger("input");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_id !== people.my_current_user_id()) {
|
||||||
|
// For "Manage user" modal, API request is made after
|
||||||
|
// clicking on "Save changes" button.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
if (date_str) {
|
||||||
|
fields.push({id: field_id, value: date_str});
|
||||||
|
update_user_custom_profile_fields(fields, channel.patch);
|
||||||
|
} else {
|
||||||
|
fields.push({id: field_id});
|
||||||
|
update_user_custom_profile_fields(fields, channel.del);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flatpickr($date_picker_elements, {
|
flatpickr($date_picker_elements, {
|
||||||
altInput: true,
|
altInput: true,
|
||||||
|
// We would need to handle the altInput separately
|
||||||
|
// than ".custom_user_field_value" elements to handle
|
||||||
|
// invalid values typed in the input.
|
||||||
|
altInputClass: "date-field-alt-input settings_text_input",
|
||||||
altFormat: "F j, Y",
|
altFormat: "F j, Y",
|
||||||
allowInput: true,
|
allowInput: true,
|
||||||
static: true,
|
static: true,
|
||||||
|
// This helps us in accepting inputs in other formats
|
||||||
|
// like MM/DD/YYYY and basically any other format
|
||||||
|
// which is accepted by Date.
|
||||||
|
parseDate: (date_str) => new Date(date_str),
|
||||||
|
// We pass allowInvalidPreload as true because we handle
|
||||||
|
// invalid values typed in the input ourselves. Also,
|
||||||
|
// formatDate function is customized to handle "undefined"
|
||||||
|
// values, which are returned by parseDate for invalid
|
||||||
|
// values.
|
||||||
|
formatDate: format_date,
|
||||||
|
allowInvalidPreload: true,
|
||||||
|
onChange(_selected_dates, date_str, instance) {
|
||||||
|
update_date(instance, date_str);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This "change" event handler is needed to make sure that
|
||||||
|
// the date is successfully changed when typing a new value
|
||||||
|
// in the input and blurring the input by clicking outside
|
||||||
|
// while the calendar popover is opened, because onChange
|
||||||
|
// callback is not executed in such a scenario.
|
||||||
|
//
|
||||||
|
// https://github.com/flatpickr/flatpickr/issues/1551#issuecomment-1601830680
|
||||||
|
// has explanation on why that happens.
|
||||||
|
//
|
||||||
|
// However, this leads to a problem in a couple of cases
|
||||||
|
// where both onChange callback and this "change" handlers
|
||||||
|
// are executed when changing the date by typing in the
|
||||||
|
// input. This occurs when pressing Enter while the input
|
||||||
|
// is focused, and also when blurring the input by clicking
|
||||||
|
// outside while the calendar popover is closed.
|
||||||
|
$(element_id)
|
||||||
|
.find<HTMLInputElement>("input.date-field-alt-input")
|
||||||
|
.on("change", function (this: HTMLInputElement) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const $datepicker = $(this).parent().find(".datepicker")[0] as HTMLInputElement & {
|
||||||
|
_flatpickr: flatpickr.Instance;
|
||||||
|
};
|
||||||
|
const instance = $datepicker._flatpickr;
|
||||||
|
const date = new Date($(this).val()!);
|
||||||
|
const date_str = format_date(date, "Y-m-d");
|
||||||
|
update_date(instance, date_str);
|
||||||
|
});
|
||||||
|
|
||||||
// Enable the label associated to this field to open the datepicker when clicked.
|
// Enable the label associated to this field to open the datepicker when clicked.
|
||||||
$(element_id)
|
$(element_id)
|
||||||
.find(".custom_user_field label.settings-field-label")
|
.find(".custom_user_field label.settings-field-label")
|
||||||
@@ -234,6 +347,8 @@ export function initialize_custom_date_type_fields(element_id: string): void {
|
|||||||
.find(".custom_user_field .remove_date")
|
.find(".custom_user_field .remove_date")
|
||||||
.on("click", function () {
|
.on("click", function () {
|
||||||
const $custom_user_field = $(this).parent().find(".custom_user_field_value");
|
const $custom_user_field = $(this).parent().find(".custom_user_field_value");
|
||||||
|
const $displayed_input = $(this).parent().find(".date-field-alt-input");
|
||||||
|
$displayed_input.val("");
|
||||||
$custom_user_field.val("");
|
$custom_user_field.val("");
|
||||||
$custom_user_field.trigger("input");
|
$custom_user_field.trigger("input");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import assert from "minimalistic-assert";
|
||||||
|
|
||||||
import render_dialog_widget from "../templates/dialog_widget.hbs";
|
import render_dialog_widget from "../templates/dialog_widget.hbs";
|
||||||
|
|
||||||
import type {AjaxRequestHandler} from "./channel.ts";
|
import type {AjaxRequestHandler} from "./channel.ts";
|
||||||
|
import * as custom_profile_fields_ui from "./custom_profile_fields_ui.ts";
|
||||||
import {$t_html} from "./i18n.ts";
|
import {$t_html} from "./i18n.ts";
|
||||||
import * as loading from "./loading.ts";
|
import * as loading from "./loading.ts";
|
||||||
import * as modals from "./modals.ts";
|
import * as modals from "./modals.ts";
|
||||||
@@ -135,6 +137,24 @@ export function get_current_values($inputs: JQuery): Record<string, unknown> {
|
|||||||
current_values[property_name] = $(this).val();
|
current_values[property_name] = $(this).val();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($(this).hasClass("date-field-alt-input")) {
|
||||||
|
// For date type custom profile fields, we convert the
|
||||||
|
// input to the date format passed to the API.
|
||||||
|
const value = $(this).val()!;
|
||||||
|
const name = $(this).parent().find(".custom_user_field_value").attr("name")!;
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
// This case is handled separately, because it will
|
||||||
|
// otherwise be parsed as an invalid date.
|
||||||
|
current_values[name] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(typeof value === "string");
|
||||||
|
const date_str = new Date(value);
|
||||||
|
current_values[name] = custom_profile_fields_ui.format_date(date_str, "Y-m-d");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return current_values;
|
return current_values;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,10 @@ export function add_custom_profile_fields_to_settings(): void {
|
|||||||
true,
|
true,
|
||||||
pill_update_handler,
|
pill_update_handler,
|
||||||
);
|
);
|
||||||
custom_profile_fields_ui.initialize_custom_date_type_fields(element_id);
|
custom_profile_fields_ui.initialize_custom_date_type_fields(
|
||||||
|
element_id,
|
||||||
|
people.my_current_user_id(),
|
||||||
|
);
|
||||||
custom_profile_fields_ui.initialize_custom_pronouns_type_fields(element_id);
|
custom_profile_fields_ui.initialize_custom_pronouns_type_fields(element_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,22 +727,26 @@ export function set_up(): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
$("#profile-settings").on("change", ".custom_user_field_value", function (this: HTMLElement) {
|
$("#profile-settings").on(
|
||||||
const fields: CustomProfileFieldData[] = [];
|
"change",
|
||||||
const value = $(this).val()!;
|
".custom_user_field_value:not(.datepicker)",
|
||||||
assert(typeof value === "string");
|
function (this: HTMLElement) {
|
||||||
const field_id = Number.parseInt(
|
const fields: CustomProfileFieldData[] = [];
|
||||||
$(this).closest(".custom_user_field").attr("data-field-id")!,
|
const value = $(this).val()!;
|
||||||
10,
|
assert(typeof value === "string");
|
||||||
);
|
const field_id = Number.parseInt(
|
||||||
if (value) {
|
$(this).closest(".custom_user_field").attr("data-field-id")!,
|
||||||
fields.push({id: field_id, value});
|
10,
|
||||||
custom_profile_fields_ui.update_user_custom_profile_fields(fields, channel.patch);
|
);
|
||||||
} else {
|
if (value) {
|
||||||
fields.push({id: field_id});
|
fields.push({id: field_id, value});
|
||||||
custom_profile_fields_ui.update_user_custom_profile_fields(fields, channel.del);
|
custom_profile_fields_ui.update_user_custom_profile_fields(fields, channel.patch);
|
||||||
}
|
} else {
|
||||||
});
|
fields.push({id: field_id});
|
||||||
|
custom_profile_fields_ui.update_user_custom_profile_fields(fields, channel.del);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
$("#account-settings .deactivate_realm_button").on(
|
$("#account-settings .deactivate_realm_button").on(
|
||||||
"click",
|
"click",
|
||||||
|
|||||||
@@ -1054,16 +1054,10 @@ function get_human_profile_data(fields_user_pills: Map<number, user_pill.UserPil
|
|||||||
*/
|
*/
|
||||||
const new_profile_data = [];
|
const new_profile_data = [];
|
||||||
$("#edit-user-form .custom_user_field_value").each(function () {
|
$("#edit-user-form .custom_user_field_value").each(function () {
|
||||||
// Remove duplicate datepicker input element generated flatpickr library
|
new_profile_data.push({
|
||||||
if (!$(this).hasClass("form-control")) {
|
id: Number.parseInt($(this).closest(".custom_user_field").attr("data-field-id")!, 10),
|
||||||
new_profile_data.push({
|
value: $(this).val(),
|
||||||
id: Number.parseInt(
|
});
|
||||||
$(this).closest(".custom_user_field").attr("data-field-id")!,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
value: $(this).val(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Append user type field values also
|
// Append user type field values also
|
||||||
for (const [field_id, field_pills] of fields_user_pills) {
|
for (const [field_id, field_pills] of fields_user_pills) {
|
||||||
@@ -1083,7 +1077,7 @@ function get_current_values(
|
|||||||
$edit_form: JQuery,
|
$edit_form: JQuery,
|
||||||
): Record<string, unknown> & {user_id?: string | undefined} {
|
): Record<string, unknown> & {user_id?: string | undefined} {
|
||||||
const raw_current_values = dialog_widget.get_current_values(
|
const raw_current_values = dialog_widget.get_current_values(
|
||||||
$edit_form.find("input, select, textarea, button, .pill-container"),
|
$edit_form.find("input:not(.datepicker), select, textarea, button, .pill-container"),
|
||||||
);
|
);
|
||||||
const schema = z.intersection(
|
const schema = z.intersection(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -1150,7 +1144,10 @@ export function show_edit_user_info_modal(user_id: number, $container: JQuery):
|
|||||||
custom_profile_field_form_selector,
|
custom_profile_field_form_selector,
|
||||||
user_id,
|
user_id,
|
||||||
);
|
);
|
||||||
custom_profile_fields_ui.initialize_custom_date_type_fields(custom_profile_field_form_selector);
|
custom_profile_fields_ui.initialize_custom_date_type_fields(
|
||||||
|
custom_profile_field_form_selector,
|
||||||
|
user_id,
|
||||||
|
);
|
||||||
custom_profile_fields_ui.initialize_custom_pronouns_type_fields(
|
custom_profile_fields_ui.initialize_custom_pronouns_type_fields(
|
||||||
custom_profile_field_form_selector,
|
custom_profile_field_form_selector,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -740,6 +740,18 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#edit-user-form .alert-notification.custom-field-status,
|
||||||
|
#profile-settings .alert-notification.custom-field-status,
|
||||||
|
#profile-settings .alert-notification.full-name-status,
|
||||||
|
#profile-settings .alert-notification.timezone-setting-status {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
#profile-settings {
|
#profile-settings {
|
||||||
.custom-profile-fields-form .custom_user_field label,
|
.custom-profile-fields-form .custom_user_field label,
|
||||||
.full-name-change-container label,
|
.full-name-change-container label,
|
||||||
@@ -747,17 +759,6 @@ input[type="checkbox"] {
|
|||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-notification.custom-field-status,
|
|
||||||
.alert-notification.full-name-status,
|
|
||||||
.alert-notification.timezone-setting-status {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person_picker {
|
.person_picker {
|
||||||
/* Subtract 2 * (2px padding) + 2 * (1px border) */
|
/* Subtract 2 * (2px padding) + 2 * (1px border) */
|
||||||
min-width: calc(var(--modal-input-width) - 6px);
|
min-width: calc(var(--modal-input-width) - 6px);
|
||||||
|
|||||||
Reference in New Issue
Block a user