diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-24 18:13:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-24 18:13:02 +0000 |
commit | d48b87d4675d6b8b56dd9b40afa9eb2dce32ad3b (patch) | |
tree | 768c3d0900d3ba2910adf6abb24f433b8585be6c | |
parent | fd9a56d56f84b36779fc4db2da37204c22585fe4 (diff) | |
download | gitlab-ce-d48b87d4675d6b8b56dd9b40afa9eb2dce32ad3b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
57 files changed, 753 insertions, 125 deletions
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index a3d11d90ed2..db47b11cca1 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -68,9 +68,9 @@ export default class EditBlob { blobContent: editorEl.innerText, }); this.editor.use([ + { definition: ToolbarExtension }, { definition: SourceEditorExtension }, { definition: FileTemplateExtension }, - { definition: ToolbarExtension }, ]); fileNameEl.addEventListener('change', () => { diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index 64b6c809870..2a47eef148e 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -166,3 +166,4 @@ export const EXTENSION_MARKDOWN_BUTTONS = [ }, }, ]; +export const EXTENSION_SOFTWRAP_ID = 'soft-wrap'; diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js index 0590bb7455a..622e7117c07 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js +++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js @@ -1,8 +1,11 @@ import { Range } from 'monaco-editor'; +import { __ } from '~/locale'; import { EDITOR_TYPE_CODE, EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS, EXTENSION_BASE_LINE_NUMBERS_CLASS, + EDITOR_TOOLBAR_RIGHT_GROUP, + EXTENSION_SOFTWRAP_ID, } from '../constants'; const hashRegexp = /#?L/g; @@ -24,6 +27,13 @@ export class SourceEditorExtension { return 'BaseExtension'; } + onSetup(instance) { + this.toolbarButtons = []; + if (instance.toolbar) { + this.setupToolbar(instance); + } + } + // eslint-disable-next-line class-methods-use-this onUse(instance) { SourceEditorExtension.highlightLines(instance); @@ -32,6 +42,31 @@ export class SourceEditorExtension { } } + onBeforeUnuse(instance) { + const ids = this.toolbarButtons.map((item) => item.id); + if (instance.toolbar) { + instance.toolbar.removeItems(ids); + } + } + + setupToolbar(instance) { + this.toolbarButtons = [ + { + id: EXTENSION_SOFTWRAP_ID, + label: __('Soft wrap'), + icon: 'soft-wrap', + selected: instance.getOption(116) === 'on', + group: EDITOR_TOOLBAR_RIGHT_GROUP, + category: 'primary', + selectedLabel: __('No wrap'), + selectedIcon: 'soft-unwrap', + class: 'soft-wrap-toggle', + onClick: () => instance.toggleSoftwrap(), + }, + ]; + instance.toolbar.addItems(this.toolbarButtons); + } + static onMouseMoveHandler(e) { const target = e.target.element; if (target.classList.contains(EXTENSION_BASE_LINE_NUMBERS_CLASS)) { @@ -108,6 +143,16 @@ export class SourceEditorExtension { highlightLines(instance, bounds = null) { SourceEditorExtension.highlightLines(instance, bounds); }, + + toggleSoftwrap(instance) { + const isSoftWrapped = instance.getOption(116) === 'on'; + instance.updateOptions({ wordWrap: isSoftWrapped ? 'off' : 'on' }); + if (instance.toolbar) { + instance.toolbar.updateItem(EXTENSION_SOFTWRAP_ID, { + selected: !isSoftWrapped, + }); + } + }, }; } } diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index 42645110e48..91c029081d4 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -1,15 +1,17 @@ <script> -import { GlButton, GlLink, GlIcon } from '@gitlab/ui'; +import { GlButton, GlLink, GlIcon, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import eventHub from '../event_hub'; import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV, TRIGGER_DEFAULT_QA_SELECTOR, + TRIGGER_ELEMENT_WITH_EMOJI, + TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, } from '../constants'; export default { - components: { GlButton, GlLink, GlIcon }, + components: { GlButton, GlLink, GlIcon, GlDropdownItem }, props: { displayText: { type: String, @@ -85,6 +87,8 @@ export default { }, TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV, + TRIGGER_ELEMENT_WITH_EMOJI, + TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, }; </script> @@ -109,6 +113,23 @@ export default { </span> <span class="nav-item-name"> {{ displayText }} </span> </gl-link> + <gl-link + v-else-if="checkTrigger($options.TRIGGER_ELEMENT_WITH_EMOJI)" + v-bind="componentAttributes" + @click="openModal" + > + {{ displayText }} + <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" /> + </gl-link> + <gl-dropdown-item + v-else-if="checkTrigger($options.TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI)" + v-bind="componentAttributes" + button-class="top-nav-menu-item" + @click="openModal" + > + {{ displayText }} + <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" /> + </gl-dropdown-item> <gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal"> {{ displayText }} </gl-link> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index ac0b708c55e..0e0734159bf 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -20,6 +20,9 @@ export const USERS_FILTER_ALL = 'all'; export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id'; export const TRIGGER_ELEMENT_BUTTON = 'button'; export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav'; +export const TOP_NAV_INVITE_MEMBERS_COMPONENT = 'invite_members'; +export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji'; +export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji'; export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal'; export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button'; export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members'); diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue index bfcdcfc7292..2dfd77bc02e 100644 --- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue +++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue @@ -1,5 +1,7 @@ <script> import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants'; export default { components: { @@ -7,6 +9,7 @@ export default { GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader, + InviteMembersTrigger, }, props: { viewModel: { @@ -22,6 +25,11 @@ export default { return this.sections.length > 1; }, }, + methods: { + isInvitedMembers(menuItem) { + return menuItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT; + }, + }, }; </script> @@ -41,7 +49,16 @@ export default { {{ title }} </gl-dropdown-section-header> <template v-for="menuItem in menu_items"> + <invite-members-trigger + v-if="isInvitedMembers(menuItem)" + :key="`${index}_item_${menuItem.id}`" + :trigger-element="`dropdown-${menuItem.data.trigger_element}`" + :display-text="menuItem.title" + :icon="menuItem.icon" + :trigger-source="menuItem.data.trigger_source" + /> <gl-dropdown-item + v-else :key="`${index}_item_${menuItem.id}`" link-class="top-nav-menu-item" :href="menuItem.href" diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js index 8d0105bc681..ec085eae199 100644 --- a/app/assets/javascripts/pages/shared/wikis/wikis.js +++ b/app/assets/javascripts/pages/shared/wikis/wikis.js @@ -16,6 +16,17 @@ export default class Wikis { sidebarToggles[i].addEventListener('click', (e) => this.handleToggleSidebar(e)); } + const listToggles = document.querySelectorAll('.js-wiki-list-toggle'); + + listToggles.forEach((listToggle) => { + listToggle.querySelector('.js-wiki-list-expand-button')?.addEventListener('click', () => { + listToggle.classList.remove('collapsed'); + }); + listToggle.querySelector('.js-wiki-list-collapse-button')?.addEventListener('click', () => { + listToggle.classList.add('collapsed'); + }); + }); + window.addEventListener('resize', () => this.renderSidebar()); this.renderSidebar(); diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss index 9bbea48d2c0..55628603570 100644 --- a/app/assets/stylesheets/page_bundles/wiki.scss +++ b/app/assets/stylesheets/page_bundles/wiki.scss @@ -106,6 +106,23 @@ color: var(--black, $black); } + .active > .wiki-list { + a, + .wiki-list-expand-button, + .wiki-list-collapse-button { + color: var(--black, $black); + } + } + + .wiki-list-expand-button, + .wiki-list-collapse-button { + color: var(--gray-400, $gray-400); + + &:hover { + color: var(--black, $black); + } + } + ul.wiki-pages, ul.wiki-pages li { list-style: none; @@ -118,7 +135,7 @@ } ul.wiki-pages ul { - padding-left: 15px; + padding-left: 20px; } .wiki-sidebar-header { @@ -153,3 +170,28 @@ ul.wiki-pages-list.content-list { .wiki-form .markdown-area { max-height: 55vh; } + +.wiki-list { + .wiki-list-expand-button, + .wiki-list-collapse-button { + left: -$gl-spacing-scale-5; + } + + .wiki-list-expand-button { + display: none; + } + + &.collapsed { + .wiki-list-collapse-button { + display: none; + } + + .wiki-list-expand-button { + display: block; + } + } + + &.collapsed + ul { + display: none; + } +} diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index 98ff48bf19b..89211ed6a3e 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -53,12 +53,7 @@ module Nav menu_items.push(create_epic_menu_item(group)) if can?(current_user, :admin_group_member, group) - menu_items.push( - invite_members_menu_item( - href: group_group_members_path(group), - partial: 'groups/invite_members_top_nav_link' - ) - ) + menu_items.push(invite_members_menu_item(partial: 'groups/invite_members_top_nav_link')) end { @@ -105,12 +100,7 @@ module Nav end if can_admin_project_member?(project) - menu_items.push( - invite_members_menu_item( - href: project_project_members_path(project), - partial: 'projects/invite_members_top_nav_link' - ) - ) + menu_items.push(invite_members_menu_item(partial: 'projects/invite_members_top_nav_link')) end { @@ -161,17 +151,16 @@ module Nav } end - def invite_members_menu_item(href:, partial:) + def invite_members_menu_item(partial:) ::Gitlab::Nav::TopNavMenuItem.build( id: 'invite', title: s_('InviteMember|Invite members'), icon: 'shaking_hands', partial: partial, - href: href, + component: 'invite_members', data: { - track_action: 'click_link_invite_members', - track_label: 'plus_menu_dropdown', - track_property: 'navigation_top' + trigger_source: 'top-nav', + trigger_element: 'text-emoji' } ) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 483bfca259c..adebb91f905 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -108,6 +108,7 @@ class Issue < ApplicationRecord validates :issue_type, presence: true validates :namespace, presence: true validates :work_item_type, presence: true + validates :confidential, inclusion: { in: [true, false], message: 'must be a boolean' } validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed? validate :due_date_after_start_date diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index f122fea6967..86fb2cdf114 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module ResourceAccessTokens class CreateService < BaseService def initialize(current_user, resource, params = {}) @@ -71,21 +73,15 @@ module ResourceAccessTokens end def generate_username - base_username = "#{resource_type}_#{resource.id}_bot" - - uniquify.string(base_username) { |s| User.find_by_username(s) } + username end def generate_email - email_pattern = "#{resource_type}#{resource.id}_bot%s@noreply.#{Gitlab.config.gitlab.host}" - - uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s| - User.find_by_email(s) - end + "#{username}@noreply.#{Gitlab.config.gitlab.host}" end - def uniquify - Gitlab::Utils::Uniquify.new + def username + @username ||= "#{resource_type}_#{resource.id}_bot_#{SecureRandom.hex(8)}" end def create_personal_access_token(user) diff --git a/app/views/groups/_invite_members_top_nav_link.html.haml b/app/views/groups/_invite_members_top_nav_link.html.haml index a3a28ffd6e0..e419c479bca 100644 --- a/app/views/groups/_invite_members_top_nav_link.html.haml +++ b/app/views/groups/_invite_members_top_nav_link.html.haml @@ -1,6 +1,7 @@ -= link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do - = local_assigns.fetch(:display_text) - - if local_assigns.fetch(:icon) - = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe +- data = local_assigns.fetch(:data) +- data[:display_text] = local_assigns.fetch(:display_text) +- data[:icon] = local_assigns.fetch(:icon) + +.js-invite-members-trigger{ data: data } = render 'groups/invite_members_modal', group: local_assigns.fetch(:context) diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml index c133ce21c6c..42c2fd645da 100644 --- a/app/views/layouts/header/_new_dropdown.html.haml +++ b/app/views/layouts/header/_new_dropdown.html.haml @@ -31,8 +31,6 @@ locals: { context: view_model[:context], display_text: menu_item.fetch(:title), icon: menu_item.fetch(:icon), - css_class: menu_item.fetch(:css_class), - href: menu_item.fetch(:href), data: menu_item.fetch(:data) } - else = link_to menu_item.fetch(:title), diff --git a/app/views/projects/_invite_members_top_nav_link.html.haml b/app/views/projects/_invite_members_top_nav_link.html.haml index e3a5c40d93c..d2e68325a09 100644 --- a/app/views/projects/_invite_members_top_nav_link.html.haml +++ b/app/views/projects/_invite_members_top_nav_link.html.haml @@ -1,6 +1,7 @@ -= link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do - = local_assigns.fetch(:display_text) - - if local_assigns.fetch(:icon) - = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe +- data = local_assigns.fetch(:data) +- data[:display_text] = local_assigns.fetch(:display_text) +- data[:icon] = local_assigns.fetch(:icon) + +.js-invite-members-trigger{ data: data } = render 'projects/invite_members_modal', project: local_assigns.fetch(:context) diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 87a6b54d697..367413eb3b5 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -26,15 +26,15 @@ dismiss_key: @project.id, human_access: human_access } } - .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end - - if is_markdown - - unless Feature.enabled?(:source_editor_toolbar, current_user) + - unless Feature.enabled?(:source_editor_toolbar, current_user) + .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end + - if is_markdown = render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false - %span.soft-wrap-toggle - = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do - = _("No wrap") - = render Pajamas::ButtonComponent.new(icon: 'soft-wrap', button_options: { class: 'soft-wrap' }) do - = _("Soft wrap") + %span.soft-wrap-toggle + = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do + = _("No wrap") + = render Pajamas::ButtonComponent.new(icon: 'soft-wrap', button_options: { class: 'soft-wrap' }) do + = _("Soft wrap") .file-editor.code - if Feature.enabled?(:source_editor_toolbar, current_user) diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml index 5c2233a4db2..ced51e1f697 100644 --- a/app/views/shared/wikis/_wiki_directory.html.haml +++ b/app/views/shared/wikis/_wiki_directory.html.haml @@ -1,6 +1,9 @@ %li{ class: active_when(params[:id] == wiki_directory.slug), data: { qa_selector: 'wiki_directory_content' } } - = link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do - = wiki_directory.title + .gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list< + = sprite_icon('chevron-right', css_class: 'js-wiki-list-expand-button wiki-list-expand-button gl-mr-2 gl-cursor-pointer') + = sprite_icon('chevron-down', css_class: 'js-wiki-list-collapse-button wiki-list-collapse-button gl-mr-2 gl-cursor-pointer') + = link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do + = wiki_directory.title %ul - wiki_directory.entries.each do |entry| = render partial: entry.to_partial_path, object: entry, locals: { context: context } diff --git a/config/feature_flags/development/delayed_repository_update_mirror_worker.yml b/config/feature_flags/development/search_index_partitioning_notes.yml index acf5902716e..1abd3be35c5 100644 --- a/config/feature_flags/development/delayed_repository_update_mirror_worker.yml +++ b/config/feature_flags/development/search_index_partitioning_notes.yml @@ -1,8 +1,8 @@ --- -name: delayed_repository_update_mirror_worker -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89501 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362894 -milestone: '15.1' +name: search_index_partitioning_notes +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112402 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/392376 +milestone: '15.10' type: development -group: group::source code +group: group::global search default_enabled: false diff --git a/config/feature_flags/development/user_search_simple_query_string.yml b/config/feature_flags/development/user_search_simple_query_string.yml new file mode 100644 index 00000000000..78230c0fef9 --- /dev/null +++ b/config/feature_flags/development/user_search_simple_query_string.yml @@ -0,0 +1,8 @@ +--- +name: user_search_simple_query_string +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110623 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391955 +milestone: '15.10' +type: development +group: group::global search +default_enabled: false diff --git a/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb b/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb new file mode 100644 index 00000000000..aa3ed4837e7 --- /dev/null +++ b/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class FinalizeNullifyCreatorIdOfOrphanedProjects < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = 'NullifyCreatorIdColumnOfOrphanedProjects' + + def up + ensure_batched_background_migration_is_finished( + job_class_name: MIGRATION, + table_name: :projects, + column_name: :id, + job_arguments: [] + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb b/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb new file mode 100644 index 00000000000..68fd6de3f23 --- /dev/null +++ b/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddForeignKeyOnCreatorIdOnProjects < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :projects, :users, column: :creator_id, on_delete: :nullify, validate: false + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :projects, column: :creator_id + end + end +end diff --git a/db/schema_migrations/20230223065753 b/db/schema_migrations/20230223065753 new file mode 100644 index 00000000000..c1b7927515f --- /dev/null +++ b/db/schema_migrations/20230223065753 @@ -0,0 +1 @@ +789d72eef2573834bef2a2d20070000b580eba069c45f97fdec18a4d5af99605
\ No newline at end of file diff --git a/db/schema_migrations/20230223093704 b/db/schema_migrations/20230223093704 new file mode 100644 index 00000000000..bd35f5c493e --- /dev/null +++ b/db/schema_migrations/20230223093704 @@ -0,0 +1 @@ +39a17836884a6c07ff3f9df6e7328473f1dc2ac2d407f615821d29958f9b1808
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4f8e8f04f6e..d6a51b7a5f7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -33945,6 +33945,9 @@ ALTER TABLE ONLY service_desk_settings ALTER TABLE ONLY design_management_designs_versions ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE; +ALTER TABLE ONLY projects + ADD CONSTRAINT fk_03ec10b0d3 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL NOT VALID; + ALTER TABLE ONLY analytics_dashboards_pointers ADD CONSTRAINT fk_05d96922bd FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md index 57d7880988b..70e12df21f0 100644 --- a/doc/api/graphql/getting_started.md +++ b/doc/api/graphql/getting_started.md @@ -16,9 +16,9 @@ the API itself. The examples documented here can be run using: -- The command line. -- GraphiQL. -- Rails console. +- [Command line](#command-line). +- [GraphiQL](#graphiql). +- [Rails console](#rails-console). ### Command line diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index 00e73d41b46..29ad647f18d 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -135,6 +135,7 @@ POST /projects/import | Attribute | Type | Required | Description | | --------- | -------------- | -------- | ---------------------------------------- | +<!-- markdownlint-disable-next-line gitlab.SentenceSpacing --> | `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.<br/><br/>Requires at least the Maintainer role on the destination group to import to. Using the Developer role for this purpose was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387891) in GitLab 15.8 and will be removed in GitLab 16.0. | | `name` | string | no | The name of the project to be imported. Defaults to the path of the project if not provided | | `file` | string | yes | The file to be uploaded | diff --git a/doc/development/database/batched_background_migrations.md b/doc/development/database/batched_background_migrations.md index 64e34fcc42d..67dccd99a6c 100644 --- a/doc/development/database/batched_background_migrations.md +++ b/doc/development/database/batched_background_migrations.md @@ -747,6 +747,99 @@ You can view failures in two ways: WHERE transition_logs.next_status = '2' AND migration.job_class_name = "CLASS_NAME"; ``` +### Executing a particular batch on the database testing pipeline + +NOTE: +Only [database maintainers](https://gitlab.com/groups/gitlab-org/maintainers/database/-/group_members?with_inherited_permissions=exclude) can view the database testing pipeline artifacts. Ask one for help if you need to use this method. + +Let's assume that a batched background migration failed on a particular batch on GitLab.com and you want to figure out which query failed and why. At the moment, we don't have a good way to retrieve query information (especially the query parameters) and rerunning the entire migration with more logging would be a long process. + +Fortunately you can leverage our [database migration pipeline](database_migration_pipeline.md) to rerun a particular batch with additional logging and/or fix to see if it solves the problem. + +<!-- vale gitlab.Substitutions = NO --> +For an example see [Draft: Test PG::CardinalityViolation fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110910) but make sure to read the entire section. + +To do that, you need to: + +1. Find the batch `start_id` and `end_id` +1. Create a regular migration +1. Apply a workaround for our migration helpers (optional) +1. Start the database migration pipeline + +#### 1. Find the batch `start_id` and `end_id` + +You should be able to find those in [Kibana][#viewing-failure-error-logs]. + +#### 2. Create a regular migration + +Schedule the batch in the `up` block of a regular migration: + +```ruby +def up + instance = Gitlab::BackgroundMigration::YourBackgroundMigrationClass.new( + start_id: <batch start_id>, + end_id: <batch end_id>, + batch_table: <table name>, + batch_column: <batching column>, + sub_batch_size: <sub batch size>, + pause_ms: <miliseconds between batches>, + job_arguments: <job arguments if any>, + connection: connection + ) + + instance.perform +end + + +def down + # no-op +end +``` + +#### 3. Apply a workaround for our migration helpers (optional) + +If your batched background migration touches tables from a schema other than the one you specified by using `restrict_gitlab_migration` helper (example: the scheduling migration has `restrict_gitlab_migration gitlab_schema: :gitlab_main` but the background job uses tables from the `:gitlab_ci` schema) then the migration will fail. To prevent that from happening you'll have to monkey patch database helpers so they don't fail the testing pipeline job: + +1. Add the schema names to [`RestrictGitlabSchema`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb#L57) + +```diff +diff --git a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb +index b8d1d21a0d2d2a23d9e8c8a0a17db98ed1ed40b7..912e20659a6919f771045178c66828563cb5a4a1 100644 +--- a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb ++++ b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb +@@ -55,7 +55,7 @@ def unmatched_schemas + end + + def allowed_schemas_for_connection +- Gitlab::Database.gitlab_schemas_for_connection(connection) ++ Gitlab::Database.gitlab_schemas_for_connection(connection) << :gitlab_ci + end + end + end +``` + +1. Add the schema names to [`RestrictAllowedSchemas`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb#L82) + +```diff +diff --git a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb +index 4ae3622479f0800c0553959e132143ec9051898e..d556ec7f55adae9d46a56665ce02de782cb09f2d 100644 +--- a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb ++++ b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb +@@ -79,7 +79,7 @@ def restrict_to_dml_only(parsed) + tables = self.dml_tables(parsed) + schemas = self.dml_schemas(tables) + +- if (schemas - self.allowed_gitlab_schemas).any? ++ if (schemas - (self.allowed_gitlab_schemas << :gitlab_ci)).any? + raise DMLAccessDeniedError, \ + "Select/DML queries (SELECT/UPDATE/DELETE) do access '#{tables}' (#{schemas.to_a}) " \ + "which is outside of list of allowed schemas: '#{self.allowed_gitlab_schemas}'. " \ +``` + +#### 4. Start the database migration pipeline + +Create a Draft merge request with your changes and trigger the manual `db:gitlabcom-database-testing` job. + ### Adding indexes to support batched background migrations Sometimes it is necessary to add a new or temporary index to support a batched background migration. diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md index b19e431ebc6..aa10bbeda9c 100644 --- a/doc/development/internal_api/index.md +++ b/doc/development/internal_api/index.md @@ -37,13 +37,11 @@ is stored in a file at the path configured in `config/gitlab.yml` by default this is in the root of the rails app named `.gitlab_shell_secret` -To authenticate using that token, clients read the contents of that -file, and include the token Base64 encoded in a `secret_token` parameter -or in the `Gitlab-Shared-Secret` header. +To authenticate using that token, clients: -NOTE: -The internal API used by GitLab Pages, and GitLab agent server (`kas`) uses JSON Web Token (JWT) -authentication, which is different from GitLab Shell. +1. Read the contents of that file. +1. Use the file contents to generate a JSON Web Token (`JWT`). +1. Pass the JWT in the `Gitlab-Shell-Api-Request` header. ## Git Authentication @@ -78,7 +76,7 @@ POST /internal/allowed Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "key_id=11&project=gnuwget/wget2&action=git-upload-pack&protocol=ssh" \ "http://localhost:3001/api/v4/internal/allowed" ``` @@ -128,7 +126,7 @@ information for LFS clients when the repository is accessed over SSH. Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "key_id=11&project=gnuwget/wget2" "http://localhost:3001/api/v4/internal/lfs_authenticate" ``` @@ -148,12 +146,12 @@ curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \ ## Authorized Keys Check This endpoint is called by the GitLab Shell authorized keys -check. Which is called by OpenSSH for +check. Which is called by OpenSSH or GitLab SSHD for [fast SSH key lookup](../../administration/operations/fast_ssh_key_lookup.md). | Attribute | Type | Required | Description | |:----------|:-------|:---------|:------------| -| `key` | string | yes | SSH key as passed by OpenSSH to GitLab Shell | +| `key` | string | yes | An authorized key used for public key authentication. | ```plaintext GET /internal/authorized_keys @@ -162,7 +160,7 @@ GET /internal/authorized_keys Example request: ```shell -curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/authorized_keys?key=<key as passed by OpenSSH>" +curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/authorized_keys?key=<key>" ``` Example response: @@ -197,7 +195,7 @@ GET /internal/discover Example request: ```shell -curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/discover?key_id=7" +curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/discover?key_id=7" ``` Example response: @@ -226,7 +224,7 @@ GET /internal/check Example request: ```shell -curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/check" +curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/check" ``` Example response: @@ -263,7 +261,7 @@ GET /internal/two_factor_recovery_codes Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "key_id=7" "http://localhost:3001/api/v4/internal/two_factor_recovery_codes" ``` @@ -311,7 +309,7 @@ POST /internal/personal_access_token Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "user_id=29&name=mytokenname&scopes[]=read_user&scopes[]=read_repository&expires_at=2020-07-24" \ "http://localhost:3001/api/v4/internal/personal_access_token" ``` @@ -348,7 +346,7 @@ POST /internal/error_tracking/allowed Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "project_id=111&public_key=generated-error-tracking-key" \ "http://localhost:3001/api/v4/internal/error_tracking/allowed" ``` @@ -379,7 +377,7 @@ POST /internal/pre_receive Example request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "gl_repository=project-7" "http://localhost:3001/api/v4/internal/pre_receive" ``` @@ -412,7 +410,7 @@ POST /internal/post_receive Example Request: ```shell -curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ +curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \ --data "gl_repository=project-7" --data "identifier=user-1" \ --data "changes=0000000000000000000000000000000000000000 fd9e76b9136bdd9fe217061b497745792fe5a5ee gh-pages\n" \ "http://localhost:3001/api/v4/internal/post_receive" diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md index dc499b96f3c..29170af74f9 100644 --- a/doc/user/group/settings/group_access_tokens.md +++ b/doc/user/group/settings/group_access_tokens.md @@ -84,8 +84,8 @@ or API. However, administrators can use a workaround: # Set the group group you want to create a token for. For example, group with ID 109. group = Group.find(109) - # Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot2@example.com. - bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot@example.com", user_type: :project_bot }).execute + # Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot_{random_string}@example.com. + bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot_4ffca233d8298ea1@example.com", user_type: :project_bot }).execute # Confirm the group bot. bot.confirm @@ -169,7 +169,11 @@ to groups instead of projects. Bot users for groups: - Do not count as licensed seats. - Can have a maximum role of Owner for a group. For more information, see [Create a group access token](../../../api/group_access_tokens.md#create-a-group-access-token). -- Have a username set to `group_{group_id}_bot` for the first access token. For example, `group_123_bot`. -- Have an email set to `group{group_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `group123_bot@noreply.example.com`. +- Have a username set to `group_{group_id}_bot_{random_string}`. For example, `group_123_bot_4ffca233d8298ea1`. +- Have an email set to `group{group_id}_bot_{random_string}@noreply.{Gitlab.config.gitlab.host}`. For example, `group123_bot_4ffca233d8298ea1@noreply.example.com`. All other properties are similar to [bot users for projects](../../project/settings/project_access_tokens.md#bot-users-for-projects). + +## Token availability + +Group access tokens are only available in paid subscriptions, and not available in trial subscriptions. For more information, see the ["What is included" section of the GitLab Trial FAQ](https://about.gitlab.com/free-trial/#what-is-included-in-my-free-trial-what-is-excluded). diff --git a/doc/user/permissions.md b/doc/user/permissions.md index ca2fdeb98ae..aa292ca18a3 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -511,7 +511,7 @@ To remove a custom role from a group member, use the [Group and Project Members and pass an empty `member_role_id` value. ```shell -curl --request DELETE --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID" +curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID" ``` Now the user is a regular Guest. diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 1862e8d01bc..98017fb5542 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -417,7 +417,7 @@ To set a WIP limit for a list, in an issue board: 1. On the top of the list you want to edit, select **List actions** (**{ellipsis_v}**) **> Edit list settings**. The list settings sidebar opens on the right. -1. Next to **Work in progress Limit**, select **Edit**. +1. Next to **Work in progress limit**, select **Edit**. 1. Enter the maximum number of issues. 1. Press <kbd>Enter</kbd> to save. diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md index ea45de4efc4..8c7c94613a7 100644 --- a/doc/user/project/repository/forking_workflow.md +++ b/doc/user/project/repository/forking_workflow.md @@ -137,3 +137,11 @@ You can unlink your fork from its upstream project in the [advanced settings](.. - GitLab blog post: [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/). - GitLab community forum: [Refreshing a fork](https://forum.gitlab.com/t/refreshing-a-fork/). + +## Troubleshooting + +### An error occurred while forking the project. Please try again + +This error can be due to a mismatch in shared runner settings between the forked project +and the new namespace. See [Forks](../../../ci/runners/configure_runners.md#forks) +in the Runner documentation for more information. diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index 19db5032ea9..4fba6e0d5b8 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -118,12 +118,8 @@ The bot users for projects have [permissions](../../permissions.md#project-membe selected role and [scope](#scopes-for-a-project-access-token) of the project access token. - The name is set to the name of the token. -- The username is set to `project_{project_id}_bot` for the first access token. For example, `project_123_bot`. -- The email is set to `project{project_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `project123_bot@noreply.example.com`. -- For additional access tokens in the same project, the username is set to `project_{project_id}_bot{bot_count}`. For - example, `project_123_bot1`. -- For additional access tokens in the same project, the email is set to `project{project_id}_bot{bot_count}@noreply.{Gitlab.config.gitlab.host}`. - For example, `project123_bot1@noreply.example.com`. +- The username is set to `project_{project_id}_bot_{random_string}`. For example, `project_123_bot_4ffca233d8298ea1`. +- The email is set to `project{project_id}_bot_{random_string}@noreply.{Gitlab.config.gitlab.host}`. For example, `project123_bot_4ffca233d8298ea1@noreply.example.com`. API calls made with a project access token are associated with the corresponding bot user. @@ -140,3 +136,7 @@ When the project access token is [revoked](#revoke-a-project-access-token): - All records are moved to a system-wide user with the username [Ghost User](../../profile/account/delete_account.md#associated-records). See also [Bot users for groups](../../group/settings/group_access_tokens.md#bot-users-for-groups). + +## Token availability + +Project access tokens are only available in paid subscriptions, and not available in trial subscriptions. For more information, see the ["What is included" section of the GitLab Trial FAQ](https://about.gitlab.com/free-trial/#what-is-included-in-my-free-trial-what-is-excluded). diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 88e99b29587..d033913aa71 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -99,7 +99,7 @@ module API optional :add_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :remove_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' - optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' + optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential', allow_blank: false optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}" diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb index 52c87dda347..a20fec4b2ba 100644 --- a/lib/gitlab/github_import/importer/label_links_importer.rb +++ b/lib/gitlab/github_import/importer/label_links_importer.rb @@ -25,6 +25,8 @@ module Gitlab items = [] target_id = find_target_id + return if target_id.blank? + issue.label_names.each do |label_name| # Although unlikely it's technically possible for an issue to be # given a label that was created and assigned after we imported all diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb index af88f55014c..a83cdbe15df 100644 --- a/lib/gitlab/nav/top_nav_menu_item.rb +++ b/lib/gitlab/nav/top_nav_menu_item.rb @@ -10,7 +10,7 @@ module Gitlab # rubocop: disable Metrics/ParameterLists def self.build( id:, title:, active: false, icon: '', href: '', view: '', - css_class: nil, data: nil, partial: nil + css_class: nil, data: nil, partial: nil, component: nil ) { id: id, @@ -22,7 +22,8 @@ module Gitlab view: view.to_s, css_class: css_class, data: data || { qa_selector: 'menu_item_link', qa_title: title }, - partial: partial + partial: partial, + component: component } end # rubocop: enable Metrics/ParameterLists diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8eabffd78b7..800abad7e33 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -48616,7 +48616,7 @@ msgstr "" msgid "Work in progress (open and unassigned)" msgstr "" -msgid "Work in progress Limit" +msgid "Work in progress limit" msgstr "" msgid "WorkItem|%{count} more assignees" diff --git a/package.json b/package.json index 7c4b4e2542b..e5081047ee8 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@gitlab/svgs": "3.21.0", "@gitlab/ui": "56.1.2", "@gitlab/visual-review-tools": "1.7.3", - "@gitlab/web-ide": "0.0.1-dev-20230216131813", + "@gitlab/web-ide": "0.0.1-dev-20230223005157", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", "@sourcegraph/code-host-integration": "0.0.84", diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 6a7539aeb2f..756cb23b7a4 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -95,7 +95,7 @@ RSpec.describe 'Database schema', feature_category: :database do project_error_tracking_settings: %w[sentry_project_id], project_group_links: %w[group_id], project_statistics: %w[namespace_id], - projects: %w[creator_id ci_id mirror_user_id], + projects: %w[ci_id mirror_user_id], redirect_routes: %w[source_id], repository_languages: %w[programming_language_id], routes: %w[source_id], diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index e8d58656178..8395124cbe4 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -14,13 +14,13 @@ ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true' # Enable zero monkey patching mode before loading any other RSpec code. RSpec.configure(&:disable_monkey_patching!) +require 'active_support/all' require_relative 'rails_autoload' require_relative '../config/settings' require_relative 'support/rspec' require_relative '../lib/gitlab/utils' require_relative '../lib/gitlab/utils/strong_memoize' -require 'active_support/all' require_relative 'simplecov_env' SimpleCovEnv.start! diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb index 56f9d373f00..f1862a3b9b2 100644 --- a/spec/features/nav/top_nav_responsive_spec.rb +++ b/spec/features/nav/top_nav_responsive_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'top nav responsive', :js, feature_category: :navigation do include MobileHelpers + include Spec::Support::Helpers::Features::InviteMembersModalHelper let_it_be(:user) { create(:user) } @@ -61,10 +62,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do visit project_path(project) end - it 'the add menu contains invite members dropdown option and goes to the members page' do + it 'the add menu contains invite members dropdown option and opens invite modal' do invite_members_from_menu - expect(page).to have_current_path(project_project_members_path(project)) + page.within invite_modal_selector do + expect(page).to have_content("You're inviting members to the #{project.name} project") + end end end @@ -75,10 +78,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do visit group_path(group) end - it 'the add menu contains invite members dropdown option and goes to the members page' do + it 'the add menu contains invite members dropdown option and opens invite modal' do invite_members_from_menu - expect(page).to have_current_path(group_group_members_path(group)) + page.within invite_modal_selector do + expect(page).to have_content("You're inviting members to the #{group.name} group") + end end end @@ -86,7 +91,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do click_button('Menu') create_new_button.click - click_link('Invite members') + click_button('Invite members') end def create_new_button diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb index cc20b626e30..d2c0286cb4d 100644 --- a/spec/features/nav/top_nav_spec.rb +++ b/spec/features/nav/top_nav_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'top nav responsive', :js, feature_category: :navigation do + include Spec::Support::Helpers::Features::InviteMembersModalHelper + let_it_be(:user) { create(:user) } before do @@ -16,10 +18,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do visit project_path(project) end - it 'the add menu contains invite members dropdown option and goes to the members page' do + it 'the add menu contains invite members dropdown option and opens invite modal' do invite_members_from_menu - expect(page).to have_current_path(project_project_members_path(project)) + page.within invite_modal_selector do + expect(page).to have_content("You're inviting members to the #{project.name} project") + end end end @@ -30,10 +34,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do visit group_path(group) end - it 'the add menu contains invite members dropdown option and goes to the members page' do + it 'the add menu contains invite members dropdown option and opens invite modal' do invite_members_from_menu - expect(page).to have_current_path(group_group_members_path(group)) + page.within invite_modal_selector do + expect(page).to have_content("You're inviting members to the #{group.name} group") + end end end diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index dda46e97b85..9ab20fc2cd7 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -20,9 +20,9 @@ jest.mock('~/editor/extensions/source_editor_toolbar_ext'); const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown'; const defaultExtensions = [ + { definition: ToolbarExtension }, { definition: SourceEditorExtension }, { definition: FileTemplateExtension }, - { definition: ToolbarExtension }, ]; const markdownExtensions = [ { definition: EditorMarkdownExtension }, diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js index eab39ccaba1..b1b8173188c 100644 --- a/spec/frontend/editor/source_editor_extension_base_spec.js +++ b/spec/frontend/editor/source_editor_extension_base_spec.js @@ -7,6 +7,7 @@ import { EDITOR_TYPE_DIFF, EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS, EXTENSION_BASE_LINE_NUMBERS_CLASS, + EXTENSION_SOFTWRAP_ID, } from '~/editor/constants'; import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import EditorInstance from '~/editor/source_editor_instance'; @@ -35,8 +36,18 @@ describe('The basis for an Source Editor extension', () => { }, }; }; - const createInstance = (baseInstance = {}) => { - return new EditorInstance(baseInstance); + const baseInstance = { + getOption: jest.fn(), + }; + + const createInstance = (base = baseInstance) => { + return new EditorInstance(base); + }; + + const toolbar = { + addItems: jest.fn(), + updateItem: jest.fn(), + removeItems: jest.fn(), }; beforeEach(() => { @@ -49,6 +60,66 @@ describe('The basis for an Source Editor extension', () => { resetHTMLFixture(); }); + describe('onSetup callback', () => { + let instance; + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + }); + + it('adds correct buttons to the toolbar', () => { + instance.use({ definition: SourceEditorExtension }); + expect(instance.toolbar.addItems).toHaveBeenCalledWith([ + expect.objectContaining({ + id: EXTENSION_SOFTWRAP_ID, + }), + ]); + }); + + it('does not fail if toolbar is not available', () => { + instance.toolbar = null; + expect(() => instance.use({ definition: SourceEditorExtension })).not.toThrow(); + }); + + it.each` + optionValue | expectSelected + ${'on'} | ${true} + ${'off'} | ${false} + ${'foo'} | ${false} + ${undefined} | ${false} + ${null} | ${false} + `( + 'correctly sets the initial state of the button when wordWrap option is "$optionValue"', + ({ optionValue, expectSelected }) => { + instance.getOption.mockReturnValue(optionValue); + instance.use({ definition: SourceEditorExtension }); + expect(instance.toolbar.addItems).toHaveBeenCalledWith([ + expect.objectContaining({ + selected: expectSelected, + }), + ]); + }, + ); + }); + + describe('onBeforeUnuse', () => { + let instance; + let extension; + + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + extension = instance.use({ definition: SourceEditorExtension }); + }); + it('removes the registered buttons from the toolbar', () => { + expect(instance.toolbar.removeItems).not.toHaveBeenCalled(); + instance.unuse(extension); + expect(instance.toolbar.removeItems).toHaveBeenCalledWith([EXTENSION_SOFTWRAP_ID]); + }); + }); + describe('onUse callback', () => { it('initializes the line highlighting', () => { const instance = createInstance(); @@ -66,6 +137,7 @@ describe('The basis for an Source Editor extension', () => { '$description the line linking for $instanceType instance', ({ instanceType, shouldBeCalled }) => { const instance = createInstance({ + ...baseInstance, getEditorType: jest.fn().mockReturnValue(instanceType), onMouseMove: jest.fn(), onMouseDown: jest.fn(), @@ -82,10 +154,44 @@ describe('The basis for an Source Editor extension', () => { ); }); + describe('toggleSoftwrap', () => { + let instance; + + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + instance.use({ definition: SourceEditorExtension }); + }); + + it.each` + currentWordWrap | newWordWrap | expectSelected + ${'on'} | ${'off'} | ${false} + ${'off'} | ${'on'} | ${true} + ${'foo'} | ${'on'} | ${true} + ${undefined} | ${'on'} | ${true} + ${null} | ${'on'} | ${true} + `( + 'correctly updates wordWrap option in editor and the state of the button when currentWordWrap is "$currentWordWrap"', + ({ currentWordWrap, newWordWrap, expectSelected }) => { + instance.getOption.mockReturnValue(currentWordWrap); + instance.updateOptions = jest.fn(); + instance.toggleSoftwrap(); + expect(instance.updateOptions).toHaveBeenCalledWith({ + wordWrap: newWordWrap, + }); + expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, { + selected: expectSelected, + }); + }, + ); + }); + describe('highlightLines', () => { const revealSpy = jest.fn(); const decorationsSpy = jest.fn(); const instance = createInstance({ + ...baseInstance, revealLineInCenter: revealSpy, deltaDecorations: decorationsSpy, }); @@ -174,6 +280,7 @@ describe('The basis for an Source Editor extension', () => { beforeEach(() => { instance = createInstance({ + ...baseInstance, deltaDecorations: decorationsSpy, lineDecorations, }); @@ -188,6 +295,7 @@ describe('The basis for an Source Editor extension', () => { describe('setupLineLinking', () => { const instance = { + ...baseInstance, onMouseMove: jest.fn(), onMouseDown: jest.fn(), deltaDecorations: jest.fn(), diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index c522abe63c5..e5af38d6ed8 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlLink, GlIcon } from '@gitlab/ui'; +import { GlButton, GlLink, GlIcon, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import eventHub from '~/invite_members/event_hub'; @@ -6,7 +6,10 @@ import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV, TRIGGER_DEFAULT_QA_SELECTOR, + TRIGGER_ELEMENT_WITH_EMOJI, + TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, } from '~/invite_members/constants'; +import { GlEmoji } from '../mock_data/member_modal'; jest.mock('~/experimentation/experiment_tracking'); @@ -20,6 +23,8 @@ const triggerComponent = { button: GlButton, anchor: GlLink, 'side-nav': GlLink, + 'text-emoji': GlLink, + 'dropdown-text-emoji': GlDropdownItem, }; const createComponent = (props = {}) => { @@ -29,6 +34,9 @@ const createComponent = (props = {}) => { ...triggerProps, ...props, }, + stubs: { + GlEmoji, + }, }); }; @@ -43,6 +51,10 @@ const triggerItems = [ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus', }, + { + triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, + icon: 'shaking_hands', + }, ]; describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { @@ -119,3 +131,25 @@ describe('side-nav with icon', () => { expect(findIcon().props('name')).toBe('plus'); }); }); + +describe('link with emoji', () => { + it('includes the specified icon with correct size when triggerElement is link', () => { + const findEmoji = () => wrapper.findComponent(GlEmoji); + + createComponent({ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, icon: 'shaking_hands' }); + + expect(findEmoji().exists()).toBe(true); + expect(findEmoji().attributes('data-name')).toBe('shaking_hands'); + }); +}); + +describe('dropdown item with emoji', () => { + it('includes the specified icon with correct size when triggerElement is link', () => { + const findEmoji = () => wrapper.findComponent(GlEmoji); + + createComponent({ triggerElement: TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, icon: 'shaking_hands' }); + + expect(findEmoji().exists()).toBe(true); + expect(findEmoji().attributes('data-name')).toBe('shaking_hands'); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js index 18210658b89..533650cd149 100644 --- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js +++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js @@ -1,6 +1,8 @@ import { GlDropdown } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants'; const TEST_VIEW_MODEL = { title: 'Dropdown', @@ -18,6 +20,16 @@ const TEST_VIEW_MODEL = { menu_items: [ { id: 'bar-1', title: 'Bar 1', href: '/bar/1' }, { id: 'bar-2', title: 'Bar 2', href: '/bar/2' }, + { + id: 'invite', + title: '_invite members title_', + component: TOP_NAV_INVITE_MEMBERS_COMPONENT, + icon: '_icon_', + data: { + trigger_element: '_trigger_element_', + trigger_source: '_trigger_source_', + }, + }, ], }, ], @@ -36,6 +48,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }; const findDropdown = () => wrapper.findComponent(GlDropdown); + const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); const findDropdownContents = () => findDropdown() .findAll('[data-testid]') @@ -73,6 +86,10 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }); it('renders dropdown content', () => { + const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) => + Boolean(item.href), + ); + expect(findDropdownContents()).toEqual([ { type: 'header', @@ -90,12 +107,18 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { type: 'header', text: TEST_VIEW_MODEL.menu_sections[1].title, }, - ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({ + ...hrefItems.map(({ title, href }) => ({ type: 'item', href, text: title, })), ]); + expect(findInviteMembersTrigger().props()).toMatchObject({ + displayText: '_invite members title_', + icon: '_icon_', + triggerElement: 'dropdown-_trigger_element_', + triggerSource: '_trigger_source_', + }); }); }); diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 1df0ee8c18a..174a5a668a8 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -35,12 +35,11 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do id: 'invite', title: 'Invite members', icon: 'shaking_hands', - href: expected_href, partial: partial, + component: 'invite_members', data: { - track_action: 'click_link_invite_members', - track_label: 'plus_menu_dropdown', - track_property: 'navigation_top' + trigger_source: 'top-nav', + trigger_element: 'text-emoji' } ) ) diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index fa4d4071e37..aa6b1dba6e8 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -170,8 +170,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do items: array_including( { href: "/projects/new", text: "New project/repository" }, { href: "/groups/new#create-group-pane", text: "New subgroup" }, - { href: "/groups/#{group.full_path}/-/group_members", - text: "Invite members" } + { href: '', text: "Invite members" } ) ), a_hash_including( diff --git a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb index a8574411957..f671a673a08 100644 --- a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects do +RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects, + schema: 20230130073109 do let(:users) { table(:users) } let(:projects) { table(:projects) } let(:namespaces) { table(:namespaces) } diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb index e005d8eda84..16816dfbcea 100644 --- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb @@ -44,6 +44,10 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do end it 'does not insert label links for non-existing labels' do + expect(importer) + .to receive(:find_target_id) + .and_return(4) + expect(importer.label_finder) .to receive(:id_for) .with('bug') @@ -55,6 +59,20 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do importer.create_labels end + + it 'does not insert label links for non-existing targets' do + expect(importer) + .to receive(:find_target_id) + .and_return(nil) + + expect(importer.label_finder) + .not_to receive(:id_for) + + expect(LabelLink) + .not_to receive(:bulk_insert!) + + importer.create_labels + end end describe '#find_target_id' do diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb index 3377da5ca3a..1d3452a004a 100644 --- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb +++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb @@ -14,7 +14,8 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem, feature_category: :navigation do view: 'view', css_class: 'css_class', data: {}, - partial: 'groups/some_view_partial_file' + partial: 'groups/some_view_partial_file', + component: '_some_component_used_as_a_trigger_for_frontend_dropdown_item_render_' } expect(described_class.build(**item)).to eq(item.merge(type: :item)) diff --git a/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb new file mode 100644 index 00000000000..9163c30fe30 --- /dev/null +++ b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe FinalizeNullifyCreatorIdOfOrphanedProjects, :migration, feature_category: :projects do + let(:batched_migrations) { table(:batched_background_migrations) } + let(:batch_failed_status) { 2 } + let(:batch_finalized_status) { 3 } + + let!(:migration) { described_class::MIGRATION } + + describe '#up' do + shared_examples 'finalizes the migration' do + it 'finalizes the migration' do + expect do + migrate! + + migration_record.reload + failed_job.reload + end.to change { migration_record.status }.from(migration_record.status).to(3).and( + change { failed_job.status }.from(batch_failed_status).to(batch_finalized_status) + ) + end + end + + context 'when migration is missing' do + it 'warns migration not found' do + expect(Gitlab::AppLogger) + .to receive(:warn).with(/Could not find batched background migration for the given configuration:/) + + migrate! + end + end + + context 'with migration present' do + let!(:migration_record) do + batched_migrations.create!( + job_class_name: 'NullifyCreatorIdColumnOfOrphanedProjects', + table_name: :projects, + column_name: :id, + job_arguments: [], + interval: 2.minutes, + min_value: 1, + max_value: 2, + batch_size: 1000, + sub_batch_size: 500, + max_batch_size: 5000, + gitlab_schema: :gitlab_main, + status: 3 # finished + ) + end + + context 'when migration finished successfully' do + it 'does not raise exception' do + expect { migrate! }.not_to raise_error + end + end + + context 'with different migration statuses', :redis do + using RSpec::Parameterized::TableSyntax + + where(:status, :description) do + 0 | 'paused' + 1 | 'active' + 4 | 'failed' + 5 | 'finalizing' + end + + with_them do + let!(:failed_job) do + table(:batched_background_migration_jobs).create!( + batched_background_migration_id: migration_record.id, + status: batch_failed_status, + min_value: 1, + max_value: 10, + attempts: 2, + batch_size: 100, + sub_batch_size: 10 + ) + end + + before do + migration_record.update!(status: status) + end + + it_behaves_like 'finalizes the migration' + end + end + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index e29318a7e83..d00f2f29fed 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -70,6 +70,11 @@ RSpec.describe Issue, feature_category: :team_planning do end describe 'validations' do + it { is_expected.not_to allow_value(nil).for(:confidential) } + it { is_expected.to allow_value(true, false).for(:confidential) } + end + + describe 'custom validations' do subject(:valid?) { issue.valid? } describe 'due_date_after_start_date' do diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 4b60eaadcbc..078f00334c3 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -1139,6 +1139,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['issue_type']).to eq('issue') end + context 'when confidential is null' do + it 'responds with 400 error' do + post api("/projects/#{project.id}/issues", user), params: { title: 'issue', confidential: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('confidential is empty') + end + end + context 'when issue create service returns an unrecoverable error' do before do allow_next_instance_of(Issues::CreateService) do |create_service| diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 442232920f9..3b6f2bc1368 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ResourceAccessTokens::CreateService do +RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_access do subject { described_class.new(user, resource, params).execute } let_it_be(:user) { create(:user) } @@ -99,6 +99,22 @@ RSpec.describe ResourceAccessTokens::CreateService do expect(access_token.user.email).to end_with("@noreply.#{Gitlab.config.gitlab.host}") end + + it 'contains SecureRandom part' do + expect(SecureRandom).to receive(:hex).at_least(:once).and_return('randomhex') + response = subject + access_token = response.payload[:access_token] + + expect(access_token.user.email).to include('_randomhex@noreply') + end + + it 'email is the same as username' do + expect(SecureRandom).to receive(:hex).at_least(:once).and_return('randomhex') + response = subject + access_token = response.payload[:access_token] + + expect(access_token.user.email).to include(access_token.user.username) + end end context 'access level' do diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb index 639eb3f2b99..b3378c76658 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -84,6 +84,30 @@ RSpec.shared_examples 'User views wiki sidebar' do expect(page).not_to have_link('View All Pages') end + it 'shows all collapse buttons in the sidebar' do + visit wiki_path(wiki) + + within('.right-sidebar') do + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + end + end + + it 'collapses/expands children when click collapse/expand button in the sidebar', :js do + visit wiki_path(wiki) + + within('.right-sidebar') do + first("[data-testid='chevron-down-icon']").click + (11..15).each { |i| expect(page).not_to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1) + + first("[data-testid='chevron-right-icon']").click + (11..15).each { |i| expect(page).to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0) + end + end + context 'when there are more than 15 existing pages' do before do create(:wiki_page, wiki: wiki, title: 'my page 16') diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb index ef8b859c9d9..a547c1be2f4 100644 --- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -7,14 +7,15 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do shared_examples_for 'invite member selector' do context 'with ability to invite members' do - it { is_expected.to have_link('Invite members', href: href) } + it { is_expected.to have_selector('.js-invite-members-trigger') } it { is_expected.to have_selector('.js-invite-members-modal') } end context 'without ability to invite members' do let(:invite_member) { false } - it { is_expected.not_to have_link('Invite members') } + it { is_expected.not_to have_selector('.js-invite-members-trigger') } + it { is_expected.not_to have_selector('.js-invite-members-modal') } end end diff --git a/yarn.lock b/yarn.lock index 84607f089e9..5337bb7cc1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1355,10 +1355,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== -"@gitlab/web-ide@0.0.1-dev-20230216131813": - version "0.0.1-dev-20230216131813" - resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230216131813.tgz#f1c5de4e9bea0a1736296b0e63356ca39c065f54" - integrity sha512-eqGh/gol3vnT62Fs2dR1JWfkX/hVDwcJaYT1JkFPtiaTi2hFwvqw7k3uKL3qIiYzL7V22avxwRIkbAZCI7aKbQ== +"@gitlab/web-ide@0.0.1-dev-20230223005157": + version "0.0.1-dev-20230223005157" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230223005157.tgz#e8ef36229dbe25bbfccfc57d5f02cce0c94b53c5" + integrity sha512-JLSIC/elod3txZL6CPe6hk07a97VAMjv+DmAvQFAJ/W/cj2wE+AG/ztr/brX2O88Cf4dX4nh48s1n49efMlajQ== "@graphql-eslint/eslint-plugin@3.12.0": version "3.12.0" |