Commit 73773204 authored by Kerri Miller's avatar Kerri Miller

Merge branch '324097-fe-cascading-setting-for-delayed-project-deletion' into 'master'

Cascading Setting for "Enable delayed project removal" group setting

See merge request gitlab-org/gitlab!57878
parents e9587bb3 b73353e4
<script>
import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
name: 'LockPopovers',
components: {
GlPopover,
GlSprintf,
GlLink,
},
data() {
return {
targets: [],
};
},
mounted() {
this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-popover-target')].map(
(el) => {
const {
dataset: { popoverData },
} = el;
const {
lockedByAncestor,
lockedByApplicationSetting,
ancestorNamespace,
} = convertObjectPropsToCamelCase(JSON.parse(popoverData || '{}'), { deep: true });
return {
el,
lockedByAncestor,
lockedByApplicationSetting,
ancestorNamespace,
};
},
);
},
};
</script>
<template>
<div>
<template
v-for="(
{ el, lockedByApplicationSetting, lockedByAncestor, ancestorNamespace }, index
) in targets"
>
<gl-popover
v-if="lockedByApplicationSetting || lockedByAncestor"
:key="index"
:target="el"
placement="top"
>
<template #title>{{ s__('CascadingSettings|Setting enforced') }}</template>
<p data-testid="cascading-settings-lock-popover">
<template v-if="lockedByApplicationSetting">{{
s__('CascadingSettings|This setting has been enforced by an instance admin.')
}}</template>
<gl-sprintf
v-else-if="lockedByAncestor && ancestorNamespace"
:message="
s__('CascadingSettings|This setting has been enforced by an owner of %{link}.')
"
>
<template #link>
<gl-link :href="ancestorNamespace.path" class="gl-font-sm">{{
ancestorNamespace.fullName
}}</gl-link>
</template>
</gl-sprintf>
</p>
</gl-popover>
</template>
</div>
</template>
import Vue from 'vue';
import LockPopovers from './components/lock_popovers.vue';
export const initCascadingSettingsLockPopovers = () => {
const el = document.querySelector('.js-cascading-settings-lock-popovers');
if (!el) return false;
return new Vue({
el,
render(createElement) {
return createElement(LockPopovers);
},
});
};
...@@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; ...@@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
import TransferDropdown from '~/groups/transfer_dropdown'; import TransferDropdown from '~/groups/transfer_dropdown';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select'; import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings'; import initSearchSettings from '~/search_settings';
...@@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
projectSelect(); projectSelect();
initSearchSettings(); initSearchSettings();
initCascadingSettingsLockPopovers();
return new TransferDropdown(); return new TransferDropdown();
}); });
...@@ -56,6 +56,33 @@ module NamespacesHelper ...@@ -56,6 +56,33 @@ module NamespacesHelper
namespaces_options(selected, **options) namespaces_options(selected, **options)
end end
def cascading_namespace_settings_enabled?
NamespaceSetting.cascading_settings_feature_enabled?
end
def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper)
locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend
popover_data = {
locked_by_application_setting: group.namespace_settings.public_send("#{attribute}_locked_by_application_setting?"), # rubocop:disable GitlabSecurity/PublicSend
locked_by_ancestor: locked_by_ancestor
}
if locked_by_ancestor
ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend
popover_data[:ancestor_namespace] = {
full_name: ancestor_namespace.full_name,
path: settings_path_helper.call(ancestor_namespace)
}
end
{
popover_data: popover_data.to_json,
testid: 'cascading-settings-lock-icon'
}
end
private private
# Many importers create a temporary Group, so use the real # Many importers create a temporary Group, so use the real
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default? - expanded = expanded_by_default?
= render 'shared/namespaces/cascading_settings/lock_popovers'
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
.settings-header .settings-header
......
...@@ -8,27 +8,27 @@ ...@@ -8,27 +8,27 @@
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f
.form-group.gl-mb-3 .form-group.gl-mb-3
.form-check .gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input'
= f.label :share_with_group_lock, class: 'form-check-label' do = f.label :share_with_group_lock, class: 'custom-control-label' do
%span.d-block %span
- group_link = link_to @group.name, group_path(@group) - group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
%span.js-descr.text-muted= share_with_group_lock_help_text(@group) %p.js-descr.help-text= share_with_group_lock_help_text(@group)
.form-group.gl-mb-3 .form-group.gl-mb-3
.form-check .gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input'
= f.label :emails_disabled, class: 'form-check-label' do = f.label :emails_disabled, class: 'custom-control-label' do
%span.d-block= s_('GroupSettings|Disable email notifications') %span= s_('GroupSettings|Disable email notifications')
%span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
.form-group.gl-mb-3 .form-group.gl-mb-3
.form-check .gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input' = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input'
= f.label :mentions_disabled, class: 'form-check-label' do = f.label :mentions_disabled, class: 'custom-control-label' do
%span.d-block= s_('GroupSettings|Disable group mentions') %span= s_('GroupSettings|Disable group mentions')
%span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group = render 'groups/settings/project_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group = render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
......
- return unless render_setting_to_allow_project_access_token_creation?(group) - return unless render_setting_to_allow_project_access_token_creation?(group)
.form-group.gl-mb-3 .form-group.gl-mb-3
.form-check .gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'form-check-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
= f.label :resource_access_token_creation_allowed, class: 'form-check-label' do = f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do
%span.gl-display-block= s_('GroupSettings|Allow project access token creation') %span= s_('GroupSettings|Allow project access token creation')
- project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens') - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link } - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
%span.text-muted= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } %p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- attribute = local_assigns.fetch(:attribute, nil)
- group = local_assigns.fetch(:group, nil)
- form = local_assigns.fetch(:form, nil)
- return unless attribute && group && form && cascading_namespace_settings_enabled?
- return if group.namespace_settings.public_send("#{attribute}_locked?")
- lock_attribute = "lock_#{attribute}"
.gl-form-checkbox.custom-control.custom-checkbox
= form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
= form.label lock_attribute, class: 'custom-control-label' do
%span= s_('CascadingSettings|Enforce for all subgroups')
%p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.')
- attribute = local_assigns.fetch(:attribute, nil)
- group = local_assigns.fetch(:group, nil)
- form = local_assigns.fetch(:form, nil)
- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
- help_text = local_assigns.fetch(:help_text, nil)
- return unless attribute && group && form && settings_path_helper
- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?")
= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
%span.position-relative.gl-pr-6.gl-display-inline-flex
= yield
- if setting_locked
%button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
type: 'button',
data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
= sprite_icon('lock', size: 16)
- if help_text
%p.help-text
= help_text
...@@ -79,8 +79,12 @@ module EE ...@@ -79,8 +79,12 @@ module EE
params_ee << :allowed_email_domains_list if current_group&.feature_available?(:group_allowed_email_domains) params_ee << :allowed_email_domains_list if current_group&.feature_available?(:group_allowed_email_domains)
params_ee << :max_pages_size if can?(current_user, :update_max_pages_size) params_ee << :max_pages_size if can?(current_user, :update_max_pages_size)
params_ee << :max_personal_access_token_lifetime if current_group&.personal_access_token_expiration_policy_available? params_ee << :max_personal_access_token_lifetime if current_group&.personal_access_token_expiration_policy_available?
params_ee << :delayed_project_removal if current_group&.feature_available?(:adjourned_deletion_for_projects_and_groups)
params_ee << :prevent_forking_outside_group if can_change_prevent_forking?(current_user, current_group) params_ee << :prevent_forking_outside_group if can_change_prevent_forking?(current_user, current_group)
if current_group&.feature_available?(:adjourned_deletion_for_projects_and_groups)
params_ee << :delayed_project_removal
params_ee << :lock_delayed_project_removal
end
end end
end end
......
# frozen_string_literal: true
module Groups
module SettingsHelper
def delayed_project_removal_help_text
html_escape(delayed_project_removal_i18n_string) % {
waiting_period: ::Gitlab::CurrentSettings.deletion_adjourned_period,
link_start: '<a href="%{url}">'.html_safe % { url: general_admin_application_settings_path(anchor: 'js-visibility-settings') },
link_end: '</a>'.html_safe
}
end
private
def delayed_project_removal_i18n_string
if cascading_namespace_settings_enabled?
if current_user&.can_admin_all_resources?
s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings. Inherited by subgroups.')
else
s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. Inherited by subgroups.')
end
else
if current_user&.can_admin_all_resources?
s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings.')
else
s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay.')
end
end
end
end
end
- return unless show_delayed_project_removal_setting?(group) - return unless show_delayed_project_removal_setting?(group)
.form-group.gl-mb-3 .form-group{ data: { testid: 'delayed-project-removal-form-group' } }
.form-check .gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :delayed_project_removal, checked: group.namespace_settings.delayed_project_removal?, class: 'form-check-input' = f.check_box :delayed_project_removal, checked: group.namespace_settings.delayed_project_removal?, disabled: group.namespace_settings.delayed_project_removal_locked?, class: 'custom-control-input', data: { testid: 'delayed-project-removal-checkbox' }
= f.label :delayed_project_removal, class: 'form-check-label' do = render 'shared/namespaces/cascading_settings/setting_label', attribute: :delayed_project_removal,
%span.gl-display-block= s_('GroupSettings|Enable delayed project removal') group: group,
%span.gl-text-gray-400= s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{customization_link} in instance settings').html_safe % { waiting_period: ::Gitlab::CurrentSettings.deletion_adjourned_period, customization_link: link_to('customized by an admin', general_admin_application_settings_path) } form: f,
settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') },
help_text: delayed_project_removal_help_text do
= s_('GroupSettings|Enable delayed project removal')
= render 'shared/namespaces/cascading_settings/enforcement_checkbox', attribute: :delayed_project_removal, group: group, form: f
...@@ -514,8 +514,8 @@ RSpec.describe GroupsController do ...@@ -514,8 +514,8 @@ RSpec.describe GroupsController do
end end
end end
context 'when delayed_project_removal param is specified' do context 'when `delayed_project_removal` and `lock_delayed_project_removal` params are specified' do
let_it_be(:params) { { delayed_project_removal: true } } let_it_be(:params) { { delayed_project_removal: true, lock_delayed_project_removal: true } }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
subject do subject do
...@@ -531,22 +531,24 @@ RSpec.describe GroupsController do ...@@ -531,22 +531,24 @@ RSpec.describe GroupsController do
context 'when feature is available' do context 'when feature is available' do
let(:available) { true } let(:available) { true }
it 'allows storing of setting' do it 'allows storing of settings' do
subject subject
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
expect(group.reload.namespace_settings.delayed_project_removal).to eq(params[:delayed_project_removal]) expect(group.reload.namespace_settings.delayed_project_removal).to eq(params[:delayed_project_removal])
expect(group.reload.namespace_settings.lock_delayed_project_removal).to eq(params[:lock_delayed_project_removal])
end end
end end
context 'when feature is not available' do context 'when feature is not available' do
let(:available) { false } let(:available) { false }
it 'does not allow storing of setting' do it 'does not allow storing of settings' do
subject subject
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
expect(group.reload.namespace_settings.delayed_project_removal).not_to eq(params[:delayed_project_removal]) expect(group.reload.namespace_settings.delayed_project_removal).not_to eq(params[:delayed_project_removal])
expect(group.reload.namespace_settings.lock_delayed_project_removal).not_to eq(params[:lock_delayed_project_removal])
end end
end end
end end
......
...@@ -7,7 +7,7 @@ RSpec.describe 'Edit group settings' do ...@@ -7,7 +7,7 @@ RSpec.describe 'Edit group settings' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group, path: 'foo') } let_it_be(:group) { create(:group, name: 'Foo bar', path: 'foo') }
before_all do before_all do
group.add_owner(user) group.add_owner(user)
...@@ -153,6 +153,23 @@ RSpec.describe 'Edit group settings' do ...@@ -153,6 +153,23 @@ RSpec.describe 'Edit group settings' do
end end
end end
context 'enable delayed project removal' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
let_it_be(:subgroup) { create(:group, parent: group) }
let(:form_group_selector) { '[data-testid="delayed-project-removal-form-group"]' }
let(:setting_field_selector) { '[data-testid="delayed-project-removal-checkbox"]' }
let(:setting_path) { edit_group_path(group, anchor: 'js-permissions-settings') }
let(:group_path) { edit_group_path(group) }
let(:subgroup_path) { edit_group_path(subgroup) }
let(:click_save_button) { save_permissions_group }
it_behaves_like 'a cascading setting'
end
context 'when custom_project_templates feature' do context 'when custom_project_templates feature' do
let!(:subgroup) { create(:group, :public, parent: group) } let!(:subgroup) { create(:group, :public, parent: group) }
let!(:subgroup_1) { create(:group, :public, parent: subgroup) } let!(:subgroup_1) { create(:group, :public, parent: subgroup) }
...@@ -328,4 +345,10 @@ RSpec.describe 'Edit group settings' do ...@@ -328,4 +345,10 @@ RSpec.describe 'Edit group settings' do
end end
end end
end end
def save_permissions_group
page.within('.gs-permissions') do
click_button 'Save changes'
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::SettingsHelper do
include AdminModeHelper
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
describe('#delayed_project_removal_help_text') do
using RSpec::Parameterized::TableSyntax
settings_path = '/admin/application_settings/general#js-visibility-settings'
where(:is_cascading_namespace_settings_enabled, :is_admin, :expected) do
true | true | "Projects will be permanently deleted after a 7-day delay. This delay can be <a href=\"#{settings_path}\">customized by an admin</a> in instance settings. Inherited by subgroups."
true | false | 'Projects will be permanently deleted after a 7-day delay. Inherited by subgroups.'
false | true | "Projects will be permanently deleted after a 7-day delay. This delay can be <a href=\"#{settings_path}\">customized by an admin</a> in instance settings."
false | false | 'Projects will be permanently deleted after a 7-day delay.'
end
with_them do
before do
stub_application_setting(deletion_adjourned_period: 7)
allow(helper).to receive(:general_admin_application_settings_path).with(anchor: 'js-visibility-settings').and_return(settings_path)
stub_feature_flags(cascading_namespace_settings: is_cascading_namespace_settings_enabled)
if is_admin
allow(helper).to receive(:current_user).and_return(admin)
enable_admin_mode!(admin)
else
allow(helper).to receive(:current_user).and_return(user)
end
end
it "returns expected helper text" do
expect(helper.delayed_project_removal_help_text).to eq expected
end
end
end
end
...@@ -5682,6 +5682,21 @@ msgstr "" ...@@ -5682,6 +5682,21 @@ msgstr ""
msgid "Capacity threshold" msgid "Capacity threshold"
msgstr "" msgstr ""
msgid "CascadingSettings|Enforce for all subgroups"
msgstr ""
msgid "CascadingSettings|Setting enforced"
msgstr ""
msgid "CascadingSettings|Subgroups cannot change this setting."
msgstr ""
msgid "CascadingSettings|This setting has been enforced by an instance admin."
msgstr ""
msgid "CascadingSettings|This setting has been enforced by an owner of %{link}."
msgstr ""
msgid "CascadingSettings|cannot be changed because it is locked by an ancestor" msgid "CascadingSettings|cannot be changed because it is locked by an ancestor"
msgstr "" msgstr ""
...@@ -15359,7 +15374,16 @@ msgstr "" ...@@ -15359,7 +15374,16 @@ msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "" msgstr ""
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{customization_link} in instance settings" msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay."
msgstr ""
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. Inherited by subgroups."
msgstr ""
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings."
msgstr ""
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings. Inherited by subgroups."
msgstr "" msgstr ""
msgid "GroupSettings|Select a sub-group as the custom project template source for this group." msgid "GroupSettings|Select a sub-group as the custom project template source for this group."
......
import { GlPopover } from '@gitlab/ui';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import LockPopovers from '~/namespaces/cascading_settings/components/lock_popovers.vue';
describe('LockPopovers', () => {
const mockNamespace = {
full_name: 'GitLab Org / GitLab',
path: '/gitlab-org/gitlab/-/edit',
};
const createPopoverMountEl = ({
lockedByApplicationSetting = false,
lockedByAncestor = false,
}) => {
const popoverMountEl = document.createElement('div');
popoverMountEl.classList.add('js-cascading-settings-lock-popover-target');
const popoverData = {
locked_by_application_setting: lockedByApplicationSetting,
locked_by_ancestor: lockedByAncestor,
};
if (lockedByApplicationSetting) {
popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
} else if (lockedByAncestor) {
popoverMountEl.setAttribute(
'data-popover-data',
JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
);
}
document.body.appendChild(popoverMountEl);
return popoverMountEl;
};
let wrapper;
const createWrapper = () => {
wrapper = mountExtended(LockPopovers);
};
const findPopover = () => extendedWrapper(wrapper.find(GlPopover));
const findByTextInPopover = (text, options) =>
findPopover().findByText((_, element) => element.textContent === text, options);
const expectPopoverMessageExists = (message) => {
expect(findByTextInPopover(message).exists()).toBe(true);
};
const expectCorrectPopoverTarget = (popoverMountEl, popover = findPopover()) => {
expect(popover.props('target')).toEqual(popoverMountEl);
};
afterEach(() => {
document.body.innerHTML = '';
});
describe('when setting is locked by an application setting', () => {
let popoverMountEl;
beforeEach(() => {
popoverMountEl = createPopoverMountEl({ lockedByApplicationSetting: true });
createWrapper();
});
it('displays correct popover message', () => {
expectPopoverMessageExists('This setting has been enforced by an instance admin.');
});
it('sets `target` prop correctly', () => {
expectCorrectPopoverTarget(popoverMountEl);
});
});
describe('when setting is locked by an ancestor namespace', () => {
let popoverMountEl;
beforeEach(() => {
popoverMountEl = createPopoverMountEl({ lockedByAncestor: true });
createWrapper();
});
it('displays correct popover message', () => {
expectPopoverMessageExists(
`This setting has been enforced by an owner of ${mockNamespace.full_name}.`,
);
});
it('displays link to ancestor namespace', () => {
expect(
findByTextInPopover(mockNamespace.full_name, {
selector: `a[href="${mockNamespace.path}"]`,
}).exists(),
).toBe(true);
});
it('sets `target` prop correctly', () => {
expectCorrectPopoverTarget(popoverMountEl);
});
});
describe('when setting is locked by an application setting and an ancestor namespace', () => {
let popoverMountEl;
beforeEach(() => {
popoverMountEl = createPopoverMountEl({
lockedByAncestor: true,
lockedByApplicationSetting: true,
});
createWrapper();
});
it('application setting takes precedence and correct message is shown', () => {
expectPopoverMessageExists('This setting has been enforced by an instance admin.');
});
it('sets `target` prop correctly', () => {
expectCorrectPopoverTarget(popoverMountEl);
});
});
describe('when setting is not locked', () => {
beforeEach(() => {
createPopoverMountEl({
lockedByAncestor: false,
lockedByApplicationSetting: false,
});
createWrapper();
});
it('does not render popover', () => {
expect(findPopover().exists()).toBe(false);
});
});
describe('when there are multiple mount elements', () => {
let popoverMountEl1;
let popoverMountEl2;
beforeEach(() => {
popoverMountEl1 = createPopoverMountEl({ lockedByApplicationSetting: true });
popoverMountEl2 = createPopoverMountEl({ lockedByAncestor: true });
createWrapper();
});
it('mounts multiple popovers', () => {
const popovers = wrapper.findAll(GlPopover).wrappers;
expectCorrectPopoverTarget(popoverMountEl1, popovers[0]);
expectCorrectPopoverTarget(popoverMountEl2, popovers[1]);
});
});
});
...@@ -194,4 +194,75 @@ RSpec.describe NamespacesHelper do ...@@ -194,4 +194,75 @@ RSpec.describe NamespacesHelper do
end end
end end
end end
describe '#cascading_namespace_settings_enabled?' do
subject { helper.cascading_namespace_settings_enabled? }
context 'when `cascading_namespace_settings` feature flag is enabled' do
it 'returns `true`' do
expect(subject).to be(true)
end
end
context 'when `cascading_namespace_settings` feature flag is disabled' do
before do
stub_feature_flags(cascading_namespace_settings: false)
end
it 'returns `false`' do
expect(subject).to be(false)
end
end
end
describe '#cascading_namespace_settings_popover_data' do
attribute = :delayed_project_removal
subject do
helper.cascading_namespace_settings_popover_data(
attribute,
subgroup1,
-> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') }
)
end
context 'when locked by an application setting' do
before do
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_application_setting?").and_return(true)
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_ancestor?").and_return(false)
end
it 'returns expected hash' do
expect(subject).to match({
popover_data: {
locked_by_application_setting: true,
locked_by_ancestor: false
}.to_json,
testid: 'cascading-settings-lock-icon'
})
end
end
context 'when locked by an ancestor namespace' do
before do
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_application_setting?").and_return(false)
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_ancestor?").and_return(true)
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_ancestor").and_return(admin_group.namespace_settings)
end
it 'returns expected hash' do
expect(subject).to match({
popover_data: {
locked_by_application_setting: false,
locked_by_ancestor: true,
ancestor_namespace: {
full_name: admin_group.full_name,
path: edit_group_path(admin_group, anchor: 'js-permissions-settings')
}
}.to_json,
testid: 'cascading-settings-lock-icon'
})
end
end
end
end end
# frozen_string_literal: true
RSpec.shared_examples 'a cascading setting' do
context 'when setting is enforced by an ancestor group' do
before do
visit group_path
page.within form_group_selector do
find(setting_field_selector).check
find('[data-testid="enforce-for-all-subgroups-checkbox"]').check
end
click_save_button
end
it 'disables setting in subgroups' do
visit subgroup_path
expect(find("#{setting_field_selector}[disabled]")).to be_checked
end
it 'does not show enforcement checkbox in subgroups' do
visit subgroup_path
expect(page).not_to have_selector '[data-testid="enforce-for-all-subgroups-checkbox"]'
end
it 'displays lock icon with popover', :js do
visit subgroup_path
page.within form_group_selector do
find('[data-testid="cascading-settings-lock-icon"]').click
end
page.within '[data-testid="cascading-settings-lock-popover"]' do
expect(page).to have_text 'This setting has been enforced by an owner of Foo bar.'
expect(page).to have_link 'Foo bar', href: setting_path
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment