From 3834e6b30ab570edfc44f7f018bedeb0b7269d51 Mon Sep 17 00:00:00 2001 From: Lindsey Shelton <lshelton@gitlab.com> Date: Fri, 2 Aug 2024 18:12:01 +0000 Subject: [PATCH] Lock Tooltip and Button changes Cleanup of branch into singular commit for Lock changes Upgrades LockPopover to LockTooltip Adds new CascadingLockButton Other changes include: Removing BE changes so into new Issue 474535 Put changes into .patch --- .../components/cascading_lock_icon.vue | 60 +++++++++++++ ...ck_popovers.vue => haml_lock_tooltips.vue} | 18 ++-- .../{lock_popover.vue => lock_tooltip.vue} | 14 +-- .../namespaces/cascading_settings/index.js | 8 +- .../javascripts/pages/groups/edit/index.js | 4 +- .../components/project_setting_row.vue | 16 +++- .../permissions/components/settings_panel.vue | 2 +- .../projects/shared/permissions/index.js | 7 +- app/helpers/namespaces_helper.rb | 8 +- app/views/groups/edit.html.haml | 2 +- .../cascading_settings/_lock_icon.html.haml | 2 +- .../_lock_popovers.html.haml | 1 - .../_lock_tooltips.html.haml | 1 + doc/development/cascading_settings.md | 14 +-- locale/gitlab.pot | 3 + .../settings/packages_settings_spec.rb | 20 +++-- .../components/cascading_lock_icon_spec.js | 89 +++++++++++++++++++ ...ers_spec.js => haml_lock_tooltips_spec.js} | 58 ++++++------ ...k_popover_spec.js => lock_tooltip_spec.js} | 50 ++++++----- .../components/project_setting_row_spec.js | 30 +++++-- spec/helpers/namespaces_helper_spec.rb | 8 +- .../cascading_settings_shared_examples.rb | 4 +- 22 files changed, 301 insertions(+), 118 deletions(-) create mode 100644 app/assets/javascripts/namespaces/cascading_settings/components/cascading_lock_icon.vue rename app/assets/javascripts/namespaces/cascading_settings/components/{haml_lock_popovers.vue => haml_lock_tooltips.vue} (75%) rename app/assets/javascripts/namespaces/cascading_settings/components/{lock_popover.vue => lock_tooltip.vue} (86%) delete mode 100644 app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml create mode 100644 app/views/shared/namespaces/cascading_settings/_lock_tooltips.html.haml create mode 100644 spec/frontend/cascading_settings/components/cascading_lock_icon_spec.js rename spec/frontend/cascading_settings/components/{haml_lock_popovers_spec.js => haml_lock_tooltips_spec.js} (56%) rename spec/frontend/cascading_settings/components/{lock_popover_spec.js => lock_tooltip_spec.js} (61%) diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/cascading_lock_icon.vue b/app/assets/javascripts/namespaces/cascading_settings/components/cascading_lock_icon.vue new file mode 100644 index 0000000000000..6cb344ad711d6 --- /dev/null +++ b/app/assets/javascripts/namespaces/cascading_settings/components/cascading_lock_icon.vue @@ -0,0 +1,60 @@ +<script> +import { GlIcon, GlButton } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; +import { s__ } from '~/locale'; +import LockTooltip from './lock_tooltip.vue'; + +export default { + name: 'CascadingLockIcon', + i18n: { + lockIconLabel: s__('CascadingSettings|Lock tooltip icon'), + }, + components: { + GlIcon, + GlButton, + LockTooltip, + }, + props: { + ancestorNamespace: { + type: Object, + required: false, + default: null, + validator: (value) => value?.path && value?.fullName, + }, + isLockedByApplicationSettings: { + type: Boolean, + required: true, + }, + isLockedByGroupAncestor: { + type: Boolean, + required: true, + }, + }, + data() { + return { + targetElement: null, + }; + }, + async mounted() { + // Wait until all children components are mounted + await this.$nextTick(); + this.targetElement = this.$refs[this.$options.refName].$el; + }, + refName: uniqueId('cascading-lock-icon-'), +}; +</script> + +<template> + <span> + <gl-button :ref="$options.refName" class="gl-hover-bg-transparent! !gl-p-0" category="tertiary"> + <gl-icon name="lock" :aria-label="$options.i18n.lockIconLabel" class="!gl-text-gray-400" /> + </gl-button> + <lock-tooltip + v-if="targetElement" + :ancestor-namespace="ancestorNamespace" + :is-locked-by-admin="isLockedByApplicationSettings" + :is-locked-by-group-ancestor="isLockedByGroupAncestor" + :target-element="targetElement" + /> + </span> +</template> diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_popovers.vue b/app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_tooltips.vue similarity index 75% rename from app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_popovers.vue rename to app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_tooltips.vue index 585fdc93bbf1a..6a95c16302471 100644 --- a/app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_popovers.vue +++ b/app/assets/javascripts/namespaces/cascading_settings/components/haml_lock_tooltips.vue @@ -1,17 +1,17 @@ <script> /** * This component is a utility that can be used in a HAML settings pages - * It will get all popover targets and create a popover for each one. + * It will get all tooltip targets and create a tooltip for each one. * This should not be used in Vue Apps as we we are breaking component isolation. - * Instead, use `lock_popover.vue` and provide a list of vue $refs to loop through. + * Instead, use `lock_tooltip.vue` and provide a list of vue $refs to loop through. */ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import LockPopover from './lock_popover.vue'; +import LockTooltip from './lock_tooltip.vue'; export default { - name: 'HamlLockPopovers', + name: 'HamlLockTooltips', components: { - LockPopover, + LockTooltip, }, data() { return { @@ -19,14 +19,14 @@ export default { }; }, mounted() { - this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-popover-target')].map( + this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-tooltip-target')].map( (el) => { const { - dataset: { popoverData }, + dataset: { tooltipData }, } = el; const { lockedByAncestor, lockedByApplicationSetting, ancestorNamespace } = - convertObjectPropsToCamelCase(JSON.parse(popoverData || '{}'), { deep: true }); + convertObjectPropsToCamelCase(JSON.parse(tooltipData || '{}'), { deep: true }); return { el, @@ -42,7 +42,7 @@ export default { <template> <div> - <lock-popover + <lock-tooltip v-for="( { el, lockedByApplicationSetting, lockedByAncestor, ancestorNamespace }, index ) in targets" diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/lock_popover.vue b/app/assets/javascripts/namespaces/cascading_settings/components/lock_tooltip.vue similarity index 86% rename from app/assets/javascripts/namespaces/cascading_settings/components/lock_popover.vue rename to app/assets/javascripts/namespaces/cascading_settings/components/lock_tooltip.vue index 367af68e2d21a..300b980023343 100644 --- a/app/assets/javascripts/namespaces/cascading_settings/components/lock_popover.vue +++ b/app/assets/javascripts/namespaces/cascading_settings/components/lock_tooltip.vue @@ -1,10 +1,10 @@ <script> -import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlTooltip, GlSprintf, GlLink } from '@gitlab/ui'; export default { - name: 'LockPopover', + name: 'LockTooltip', components: { - GlPopover, + GlTooltip, GlSprintf, GlLink, }, @@ -24,8 +24,8 @@ export default { required: true, }, targetElement: { - required: true, type: Element, + required: true, }, }, computed: { @@ -37,9 +37,9 @@ export default { </script> <template> - <gl-popover v-if="isLocked" :target="targetElement" placement="top"> + <gl-tooltip v-if="isLocked" :target="targetElement" placement="top"> <template #title>{{ s__('CascadingSettings|Setting cannot be changed') }}</template> - <span data-testid="cascading-settings-lock-popover"> + <span data-testid="cascading-settings-lock-tooltip"> <template v-if="isLockedByAdmin">{{ s__( 'CascadingSettings|An administrator selected this setting for the instance and you cannot change it.', @@ -61,5 +61,5 @@ export default { }} </template> </span> - </gl-popover> + </gl-tooltip> </template> diff --git a/app/assets/javascripts/namespaces/cascading_settings/index.js b/app/assets/javascripts/namespaces/cascading_settings/index.js index d0368b054e137..af4e2ee100258 100644 --- a/app/assets/javascripts/namespaces/cascading_settings/index.js +++ b/app/assets/javascripts/namespaces/cascading_settings/index.js @@ -1,15 +1,15 @@ import Vue from 'vue'; -import HamlLockPopovers from './components/haml_lock_popovers.vue'; +import HamlLockTooltips from './components/haml_lock_tooltips.vue'; -export const initCascadingSettingsLockPopovers = () => { - const el = document.querySelector('.js-cascading-settings-lock-popovers'); +export const initCascadingSettingsLockTooltips = () => { + const el = document.querySelector('.js-cascading-settings-lock-tooltips'); if (!el) return false; return new Vue({ el, render(createElement) { - return createElement(HamlLockPopovers); + return createElement(HamlLockTooltips); }, }); }; diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 560e41396f19f..853d9fe9191aa 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -4,7 +4,7 @@ import initFilePickers from '~/file_pickers'; import initTransferGroupForm from '~/groups/init_transfer_group_form'; import { initGroupSelects } from '~/vue_shared/components/entity_select/init_group_selects'; import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; -import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; +import { initCascadingSettingsLockTooltips } from '~/namespaces/cascading_settings'; import { initDormantUsersInputSection } from '~/pages/admin/application_settings/account_and_limits'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import initSearchSettings from '~/search_settings'; @@ -42,6 +42,6 @@ initGroupSelects(); initProjectSelects(); initSearchSettings(); -initCascadingSettingsLockPopovers(); +initCascadingSettingsLockTooltips(); initGroupSettingsReadme(); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index cc92a8cd4768a..10a00d37ad395 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -16,17 +16,25 @@ export default { required: false, default: null, }, + locked: { + type: Boolean, + required: false, + default: false, + }, }, }; </script> <template> <div class="project-feature-row"> - <label v-if="label" class="label-bold"> - {{ label }} - </label> + <div class="gl-flex"> + <label v-if="label" class="label-bold" :class="{ 'gl-text-gray-400': locked }"> + {{ label }} + </label> + <slot name="label-icon"></slot> + </div> <div> - <span v-if="helpText" class="text-muted"> {{ helpText }} </span> + <span v-if="helpText" class="gl-text-gray-400"> {{ helpText }} </span> <span v-if="helpPath" ><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a >.</span diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 4ca6defff6cfb..1d10579af5a9d 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -114,7 +114,6 @@ export default { ), }, mixins: [settingsMixin, glFeatureFlagMixin()], - props: { requestCveAvailable: { type: Boolean, @@ -1056,6 +1055,7 @@ export default { :label="$options.i18n.duoLabel" :help-text="$options.i18n.duoHelpText" :help-path="$options.duoHelpPath" + :locked="duoFeaturesLocked" > <gl-toggle v-model="duoFeaturesEnabled" diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js index 78dd456aad93b..a900d028bd8ba 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/index.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; - import { parseBoolean } from '~/lib/utils/common_utils'; import settingsPanel from './components/settings_panel.vue'; @@ -20,12 +19,12 @@ export default function initProjectPermissionsSettings() { const componentProps = JSON.parse(componentPropsEl.innerHTML); const { - targetFormId, additionalInformation, - confirmDangerMessage, confirmButtonText, - showVisibilityConfirmModal, + confirmDangerMessage, htmlConfirmationMessage, + showVisibilityConfirmModal, + targetFormId, phrase: confirmationPhrase, } = mountPoint.dataset; diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index ff5e4248d9844..24172d177c44a 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -5,10 +5,10 @@ def namespace_id_from(params) params.dig(:project, :namespace_id) || params[:namespace_id] end - def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) + def cascading_namespace_settings_tooltip_data(attribute, group, settings_path_helper) locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend - popover_data = { + tooltip_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 } @@ -16,14 +16,14 @@ def cascading_namespace_settings_popover_data(attribute, group, settings_path_he if locked_by_ancestor ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend - popover_data[:ancestor_namespace] = { + tooltip_data[:ancestor_namespace] = { full_name: ancestor_namespace.full_name, path: settings_path_helper.call(ancestor_namespace) } end { - popover_data: popover_data.to_json, + tooltip_data: tooltip_data.to_json, testid: 'cascading-settings-lock-icon' } end diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0e891095d2cfa..f8bce65a39494 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,7 +3,7 @@ - expanded = expanded_by_default? - @force_desktop_expanded_sidebar = true -= render 'shared/namespaces/cascading_settings/lock_popovers' += render 'shared/namespaces/cascading_settings/lock_tooltips' %h1.gl-sr-only= @breadcrumb_title diff --git a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml index b2c13c8b1d78e..3b105bc1e7866 100644 --- a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml +++ b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml @@ -1,3 +1,3 @@ - class_list = local_assigns.fetch(:class_list, '') -= render Pajamas::ButtonComponent.new(category: 'tertiary', icon: 'lock', button_options: { class: "gl-absolute gl-top-3 gl-right-0 -gl-translate-y-1/2 gl-p-1! gl-bg-transparent! gl-cursor-default! js-cascading-settings-lock-popover-target #{class_list}", data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }) += render Pajamas::ButtonComponent.new(category: 'tertiary', icon: 'lock', button_options: { class: "gl-absolute gl-top-3 gl-right-0 -gl-translate-y-1/2 !gl-p-1 !gl-bg-transparent !gl-cursor-default js-cascading-settings-lock-tooltip-target #{class_list}", data: cascading_namespace_settings_tooltip_data(attribute, group, settings_path_helper) }) diff --git a/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml deleted file mode 100644 index 91458bf180b8c..0000000000000 --- a/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml +++ /dev/null @@ -1 +0,0 @@ -.js-cascading-settings-lock-popovers diff --git a/app/views/shared/namespaces/cascading_settings/_lock_tooltips.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_tooltips.html.haml new file mode 100644 index 0000000000000..91aff10a9c55a --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_lock_tooltips.html.haml @@ -0,0 +1 @@ +.js-cascading-settings-lock-tooltips diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md index adadeed2e2426..ab54c3bb161b1 100644 --- a/doc/development/cascading_settings.md +++ b/doc/development/cascading_settings.md @@ -159,15 +159,15 @@ Renders the label for a `fieldset` setting. | `settings_path_helper` | Lambda function that generates a path to the ancestor setting. For example, `-> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') }` | `Lambda` | `true` | | `help_text` | Text shown below the checkbox. | `String` | `false` (`nil`) | -[`_lock_popovers.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml) +[`_lock_tooltips.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/views/shared/namespaces/cascading_settings/_lock_tooltips.html.haml) -Renders the mount element needed to initialize the JavaScript used to display the popover when hovering over the lock icon. This partial is only needed once per page. +Renders the mount element needed to initialize the JavaScript used to display the tooltip when hovering over the lock icon. This partial is only needed once per page. ### JavaScript -[`initCascadingSettingsLockPopovers`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/assets/javascripts/namespaces/cascading_settings/index.js#L4) +[`initCascadingSettingsLockTooltips`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/assets/javascripts/namespaces/cascading_settings/index.js#L4) -Initializes the JavaScript needed to display the popover when hovering over the lock icon (**{lock}**). +Initializes the JavaScript needed to display the tooltip when hovering over the lock icon (**{lock}**). This function should be imported and called in the [page-specific JavaScript](fe_guide/performance.md#page-specific-javascript). ### Put it all together @@ -175,7 +175,7 @@ This function should be imported and called in the [page-specific JavaScript](fe ```haml -# app/views/groups/edit.html.haml -= render 'shared/namespaces/cascading_settings/lock_popovers' += render 'shared/namespaces/cascading_settings/lock_tooltips' - delayed_project_removal_locked = cascading_namespace_setting_locked?(:delayed_project_removal, @group) - merge_method_locked = cascading_namespace_setting_locked?(:merge_method, @group) @@ -222,7 +222,7 @@ This function should be imported and called in the [page-specific JavaScript](fe ```javascript // app/assets/javascripts/pages/groups/edit/index.js -import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; +import { initCascadingSettingsLockTooltips } from '~/namespaces/cascading_settings'; -initCascadingSettingsLockPopovers(); +initCascadingSettingsLockTooltips(); ``` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 946fe9e6a4911..6cd275b949aa0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10530,6 +10530,9 @@ msgstr "" msgid "CascadingSettings|Enforce for all subgroups" msgstr "" +msgid "CascadingSettings|Lock tooltip icon" +msgstr "" + msgid "CascadingSettings|Setting cannot be changed" msgstr "" diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb index 28fa27471ef6d..9ffdd775d69a7 100644 --- a/spec/features/projects/settings/packages_settings_spec.rb +++ b/spec/features/projects/settings/packages_settings_spec.rb @@ -19,15 +19,17 @@ let(:packages_enabled) { true } it 'displays the packages access level setting' do - expect(page).to have_selector('[data-testid="package-registry-access-level"] > label', text: 'Package registry') - expect(page).to have_selector('input[name="package_registry_enabled"]', visible: false) - expect(page).to have_selector('input[name="package_registry_enabled"] + button', visible: true) - expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"]', visible: false) - expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"] + button', visible: true) - expect(page).to have_selector( - 'input[name="project[project_feature_attributes][package_registry_access_level]"]', - visible: false - ) + within_testid('package-registry-access-level') do + expect(page).to have_content('Package registry') + expect(page).to have_selector('input[name="package_registry_enabled"]', visible: false) + expect(page).to have_selector('input[name="package_registry_enabled"] + button', visible: true) + expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"]', visible: false) + expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"] + button', visible: true) + expect(page).to have_selector( + 'input[name="project[project_feature_attributes][package_registry_access_level]"]', + visible: false + ) + end end end diff --git a/spec/frontend/cascading_settings/components/cascading_lock_icon_spec.js b/spec/frontend/cascading_settings/components/cascading_lock_icon_spec.js new file mode 100644 index 0000000000000..55131697cb5c6 --- /dev/null +++ b/spec/frontend/cascading_settings/components/cascading_lock_icon_spec.js @@ -0,0 +1,89 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import CascadingLockIcon from '~/namespaces/cascading_settings/components/cascading_lock_icon.vue'; +import LockTooltip from '~/namespaces/cascading_settings/components/lock_tooltip.vue'; + +describe('CascadingLockIcon', () => { + let wrapper; + + const createComponent = (props = {}) => { + return shallowMount(CascadingLockIcon, { + propsData: { + isLockedByApplicationSettings: false, + isLockedByGroupAncestor: false, + ...props, + }, + }); + }; + + const findLockTooltip = () => wrapper.findComponent(LockTooltip); + const findIcon = () => wrapper.findComponent(GlIcon); + + beforeEach(() => { + wrapper = createComponent(); + }); + + it('renders the GlIcon component', () => { + expect(findIcon().exists()).toBe(true); + }); + + it('sets correct attributes on GlIcon', () => { + wrapper = createComponent(); + expect(findIcon().props()).toMatchObject({ + name: 'lock', + ariaLabel: 'Lock tooltip icon', + }); + }); + + it('does not render LockTooltip when targetElement is null', () => { + wrapper = createComponent(); + expect(findLockTooltip().exists()).toBe(false); + }); + + it('renders LockTooltip after mounting', async () => { + wrapper = createComponent(); + await nextTick(); + await nextTick(); + expect(findLockTooltip().exists()).toBe(true); + }); + + it('sets targetElement after mounting', async () => { + wrapper = createComponent(); + await nextTick(); + await nextTick(); + expect(findLockTooltip().props().targetElement).not.toBeNull(); + }); + + it('passes correct props to LockTooltip', async () => { + const ancestorNamespace = { path: '/test', fullName: 'Test' }; + wrapper = createComponent({ + ancestorNamespace, + isLockedByApplicationSettings: true, + isLockedByGroupAncestor: true, + }); + + await nextTick(); + await nextTick(); + + expect(findLockTooltip().props()).toMatchObject({ + ancestorNamespace, + isLockedByAdmin: true, + isLockedByGroupAncestor: true, + }); + }); + + it('validates ancestorNamespace prop', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Valid prop + createComponent({ ancestorNamespace: { path: '/test', fullName: 'Test' } }); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + // Invalid prop + createComponent({ ancestorNamespace: { path: '/test' } }); + expect(consoleErrorSpy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/spec/frontend/cascading_settings/components/haml_lock_popovers_spec.js b/spec/frontend/cascading_settings/components/haml_lock_tooltips_spec.js similarity index 56% rename from spec/frontend/cascading_settings/components/haml_lock_popovers_spec.js rename to spec/frontend/cascading_settings/components/haml_lock_tooltips_spec.js index 40124f5749ce4..338cca4efb707 100644 --- a/spec/frontend/cascading_settings/components/haml_lock_popovers_spec.js +++ b/spec/frontend/cascading_settings/components/haml_lock_tooltips_spec.js @@ -1,44 +1,44 @@ -import { GlPopover } from '@gitlab/ui'; +import { GlTooltip } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import HamlLockPopover from '~/namespaces/cascading_settings/components/haml_lock_popovers.vue'; -import LockPopover from '~/namespaces/cascading_settings/components/lock_popover.vue'; +import HamlLockTooltips from '~/namespaces/cascading_settings/components/haml_lock_tooltips.vue'; +import LockTooltip from '~/namespaces/cascading_settings/components/lock_tooltip.vue'; -describe('HamlLockPopover', () => { +describe('HamlLockTooltips', () => { const mockNamespace = { fullName: 'GitLab Org / GitLab', path: '/gitlab-org/gitlab/-/edit', }; - const createPopoverMountEl = ({ + const createTooltipMountEl = ({ lockedByApplicationSetting = false, lockedByAncestor = false, }) => { - const popoverMountEl = document.createElement('div'); - popoverMountEl.classList.add('js-cascading-settings-lock-popover-target'); + const tooltipMountEl = document.createElement('div'); + tooltipMountEl.classList.add('js-cascading-settings-lock-tooltip-target'); - const popoverData = { + const tooltipData = { locked_by_application_setting: lockedByApplicationSetting, locked_by_ancestor: lockedByAncestor, }; - popoverMountEl.dataset.popoverData = JSON.stringify(popoverData); - popoverMountEl.dataset.popoverData = JSON.stringify({ - ...popoverData, + tooltipMountEl.dataset.tooltipData = JSON.stringify(tooltipData); + tooltipMountEl.dataset.tooltipData = JSON.stringify({ + ...tooltipData, ancestor_namespace: lockedByAncestor && !lockedByApplicationSetting ? mockNamespace : null, }); - document.body.appendChild(popoverMountEl); + document.body.appendChild(tooltipMountEl); - return popoverMountEl; + return tooltipMountEl; }; let wrapper; const createWrapper = () => { - wrapper = mountExtended(HamlLockPopover); + wrapper = mountExtended(HamlLockTooltips); }; - const findLockPopovers = () => wrapper.findAllComponents(LockPopover); + const findLockTooltips = () => wrapper.findAllComponents(LockTooltip); afterEach(() => { document.body.innerHTML = ''; @@ -57,7 +57,7 @@ describe('HamlLockPopover', () => { 'when locked_by_application_setting is $lockedByApplicationSetting and locked_by_ancestor is $lockedByAncestor and ancestor_namespace is $ancestorNamespace', ({ ancestorNamespace, lockedByAncestor, lockedByApplicationSetting }) => { beforeEach(() => { - domElement = createPopoverMountEl({ + domElement = createTooltipMountEl({ ancestorNamespace, lockedByApplicationSetting, lockedByAncestor, @@ -66,40 +66,40 @@ describe('HamlLockPopover', () => { }); it('locked_by_application_setting attribute', () => { - expect(findLockPopovers().at(0).props().isLockedByAdmin).toBe(lockedByApplicationSetting); + expect(findLockTooltips().at(0).props().isLockedByAdmin).toBe(lockedByApplicationSetting); }); it('locked_by_ancestor attribute', () => { - expect(findLockPopovers().at(0).props().isLockedByGroupAncestor).toBe(lockedByAncestor); + expect(findLockTooltips().at(0).props().isLockedByGroupAncestor).toBe(lockedByAncestor); }); it('ancestor_namespace attribute', () => { - expect(findLockPopovers().at(0).props().ancestorNamespace).toEqual(ancestorNamespace); + expect(findLockTooltips().at(0).props().ancestorNamespace).toEqual(ancestorNamespace); }); it('target element', () => { - expect(findLockPopovers().at(0).props().targetElement).toBe(domElement); + expect(findLockTooltips().at(0).props().targetElement).toBe(domElement); }); }, ); }); describe('when there are multiple mount elements', () => { - let popoverMountEl1; - let popoverMountEl2; + let tooltipMountEl1; + let tooltipMountEl2; beforeEach(() => { - popoverMountEl1 = createPopoverMountEl({ lockedByApplicationSetting: true }); - popoverMountEl2 = createPopoverMountEl({ lockedByAncestor: true }); + tooltipMountEl1 = createTooltipMountEl({ lockedByApplicationSetting: true }); + tooltipMountEl2 = createTooltipMountEl({ lockedByAncestor: true }); createWrapper(); }); - it('mounts multiple popovers', () => { - const popovers = wrapper.findAllComponents(GlPopover).wrappers; + it('mounts multiple tooltips', () => { + const tooltips = wrapper.findAllComponents(GlTooltip).wrappers; - expect(popovers).toHaveLength(2); - expect(popovers[0].props('target')).toBe(popoverMountEl1); - expect(popovers[1].props('target')).toBe(popoverMountEl2); + expect(tooltips).toHaveLength(2); + expect(tooltips[0].props('target')).toBe(tooltipMountEl1); + expect(tooltips[1].props('target')).toBe(tooltipMountEl2); }); }); }); diff --git a/spec/frontend/cascading_settings/components/lock_popover_spec.js b/spec/frontend/cascading_settings/components/lock_tooltip_spec.js similarity index 61% rename from spec/frontend/cascading_settings/components/lock_popover_spec.js rename to spec/frontend/cascading_settings/components/lock_tooltip_spec.js index 516f2875289da..c874afa7287af 100644 --- a/spec/frontend/cascading_settings/components/lock_popover_spec.js +++ b/spec/frontend/cascading_settings/components/lock_tooltip_spec.js @@ -1,8 +1,8 @@ -import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlTooltip, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import LockPopover from '~/namespaces/cascading_settings/components/lock_popover.vue'; +import LockTooltip from '~/namespaces/cascading_settings/components/lock_tooltip.vue'; -describe('LockPopover', () => { +describe('LockTooltip', () => { const mockNamespace = { fullName: 'GitLab Org / GitLab', path: '/gitlab-org/gitlab/-/edit', @@ -12,15 +12,15 @@ describe('LockPopover', () => { 'An administrator selected this setting for the instance and you cannot change it.'; let wrapper; - const popoverMountEl = document.createElement('div'); + const tooltipMountEl = document.createElement('div'); const createWrapper = (props = {}) => { - wrapper = shallowMount(LockPopover, { + wrapper = shallowMount(LockTooltip, { propsData: { ancestorNamespace: mockNamespace, isLockedByAdmin: false, - isLockedByGroupAncestor: true, - targetElement: popoverMountEl, + isLockedByGroupAncestor: false, + targetElement: tooltipMountEl, ...props, }, stubs: { @@ -30,30 +30,33 @@ describe('LockPopover', () => { }; const findLink = () => wrapper.findComponent(GlLink); - const findPopover = () => wrapper.findComponent(GlPopover); + const findTooltip = () => wrapper.findComponent(GlTooltip); describe('when setting is locked by an admin setting', () => { beforeEach(() => { createWrapper({ isLockedByAdmin: true }); }); - it('displays correct popover message', () => { - expect(findPopover().text()).toBe(applicationSettingMessage); + it('displays correct tooltip message', () => { + expect(findTooltip().text()).toBe(applicationSettingMessage); }); it('sets `target` prop correctly', () => { - expect(findPopover().props().target).toBe(popoverMountEl); + expect(findTooltip().props().target).toBe(tooltipMountEl); }); }); describe('when setting is locked by an ancestor namespace', () => { describe('and ancestorNamespace is set', () => { beforeEach(() => { - createWrapper({ isLockedByGroupAncestor: true, ancestorNamespace: mockNamespace }); + createWrapper({ + isLockedByGroupAncestor: true, + ancestorNamespace: mockNamespace, + }); }); - it('displays correct popover message', () => { - expect(findPopover().text()).toBe( + it('displays correct tooltip message', () => { + expect(findTooltip().text()).toBe( `This setting has been enforced by an owner of ${mockNamespace.fullName}.`, ); }); @@ -63,7 +66,7 @@ describe('LockPopover', () => { }); it('sets `target` prop correctly', () => { - expect(findPopover().props().target).toBe(popoverMountEl); + expect(findTooltip().props().target).toBe(tooltipMountEl); }); }); @@ -73,7 +76,7 @@ describe('LockPopover', () => { }); it('displays a generic message', () => { - expect(findPopover().text()).toBe( + expect(findTooltip().text()).toBe( `This setting has been enforced by an owner and cannot be changed.`, ); }); @@ -82,15 +85,18 @@ describe('LockPopover', () => { describe('when setting is locked by an application setting and an ancestor namespace', () => { beforeEach(() => { - createWrapper({ isLockedByAdmin: true, isLockedByGroupAncestor: true }); + createWrapper({ + isLockedByAdmin: true, + isLockedByGroupAncestor: true, + }); }); - it('displays correct popover message', () => { - expect(findPopover().text()).toBe(applicationSettingMessage); + it('displays correct tooltip message', () => { + expect(findTooltip().text()).toBe(applicationSettingMessage); }); it('sets `target` prop correctly', () => { - expect(findPopover().props().target).toBe(popoverMountEl); + expect(findTooltip().props().target).toBe(tooltipMountEl); }); }); @@ -99,8 +105,8 @@ describe('LockPopover', () => { createWrapper({ isLockedByAdmin: false, isLockedByGroupAncestor: false }); }); - it('does not render popover', () => { - expect(findPopover().exists()).toBe(false); + it('does not render tooltip', () => { + expect(findTooltip().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js index 91d3057aec51d..3ff6687d06961 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js @@ -1,22 +1,22 @@ import { shallowMount } from '@vue/test-utils'; - import { nextTick } from 'vue'; +import { GlIcon } from '@gitlab/ui'; import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue'; describe('Project Setting Row', () => { let wrapper; - const mountComponent = (customProps = {}) => { + const createComponent = (customProps = {}) => { const propsData = { ...customProps }; return shallowMount(projectSettingRow, { propsData }); }; beforeEach(() => { - wrapper = mountComponent(); + wrapper = createComponent(); }); it('should show the label if it is set', async () => { - wrapper.setProps({ label: 'Test label' }); + wrapper = createComponent({ label: 'Test label' }); await nextTick(); expect(wrapper.find('label').text()).toEqual('Test label'); @@ -26,8 +26,24 @@ describe('Project Setting Row', () => { expect(wrapper.find('label').exists()).toBe(false); }); + it('should apply gl-text-gray-400 class to label when locked', async () => { + wrapper = createComponent({ label: 'Test label', locked: true }); + + await nextTick(); + expect(wrapper.find('label').classes()).toContain('gl-text-gray-400'); + }); + + it('should render default slot content', () => { + wrapper = shallowMount(projectSettingRow, { + slots: { + 'label-icon': GlIcon, + }, + }); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); + }); + it('should show the help icon with the correct help path if it is set', async () => { - wrapper.setProps({ label: 'Test label', helpPath: '/123' }); + wrapper = createComponent({ label: 'Test label', helpPath: '/123' }); await nextTick(); const link = wrapper.find('a'); @@ -37,14 +53,14 @@ describe('Project Setting Row', () => { }); it('should hide the help icon if no help path is set', async () => { - wrapper.setProps({ label: 'Test label' }); + wrapper = createComponent({ label: 'Test label' }); await nextTick(); expect(wrapper.find('a').exists()).toBe(false); }); it('should show the help text if it is set', async () => { - wrapper.setProps({ helpText: 'Test text' }); + wrapper = createComponent({ helpText: 'Test text' }); await nextTick(); expect(wrapper.find('span').text()).toEqual('Test text'); diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb index 2f90f251a04a9..a888448ad7575 100644 --- a/spec/helpers/namespaces_helper_spec.rb +++ b/spec/helpers/namespaces_helper_spec.rb @@ -42,11 +42,11 @@ user_group.add_owner(user) end - describe '#cascading_namespace_settings_popover_data' do + describe '#cascading_namespace_settings_tooltip_data' do attribute = :math_rendering_limits_enabled subject do - helper.cascading_namespace_settings_popover_data( + helper.cascading_namespace_settings_tooltip_data( attribute, subgroup1, ->(locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') } @@ -61,7 +61,7 @@ it 'returns expected hash' do expect(subject).to match({ - popover_data: { + tooltip_data: { locked_by_application_setting: true, locked_by_ancestor: false }.to_json, @@ -79,7 +79,7 @@ it 'returns expected hash' do expect(subject).to match({ - popover_data: { + tooltip_data: { locked_by_application_setting: false, locked_by_ancestor: true, ancestor_namespace: { diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb index 2bda352c11fc5..14d179b2cdf39 100644 --- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb +++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb @@ -30,14 +30,14 @@ expect(page).not_to have_selector '[data-testid="enforce-for-all-subgroups-checkbox"]' end - it 'displays lock icon with popover', :js do + it 'displays lock icon with tooltip', :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 + page.within '[data-testid="cascading-settings-lock-tooltip"]' 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 -- GitLab