From 8a15f53183384fd85e7bae7271687db842c31e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Thu, 9 Oct 2025 20:33:16 +0200 Subject: [PATCH 1/6] UI for account beneficiaries User is able to designate account beneficiaries, both manager and successor. Changlog: added --- .../_designated_account_manager.html.haml | 43 ++++++++++++++++ .../_designated_account_successor.html.haml | 48 +++++++++++++++++ app/views/profiles/accounts/show.html.haml | 5 ++ .../profile/account/account_succession.md | 2 - locale/gitlab.pot | 51 +++++++++++++++++++ 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 app/views/profiles/accounts/_designated_account_manager.html.haml create mode 100644 app/views/profiles/accounts/_designated_account_successor.html.haml diff --git a/app/views/profiles/accounts/_designated_account_manager.html.haml b/app/views/profiles/accounts/_designated_account_manager.html.haml new file mode 100644 index 00000000000000..a79aae1ef7e474 --- /dev/null +++ b/app/views/profiles/accounts/_designated_account_manager.html.haml @@ -0,0 +1,43 @@ +-# - @designated_account_manager = { name: 'Oliver Jones', email: 'ojones@example.com', delete_path: 'https://todo.com'} +- learn_more_link = link_to('', help_page_path('user/profile/account/account_succession.md'), target: '_blank', rel: 'noreferrer') + +-# rubocop: disable Lint/LiteralAsCondition -- TODO if there is an error, expand/show the form += render ::Layouts::CrudComponent.new(s_('Profiles|Designated account manager'), + description: safe_format(s_('Profiles|Assign an individual who has legal authority to manage your GitLab account in the event of your incapacity. %{link_start}Learn more%{link_end}.'), tag_pair(learn_more_link, :link_start, :link_end)), + form_options: { class: false ? '' : 'gl-hidden js-toggle-content' }, + options: { class: 'js-toggle-container js-token-card' }) do |c| + -# rubocop: enable Lint/LiteralAsCondition + - c.with_actions do + - if @designated_account_manager.blank? + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do + = s_('Profiles|Add account manager') + - c.with_form do + = gitlab_ui_form_for '', url: 'todo', html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + .form-group + = f.label :name, s_('Profiles|Full name'), class: 'label-bold' + = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Full name is required.'), required: true + .form-group + = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' + = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' + = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do + = s_('Profiles|Add account manager') + = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do + = _('Cancel') + - c.with_body do + - if @designated_account_manager.present? + .table-holder + %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' } + %thead + %tr + %th= s_('Profiles|Full name') + %th= s_('Profiles|Email address') + %th= _('Actions') + %tbody + %tr + %td{ data: { label: s_('Profiles|Name') } }= @designated_account_manager[:name] + %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_manager[:email] + %td{ data: { label: _('Actions') }, class: '!gl-py-3' } + = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_manager[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account manager? This will permanently delete the designated account manager.'), confirm_btn_variant: 'danger' }}) + = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) + - else + = s_('Profiles|No designated account manager assigned.') diff --git a/app/views/profiles/accounts/_designated_account_successor.html.haml b/app/views/profiles/accounts/_designated_account_successor.html.haml new file mode 100644 index 00000000000000..6779469b819c07 --- /dev/null +++ b/app/views/profiles/accounts/_designated_account_successor.html.haml @@ -0,0 +1,48 @@ +-# - @designated_account_successor = { name: 'Oliver Jones', email: 'ojones@example.com', relationship: 'Sister', delete_path: 'https://todo.com'} +- learn_more_link = link_to('', help_page_path('user/profile/account/account_succession.md'), target: '_blank', rel: 'noreferrer') + +-# rubocop: disable Lint/LiteralAsCondition -- TODO if there is an error, expand/show the form += render ::Layouts::CrudComponent.new(s_('Profiles|Designated account successor'), + description: safe_format(s_('Profiles|Assign an individual who will have legal authority to assume ownership of your GitLab account upon your death. %{link_start}Learn more%{link_end}.'), tag_pair(learn_more_link, :link_start, :link_end)), + form_options: { class: false ? '' : 'gl-hidden js-toggle-content' }, + options: { class: 'gl-mt-5 js-toggle-container js-token-card' }) do |c| + -# rubocop: enable Lint/LiteralAsCondition + - c.with_actions do + - if @designated_account_successor.blank? + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do + = s_('Profiles|Add account successor') + - c.with_form do + = gitlab_ui_form_for '', url: 'todo', html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + .form-group + = f.label :name, s_('Profiles|Full name'), class: 'label-bold' + = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Full name is required.'), required: true + .form-group + = f.label :name, s_('Profiles|Relationship with you'), class: 'label-bold' + = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Relationship is required.'), required: true + .form-group + = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' + = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' + = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do + = s_('Profiles|Add account successor') + = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do + = _('Cancel') + - c.with_body do + - if @designated_account_successor.present? + .table-holder + %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' } + %thead + %tr + %th= s_('Profiles|Full name') + %th= s_('Profiles|Email address') + %th= s_('Profiles|Relationship') + %th= _('Actions') + %tbody + %tr + %td{ data: { label: s_('Profiles|Name') } }= @designated_account_successor[:name] + %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_successor[:email] + %td{ data: { label: s_('Profiles|Relationship') } }= @designated_account_successor[:relationship] + %td{ data: { label: _('Actions') }, class: '!gl-py-3' } + = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_successor[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account successor? This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }}) + = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) + - else + = s_('Profiles|No designated account successor assigned.') diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index b5984db6e7a111..1bd516be2652a9 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -77,6 +77,11 @@ - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) } #update-username{ data: data } += render ::Layouts::SettingsSectionComponent.new(s_('Profiles|Legacy contacts')) do |c| + - c.with_body do + = render 'designated_account_manager' + = render 'designated_account_successor' + = render ::Layouts::SettingsSectionComponent.new(s_('Profiles|Scheduled pipelines you own')) do |c| - c.with_description do = s_("Profiles|View, edit, or transfer ownership of your active scheduled pipelines.") diff --git a/doc/user/profile/account/account_succession.md b/doc/user/profile/account/account_succession.md index b8de772c46b23a..eab5989e3c35fc 100644 --- a/doc/user/profile/account/account_succession.md +++ b/doc/user/profile/account/account_succession.md @@ -4,8 +4,6 @@ group: Authentication info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Designate an account successor or account manager description: Designate an account successor or account manager for your GitLab account. -ignore_in_report: true -noindex: true --- {{< details >}} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 134f17b18cd641..16f8b465c3275b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22389,6 +22389,12 @@ msgstr "" msgid "Delete account" msgstr "" +msgid "Delete account manager? This will permanently delete the designated account manager." +msgstr "" + +msgid "Delete account successor? This will permanently delete the designated account successor." +msgstr "" + msgid "Delete asset" msgstr "" @@ -50292,6 +50298,12 @@ msgstr "" msgid "Profiles|Achievements" msgstr "" +msgid "Profiles|Add account manager" +msgstr "" + +msgid "Profiles|Add account successor" +msgstr "" + msgid "Profiles|Add email address" msgstr "" @@ -50307,6 +50319,12 @@ msgstr "" msgid "Profiles|An error occurred while updating your username, please try again." msgstr "" +msgid "Profiles|Assign an individual who has legal authority to manage your GitLab account in the event of your incapacity. %{link_start}Learn more%{link_end}." +msgstr "" + +msgid "Profiles|Assign an individual who will have legal authority to assume ownership of your GitLab account upon your death. %{link_start}Learn more%{link_end}." +msgstr "" + msgid "Profiles|Avatar cropper" msgstr "" @@ -50370,6 +50388,12 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Designated account manager" +msgstr "" + +msgid "Profiles|Designated account successor" +msgstr "" + msgid "Profiles|Disconnect %{provider}" msgstr "" @@ -50403,6 +50427,9 @@ msgstr "" msgid "Profiles|Email address" msgstr "" +msgid "Profiles|Email address (optional)" +msgstr "" + msgid "Profiles|Email addresses" msgstr "" @@ -50448,6 +50475,9 @@ msgstr "" msgid "Profiles|Full name" msgstr "" +msgid "Profiles|Full name is required." +msgstr "" + msgid "Profiles|Generate New PIN" msgstr "" @@ -50490,6 +50520,9 @@ msgstr "" msgid "Profiles|Last used" msgstr "" +msgid "Profiles|Legacy contacts" +msgstr "" + msgid "Profiles|Linked emails" msgstr "" @@ -50505,12 +50538,21 @@ msgstr "" msgid "Profiles|Manage two-factor authentication" msgstr "" +msgid "Profiles|Name" +msgstr "" + msgid "Profiles|New Support PIN generated successfully." msgstr "" msgid "Profiles|No \"<\" or \">\" characters, please." msgstr "" +msgid "Profiles|No designated account manager assigned." +msgstr "" + +msgid "Profiles|No designated account successor assigned." +msgstr "" + msgid "Profiles|No file chosen." msgstr "" @@ -50559,6 +50601,15 @@ msgstr "" msgid "Profiles|Publicly visible private SSH keys can compromise your system." msgstr "" +msgid "Profiles|Relationship" +msgstr "" + +msgid "Profiles|Relationship is required." +msgstr "" + +msgid "Profiles|Relationship with you" +msgstr "" + msgid "Profiles|Remove avatar" msgstr "" -- GitLab From f81eb7f6d5330d48d09eddc586b82c950fbdb099 Mon Sep 17 00:00:00 2001 From: Aleksei Lipniagov Date: Wed, 22 Oct 2025 13:04:05 +0200 Subject: [PATCH 2/6] Add model validations, routes, controller Work in progress. --- .../profiles/accounts_controller.rb | 17 +++++- .../designated_beneficiaries_controller.rb | 59 +++++++++++++++++++ app/models/user.rb | 10 ++++ app/models/users/designated_beneficiary.rb | 23 ++++++++ .../_designated_account_manager.html.haml | 9 ++- .../_designated_account_successor.html.haml | 14 +++-- config/routes/profile.rb | 2 + locale/gitlab.pot | 15 +++++ 8 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 app/controllers/profiles/designated_beneficiaries_controller.rb diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index eb86df6d14418e..2839b486e47d90 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -7,7 +7,8 @@ class Profiles::AccountsController < Profiles::ApplicationController urgency :low, [:show] def show - render(locals: show_view_variables) + @designated_account_manager = format_beneficiary_data(current_user.designated_account_manager) + @designated_account_successor = format_beneficiary_data(current_user.designated_account_successor) end def unlink @@ -38,8 +39,18 @@ def generate_support_pin private - def show_view_variables - {} + # TODO: move to helper? + def format_beneficiary_data(beneficiary) + return {} unless beneficiary + + { + id: beneficiary.id, + name: beneficiary.name, + email: beneficiary.email, + relationship: beneficiary.relationship, + delete_path: profile_designated_beneficiary_path(beneficiary), + object: beneficiary + } end def find_identity(provider) diff --git a/app/controllers/profiles/designated_beneficiaries_controller.rb b/app/controllers/profiles/designated_beneficiaries_controller.rb new file mode 100644 index 00000000000000..34fc7963ec6011 --- /dev/null +++ b/app/controllers/profiles/designated_beneficiaries_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Profiles + class DesignatedBeneficiariesController < Profiles::ApplicationController + before_action :find_designated_beneficiary, only: [:update, :destroy] + + feature_category :user_profile + + def create + @designated_beneficiary = current_user.designated_beneficiaries.build(designated_beneficiary_params) + + if @designated_beneficiary.save + flash[:notice] = success_message(@designated_beneficiary.type, "created") + else + flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence + end + + redirect_to profile_account_path + end + + def update + if @designated_beneficiary.update(designated_beneficiary_params) + flash[:notice] = success_message(@designated_beneficiary.type, "updated") + else + flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence + end + + redirect_to profile_account_path + end + + def destroy + type = @designated_beneficiary.type + + if @designated_beneficiary.destroy + flash[:notice] = success_message(type, "deleted") + else + # TODO: edit message + flash[:alert] = s_('Profiles|FAILED MESSAGE TBD') + end + + redirect_to profile_account_path, status: :see_other + end + + private + + def find_designated_beneficiary + @designated_beneficiary = current_user.designated_beneficiaries.find(params.require(:id)) + end + + def designated_beneficiary_params + params.require(:users_designated_beneficiary).permit(:name, :email, :relationship, :type) + end + + # TODO: customize message according to designs + def success_message(type, action) + format(s_('Profiles|Designated account %{type} %{action} successfully.'), type: type, action: action) + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index cd2e538a50e289..cd1e52bba8b485 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2768,6 +2768,16 @@ def composite_identity_enforced! @composite_identity_enforced_override = true end + # For now, we only support max one account manager, but we may change that + def designated_account_manager + designated_beneficiaries.manager.last + end + + # For now, we only support max one account successor, but we may change that + def designated_account_successor + designated_beneficiaries.successor.last + end + protected # override, from Devise::Validatable diff --git a/app/models/users/designated_beneficiary.rb b/app/models/users/designated_beneficiary.rb index 034e8e0b4fd8b7..58586c3012f0b9 100644 --- a/app/models/users/designated_beneficiary.rb +++ b/app/models/users/designated_beneficiary.rb @@ -2,6 +2,29 @@ module Users class DesignatedBeneficiary < ApplicationRecord + # Two types are very identical. The only difference is that successor has mandatory `relationship`. + # It's easier to keep them as a single class until we need deeper customization. + self.inheritance_column = nil # rubocop:disable Database/AvoidInheritanceColumn -- suppress single table inheritance + belongs_to :user + + enum :type, { + manager: 0, + successor: 1 + } + + validates :name, presence: true, length: { maximum: 255 } + validates :email, length: { maximum: 255 }, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true + validates :relationship, length: { maximum: 255 } + validates :relationship, presence: true, if: :successor? + validates :type, presence: true + validates :user_id, uniqueness: { + scope: :type, + message: ->(object, _data) do + # rubocop:disable Layout/LineLength -- This is more readable + format(_("Designated account %{type} already exists. You can edit or delete in the legacy contacts section below."), type: object.type) + # rubocop:enable Layout/LineLength + end + } end end diff --git a/app/views/profiles/accounts/_designated_account_manager.html.haml b/app/views/profiles/accounts/_designated_account_manager.html.haml index a79aae1ef7e474..cfcd8d7482dd59 100644 --- a/app/views/profiles/accounts/_designated_account_manager.html.haml +++ b/app/views/profiles/accounts/_designated_account_manager.html.haml @@ -1,4 +1,3 @@ --# - @designated_account_manager = { name: 'Oliver Jones', email: 'ojones@example.com', delete_path: 'https://todo.com'} - learn_more_link = link_to('', help_page_path('user/profile/account/account_succession.md'), target: '_blank', rel: 'noreferrer') -# rubocop: disable Lint/LiteralAsCondition -- TODO if there is an error, expand/show the form @@ -12,7 +11,11 @@ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do = s_('Profiles|Add account manager') - c.with_form do - = gitlab_ui_form_for '', url: 'todo', html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + - object = @designated_account_manager[:object] || Users::DesignatedBeneficiary.new + - url = @designated_account_manager.present? ? profile_designated_beneficiary_path(@designated_account_manager[:id]) : profile_designated_beneficiaries_path + - method = @designated_account_manager.present? ? :patch : :post + = gitlab_ui_form_for object, url: url, method: method, html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + = f.hidden_field :type, value: 'manager' .form-group = f.label :name, s_('Profiles|Full name'), class: 'label-bold' = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Full name is required.'), required: true @@ -20,7 +23,7 @@ = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do - = s_('Profiles|Add account manager') + = @designated_account_manager.present? ? s_('Profiles|Update account manager') : s_('Profiles|Add account manager') = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do = _('Cancel') - c.with_body do diff --git a/app/views/profiles/accounts/_designated_account_successor.html.haml b/app/views/profiles/accounts/_designated_account_successor.html.haml index 6779469b819c07..0ba559842d9af7 100644 --- a/app/views/profiles/accounts/_designated_account_successor.html.haml +++ b/app/views/profiles/accounts/_designated_account_successor.html.haml @@ -12,18 +12,22 @@ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do = s_('Profiles|Add account successor') - c.with_form do - = gitlab_ui_form_for '', url: 'todo', html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + - object = @designated_account_successor[:object] || Users::DesignatedBeneficiary.new + - url = @designated_account_successor.present? ? profile_designated_beneficiary_path(@designated_account_successor[:id]) : profile_designated_beneficiaries_path + - method = @designated_account_successor.present? ? :patch : :post + = gitlab_ui_form_for object, url: url, method: method, html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| + = f.hidden_field :type, value: 'successor' .form-group = f.label :name, s_('Profiles|Full name'), class: 'label-bold' = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Full name is required.'), required: true .form-group - = f.label :name, s_('Profiles|Relationship with you'), class: 'label-bold' - = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Relationship is required.'), required: true + = f.label :relationship, s_('Profiles|Relationship with you'), class: 'label-bold' + = f.text_field :relationship, class: 'form-control gl-form-input gl-form-input-xl', title: s_('Profiles|Relationship is required.'), required: true .form-group = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do - = s_('Profiles|Add account successor') + = @designated_account_successor.present? ? s_('Profiles|Update account successor') : s_('Profiles|Add account successor') = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do = _('Cancel') - c.with_body do @@ -42,7 +46,7 @@ %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_successor[:email] %td{ data: { label: s_('Profiles|Relationship') } }= @designated_account_successor[:relationship] %td{ data: { label: _('Actions') }, class: '!gl-py-3' } - = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_successor[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account successor? This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }}) + = render Pajamas::ButtonComponent.new(icon: 'remove', href: profile_designated_beneficiary_path(@designated_account_successor[:id]), method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account successor? This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }}) = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) - else = s_('Profiles|No designated account successor assigned.') diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 921df57f53b8b3..ef6ade0e5f2470 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -22,6 +22,8 @@ post :generate_support_pin end + resources :designated_beneficiaries, only: [:create, :update, :destroy], path: 'designated-beneficiaries' + resource :notifications, only: [:show, :update] do scope( path: 'groups/*id', diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 16f8b465c3275b..651178ce6e4648 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23743,6 +23743,9 @@ msgstr "" msgid "DesignManagement|of %{designs_count}" msgstr "" +msgid "Designated account %{type} already exists. You can edit or delete in the legacy contacts section below." +msgstr "" + msgid "Designs" msgstr "" @@ -50388,6 +50391,9 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Designated account %{type} %{action} successfully." +msgstr "" + msgid "Profiles|Designated account manager" msgstr "" @@ -50460,6 +50466,9 @@ msgstr "" msgid "Profiles|Expires" msgstr "" +msgid "Profiles|FAILED MESSAGE TBD" +msgstr "" + msgid "Profiles|Failed to generate new Support PIN." msgstr "" @@ -50703,6 +50712,12 @@ msgstr "" msgid "Profiles|Unverified secondary email addresses are automatically deleted after three days" msgstr "" +msgid "Profiles|Update account manager" +msgstr "" + +msgid "Profiles|Update account successor" +msgstr "" + msgid "Profiles|Update profile settings" msgstr "" -- GitLab From 03bd96ea882b1d254d77557149a92823cbdca89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Wed, 22 Oct 2025 18:19:45 +0200 Subject: [PATCH 3/6] Improve modals --- .../_designated_account_manager.html.haml | 2 +- .../_designated_account_successor.html.haml | 2 +- locale/gitlab.pot | 24 ++++++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/views/profiles/accounts/_designated_account_manager.html.haml b/app/views/profiles/accounts/_designated_account_manager.html.haml index cfcd8d7482dd59..cf28869d30cc86 100644 --- a/app/views/profiles/accounts/_designated_account_manager.html.haml +++ b/app/views/profiles/accounts/_designated_account_manager.html.haml @@ -40,7 +40,7 @@ %td{ data: { label: s_('Profiles|Name') } }= @designated_account_manager[:name] %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_manager[:email] %td{ data: { label: _('Actions') }, class: '!gl-py-3' } - = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_manager[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account manager? This will permanently delete the designated account manager.'), confirm_btn_variant: 'danger' }}) + = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_manager[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account manager?'), confirm: _('This will permanently delete the designated account manager.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account manager') }}) = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) - else = s_('Profiles|No designated account manager assigned.') diff --git a/app/views/profiles/accounts/_designated_account_successor.html.haml b/app/views/profiles/accounts/_designated_account_successor.html.haml index 0ba559842d9af7..abacb0347a3c82 100644 --- a/app/views/profiles/accounts/_designated_account_successor.html.haml +++ b/app/views/profiles/accounts/_designated_account_successor.html.haml @@ -46,7 +46,7 @@ %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_successor[:email] %td{ data: { label: s_('Profiles|Relationship') } }= @designated_account_successor[:relationship] %td{ data: { label: _('Actions') }, class: '!gl-py-3' } - = render Pajamas::ButtonComponent.new(icon: 'remove', href: profile_designated_beneficiary_path(@designated_account_successor[:id]), method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { confirm: _('Delete account successor? This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }}) + = render Pajamas::ButtonComponent.new(icon: 'remove', href: profile_designated_beneficiary_path(@designated_account_successor[:id]), method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account successor?'), confirm: s_('Profiles|This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account successor') }}) = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) - else = s_('Profiles|No designated account successor assigned.') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 651178ce6e4648..148f9174b1777e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22389,12 +22389,6 @@ msgstr "" msgid "Delete account" msgstr "" -msgid "Delete account manager? This will permanently delete the designated account manager." -msgstr "" - -msgid "Delete account successor? This will permanently delete the designated account successor." -msgstr "" - msgid "Delete asset" msgstr "" @@ -50388,6 +50382,18 @@ msgstr "" msgid "Profiles|Delete account" msgstr "" +msgid "Profiles|Delete account manager" +msgstr "" + +msgid "Profiles|Delete account manager?" +msgstr "" + +msgid "Profiles|Delete account successor" +msgstr "" + +msgid "Profiles|Delete account successor?" +msgstr "" + msgid "Profiles|Deleting an account has the following effects:" msgstr "" @@ -50694,6 +50700,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile." msgstr "" +msgid "Profiles|This will permanently delete the designated account successor." +msgstr "" + msgid "Profiles|Time settings" msgstr "" @@ -67739,6 +67748,9 @@ msgstr "" msgid "This vulnerability was automatically resolved because its vulnerability type was disabled in this project or removed from GitLab's default ruleset. For details about SAST rule changes, see https://docs.gitlab.com/ee/user/application_security/sast/rules#important-rule-changes." msgstr "" +msgid "This will permanently delete the designated account manager." +msgstr "" + msgid "This will rebase all commits from the source branch onto the target branch." msgstr "" -- GitLab From 31ed144cd447c6b4f19d95efc653637c2bf2c69f Mon Sep 17 00:00:00 2001 From: Aleksei Lipniagov Date: Thu, 23 Oct 2025 10:36:20 +0200 Subject: [PATCH 4/6] Address review comments 1. Use show_view_variables and local_assigns. 2. Fix CI. 3. Use green flash. 4. Handle concurrent deletions. 5. Customize error messages. --- .../profiles/accounts_controller.rb | 12 ++++++++--- .../designated_beneficiaries_controller.rb | 13 +++++++----- .../_designated_account_manager.html.haml | 18 ++++++++-------- .../_designated_account_successor.html.haml | 21 +++++++++---------- app/views/profiles/accounts/show.html.haml | 4 ++-- locale/gitlab.pot | 5 ++++- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 2839b486e47d90..6d2e7a3ff9b938 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -7,8 +7,7 @@ class Profiles::AccountsController < Profiles::ApplicationController urgency :low, [:show] def show - @designated_account_manager = format_beneficiary_data(current_user.designated_account_manager) - @designated_account_successor = format_beneficiary_data(current_user.designated_account_successor) + render(locals: show_view_variables) end def unlink @@ -39,9 +38,16 @@ def generate_support_pin private + def show_view_variables + { + designated_account_manager: format_beneficiary_data(current_user.designated_account_manager), + designated_account_successor: format_beneficiary_data(current_user.designated_account_successor) + } + end + # TODO: move to helper? def format_beneficiary_data(beneficiary) - return {} unless beneficiary + return unless beneficiary { id: beneficiary.id, diff --git a/app/controllers/profiles/designated_beneficiaries_controller.rb b/app/controllers/profiles/designated_beneficiaries_controller.rb index 34fc7963ec6011..f5d74ce2742f5a 100644 --- a/app/controllers/profiles/designated_beneficiaries_controller.rb +++ b/app/controllers/profiles/designated_beneficiaries_controller.rb @@ -10,7 +10,7 @@ def create @designated_beneficiary = current_user.designated_beneficiaries.build(designated_beneficiary_params) if @designated_beneficiary.save - flash[:notice] = success_message(@designated_beneficiary.type, "created") + flash[:success] = success_message(@designated_beneficiary.type, "created") else flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence end @@ -20,7 +20,7 @@ def create def update if @designated_beneficiary.update(designated_beneficiary_params) - flash[:notice] = success_message(@designated_beneficiary.type, "updated") + flash[:success] = success_message(@designated_beneficiary.type, "updated") else flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence end @@ -32,10 +32,10 @@ def destroy type = @designated_beneficiary.type if @designated_beneficiary.destroy - flash[:notice] = success_message(type, "deleted") + flash[:success] = success_message(type, "deleted") else - # TODO: edit message - flash[:alert] = s_('Profiles|FAILED MESSAGE TBD') + # We don't expect to reach there unless we set up before_destroy callbacks or dependent associations. + flash[:alert] = s_('Profiles|Failed to delete designated account beneficiary.') end redirect_to profile_account_path, status: :see_other @@ -45,6 +45,9 @@ def destroy def find_designated_beneficiary @designated_beneficiary = current_user.designated_beneficiaries.find(params.require(:id)) + rescue ActiveRecord::RecordNotFound # Handle concurrent deletions gracefully (two browser tabs) + flash[:notice] = s_('Profiles|Designated account beneficiary already deleted.') + redirect_to profile_account_path, status: :see_other end def designated_beneficiary_params diff --git a/app/views/profiles/accounts/_designated_account_manager.html.haml b/app/views/profiles/accounts/_designated_account_manager.html.haml index cf28869d30cc86..23d25cb0c5cacd 100644 --- a/app/views/profiles/accounts/_designated_account_manager.html.haml +++ b/app/views/profiles/accounts/_designated_account_manager.html.haml @@ -7,13 +7,13 @@ options: { class: 'js-toggle-container js-token-card' }) do |c| -# rubocop: enable Lint/LiteralAsCondition - c.with_actions do - - if @designated_account_manager.blank? + - if designated_account_manager.blank? = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do = s_('Profiles|Add account manager') - c.with_form do - - object = @designated_account_manager[:object] || Users::DesignatedBeneficiary.new - - url = @designated_account_manager.present? ? profile_designated_beneficiary_path(@designated_account_manager[:id]) : profile_designated_beneficiaries_path - - method = @designated_account_manager.present? ? :patch : :post + - object = designated_account_manager.present? ? designated_account_manager[:object] : Users::DesignatedBeneficiary.new + - url = designated_account_manager.present? ? profile_designated_beneficiary_path(designated_account_manager[:id]) : profile_designated_beneficiaries_path + - method = designated_account_manager.present? ? :patch : :post = gitlab_ui_form_for object, url: url, method: method, html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| = f.hidden_field :type, value: 'manager' .form-group @@ -23,11 +23,11 @@ = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do - = @designated_account_manager.present? ? s_('Profiles|Update account manager') : s_('Profiles|Add account manager') + = designated_account_manager.present? ? s_('Profiles|Update account manager') : s_('Profiles|Add account manager') = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do = _('Cancel') - c.with_body do - - if @designated_account_manager.present? + - if designated_account_manager.present? .table-holder %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' } %thead @@ -37,10 +37,10 @@ %th= _('Actions') %tbody %tr - %td{ data: { label: s_('Profiles|Name') } }= @designated_account_manager[:name] - %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_manager[:email] + %td{ data: { label: s_('Profiles|Name') } }= designated_account_manager[:name] + %td{ data: { label: s_('Profiles|Email address') } }= designated_account_manager[:email] %td{ data: { label: _('Actions') }, class: '!gl-py-3' } - = render Pajamas::ButtonComponent.new(icon: 'remove', href: @designated_account_manager[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account manager?'), confirm: _('This will permanently delete the designated account manager.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account manager') }}) + = render Pajamas::ButtonComponent.new(icon: 'remove', href: designated_account_manager[:delete_path], method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account manager?'), confirm: _('This will permanently delete the designated account manager.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account manager') }}) = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) - else = s_('Profiles|No designated account manager assigned.') diff --git a/app/views/profiles/accounts/_designated_account_successor.html.haml b/app/views/profiles/accounts/_designated_account_successor.html.haml index abacb0347a3c82..0f65225d885f12 100644 --- a/app/views/profiles/accounts/_designated_account_successor.html.haml +++ b/app/views/profiles/accounts/_designated_account_successor.html.haml @@ -1,4 +1,3 @@ --# - @designated_account_successor = { name: 'Oliver Jones', email: 'ojones@example.com', relationship: 'Sister', delete_path: 'https://todo.com'} - learn_more_link = link_to('', help_page_path('user/profile/account/account_succession.md'), target: '_blank', rel: 'noreferrer') -# rubocop: disable Lint/LiteralAsCondition -- TODO if there is an error, expand/show the form @@ -8,13 +7,13 @@ options: { class: 'gl-mt-5 js-toggle-container js-token-card' }) do |c| -# rubocop: enable Lint/LiteralAsCondition - c.with_actions do - - if @designated_account_successor.blank? + - if designated_account_successor.blank? = render Pajamas::ButtonComponent.new(button_options: { class: 'js-toggle-button js-toggle-content' }) do = s_('Profiles|Add account successor') - c.with_form do - - object = @designated_account_successor[:object] || Users::DesignatedBeneficiary.new - - url = @designated_account_successor.present? ? profile_designated_beneficiary_path(@designated_account_successor[:id]) : profile_designated_beneficiaries_path - - method = @designated_account_successor.present? ? :patch : :post + - object = designated_account_successor.present? ? designated_account_successor[:object] : Users::DesignatedBeneficiary.new + - url = designated_account_successor.present? ? profile_designated_beneficiary_path(designated_account_successor[:id]) : profile_designated_beneficiaries_path + - method = designated_account_successor.present? ? :patch : :post = gitlab_ui_form_for object, url: url, method: method, html: { class: 'gl-show-field-errors', autocomplete: 'off', aria: { live: 'assertive' }} do |f| = f.hidden_field :type, value: 'successor' .form-group @@ -27,11 +26,11 @@ = f.label :email, s_('Profiles|Email address (optional)'), class: 'label-bold' = f.email_field :email, class: 'form-control gl-form-input gl-form-input-xl' = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit) do - = @designated_account_successor.present? ? s_('Profiles|Update account successor') : s_('Profiles|Add account successor') + = designated_account_successor.present? ? s_('Profiles|Update account successor') : s_('Profiles|Add account successor') = render Pajamas::ButtonComponent.new(type: :reset, button_options: { class: 'gl-ml-2 js-toggle-button' }) do = _('Cancel') - c.with_body do - - if @designated_account_successor.present? + - if designated_account_successor.present? .table-holder %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' } %thead @@ -42,11 +41,11 @@ %th= _('Actions') %tbody %tr - %td{ data: { label: s_('Profiles|Name') } }= @designated_account_successor[:name] - %td{ data: { label: s_('Profiles|Email address') } }= @designated_account_successor[:email] - %td{ data: { label: s_('Profiles|Relationship') } }= @designated_account_successor[:relationship] + %td{ data: { label: s_('Profiles|Name') } }= designated_account_successor[:name] + %td{ data: { label: s_('Profiles|Email address') } }= designated_account_successor[:email] + %td{ data: { label: s_('Profiles|Relationship') } }= designated_account_successor[:relationship] %td{ data: { label: _('Actions') }, class: '!gl-py-3' } - = render Pajamas::ButtonComponent.new(icon: 'remove', href: profile_designated_beneficiary_path(@designated_account_successor[:id]), method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account successor?'), confirm: s_('Profiles|This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account successor') }}) + = render Pajamas::ButtonComponent.new(icon: 'remove', href: profile_designated_beneficiary_path(designated_account_successor[:id]), method: :delete, button_options: { title: _('Delete'), class: 'has-tooltip', data: { title: s_('Profiles|Delete account successor?'), confirm: s_('Profiles|This will permanently delete the designated account successor.'), confirm_btn_variant: 'danger' }, aria: { label: s_('Profiles|Delete account successor') }}) = render Pajamas::ButtonComponent.new(icon: 'pencil', button_options: { title: _('Edit'), class: 'gl-ml-2 js-toggle-button js-toggle-content has-tooltip' }) - else = s_('Profiles|No designated account successor assigned.') diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 1bd516be2652a9..f636d34160b846 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -79,8 +79,8 @@ = render ::Layouts::SettingsSectionComponent.new(s_('Profiles|Legacy contacts')) do |c| - c.with_body do - = render 'designated_account_manager' - = render 'designated_account_successor' + = render 'designated_account_manager', designated_account_manager: local_assigns[:designated_account_manager] + = render 'designated_account_successor', designated_account_successor: local_assigns[:designated_account_successor] = render ::Layouts::SettingsSectionComponent.new(s_('Profiles|Scheduled pipelines you own')) do |c| - c.with_description do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 148f9174b1777e..ed6ae1a4379e9e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -50400,6 +50400,9 @@ msgstr "" msgid "Profiles|Designated account %{type} %{action} successfully." msgstr "" +msgid "Profiles|Designated account beneficiary already deleted." +msgstr "" + msgid "Profiles|Designated account manager" msgstr "" @@ -50472,7 +50475,7 @@ msgstr "" msgid "Profiles|Expires" msgstr "" -msgid "Profiles|FAILED MESSAGE TBD" +msgid "Profiles|Failed to delete designated account beneficiary." msgstr "" msgid "Profiles|Failed to generate new Support PIN." -- GitLab From aa0f15965642bf6b020ffa8900f5624b3bb146a8 Mon Sep 17 00:00:00 2001 From: Aleksei Lipniagov Date: Thu, 23 Oct 2025 13:37:12 +0200 Subject: [PATCH 5/6] Add proper notifications copy Update success notifications on create, update, destroy. --- .../profiles/accounts_controller.rb | 1 - .../designated_beneficiaries_controller.rb | 42 +++++++++++++------ locale/gitlab.pot | 9 ++-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 6d2e7a3ff9b938..cac11172322b0d 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -45,7 +45,6 @@ def show_view_variables } end - # TODO: move to helper? def format_beneficiary_data(beneficiary) return unless beneficiary diff --git a/app/controllers/profiles/designated_beneficiaries_controller.rb b/app/controllers/profiles/designated_beneficiaries_controller.rb index f5d74ce2742f5a..4eed9e1f5f1ef8 100644 --- a/app/controllers/profiles/designated_beneficiaries_controller.rb +++ b/app/controllers/profiles/designated_beneficiaries_controller.rb @@ -2,6 +2,8 @@ module Profiles class DesignatedBeneficiariesController < Profiles::ApplicationController + include SafeFormatHelper + before_action :find_designated_beneficiary, only: [:update, :destroy] feature_category :user_profile @@ -9,30 +11,30 @@ class DesignatedBeneficiariesController < Profiles::ApplicationController def create @designated_beneficiary = current_user.designated_beneficiaries.build(designated_beneficiary_params) - if @designated_beneficiary.save - flash[:success] = success_message(@designated_beneficiary.type, "created") + if designated_beneficiary.save + flash[:success] = create_update_success_message(designated_beneficiary, "added") else - flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence + flash[:alert] = designated_beneficiary.errors.messages.values.flatten.to_sentence end redirect_to profile_account_path end def update - if @designated_beneficiary.update(designated_beneficiary_params) - flash[:success] = success_message(@designated_beneficiary.type, "updated") + if designated_beneficiary.update(designated_beneficiary_params) + flash[:success] = create_update_success_message(designated_beneficiary, "updated") else - flash[:alert] = @designated_beneficiary.errors.messages.values.flatten.to_sentence + flash[:alert] = designated_beneficiary.errors.messages.values.flatten.to_sentence end redirect_to profile_account_path end def destroy - type = @designated_beneficiary.type + type = designated_beneficiary.type - if @designated_beneficiary.destroy - flash[:success] = success_message(type, "deleted") + if designated_beneficiary.destroy + flash[:notice] = destroy_success_message(type) else # We don't expect to reach there unless we set up before_destroy callbacks or dependent associations. flash[:alert] = s_('Profiles|Failed to delete designated account beneficiary.') @@ -43,6 +45,8 @@ def destroy private + attr_reader :designated_beneficiary + def find_designated_beneficiary @designated_beneficiary = current_user.designated_beneficiaries.find(params.require(:id)) rescue ActiveRecord::RecordNotFound # Handle concurrent deletions gracefully (two browser tabs) @@ -54,9 +58,23 @@ def designated_beneficiary_params params.require(:users_designated_beneficiary).permit(:name, :email, :relationship, :type) end - # TODO: customize message according to designs - def success_message(type, action) - format(s_('Profiles|Designated account %{type} %{action} successfully.'), type: type, action: action) + def destroy_success_message(type) + format(s_('Profiles|Account %{type} deleted successfully.'), type: type) + end + + def create_update_success_message(beneficiary, action) + # TODO: update with actual url when provided! + contact_url = 'https://about.gitlab.com/get-help/' + contact_link = helpers.link_to('', contact_url, target: '_blank', rel: 'noopener noreferrer') + tag_pair_contact_link = tag_pair(contact_link, :link_start, :link_end) + event = beneficiary.manager? ? 'incapacitation' : 'death' # for now, we only support manager and successor + + # rubocop:disable Layout/LineLength -- For readability of copy + safe_format( + s_('Profiles|Account %{type} %{action} successfully. They can %{link_start}contact GitLab%{link_end} to gain access to your account in the event of your %{event}.'), + tag_pair_contact_link.merge(type: beneficiary.type, action: action, event: event) + ) + # rubocop:enable Layout/LineLength end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ed6ae1a4379e9e..ebf74f3860237f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -50280,6 +50280,12 @@ msgstr "" msgid "Profiles|@username" msgstr "" +msgid "Profiles|Account %{type} %{action} successfully. They can %{link_start}contact GitLab%{link_end} to gain access to your account in the event of your %{event}." +msgstr "" + +msgid "Profiles|Account %{type} deleted successfully." +msgstr "" + msgid "Profiles|Account could not be deleted. GitLab was unable to verify your identity." msgstr "" @@ -50397,9 +50403,6 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" -msgid "Profiles|Designated account %{type} %{action} successfully." -msgstr "" - msgid "Profiles|Designated account beneficiary already deleted." msgstr "" -- GitLab From df187613c333533f977bce327af1a1760e9c0041 Mon Sep 17 00:00:00 2001 From: Aleksei Lipniagov Date: Thu, 23 Oct 2025 15:47:55 +0200 Subject: [PATCH 6/6] Customize "too long" input error messages Work in progress. --- app/models/users/designated_beneficiary.rb | 10 +++++++--- locale/gitlab.pot | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/models/users/designated_beneficiary.rb b/app/models/users/designated_beneficiary.rb index 58586c3012f0b9..0515ab4f6a1bca 100644 --- a/app/models/users/designated_beneficiary.rb +++ b/app/models/users/designated_beneficiary.rb @@ -13,9 +13,13 @@ class DesignatedBeneficiary < ApplicationRecord successor: 1 } - validates :name, presence: true, length: { maximum: 255 } - validates :email, length: { maximum: 255 }, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true - validates :relationship, length: { maximum: 255 } + validates :name, presence: true, + length: { maximum: 255, too_long: N_("Full name is too long (maximum is 255 characters)") } + validates :email, length: { maximum: 255, too_long: N_("Email is too long (maximum is 255 characters)") }, + format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true + validates :relationship, + length: { maximum: 255, too_long: N_("Relationship input is too long (maximum is 255 characters)") } + validates :relationship, presence: true, if: :successor? validates :type, presence: true validates :user_id, uniqueness: { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ebf74f3860237f..2bb5c851715b09 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25783,6 +25783,9 @@ msgstr "" msgid "Email exclusion pattern" msgstr "" +msgid "Email is too long (maximum is 255 characters)" +msgstr "" + msgid "Email must be provided." msgstr "" @@ -29161,6 +29164,9 @@ msgstr "" msgid "Full log" msgstr "" +msgid "Full name is too long (maximum is 255 characters)" +msgstr "" + msgid "Fuzz testing" msgstr "" @@ -54021,6 +54027,9 @@ msgstr "" msgid "Relation import tracker cannot be created for project with ongoing import" msgstr "" +msgid "Relationship input is too long (maximum is 255 characters)" +msgstr "" + msgid "Release" msgid_plural "Releases" msgstr[0] "" -- GitLab