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:
Sahil Batra
2025-05-08 21:29:30 +05:30
committed by Tim Abbott
parent 0a684ab0f5
commit 926716d9f2
6 changed files with 182 additions and 42 deletions

View File

@@ -34,7 +34,7 @@ async function open_settings(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.waitForSelector(".flatpickr-calendar", {visible: true});

View File

@@ -14,6 +14,7 @@ import * as settings_components from "./settings_components.ts";
import * as settings_ui from "./settings_ui.ts";
import {current_user, realm} from "./state_data.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 * as user_pill from "./user_pill.ts";
@@ -200,19 +201,131 @@ export function initialize_custom_user_type_fields(
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");
if ($date_picker_elements.length === 0) {
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, {
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",
allowInput: 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.
$(element_id)
.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")
.on("click", function () {
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.trigger("input");
});

View File

@@ -1,9 +1,11 @@
import $ from "jquery";
import _ from "lodash";
import assert from "minimalistic-assert";
import render_dialog_widget from "../templates/dialog_widget.hbs";
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 * as loading from "./loading.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();
}
}
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;
}

View File

@@ -233,7 +233,10 @@ export function add_custom_profile_fields_to_settings(): void {
true,
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);
}
@@ -724,22 +727,26 @@ export function set_up(): void {
},
);
$("#profile-settings").on("change", ".custom_user_field_value", function (this: HTMLElement) {
const fields: CustomProfileFieldData[] = [];
const value = $(this).val()!;
assert(typeof value === "string");
const field_id = Number.parseInt(
$(this).closest(".custom_user_field").attr("data-field-id")!,
10,
);
if (value) {
fields.push({id: field_id, value});
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);
}
});
$("#profile-settings").on(
"change",
".custom_user_field_value:not(.datepicker)",
function (this: HTMLElement) {
const fields: CustomProfileFieldData[] = [];
const value = $(this).val()!;
assert(typeof value === "string");
const field_id = Number.parseInt(
$(this).closest(".custom_user_field").attr("data-field-id")!,
10,
);
if (value) {
fields.push({id: field_id, value});
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(
"click",

View File

@@ -1054,16 +1054,10 @@ function get_human_profile_data(fields_user_pills: Map<number, user_pill.UserPil
*/
const new_profile_data = [];
$("#edit-user-form .custom_user_field_value").each(function () {
// Remove duplicate datepicker input element generated flatpickr library
if (!$(this).hasClass("form-control")) {
new_profile_data.push({
id: Number.parseInt(
$(this).closest(".custom_user_field").attr("data-field-id")!,
10,
),
value: $(this).val(),
});
}
new_profile_data.push({
id: Number.parseInt($(this).closest(".custom_user_field").attr("data-field-id")!, 10),
value: $(this).val(),
});
});
// Append user type field values also
for (const [field_id, field_pills] of fields_user_pills) {
@@ -1083,7 +1077,7 @@ function get_current_values(
$edit_form: JQuery,
): Record<string, unknown> & {user_id?: string | undefined} {
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(
z.object({
@@ -1150,7 +1144,10 @@ export function show_edit_user_info_modal(user_id: number, $container: JQuery):
custom_profile_field_form_selector,
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_field_form_selector,
);

View File

@@ -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 {
.custom-profile-fields-form .custom_user_field label,
.full-name-change-container label,
@@ -747,17 +759,6 @@ input[type="checkbox"] {
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 {
/* Subtract 2 * (2px padding) + 2 * (1px border) */
min-width: calc(var(--modal-input-width) - 6px);