From f758ca596bc8568029e6d4190b092fc8b20575f5 Mon Sep 17 00:00:00 2001 From: Vector73 Date: Tue, 19 Mar 2024 18:52:03 +0530 Subject: [PATCH] custom_profile_fields: Add "required" parameter to the profile fields. Fixes #28512. --- api_docs/changelog.md | 9 ++++++ tools/test-js-with-node | 1 + version.py | 2 +- web/src/custom_profile_fields_ui.js | 1 + web/src/navbar_alerts.js | 29 +++++++++++++++++ web/src/server_events_dispatch.js | 1 + web/src/settings_profile_fields.js | 32 +++++++++++++++++++ web/src/state_data.ts | 1 + web/src/tippyjs.ts | 11 +++++++ web/src/user_events.js | 30 ++++++++++++++++- web/src/user_profile.js | 1 + web/styles/dark_theme.css | 4 +++ web/styles/settings.css | 31 ++++++++++++++++-- .../empty_required_profile_fields.hbs | 6 ++++ .../add_new_custom_profile_field_form.hbs | 7 ++++ .../settings/admin_profile_field_list.hbs | 8 +++++ .../settings/custom_user_profile_field.hbs | 7 ++-- .../edit_custom_profile_field_form.hbs | 7 ++++ .../settings/profile_field_settings_admin.hbs | 1 + web/tests/dispatch.test.js | 14 +++++++- web/tests/lib/events.js | 2 ++ web/tests/settings_profile_fields.test.js | 8 +++++ web/tests/user_events.test.js | 4 ++- zerver/actions/custom_profile_fields.py | 6 ++++ zerver/lib/event_schema.py | 1 + zerver/lib/types.py | 1 + .../0504_customprofilefield_required.py | 17 ++++++++++ zerver/models/custom_profile_fields.py | 2 ++ zerver/openapi/zulip.yaml | 27 ++++++++++++++++ zerver/tests/test_custom_profile_data.py | 14 ++++++++ zerver/views/custom_profile_fields.py | 5 +++ 31 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 web/templates/navbar_alerts/empty_required_profile_fields.hbs create mode 100644 zerver/migrations/0504_customprofilefield_required.py diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 1794441747..78100e78cc 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 9.0 +**Feature level 244** + +* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events), + [`POST /realm/profile_fields`](/api/create-custom-profile-field), + [`GET /realm/profile_fields`](/api/get-custom-profile-fields): Added a new + parameter `required`, on custom profile field objects, indicating whether an + organization administrator has configured the field as something users should + be required to provide. + **Feature level 243** * [`POST /register`](/api/register-queue), [`GET diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 43cb3d789b..38ff713a4f 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -265,6 +265,7 @@ EXEMPT_FILES = make_set( "web/src/url-template.d.ts", "web/src/user_card_popover.js", "web/src/user_deactivation_ui.ts", + "web/src/user_events.js", "web/src/user_group_components.js", "web/src/user_group_create.js", "web/src/user_group_create_members.js", diff --git a/version.py b/version.py index db5e2c05ed..bcaf71c261 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 243 +API_FEATURE_LEVEL = 244 # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/custom_profile_fields_ui.js b/web/src/custom_profile_fields_ui.js index 458f8f36f8..9bd1ed821b 100644 --- a/web/src/custom_profile_fields_ui.js +++ b/web/src/custom_profile_fields_ui.js @@ -62,6 +62,7 @@ export function append_custom_profile_fields(element_id, user_id) { is_select_field, field_choices, for_manage_user_modal: element_id === "#edit-user-form .custom-profile-field-form", + is_empty_required_field: field.required && !field_value.value, }); $(element_id).append(html); } diff --git a/web/src/navbar_alerts.js b/web/src/navbar_alerts.js index 356a1b3a37..c9b32249c1 100644 --- a/web/src/navbar_alerts.js +++ b/web/src/navbar_alerts.js @@ -5,6 +5,7 @@ import render_bankruptcy_alert_content from "../templates/navbar_alerts/bankrupt import render_configure_email_alert_content from "../templates/navbar_alerts/configure_outgoing_email.hbs"; import render_demo_organization_deadline_content from "../templates/navbar_alerts/demo_organization_deadline.hbs"; import render_desktop_notifications_alert_content from "../templates/navbar_alerts/desktop_notifications.hbs"; +import render_empty_required_profile_fields from "../templates/navbar_alerts/empty_required_profile_fields.hbs"; import render_insecure_desktop_app_alert_content from "../templates/navbar_alerts/insecure_desktop_app.hbs"; import render_navbar_alert_wrapper from "../templates/navbar_alerts/navbar_alert_wrapper.hbs"; import render_profile_incomplete_alert_content from "../templates/navbar_alerts/profile_incomplete.hbs"; @@ -14,6 +15,7 @@ import * as desktop_notifications from "./desktop_notifications"; import * as keydown_util from "./keydown_util"; import {localstorage} from "./localstorage"; import {page_params} from "./page_params"; +import * as people from "./people"; import {current_user, realm} from "./state_data"; import {should_display_profile_incomplete_alert} from "./timerender"; import * as unread from "./unread"; @@ -70,6 +72,30 @@ export function should_show_server_upgrade_notification(ls) { return Date.now() > upgrade_nag_dismissal_duration; } +export function maybe_show_empty_required_profile_fields_alert() { + const empty_required_profile_fields_exist = realm.custom_profile_fields + .map((f) => ({ + ...f, + value: people.my_custom_profile_data(f.id)?.value, + })) + .find((f) => f.required && !f.value); + if (!empty_required_profile_fields_exist) { + return; + } + + const $navbar_alert = $("#navbar_alerts_wrapper").children(".alert").first(); + if ( + !$navbar_alert?.length || + $navbar_alert.is("#empty-required-profile-fields-warning") || + $navbar_alert.is(":hidden") + ) { + open({ + data_process: "profile-missing-required", + rendered_alert_content_html: render_empty_required_profile_fields(), + }); + } +} + export function dismiss_upgrade_nag(ls) { $(".alert[data-process='server-needs-upgrade'").hide(); if (localstorage.supported()) { @@ -171,6 +197,8 @@ export function initialize() { data_process: "profile-incomplete", rendered_alert_content_html: render_profile_incomplete_alert_content(), }); + } else { + maybe_show_empty_required_profile_fields_alert(); } // Configure click handlers. @@ -208,6 +236,7 @@ export function initialize() { show_step($process, 2); } else { $(this).closest(".alert").hide(); + maybe_show_empty_required_profile_fields_alert(); } $(window).trigger("resize"); }); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index acac737b0b..e33b9b39b4 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -104,6 +104,7 @@ export function dispatch_normal_event(event) { realm.custom_profile_fields = event.fields; settings_profile_fields.populate_profile_fields(realm.custom_profile_fields); settings_account.add_custom_profile_fields_to_settings(); + navbar_alerts.maybe_show_empty_required_profile_fields_alert(); break; case "default_streams": diff --git a/web/src/settings_profile_fields.js b/web/src/settings_profile_fields.js index b3a02a2889..1fcd450367 100644 --- a/web/src/settings_profile_fields.js +++ b/web/src/settings_profile_fields.js @@ -269,6 +269,7 @@ function open_custom_profile_field_form_modal() { display_in_profile_summary: $("#profile_field_display_in_profile_summary").is( ":checked", ), + required: $("#profile-field-required").is(":checked"), }; const url = "/json/realm/profile_fields"; const opts = { @@ -454,6 +455,7 @@ function open_edit_form_modal(e) { hint: field.hint, choices, display_in_profile_summary: field.display_in_profile_summary === true, + required: field.required === true, is_select_field: field.type === field_types.SELECT.id, is_external_account_field: field.type === field_types.EXTERNAL_ACCOUNT.id, valid_to_display_in_summary: is_valid_to_display_in_summary(field.type), @@ -510,6 +512,7 @@ function open_edit_form_modal(e) { data.display_in_profile_summary = $profile_field_form .find("input[name=display_in_profile_summary]") .is(":checked"); + data.required = $profile_field_form.find("input[name=required]").is(":checked"); const new_field_data = read_field_data_from_form( Number.parseInt(field.type, 10), @@ -588,6 +591,7 @@ function toggle_display_in_profile_summary_profile_field(e) { hint: field.hint, field_data, display_in_profile_summary: !field.display_in_profile_summary, + required: field.required, }; const $profile_field_status = $("#admin-profile-field-status").expectOne(); @@ -599,6 +603,31 @@ function toggle_display_in_profile_summary_profile_field(e) { ); } +function toggle_required(e) { + const field_id = Number.parseInt($(e.currentTarget).attr("data-profile-field-id"), 10); + const field = get_profile_field(field_id); + + let field_data; + if (field.field_data) { + field_data = field.field_data; + } + + const data = { + name: field.name, + hint: field.hint, + field_data, + display_in_profile_summary: field.display_in_profile_summary, + required: !field.required, + }; + const $profile_field_status = $("#admin-profile-field-status").expectOne(); + + settings_ui.do_settings_change( + channel.patch, + "/json/realm/profile_fields/" + field_id, + data, + $profile_field_status, + ); +} export function reset() { meta.loaded = false; } @@ -648,6 +677,7 @@ export function do_populate_profile_fields(profile_fields_data) { } const display_in_profile_summary = profile_field.display_in_profile_summary === true; + const required = profile_field.required === true; $profile_fields_table.append( render_admin_profile_field_list({ profile_field: { @@ -661,6 +691,7 @@ export function do_populate_profile_fields(profile_fields_data) { profile_field.type === field_types.EXTERNAL_ACCOUNT.id, display_in_profile_summary, valid_to_display_in_summary: is_valid_to_display_in_summary(profile_field.type), + required, }, can_modify: current_user.is_admin, realm_default_external_accounts: realm.realm_default_external_accounts, @@ -767,4 +798,5 @@ export function build_page() { ".display_in_profile_summary", toggle_display_in_profile_summary_profile_field, ); + $("#admin_profile_fields_table").on("click", ".required-field-toggle", toggle_required); } diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 7bb697c952..89d09e8b6a 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -89,6 +89,7 @@ export const realm_schema = z.object({ id: z.number(), name: z.string(), order: z.number(), + required: z.boolean(), type: z.number(), }), ), diff --git a/web/src/tippyjs.ts b/web/src/tippyjs.ts index 2d481ad320..5108977289 100644 --- a/web/src/tippyjs.ts +++ b/web/src/tippyjs.ts @@ -592,4 +592,15 @@ export function initialize(): void { ); }, }); + + delegate("body", { + target: ".custom-user-field-label-wrapper", + content: $t({ + defaultMessage: "This profile field is required.", + }), + appendTo: () => document.body, + onHidden(instance) { + instance.destroy(); + }, + }); } diff --git a/web/src/user_events.js b/web/src/user_events.js index babf93954e..2922e3814e 100644 --- a/web/src/user_events.js +++ b/web/src/user_events.js @@ -10,6 +10,7 @@ import {buddy_list} from "./buddy_list"; import * as compose_state from "./compose_state"; import * as message_live_update from "./message_live_update"; import * as narrow_state from "./narrow_state"; +import * as navbar_alerts from "./navbar_alerts"; import * as people from "./people"; import * as pm_list from "./pm_list"; import * as settings from "./settings"; @@ -21,7 +22,7 @@ import * as settings_profile_fields from "./settings_profile_fields"; import * as settings_realm_user_settings_defaults from "./settings_realm_user_settings_defaults"; import * as settings_streams from "./settings_streams"; import * as settings_users from "./settings_users"; -import {current_user} from "./state_data"; +import {current_user, realm} from "./state_data"; import * as stream_events from "./stream_events"; export const update_person = function update(person) { @@ -128,6 +129,33 @@ export const update_person = function update(person) { if (Object.hasOwn(person, "custom_profile_field")) { people.set_custom_profile_field_data(person.user_id, person.custom_profile_field); + if (person.user_id === people.my_current_user_id()) { + navbar_alerts.maybe_show_empty_required_profile_fields_alert(); + + const field_id = person.custom_profile_field.id; + const field_value = people.get_custom_profile_data(person.user_id, field_id)?.value; + const is_field_required = realm.custom_profile_fields?.find( + (f) => field_id === f.id, + )?.required; + if (is_field_required) { + const $custom_user_field = $( + `.profile-settings-form .custom_user_field[data-field-id="${CSS.escape(field_id)}"]`, + ); + const $field = $custom_user_field.find(".field"); + const $required_symbol = $custom_user_field.find(".required-symbol"); + if (!field_value) { + if (!$field.hasClass("empty-required-field")) { + $field.addClass("empty-required-field"); + $required_symbol.removeClass("hidden"); + } + } else { + if ($field.hasClass("empty-required-field")) { + $field.removeClass("empty-required-field"); + $required_symbol.addClass("hidden"); + } + } + } + } } if (Object.hasOwn(person, "timezone")) { diff --git a/web/src/user_profile.js b/web/src/user_profile.js index abf9854395..0390a3006d 100644 --- a/web/src/user_profile.js +++ b/web/src/user_profile.js @@ -287,6 +287,7 @@ export function get_custom_profile_field_data(user, field, field_types) { profile_field.is_external_account = field_type === field_types.EXTERNAL_ACCOUNT.id; profile_field.type = field_type; profile_field.display_in_profile_summary = field.display_in_profile_summary; + profile_field.required = field.required; switch (field_type) { case field_types.DATE.id: diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 3b809389a8..26290cf33a 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -482,6 +482,10 @@ border-width: 1px; } + .field { + border-color: hsl(3deg 73% 74%); + } + .deactivated-pill { background-color: hsl(0deg 86% 14%) !important; } diff --git a/web/styles/settings.css b/web/styles/settings.css index 0789fd2ec6..70ee5848d6 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -131,7 +131,9 @@ h3, .admin_profile_fields_table { & th.display, - td.display_in_profile_summary_cell { + & th.required, + td.display_in_profile_summary_cell, + td.required-cell { text-align: center; } } @@ -672,6 +674,15 @@ input[type="checkbox"] { } } +.required-symbol { + color: hsl(0deg 66% 60%); +} + +.settings-profile-user-field { + margin-top: 5px; + max-width: fit-content; +} + .control-label-disabled { color: hsl(0deg 0% 82%); @@ -1513,7 +1524,6 @@ $option_title_width: 180px; #edit-user-form { .person_picker { min-width: 206px; - margin-top: 5px; } & textarea { @@ -1536,7 +1546,6 @@ $option_title_width: 180px; & select { /* Override undesired Bootstrap default. */ margin-bottom: 0; - margin-top: 5px; } } @@ -1906,6 +1915,22 @@ $option_title_width: 180px; } } +.empty-required-field { + border: 1px solid hsl(3deg 57% 33%); + border-radius: 5px; + + .settings_url_input, + .settings_text_input, + .settings_textarea, + .pill-container { + border-color: hsl(0deg 0% 100%); + + &:focus { + border-color: hsl(206deg 80% 62% / 80%); + } + } +} + #generate-integration-url-modal { .inline { display: inline; diff --git a/web/templates/navbar_alerts/empty_required_profile_fields.hbs b/web/templates/navbar_alerts/empty_required_profile_fields.hbs new file mode 100644 index 0000000000..a74eafa37e --- /dev/null +++ b/web/templates/navbar_alerts/empty_required_profile_fields.hbs @@ -0,0 +1,6 @@ +
+ + {{t 'Your profile is missing required fields.'}}  + {{t 'Edit your profile'}} + +
diff --git a/web/templates/settings/add_new_custom_profile_field_form.hbs b/web/templates/settings/add_new_custom_profile_field_form.hbs index fec863b0cc..62c16f47c2 100644 --- a/web/templates/settings/add_new_custom_profile_field_form.hbs +++ b/web/templates/settings/add_new_custom_profile_field_form.hbs @@ -43,5 +43,12 @@ {{t 'Display on user card' }} +
+ +
diff --git a/web/templates/settings/admin_profile_field_list.hbs b/web/templates/settings/admin_profile_field_list.hbs index 1325c238bc..8796327d99 100644 --- a/web/templates/settings/admin_profile_field_list.hbs +++ b/web/templates/settings/admin_profile_field_list.hbs @@ -23,6 +23,14 @@ {{/if}} + + + + + {{#if ../can_modify}}