From e327940bd63d16ecbb5fe67c0bc2169b88eaf333 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 8 Oct 2026 05:37:20 +0200 Subject: [PATCH] Add action buttons with badge styling --- WebminCore.pm | 2 +- t/ui-lib-widgets.t | 49 ++++++++++++++ ui-demo/README.md | 14 +++- ui-demo/lang/en | 25 ++++++- ui-demo/ui-demo-pages.pl | 61 +++++++++++++++++ ui-lib.pl | 90 +++++++++++++++++++++++++ unauthenticated/css/ui-lib.css | 116 +++++++++++++++++++++++++++++++-- unauthenticated/js/ui-lib.js | 7 ++ 8 files changed, 355 insertions(+), 9 deletions(-) diff --git a/WebminCore.pm b/WebminCore.pm index d644da784..1dbd86c45 100644 --- a/WebminCore.pm +++ b/WebminCore.pm @@ -23,7 +23,7 @@ $main::export_to_caller = 1; # Add functions in web-lib-funcs.pl # Generated with : # grep -h "^sub " web-lib-funcs.pl ui-lib.pl | sed -e 's/sub //' | xargs echo -@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys webmin_temp_dir_name webmin_temp_dir_path tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress ipaddress_matches_network get_download_address_callback check_download_address generate_icon urlize un_urlize include copydata copydata_len ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper ui_form_field_state_javascript js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir generate_miniserv_websocket_token allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase ui_svg_icon ui_code ui_tip ui_page_assets ui_page_start ui_page_end ui_grid ui_stack ui_cluster ui_card ui_card_start ui_card_end ui_stat ui_stats ui_dl ui_badge ui_chip ui_list ui_feed ui_empty_state ui_progress ui_toggle ui_search ui_choice ui_radio_list ui_select_switch ui_multi_select_list); +@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys webmin_temp_dir_name webmin_temp_dir_path tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress ipaddress_matches_network get_download_address_callback check_download_address generate_icon urlize un_urlize include copydata copydata_len ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper ui_form_field_state_javascript js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir generate_miniserv_websocket_token allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase ui_svg_icon ui_code ui_tip ui_page_assets ui_page_start ui_page_end ui_grid ui_stack ui_cluster ui_card ui_card_start ui_card_end ui_stat ui_stats ui_dl ui_badge ui_action_button ui_chip ui_list ui_feed ui_empty_state ui_progress ui_toggle ui_search ui_choice ui_radio_list ui_select_switch ui_multi_select_list); # Add global variables in web-lib.pl push(@EXPORT, qw(&can_read_file_under_global_acl &global_acl_file_unix_user diff --git a/t/ui-lib-widgets.t b/t/ui-lib-widgets.t index 2160f313a..53d399055 100644 --- a/t/ui-lib-widgets.t +++ b/t/ui-lib-widgets.t @@ -153,6 +153,55 @@ like(main::ui_badge('what', 'bogus<'), qr/ui_badge_neutral/, like($html, qr/\bmine\b/, 'extra class names are passed through'); } +# Action controls retain native semantics and their own theme-independent classes. +subtest 'action buttons and links' => sub { + my $button = main::ui_action_button('Save', 'ok'); + like($button, qr/]*type="button"/, 'default button does not submit'); + like($button, qr/\bui_action_success\b/, 'state aliases match badges'); + unlike($button, qr/\b(?:ui_badge|ui_button|ui_submit|ui_link|btn)\b/, + 'legacy theme classes are absent'); + my $opts = { type => 'submit', name => 'action', value => '', form => 'editor', + size => 'small', shape => 'pill', icon => 'trash', + attrs => { 'data-ui-confirm' => 'Proceed?', class => 'extra' } }; + my $submit = main::ui_action_button('Delete', 'danger', $opts); + like($submit, qr/type="submit"/, 'submit is explicit'); + like($submit, qr/name="action"/, 'submitter name is preserved'); + like($submit, qr/value=""/, 'empty submitter value is preserved'); + like($submit, qr/form="editor"/, 'external form association is preserved'); + like($submit, qr/\bui_action_small\b.*\bui_action_pill\b.*\bextra\b/, + 'size, shape and caller classes combine'); + like($submit, qr/data-ui-confirm="Proceed\?"/, 'confirmation uses the shared handler'); + like($submit, qr/{'attrs'}, { 'data-ui-confirm' => 'Proceed?', class => 'extra' }, + 'caller attributes are not modified'); + like(main::ui_action_button('Reset', undef, {type => 'reset'}), + qr/type="reset"/, 'reset is supported'); + like(main::ui_action_button('Zero', undef, {value => 0}), + qr/value="0"/, 'zero value is preserved'); + my $link = main::ui_action_button('Open', 'info', + { href => 'edit.cgi?id=1&tab=disks', type => 'submit' }); + like($link, qr/]*href="edit.cgi\?id=1&tab=disks"/, 'link has a normal URL'); + unlike($link, qr/\btype=/, 'link has no button type'); + my $disabled = main::ui_action_button('Open', 'info', + { href => 'edit.cgi', disabled => 1, attrs => {href => 'other.cgi'} }); + unlike($disabled, qr/\bhref=/, 'disabled link has no destination'); + like($disabled, qr/aria-disabled="true"/, 'disabled link exposes its state'); + like($disabled, qr/tabindex="-1"/, 'disabled link is outside keyboard navigation'); + like($disabled, qr/role="link"/, 'disabled anchor retains link semantics'); + like(main::ui_action_button('Disabled', undef, {disabled => 1}), + qr/]*\bdisabled(?:\s|>)/, 'button uses native disabled behavior'); + my $fallback = main::ui_action_button('Fallback', 'unknown', + {type => 'invalid', size => 'invalid', shape => 'invalid', attrs => {type => 'submit'}}); + like($fallback, qr/\bui_action_neutral\b.*\bui_action_normal\b.*\bui_action_square\b/, + 'unknown variants use defaults'); + like($fallback, qr/type="button"/, 'unknown type cannot accidentally submit'); + unlike(main::ui_action_button('Plain', 'danger', {icon => ''}), qr/ $xss, title => $xss, class => $xss, name => $xss, value => $xss }), + 'action text and attributes'); +}; + # The scheme option stamps the wrapper for the dark or auto palette like(main::ui_page_start({ 'scheme' => 'auto' }), qr/data-ui-scheme="auto"/, 'scheme auto stamps the wrapper'); diff --git a/ui-demo/README.md b/ui-demo/README.md index 318d561df..3400ab8ca 100644 --- a/ui-demo/README.md +++ b/ui-demo/README.md @@ -16,7 +16,7 @@ Every tab of `index.cgi` is built by one function in `ui-demo-pages.pl` : | Elements | `demo_elements_tab` | a `ui_dl` with help bubbles and HTML values, `ui_stats` with icons and links, `ui_feed` with an HTML event, `ui_empty_state`, then badges with their icon, dot, title, small and rounded options, chips, `ui_code`, `ui_note`, `ui_help`, `ui_tip`, every `ui_progress` variation and the ring gauges, and `ui_svg_icon` | | Forms | `demo_forms_tab` | the usual `ui_table_start` / `ui_table_row` form with `ui_toggle`, `ui_search`, the date chooser, password fields and a table of inputs inside one row built with `ui_columns_start` / `ui_columns_row` with the `no-hover` class, as the Nginx module's URL rewrites; a second form of choosers : `file_chooser_button` for files and directories, `ui_user_textbox`, `ui_group_textbox`, `ui_users_textbox`, `ui_groups_textbox`, and an `hlink` help link | | Choices | `demo_choices_tab` | `ui_choice`, `ui_select_switch` and `ui_radio_list` for backup destinations and IP addresses; `ui_multi_select_list` for backup servers with modes and child folding, secondary groups, and an empty list with a custom label | -| Buttons | `demo_buttons_tab` | `ui_submit`, `ui_reset` and `ui_link_button` in one row, with a disabled and a confirmed one; a form ended by `ui_form_end`, one by `ui_form_grouped_buttons`, one by `ui_form_end_side_by_side` with a separate form at the right; a `ui_confirmation_form` page | +| Buttons | `demo_buttons_tab` | `ui_action_button` as native buttons and links in all state colors, three sizes and three shapes, with submit, reset, disabled and confirmation examples; `ui_submit`, `ui_reset` and `ui_link_button` in one row, with a disabled and a confirmed one; a form ended by `ui_form_end`, one by `ui_form_grouped_buttons`, one by `ui_form_end_side_by_side` with a separate form at the right; a `ui_confirmation_form` page | | Accordions | `demo_accordions_tab` | a settings form of `ui_table_start` followed by `ui_hidden_table_start` sections | | Tables | `demo_tables_tab` | the empty state shown instead of a table with no rows, `ui_columns_table` with a `ui_details` disclosure in its first cell (classes `inline inlined`, as grub2's boot entries), and `ui_form_columns_table`, both with the sortable flag; the tab description itself hides more text behind a `ui_details` tick (class `inline`), as Virtualmin's SSL page does | | Lists | `demo_lists_tab` | `ui_list` rows with badges, tags, meta and actions; a backup history with state icons, a filesystem list with inline progress bars, and a user list with a confirmed delete link | @@ -137,6 +137,18 @@ not core API : rather than a tab : the same form inside a tab of `index.cgi` stays a plain text area. +`ui_action_button(label, state, options)` draws a native `